Skip to main content
Glama

Shortcut MCP Server

Official
by useshortcut
epics.test.ts10.5 kB
import { beforeEach, describe, expect, mock, test } from "bun:test"; import type { CreateEpic, Epic, Member, MemberInfo } from "@shortcut/client"; import type { ShortcutClientWrapper } from "@/client/shortcut"; import type { CustomMcpServer } from "@/mcp/CustomMcpServer"; import { EpicTools } from "./epics"; describe("EpicTools", () => { const mockEpics: Epic[] = [ { entity_type: "epic", id: 1, name: "Epic 1", description: "Description for Epic 1", state: "unstarted", started: false, completed: false, archived: false, deadline: "2025-04-01", app_url: "https://app.shortcut.com/test/epic/1", group_id: null, milestone_id: null, owner_ids: [], follower_ids: [], requested_by_id: null, stats: { num_stories_backlog: 1, num_stories_unstarted: 2, num_stories_started: 3, num_stories_done: 4, }, } as unknown as Epic, { entity_type: "epic", id: 2, name: "Epic 2", description: "Description for Epic 2", state: "started", started: true, completed: false, archived: false, deadline: null, app_url: "https://app.shortcut.com/test/epic/2", group_id: null, milestone_id: null, owner_ids: [], follower_ids: [], requested_by_id: null, stats: { num_stories_backlog: 1, num_stories_unstarted: 2, num_stories_started: 3, num_stories_done: 4, }, } as unknown as Epic, { entity_type: "epic", id: 3, name: "Epic 3", description: "Description for Epic 3", state: "done", started: true, completed: true, archived: true, deadline: "2025-03-01", app_url: "https://app.shortcut.com/test/epic/3", group_id: null, milestone_id: null, owner_ids: [], follower_ids: [], requested_by_id: null, stats: { num_stories_backlog: 1, num_stories_unstarted: 2, num_stories_started: 3, num_stories_done: 4, }, } as unknown as Epic, ]; const mockCurrentUser = { id: "user1", mention_name: "testuser", name: "Test User", workspace2: { estimate_scale: [], }, } as unknown as Member & MemberInfo; const createMockClient = (methods?: object) => ({ getCurrentUser: mock(async () => mockCurrentUser), getUserMap: mock(async () => new Map()), getWorkflowMap: mock(async () => new Map()), getTeamMap: mock(async () => new Map()), getMilestone: mock(async () => null), ...methods, }) as unknown as ShortcutClientWrapper; describe("create method", () => { test("should register the correct tools with the server", () => { const mockClient = createMockClient(); const mockToolRead = mock(); const mockToolWrite = mock(); const mockServer = { addToolWithReadAccess: mockToolRead, addToolWithWriteAccess: mockToolWrite, } as unknown as CustomMcpServer; EpicTools.create(mockClient, mockServer); expect(mockToolRead).toHaveBeenCalledTimes(2); expect(mockToolRead.mock.calls?.[0]?.[0]).toBe("epics-get-by-id"); expect(mockToolRead.mock.calls?.[1]?.[0]).toBe("epics-search"); expect(mockToolWrite).toHaveBeenCalledTimes(1); expect(mockToolWrite.mock.calls?.[0]?.[0]).toBe("epics-create"); }); }); describe("getEpic method", () => { const getEpicMock = mock(async (id: number) => mockEpics.find((epic) => epic.id === id)); const mockClient = createMockClient({ getEpic: getEpicMock }); test("should return formatted epic details when epic is found", async () => { const epicTools = new EpicTools(mockClient); const result = await epicTools.getEpic(1); expect(result.content[0].type).toBe("text"); const textContent = String(result.content[0].text); expect(textContent).toContain("Epic: 1"); expect(textContent).toContain('"id": 1'); expect(textContent).toContain('"name": "Epic 1"'); expect(textContent).toContain('"description": "Description for Epic 1"'); expect(textContent).toContain('"state": "unstarted"'); expect(textContent).toContain('"deadline": "2025-04-01"'); expect(textContent).toContain('"app_url": "https://app.shortcut.com/test/epic/1"'); }); test("should return simplified epic when full = false", async () => { const epicTools = new EpicTools(mockClient); const result = await epicTools.getEpic(1, false); expect(result.content[0].type).toBe("text"); const textContent = String(result.content[0].text); expect(textContent).toContain("Epic: 1"); expect(textContent).toContain('"id": 1'); expect(textContent).toContain('"name": "Epic 1"'); // Should contain simplified additional fields when full = false expect(textContent).toContain('"description": "Description for Epic 1"'); expect(textContent).toContain('"deadline": "2025-04-01"'); expect(textContent).toContain('"comments"'); }); test("should handle completed and archived epics correctly", async () => { const epicTools = new EpicTools(mockClient); const result = await epicTools.getEpic(3, true); expect(result.content[0].type).toBe("text"); const textContent = String(result.content[0].text); expect(textContent).toContain("Epic: 3"); expect(textContent).toContain('"id": 3'); expect(textContent).toContain('"name": "Epic 3"'); expect(textContent).toContain('"description": "Description for Epic 3"'); expect(textContent).toContain('"state": "done"'); expect(textContent).toContain('"archived": true'); expect(textContent).toContain('"completed": true'); expect(textContent).toContain('"deadline": "2025-03-01"'); }); test("should handle epics with null deadline", async () => { const epicTools = new EpicTools(mockClient); const result = await epicTools.getEpic(2, true); expect(result.content[0].type).toBe("text"); const textContent = String(result.content[0].text); expect(textContent).toContain("Epic: 2"); expect(textContent).toContain('"id": 2'); expect(textContent).toContain('"name": "Epic 2"'); expect(textContent).toContain('"description": "Description for Epic 2"'); expect(textContent).toContain('"state": "started"'); expect(textContent).toContain('"started": true'); expect(textContent).toContain('"deadline": null'); }); test("should throw error when epic is not found", async () => { const epicTools = new EpicTools(mockClient); await expect(() => epicTools.getEpic(999)).toThrow( "Failed to retrieve Shortcut epic with public ID: 999", ); }); }); describe("searchEpics method", () => { const searchEpicsMock = mock(async (_: string) => ({ epics: mockEpics, total: mockEpics.length, })); const mockClient = createMockClient({ searchEpics: searchEpicsMock, }); beforeEach(() => { searchEpicsMock.mockClear(); }); test("should return formatted list of epics when epics are found", async () => { const epicTools = new EpicTools(mockClient); const result = await epicTools.searchEpics({}); expect(mockClient.getCurrentUser).toHaveBeenCalled(); expect(mockClient.searchEpics).toHaveBeenCalled(); expect(result.content[0].type).toBe("text"); const textContent = String(result.content[0].text); expect(textContent).toContain("Result (3 shown of 3 total epics found):"); expect(textContent).toContain('"id": 1'); expect(textContent).toContain('"name": "Epic 1"'); expect(textContent).toContain('"id": 2'); expect(textContent).toContain('"name": "Epic 2"'); expect(textContent).toContain('"id": 3'); expect(textContent).toContain('"name": "Epic 3"'); }); test("should return no epics found message when no epics match", async () => { const epicTools = new EpicTools( createMockClient({ searchEpics: mock(async () => ({ epics: [], total: 0 })), }), ); const result = await epicTools.searchEpics({}); expect(result.content[0].type).toBe("text"); expect(result.content[0].text).toBe("Result: No epics found."); }); test("should throw error when epics is null", async () => { const epicTools = new EpicTools( createMockClient({ searchEpics: mock(async () => ({ epics: null, total: 0 })), }), ); await expect(() => epicTools.searchEpics({})).toThrow( 'Failed to search for epics matching your query: ""', ); }); test("should handle various search parameters", async () => { const epicTools = new EpicTools(mockClient); await epicTools.searchEpics({ id: 1, name: "Test Epic", description: "Test Description", state: "started", objective: 123, owner: "me", team: "engineering", isArchived: true, }); expect(searchEpicsMock.mock.calls?.[0]?.[0]).toBe( 'id:1 title:"Test Epic" description:"Test Description" state:started objective:123 owner:testuser team:engineering is:archived', ); }); test("should handle 'me' as owner parameter", async () => { const epicTools = new EpicTools(mockClient); await epicTools.searchEpics({ owner: "me" }); expect(searchEpicsMock.mock.calls?.[0]?.[0]).toBe("owner:testuser"); }); test("should handle date parameters", async () => { const epicTools = new EpicTools(mockClient); await epicTools.searchEpics({ created: "2023-01-01", updated: "2023-01-01..2023-02-01", completed: "today", due: "tomorrow", }); expect(searchEpicsMock.mock.calls?.[0]?.[0]).toBe( "created:2023-01-01 updated:2023-01-01..2023-02-01 completed:today due:tomorrow", ); }); test("should handle boolean parameters", async () => { const epicTools = new EpicTools(mockClient); await epicTools.searchEpics({ isUnstarted: true, isStarted: false, isDone: true, isArchived: false, isOverdue: true, hasOwner: true, hasComment: false, hasDeadline: true, hasLabel: false, }); expect(searchEpicsMock.mock.calls?.[0]?.[0]).toBe( "is:unstarted !is:started is:done !is:archived is:overdue has:owner !has:comment has:deadline !has:label", ); }); }); describe("createEpic method", () => { const createEpicMock = mock(async (_: CreateEpic) => ({ id: 1, name: "Epic 1", description: "Description for Epic 1", app_url: "https://app.shortcut.com/test/epic/1", })); const mockClient = createMockClient({ createEpic: createEpicMock, }); test("should create epic", async () => { const epicTools = new EpicTools(mockClient); const result = await epicTools.createEpic({ name: "Epic 1", description: "Description for Epic 1", }); expect(result.content[0].text).toBe("Epic created with ID: 1."); }); }); });

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