Skip to content
This repository was archived by the owner on Jul 8, 2026. It is now read-only.

Commit 5b63ed2

Browse files
Merge pull request #159 from useshortcut/amcd/list-label-stories
Add tool for listing label stories.
2 parents ee68bfb + 0f03840 commit 5b63ed2

7 files changed

Lines changed: 110 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ If your IDE doesn't support HTTP-based MCP servers, or you'd prefer to run the s
9292
| Tool | Description |
9393
|------|-------------|
9494
| `labels-list` | List all labels in the Shortcut workspace |
95+
| `labels-get-stories` | Get all stories with a specific label |
9596
| `labels-create` | Create a new label in Shortcut |
9697

9798
### Custom Fields

manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
{ "name": "documents-search", "description": "Find documents" },
7676
{ "name": "documents-get-by-id", "description": "Get a document as markdown by its ID" },
7777
{ "name": "labels-list", "description": "List all labels in the Shortcut workspace" },
78+
{ "name": "labels-get-stories", "description": "Get all stories with a specific label" },
7879
{ "name": "labels-create", "description": "Create a new label in Shortcut" }
7980
],
8081
"keywords": ["shortcut", "mcp", "project-management", "stories", "epics", "iterations", "agile"],

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shortcut/mcp",
3-
"version": "0.22.0",
3+
"version": "0.23.0",
44
"description": "Shortcut MCP Server",
55
"repository": {
66
"type": "git",

src/client/shortcut.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,11 @@ export class ShortcutClientWrapper {
483483
return response?.data ?? [];
484484
}
485485

486+
async listLabelStories(labelPublicId: number): Promise<StorySlim[]> {
487+
const response = await this.client.listLabelStories(labelPublicId);
488+
return response?.data ?? [];
489+
}
490+
486491
async addTaskToStory(
487492
storyPublicId: number,
488493
taskParams: {

src/tools/labels.test.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { beforeEach, describe, expect, mock, test } from "bun:test";
2-
import type { CreateLabelParams, Label } from "@shortcut/client";
2+
import type { CreateLabelParams, Label, Story } from "@shortcut/client";
33
import type { ShortcutClientWrapper } from "@/client/shortcut";
44
import type { CustomMcpServer } from "@/mcp/CustomMcpServer";
55
import { LabelTools } from "./labels";
@@ -69,6 +69,41 @@ describe("LabelTools", () => {
6969
} as unknown as Label,
7070
];
7171

72+
const mockStories: Story[] = [
73+
{
74+
entity_type: "story",
75+
id: 123,
76+
name: "Story with label one",
77+
story_type: "feature",
78+
owner_ids: ["user1"],
79+
group_id: null,
80+
epic_id: null,
81+
iteration_id: null,
82+
workflow_id: null,
83+
workflow_state_id: 1,
84+
requested_by_id: null,
85+
follower_ids: [],
86+
app_url: "https://app.shortcut.com/test/story/123",
87+
archived: false,
88+
} as unknown as Story,
89+
{
90+
entity_type: "story",
91+
id: 456,
92+
name: "Story with label two",
93+
story_type: "bug",
94+
owner_ids: ["user2"],
95+
group_id: null,
96+
epic_id: null,
97+
iteration_id: null,
98+
workflow_id: null,
99+
workflow_state_id: 2,
100+
requested_by_id: null,
101+
follower_ids: [],
102+
app_url: "https://app.shortcut.com/test/story/456",
103+
archived: false,
104+
} as unknown as Story,
105+
];
106+
72107
const createMockClient = (methods?: object) =>
73108
({
74109
...methods,
@@ -86,14 +121,55 @@ describe("LabelTools", () => {
86121

87122
LabelTools.create(mockClient, mockServer);
88123

89-
expect(mockToolRead).toHaveBeenCalledTimes(1);
124+
expect(mockToolRead).toHaveBeenCalledTimes(2);
90125
expect(mockToolRead.mock.calls?.[0]?.[0]).toBe("labels-list");
126+
expect(mockToolRead.mock.calls?.[1]?.[0]).toBe("labels-get-stories");
91127

92128
expect(mockToolWrite).toHaveBeenCalledTimes(1);
93129
expect(mockToolWrite.mock.calls?.[0]?.[0]).toBe("labels-create");
94130
});
95131
});
96132

133+
describe("getLabelStories method", () => {
134+
const listLabelStoriesMock = mock(async () => mockStories);
135+
136+
beforeEach(() => {
137+
listLabelStoriesMock.mockClear();
138+
});
139+
140+
test("should return formatted stories for a label", async () => {
141+
const mockClient = createMockClient({
142+
listLabelStories: listLabelStoriesMock,
143+
getUserMap: mock(async () => new Map()),
144+
getWorkflowMap: mock(async () => new Map()),
145+
getTeamMap: mock(async () => new Map()),
146+
getMilestone: mock(async () => null),
147+
getIteration: mock(async () => null),
148+
getEpic: mock(async () => null),
149+
getCustomFieldMap: mock(async () => new Map()),
150+
});
151+
const labelTools = new LabelTools(mockClient);
152+
const result = await labelTools.getLabelStories(42);
153+
154+
expect(listLabelStoriesMock).toHaveBeenCalledWith(42);
155+
156+
const textContent = getTextContent(result);
157+
expect(textContent).toContain("Result (2 stories found for label 42):");
158+
expect(textContent).toContain('"name": "Story with label one"');
159+
expect(textContent).toContain('"name": "Story with label two"');
160+
});
161+
162+
test("should return no stories found message when label has no stories", async () => {
163+
const mockClient = createMockClient({
164+
listLabelStories: mock(async () => []),
165+
});
166+
const labelTools = new LabelTools(mockClient);
167+
const result = await labelTools.getLabelStories(42);
168+
169+
expect(getTextContent(result)).toBe("Result: No stories found for label 42.");
170+
});
171+
});
172+
97173
describe("listLabels method", () => {
98174
const listLabelsMock = mock(async () => mockLabels);
99175

src/tools/labels.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ export class LabelTools extends BaseTools {
2121
async (params) => await tools.listLabels(params),
2222
);
2323

24+
server.addToolWithReadAccess(
25+
"labels-get-stories",
26+
"Get all stories with a specific label.",
27+
{
28+
labelPublicId: z.number().positive().describe("Label ID"),
29+
},
30+
async ({ labelPublicId }) => await tools.getLabelStories(labelPublicId),
31+
);
32+
2433
server.addToolWithWriteAccess(
2534
"labels-create",
2635
"Create a new label.",
@@ -97,4 +106,17 @@ export class LabelTools extends BaseTools {
97106
label: this.formatLabel(label, { includeDescription: true }),
98107
});
99108
}
109+
110+
async getLabelStories(labelPublicId: number): Promise<CallToolResult> {
111+
const stories = await this.client.listLabelStories(labelPublicId);
112+
113+
if (!stories.length) {
114+
return this.toResult(`Result: No stories found for label ${labelPublicId}.`);
115+
}
116+
117+
return this.toResult(
118+
`Result (${stories.length} stories found for label ${labelPublicId}):`,
119+
await this.entitiesWithRelatedEntities(stories, "stories"),
120+
);
121+
}
100122
}

0 commit comments

Comments
 (0)