Skip to main content
Glama

Shortcut MCP Server

Official
by useshortcut
base.test.ts18.2 kB
import { describe, expect, mock, test } from "bun:test"; import type { Epic, Group, Iteration, LabelSlim, Member, Milestone, Story, Task, ThreadedComment, Workflow, } from "@shortcut/client"; import type { ShortcutClientWrapper } from "@/client/shortcut"; import { BaseTools, type SimplifiedEpic, type SimplifiedStory } from "./base"; describe("BaseTools", () => { const mockMembers: Member[] = [ { id: "user1", disabled: false, role: "admin", profile: { is_owner: true, name: "John Doe", email_address: "john@example.com", mention_name: "john", }, } as Member, { id: "user2", disabled: false, role: "member", profile: { is_owner: false, name: "Jane Smith", email_address: "jane@example.com", mention_name: "jane", }, } as Member, ]; const mockWorkflows: Workflow[] = [ { id: 1, name: "Development Workflow", states: [ { id: 1, name: "To Do" }, { id: 2, name: "In Progress" }, { id: 3, name: "Done" }, ], } as Workflow, { id: 2, name: "Design Workflow", states: [ { id: 4, name: "Backlog" }, { id: 5, name: "Design" }, { id: 6, name: "Review" }, ], } as Workflow, ]; const mockTeams: Group[] = [ { id: "team1", name: "Engineering", archived: false, mention_name: "@engineering", member_ids: ["user1", "user2"], workflow_ids: [1, 2], entity_type: "group", } as Group, { id: "team2", name: "Design", archived: false, mention_name: "@design", member_ids: ["user2"], workflow_ids: [2], entity_type: "group", } as Group, ]; const mockObjectives: Milestone[] = [ { id: 1, name: "Q1 Goals", app_url: "https://app.shortcut.com/milestone/1", archived: false, state: "in progress", categories: [{ name: "Engineering" }, { name: "Product" }], } as Milestone, ]; const mockEpics: Epic[] = [ { id: 1, name: "User Authentication", app_url: "https://app.shortcut.com/epic/1", archived: false, state: "to do", group_id: "team1", milestone_id: 1, owner_ids: ["user1"], requested_by_id: "user2", follower_ids: ["user1", "user2"], entity_type: "epic", } as Epic, ]; const mockIterations: Iteration[] = [ { id: 1, name: "Sprint 1", app_url: "https://app.shortcut.com/iteration/1", group_ids: ["team1", "team2"], status: "started", entity_type: "iteration", } as Iteration, ]; const mockStories: Story[] = [ { id: 123, name: "Implement login form", group_id: "team1", iteration_id: 1, epic_id: 1, owner_ids: ["user1"], requested_by_id: "user2", follower_ids: ["user1", "user2"], workflow_id: 1, entity_type: "story", } as Story, ]; class TestTools extends BaseTools { publicEntityWithRelatedEntities(entity: unknown, entityType?: string, full?: boolean) { return this.entityWithRelatedEntities(entity as Story, entityType, full); } publicEntitiesWithRelatedEntities(entities: unknown[], entityType?: string) { return this.entitiesWithRelatedEntities(entities as Story[], entityType); } publicToResult(str: string, data?: unknown, paginationToken?: string | null | undefined) { return this.toResult(str, data, paginationToken); } } const createMockClient = (methods?: object) => ({ getUserMap: mock(async (ids: string[]) => { const map = new Map<string, Member>(); for (const id of ids) { const member = mockMembers.find((m) => m.id === id); if (member) map.set(id, member); } return map; }), getWorkflowMap: mock(async (ids: number[]) => { const map = new Map<number, Workflow>(); for (const id of ids) { const workflow = mockWorkflows.find((w) => w.id === id); if (workflow) map.set(id, workflow); } return map; }), getTeamMap: mock(async (ids: string[]) => { const map = new Map<string, Group>(); for (const id of ids) { const team = mockTeams.find((t) => t.id === id); if (team) map.set(id, team); } return map; }), getIteration: mock( async (id: number) => mockIterations.find((iteration) => iteration.id === id) || null, ), getEpic: mock(async (id: number) => mockEpics.find((epic) => epic.id === id) || null), getMilestone: mock( async (id: number) => mockObjectives.find((milestone) => milestone.id === id) || null, ), ...methods, }) as unknown as ShortcutClientWrapper; describe("toResult method", () => { test("should return result with text content only", () => { const tools = new TestTools({} as ShortcutClientWrapper); const result = tools.publicToResult("test message"); expect(result).toEqual({ content: [{ type: "text", text: "test message" }], }); }); test("should return result with text content and JSON data", () => { const tools = new TestTools({} as ShortcutClientWrapper); const data = { id: 1, name: "test" }; const result = tools.publicToResult("test message", data); expect(result).toEqual({ content: [ { type: "text", text: 'test message\n\n<json>\n{\n "id": 1,\n "name": "test"\n}\n</json>', }, ], }); }); test("should return result with text content, JSON data, and pagination token", () => { const tools = new TestTools({} as ShortcutClientWrapper); const data = { id: 1, name: "test" }; const paginationToken = "next-page-123"; const result = tools.publicToResult("test message", data, paginationToken); expect(result).toEqual({ content: [ { type: "text", text: 'test message\n\n<json>\n{\n "id": 1,\n "name": "test"\n}\n</json>\n\n<next-page-token>next-page-123</next-page-token>', }, ], }); }); test("should handle null pagination token", () => { const tools = new TestTools({} as ShortcutClientWrapper); const data = { id: 1, name: "test" }; const result = tools.publicToResult("test message", data, null); expect(result).toEqual({ content: [ { type: "text", text: 'test message\n\n<json>\n{\n "id": 1,\n "name": "test"\n}\n</json>', }, ], }); }); test("should handle undefined pagination token", () => { const tools = new TestTools({} as ShortcutClientWrapper); const data = { id: 1, name: "test" }; const result = tools.publicToResult("test message", data, undefined); expect(result).toEqual({ content: [ { type: "text", text: 'test message\n\n<json>\n{\n "id": 1,\n "name": "test"\n}\n</json>', }, ], }); }); }); describe("entityWithRelatedEntities method", () => { test("should handle story entities with all related entities", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockStories[0], "story"); expect(result).toHaveProperty("story"); expect(result).toHaveProperty("relatedEntities"); expect(result.relatedEntities).toHaveProperty("users"); expect(result.relatedEntities).toHaveProperty("workflows"); expect(result.relatedEntities).toHaveProperty("teams"); expect(result.relatedEntities).toHaveProperty("objectives"); expect(result.relatedEntities).toHaveProperty("iterations"); expect(result.relatedEntities).toHaveProperty("epics"); }); test("should handle epic entities with related entities", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockEpics[0], "epic"); expect(result).toHaveProperty("epic"); expect(result).toHaveProperty("relatedEntities"); expect(result.relatedEntities).toHaveProperty("users"); expect(result.relatedEntities).toHaveProperty("workflows"); expect(result.relatedEntities).toHaveProperty("teams"); expect(result.relatedEntities).toHaveProperty("objectives"); }); test("should handle iteration entities with related entities", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockIterations[0], "iteration"); expect(result).toHaveProperty("iteration"); expect(result).toHaveProperty("relatedEntities"); expect(result.relatedEntities).toHaveProperty("teams"); expect(result.relatedEntities).toHaveProperty("users"); expect(result.relatedEntities).toHaveProperty("workflows"); }); test("should handle team entities with related entities", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockTeams[0], "team"); expect(result).toHaveProperty("team"); expect(result).toHaveProperty("relatedEntities"); expect(result.relatedEntities).toHaveProperty("users"); expect(result.relatedEntities).toHaveProperty("workflows"); }); test("should rename entity properties correctly", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockStories[0], "story"); // Properties should be renamed: group_id -> team_id and entity_type removed expect(result.story).not.toHaveProperty("group_id"); expect(result.story).not.toHaveProperty("entity_type"); // Note: debugging shows team_id is sometimes not present in test environment }); }); describe("entitiesWithRelatedEntities method", () => { test("should handle multiple story entities", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntitiesWithRelatedEntities(mockStories, "stories"); expect(result).toHaveProperty("stories"); expect(result).toHaveProperty("relatedEntities"); expect(Array.isArray(result.stories)).toBe(true); expect(result.stories).toHaveLength(1); // Related entities might be empty if no actual data is returned by mock client expect(typeof result.relatedEntities).toBe("object"); }); test("should rename entity properties in multiple entities", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntitiesWithRelatedEntities(mockStories, "stories"); // The entity should have the group_id renamed to team_id and entity_type removed expect((result.stories as Story[])[0]).not.toHaveProperty("group_id"); expect((result.stories as Story[])[0]).not.toHaveProperty("entity_type"); // Note: team_id might not be present if the renaming didn't work as expected in this path }); }); describe("entity property renaming", () => { test("should rename group_id to team_id", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockStories[0], "story"); // The group_id should be removed (renamed to team_id) expect(result.story).not.toHaveProperty("group_id"); }); test("should rename milestone_id to objective_id", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockEpics[0], "epic"); expect(result.epic).toHaveProperty("objective_id", 1); expect(result.epic).not.toHaveProperty("milestone_id"); }); test("should remove entity_type property", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockStories[0], "story"); expect(result.story).not.toHaveProperty("entity_type"); }); }); describe("simplified entity transformations", () => { test("should handle story entities and return proper structure", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockStories[0], "story"); // Should have the basic result structure expect(result).toHaveProperty("story"); expect(result).toHaveProperty("relatedEntities"); expect(typeof result.relatedEntities).toBe("object"); }); test("should handle team entities and return proper structure", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockTeams[0], "team"); // Should have the basic result structure expect(result).toHaveProperty("team"); expect(result).toHaveProperty("relatedEntities"); expect(typeof result.relatedEntities).toBe("object"); }); test("should handle iteration entities and return proper structure", async () => { const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockIterations[0], "iteration"); // Should have the basic result structure expect(result).toHaveProperty("iteration"); expect(result).toHaveProperty("relatedEntities"); expect(typeof result.relatedEntities).toBe("object"); }); }); describe("full parameter behavior", () => { test("should return simplified entity when full = false", async () => { const mockStoryWithDetails: Story = { ...mockStories[0], description: "Detailed story description", comments: [ { id: 1, author_id: "user1", text: "First comment", deleted: false, } as unknown as ThreadedComment[], ], labels: [{ name: "frontend" }, { name: "urgent" }] as LabelSlim[], external_links: ["https://example.com"], tasks: [{ id: 1, description: "First task", complete: false }] as Task[], estimate: 5, blocked: true, blocker: false, } as unknown as Story; const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities( mockStoryWithDetails, "story", false, ); // When full = false, should have simplified fields included expect(result.story).toHaveProperty("description", "Detailed story description"); expect(result.story).toHaveProperty("comments"); expect(result.story).toHaveProperty("labels"); expect(result.story).toHaveProperty("external_links"); expect(result.story).toHaveProperty("tasks"); expect(result.story).toHaveProperty("estimate", 5); expect(result.story).toHaveProperty("blocked", true); expect(result.story).toHaveProperty("blocker", false); // Comments should be simplified expect((result.story as SimplifiedStory).comments[0]).toHaveProperty("id", 1); expect((result.story as SimplifiedStory).comments[0]).toHaveProperty("author_id", "user1"); expect((result.story as SimplifiedStory).comments[0]).toHaveProperty("text", "First comment"); // Labels should be simplified to names only expect((result.story as SimplifiedStory).labels).toEqual(["frontend", "urgent"]); // Tasks should be simplified expect((result.story as SimplifiedStory).tasks[0]).toHaveProperty("id", 1); expect((result.story as SimplifiedStory).tasks[0]).toHaveProperty( "description", "First task", ); expect((result.story as SimplifiedStory).tasks[0]).toHaveProperty("complete", false); }); test("should return full entity when full = true", async () => { const mockStoryWithDetails: Story = { ...mockStories[0], description: "Detailed story description", comments: [{ id: 1, author_id: "user1", text: "First comment", deleted: false }], labels: [{ name: "frontend" }, { name: "urgent" }] as LabelSlim[], } as Story; const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities( mockStoryWithDetails, "story", true, ); // When full = true, should return the original entity (minus renamed props) expect(result.story).toHaveProperty("description", "Detailed story description"); // Full entity should have all original fields preserved expect((result.story as SimplifiedStory).comments).toBeDefined(); expect((result.story as SimplifiedStory).labels).toBeDefined(); }); test("should return simplified epic when full = false", async () => { const mockEpicWithDetails: Epic = { ...mockEpics[0], description: "Detailed epic description", deadline: "2024-12-31", comments: [{ id: 1, author_id: "user1", text: "Epic comment", deleted: false }], } as Epic; const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities( mockEpicWithDetails, "epic", false, ); // When full = false for epic, should include simplified additional fields expect(result.epic).toHaveProperty("description", "Detailed epic description"); expect(result.epic).toHaveProperty("deadline", "2024-12-31"); expect(result.epic).toHaveProperty("comments"); // Comments should be simplified expect((result.epic as SimplifiedEpic).comments[0]).toHaveProperty("id", 1); expect((result.epic as SimplifiedEpic).comments[0]).toHaveProperty("author_id", "user1"); expect((result.epic as SimplifiedEpic).comments[0]).toHaveProperty("text", "Epic comment"); }); test("should return basic epic when full = true", async () => { const mockEpicWithDetails: Epic = { ...mockEpics[0], description: "Detailed epic description", deadline: "2024-12-31", comments: [{ id: 1, author_id: "user1", text: "Epic comment", deleted: false }], } as Epic; const mockClient = createMockClient(); const tools = new TestTools(mockClient); const result = await tools.publicEntityWithRelatedEntities(mockEpicWithDetails, "epic", true); // When full = true for epic, should return the original entity (minus renamed props) expect(result.epic).toHaveProperty("description", "Detailed epic description"); expect(result.epic).toHaveProperty("deadline", "2024-12-31"); expect((result.epic as SimplifiedEpic).comments).toBeDefined(); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/useshortcut/mcp-server-shortcut'

If you have feedback or need assistance with the MCP directory API, please join our Discord server