Skip to content

feat(tools): add attachment download tools#83

Open
nguyenthe-hien wants to merge 6 commits intonulab:mainfrom
nguyenthe-hien:feat/download-attachment
Open

feat(tools): add attachment download tools#83
nguyenthe-hien wants to merge 6 commits intonulab:mainfrom
nguyenthe-hien:feat/download-attachment

Conversation

@nguyenthe-hien
Copy link
Copy Markdown

@nguyenthe-hien nguyenthe-hien commented Mar 31, 2026

Summary

  • Add 6 tools for listing and downloading attachments from issues, wiki pages, and pull requests
  • Add transfer_issue_attachment tool to copy attachments between issues
  • List tools (get_issue_attachments, get_wiki_attachments, get_pull_request_attachments): return attachment metadata (id, name, size, createdUser, etc.)
  • Download tools (get_issue_attachment, get_wiki_attachment, get_pull_request_attachment): download and return file content as base64-encoded data with MIME type detection
    • Images are returned as MCP image content type (inline rendering)
    • Other files are returned as MCP resource content type (embedded blob)
  • Transfer tool (transfer_issue_attachment): download an attachment from one issue and upload it to another

Motivation

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:

  • View images attached to issues
  • Download files attached to wiki pages or pull requests
  • Transfer attachments between issues or systems

This PR uses existing backlog-js methods (getIssueAttachments, getIssueAttachment, getWikisAttachments, getWikiAttachment, getPullRequestAttachments, getPullRequestAttachment, postIssueAttachment) which were available in the library but not exposed as MCP tools.

Implementation

  • List tools use standard ToolDefinition pattern (JSON output with field picking support)
  • Download tools use DynamicToolDefinition pattern to return custom CallToolResult with binary content
  • Extends Toolset type with optional dynamicTools field to support mixed tool types within the same toolset
  • registerTools() now also registers dynamicTools using direct handler (no field picking/token limit for binary content)
  • Utility: streamToBase64, getMimeType, buildAttachmentResult with improved MIME detection and base64 encoding
  • Attachment filenames are URL-decoded before MIME detection to handle encoded characters

New tools

Toolset Tool Description
issue get_issue_attachments List attachments for an issue
issue get_issue_attachment Download an attachment from an issue
issue transfer_issue_attachment Copy an attachment from one issue to another
wiki get_wiki_attachments List attachments for a wiki page
wiki get_wiki_attachment Download an attachment from a wiki page
git get_pull_request_attachments List attachments for a pull request
git get_pull_request_attachment Download an attachment from a pull request

Test plan

  • All 372 tests pass (npm test)
  • TypeScript type check passes (npx tsc --noEmit)
  • ESLint passes (npm run lint)
  • Unit tests cover: correct API calls, image vs non-image content types, error handling for missing IDs, stream-to-base64 conversion, MIME type detection, transfer attachment flow

nguyenthe-hien and others added 2 commits April 1, 2026 07:02
… 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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) or resource (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.

Comment on lines +16 to +21
export function buildFileContent(
filename: string,
mimeType: string,
base64: string,
url: string
): CallToolResult {
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned this up in a1373b9 by renaming the unused argument to _filename. npm run lint passes locally.

Comment thread src/registerTools.ts
Comment on lines +42 to +55
// 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
);
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped dynamic tool registration so exceptions are converted to CallToolResult errors via backlogErrorHandler in 75872b5. Verified with npm run lint and npm test locally.

Comment thread src/tools/getPullRequestAttachments.ts Outdated
Comment on lines +27 to +34
repoIdOrName: z
.string()
.describe(
t(
'TOOL_GET_PR_ATTACHMENTS_REPO',
'The repository ID or name'
)
),
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated get_pull_request_attachments to follow the existing git toolset convention with repoId / repoName and resolveIdOrName in 75872b5.

Comment thread src/tools/getPullRequestAttachment.ts Outdated
Comment on lines +29 to +35
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
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@katayama8000
Copy link
Copy Markdown
Collaborator

katayama8000 commented Apr 2, 2026

@nguyenthe-hien
Thank you for the PR!

But Can you run npm run lint locally and fix errors?
Also, please make sure it works locally.

@nguyenthe-hien
Copy link
Copy Markdown
Author

Fixed the attachment changes on this branch and pushed the updates through a1373b9.

Local verification:

  • npm run lint
  • npm test

Both pass locally.

@katayama8000
Copy link
Copy Markdown
Collaborator

@nguyenthe-hien
Thanks. I will review the PR soon.

I wanna ask you that when you wanna get attachments via mcp server?

@nguyenthe-hien
Copy link
Copy Markdown
Author

@katayama8000
Our main use case involves bug reports with image and video attachments, where the essential context often exists in the attachment itself rather
than in the text description.

We want agents to retrieve those attachments so they can analyze the actual visual evidence, understand the issue more accurately, and generate a
report through a third-party platform for our team.

Because of that, attachment retrieval is needed not only for analysis, but also for reporting. We want the final report to preserve or reference
the original visual context so the team can review the issue more easily and make decisions faster.

In short, the flow we need is:

  1. Retrieve the attachment from Backlog via the MCP server.
  2. Let the agent analyze the image/video when the bug depends on visual evidence.
  3. Send a clearer report, with the relevant attachment context, to our third-party platform.

…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>
@katayama8000
Copy link
Copy Markdown
Collaborator

@nguyenthe-hien
Can you run npm run lint locally and fix errors again?
then I will review it and merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants