Skip to content

Commit 00068f9

Browse files
Merge pull request #85 from useshortcut/amcd/upload-files-to-stories
Upload a file to a story.
2 parents e8ac821 + 091f7dd commit 00068f9

File tree

4 files changed

+52
-8
lines changed

4 files changed

+52
-8
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"modelcontextprotocol"
1313
],
1414
"license": "MIT",
15-
"version": "0.11.2",
15+
"version": "0.12.0",
1616
"type": "module",
1717
"main": "dist/index.js",
1818
"bin": {

src/client/shortcut.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { File } from "node:buffer";
2+
import { readFileSync } from "node:fs";
3+
import { basename } from "node:path";
14
import type {
25
ShortcutClient as BaseClient,
36
CreateDoc,
@@ -536,6 +539,19 @@ export class ShortcutClientWrapper {
536539
return doc;
537540
}
538541

542+
async uploadFile(storyId: number, filePath: string) {
543+
const fileContent = readFileSync(filePath);
544+
const fileName = basename(filePath);
545+
const file = new File([fileContent], fileName);
546+
// biome-ignore lint/suspicious/noExplicitAny: I think the JS API expects a browser File.. but Node's File type is different, but compatible.
547+
const response = await this.client.uploadFiles({ story_id: storyId, file0: file as any });
548+
const uploadedFile = response?.data ?? null;
549+
550+
if (!uploadedFile?.length) throw new Error(`Failed to upload the file: ${response.status}`);
551+
552+
return uploadedFile[0];
553+
}
554+
539555
async getCustomFieldMap(customFieldIds: string[]) {
540556
await this.loadCustomFields();
541557
return new Map(

src/tools/stories.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,18 +149,19 @@ describe("StoryTools", () => {
149149

150150
StoryTools.create(mockClient, mockServer);
151151

152-
expect(mockTool).toHaveBeenCalledTimes(15);
152+
expect(mockTool).toHaveBeenCalledTimes(16);
153153
expect(mockTool.mock.calls?.[0]?.[0]).toBe("get-story");
154154
expect(mockTool.mock.calls?.[1]?.[0]).toBe("search-stories");
155155
expect(mockTool.mock.calls?.[2]?.[0]).toBe("get-story-branch-name");
156156
expect(mockTool.mock.calls?.[3]?.[0]).toBe("create-story");
157157
expect(mockTool.mock.calls?.[4]?.[0]).toBe("update-story");
158-
expect(mockTool.mock.calls?.[5]?.[0]).toBe("assign-current-user-as-owner");
159-
expect(mockTool.mock.calls?.[6]?.[0]).toBe("unassign-current-user-as-owner");
160-
expect(mockTool.mock.calls?.[7]?.[0]).toBe("create-story-comment");
161-
expect(mockTool.mock.calls?.[8]?.[0]).toBe("add-task-to-story");
162-
expect(mockTool.mock.calls?.[9]?.[0]).toBe("add-relation-to-story");
163-
expect(mockTool.mock.calls?.[10]?.[0]).toBe("update-task");
158+
expect(mockTool.mock.calls?.[5]?.[0]).toBe("upload-file-to-story");
159+
expect(mockTool.mock.calls?.[6]?.[0]).toBe("assign-current-user-as-owner");
160+
expect(mockTool.mock.calls?.[7]?.[0]).toBe("unassign-current-user-as-owner");
161+
expect(mockTool.mock.calls?.[8]?.[0]).toBe("create-story-comment");
162+
expect(mockTool.mock.calls?.[9]?.[0]).toBe("add-task-to-story");
163+
expect(mockTool.mock.calls?.[10]?.[0]).toBe("add-relation-to-story");
164+
expect(mockTool.mock.calls?.[11]?.[0]).toBe("update-task");
164165
});
165166
});
166167

src/tools/stories.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,16 @@ The story will be added to the default state for the workflow.
215215
async (params) => await tools.updateStory(params),
216216
);
217217

218+
server.tool(
219+
"upload-file-to-story",
220+
"Upload a file and link it to a story.",
221+
{
222+
storyPublicId: z.number().positive().describe("The public ID of the story"),
223+
filePath: z.string().describe("The path to the file to upload"),
224+
},
225+
async ({ storyPublicId, filePath }) => await tools.uploadFileToStory(storyPublicId, filePath),
226+
);
227+
218228
server.tool(
219229
"assign-current-user-as-owner",
220230
"Assign the current user as the owner of a story",
@@ -530,6 +540,23 @@ The story will be added to the default state for the workflow.
530540
return this.toResult(`Updated story sc-${storyPublicId}. Story URL: ${updatedStory.app_url}`);
531541
}
532542

543+
async uploadFileToStory(storyPublicId: number, filePath: string) {
544+
if (!storyPublicId) throw new Error("Story public ID is required");
545+
if (!filePath) throw new Error("File path is required");
546+
547+
const story = await this.client.getStory(storyPublicId);
548+
if (!story)
549+
throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
550+
551+
const uploadedFile = await this.client.uploadFile(storyPublicId, filePath);
552+
553+
if (!uploadedFile) throw new Error(`Failed to upload file to story sc-${storyPublicId}`);
554+
555+
return this.toResult(
556+
`Uploaded file "${uploadedFile.name}" to story sc-${storyPublicId}. File ID is: ${uploadedFile.id}`,
557+
);
558+
}
559+
533560
async addTaskToStory({
534561
storyPublicId,
535562
taskDescription,

0 commit comments

Comments
 (0)