feat(tools): add attachment download tools#83
feat(tools): add attachment download tools#83nguyenthe-hien wants to merge 6 commits intonulab:mainfrom
Conversation
… requests Add 6 new tools to list and download attachments: - get_issue_attachments / get_issue_attachment - get_wiki_attachments / get_wiki_attachment - get_pull_request_attachments / get_pull_request_attachment List tools return attachment metadata (id, name, size, etc.). Download tools return base64-encoded file content — images are returned as MCP image content type, other files as embedded resources. Also extends Toolset type with optional `dynamicTools` field to support tools that return custom CallToolResult (needed for binary file content). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds MCP tooling to list and download Backlog attachments (issues, wiki pages, pull requests), including utilities for stream-to-base64 conversion and MIME-type/content shaping, and extends tool registration to support dynamic (binary-returning) tools.
Changes:
- Introduces attachment listing tools for issues, wiki pages, and pull requests.
- Introduces attachment download tools that return base64 content as MCP
image(inline) orresource(blob) based on MIME type. - Extends toolset typing and registration to support mixed static + dynamic tool definitions.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/streamToBase64.ts | New helper to convert Node/Web streams (or strings) to base64 for downloads. |
| src/utils/streamToBase64.test.ts | Unit tests for stream/string base64 conversion behavior. |
| src/utils/getMimeType.ts | New filename-extension → MIME type utility. |
| src/utils/getMimeType.test.ts | Unit tests for MIME type detection. |
| src/utils/buildFileContent.ts | New helper to format downloaded files into MCP image or resource content. |
| src/types/toolsets.ts | Extends Toolset to optionally include dynamicTools. |
| src/tools/tools.ts | Wires new attachment tools into issue, wiki, and git toolsets. |
| src/tools/getWikiAttachments.ts | Tool to list wiki attachments. |
| src/tools/getWikiAttachments.test.ts | Unit tests for wiki attachment listing. |
| src/tools/getWikiAttachment.ts | Dynamic tool to download a wiki attachment and return MCP content. |
| src/tools/getWikiAttachment.test.ts | Unit tests for wiki attachment download and filename decoding behavior. |
| src/tools/getPullRequestAttachments.ts | Tool to list pull request attachments. |
| src/tools/getPullRequestAttachments.test.ts | Unit tests for pull request attachment listing. |
| src/tools/getPullRequestAttachment.ts | Dynamic tool to download a pull request attachment and return MCP content. |
| src/tools/getPullRequestAttachment.test.ts | Unit tests for pull request attachment download and error path. |
| src/tools/getIssueAttachments.ts | Tool to list issue attachments. |
| src/tools/getIssueAttachments.test.ts | Unit tests for issue attachment listing. |
| src/tools/getIssueAttachment.ts | Dynamic tool to download an issue attachment and return MCP content. |
| src/tools/getIssueAttachment.test.ts | Unit tests for issue attachment download, decoding, and error path. |
| src/registerTools.ts | Registers dynamicTools in enabled toolsets using direct handlers. |
| src/registerTools.test.ts | Updates tool registration count assertions to include dynamicTools. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function buildFileContent( | ||
| filename: string, | ||
| mimeType: string, | ||
| base64: string, | ||
| url: string | ||
| ): CallToolResult { |
There was a problem hiding this comment.
buildFileContent takes a filename parameter but never uses it. With the repo’s ESLint rule @typescript-eslint/no-unused-vars (args ignored only when prefixed with _), this should fail linting. Either remove the parameter (and update call sites) or rename it to _filename (or actually incorporate the filename into the returned content, e.g. via a text item or resource metadata if supported).
There was a problem hiding this comment.
Cleaned this up in a1373b9 by renaming the unused argument to _filename. npm run lint passes locally.
| // Register dynamic tools within toolsets (e.g., attachment downloads) | ||
| for (const toolset of toolsetGroup.toolsets) { | ||
| if (!toolset.enabled || !toolset.dynamicTools) { | ||
| continue; | ||
| } | ||
|
|
||
| for (const tool of toolset.dynamicTools) { | ||
| const toolNameWithPrefix = `${prefix}${tool.name}`; | ||
| server.registerOnce( | ||
| toolNameWithPrefix, | ||
| tool.description, | ||
| tool.schema.shape, | ||
| tool.handler | ||
| ); |
There was a problem hiding this comment.
Dynamic tools are registered with tool.handler directly (no wrapWithErrorHandling / backlogErrorHandler). If backlog-js throws (e.g., 404/permission/network errors during attachment download), the exception won’t be converted into a CallToolResult with isError: true, unlike standard tools. Wrap the dynamic handler in a try/catch here (using backlogErrorHandler to format the message) so dynamic tools fail gracefully without crashing the tool call.
There was a problem hiding this comment.
Wrapped dynamic tool registration so exceptions are converted to CallToolResult errors via backlogErrorHandler in 75872b5. Verified with npm run lint and npm test locally.
| repoIdOrName: z | ||
| .string() | ||
| .describe( | ||
| t( | ||
| 'TOOL_GET_PR_ATTACHMENTS_REPO', | ||
| 'The repository ID or name' | ||
| ) | ||
| ), |
There was a problem hiding this comment.
This new tool uses a single required repoIdOrName: string parameter, which is inconsistent with the rest of the git toolset that accepts repoId?: number and repoName?: string (resolved via resolveIdOrName, e.g. getPullRequestCommentsTool in src/tools/getPullRequestComments.ts:27-38,98-110). For API consistency and better typing, consider switching this schema to the same repoId/repoName pattern and resolving to String(repoResult.value) before calling the backlog client.
There was a problem hiding this comment.
Updated get_pull_request_attachments to follow the existing git toolset convention with repoId / repoName and resolveIdOrName in 75872b5.
| repoIdOrName: z | ||
| .string() | ||
| .describe(t('TOOL_GET_PR_ATTACHMENT_REPO', 'The repository ID or name')), | ||
| number: z | ||
| .number() | ||
| .describe(t('TOOL_GET_PR_ATTACHMENT_NUMBER', 'The pull request number')), | ||
| attachmentId: z |
There was a problem hiding this comment.
This tool requires repoIdOrName: string, while other git tools in this repo consistently expose repoId?: number and repoName?: string and resolve with resolveIdOrName (see src/tools/getPullRequestTool.ts:27-34,64-76 and src/tools/getPullRequestComments.ts:27-38,98-110). Aligning the input schema here would make the toolset easier for clients to use and keep parameter conventions consistent across git tools.
There was a problem hiding this comment.
Updated get_pull_request_attachment to use the same repoId / repoName convention and repository resolution path as the rest of the git toolset in 75872b5.
|
@nguyenthe-hien But Can you run |
|
Fixed the attachment changes on this branch and pushed the updates through Local verification:
Both pass locally. |
|
@nguyenthe-hien I wanna ask you that when you wanna get attachments via mcp server? |
|
@katayama8000 We want agents to retrieve those attachments so they can analyze the actual visual evidence, understand the issue more accurately, and generate a Because of that, attachment retrieval is needed not only for analysis, but also for reporting. We want the final report to preserve or reference In short, the flow we need is:
|
…ling - Add transferIssueAttachment tool to copy attachments between issues - Extend getIssueAttachment, getWikiAttachment, getPullRequestAttachment with inline base64 support - Refactor buildAttachmentResult utility with improved MIME detection and test coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@nguyenthe-hien |
Summary
transfer_issue_attachmenttool to copy attachments between issuesget_issue_attachments,get_wiki_attachments,get_pull_request_attachments): return attachment metadata (id, name, size, createdUser, etc.)get_issue_attachment,get_wiki_attachment,get_pull_request_attachment): download and return file content as base64-encoded data with MIME type detectionimagecontent type (inline rendering)resourcecontent type (embedded blob)transfer_issue_attachment): download an attachment from one issue and upload it to anotherMotivation
The MCP server could reference attachment IDs when creating/updating issues, but had no way to list, download, or transfer existing attachments. This made it impossible for MCP clients to:
This PR uses existing
backlog-jsmethods (getIssueAttachments,getIssueAttachment,getWikisAttachments,getWikiAttachment,getPullRequestAttachments,getPullRequestAttachment,postIssueAttachment) which were available in the library but not exposed as MCP tools.Implementation
ToolDefinitionpattern (JSON output with field picking support)DynamicToolDefinitionpattern to return customCallToolResultwith binary contentToolsettype with optionaldynamicToolsfield to support mixed tool types within the same toolsetregisterTools()now also registersdynamicToolsusing direct handler (no field picking/token limit for binary content)streamToBase64,getMimeType,buildAttachmentResultwith improved MIME detection and base64 encodingNew tools
issueget_issue_attachmentsissueget_issue_attachmentissuetransfer_issue_attachmentwikiget_wiki_attachmentswikiget_wiki_attachmentgitget_pull_request_attachmentsgitget_pull_request_attachmentTest plan
npm test)npx tsc --noEmit)npm run lint)