feat: add attachment tools (list, upload, delete)
Add MCP tools for managing work package attachments: - list_attachments: list all files on a work package - upload_attachment: upload local files with multipart/form-data - delete_attachment: remove an attachment by ID Includes embed syntax hints (macro + markdown) in upload response and enhanced description fields with attachment embedding guidance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+87
-1
@@ -48,7 +48,18 @@ function formatPaginatedList(
|
||||
return [header, "", ...elements.map(formatter)].join("\n");
|
||||
}
|
||||
|
||||
export function createHandlers(client: OpenProjectClient) {
|
||||
/** Build the URL-path prefix for attachment content links (e.g. "/openproject"). */
|
||||
function getBasePath(baseUrl: string): string {
|
||||
try {
|
||||
const url = new URL(baseUrl);
|
||||
return url.pathname.replace(/\/+$/, ""); // e.g. "/openproject" or ""
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function createHandlers(client: OpenProjectClient, baseUrl: string) {
|
||||
const basePath = getBasePath(baseUrl);
|
||||
return {
|
||||
// --- Projects ---
|
||||
async list_projects(args: { offset?: number; pageSize?: number }) {
|
||||
@@ -323,6 +334,81 @@ export function createHandlers(client: OpenProjectClient) {
|
||||
.join("\n") || "No versions found.";
|
||||
},
|
||||
|
||||
// --- Attachments ---
|
||||
async list_attachments(args: { workPackageId: number }) {
|
||||
const result = await client.listAttachments(args.workPackageId);
|
||||
const elements = result._embedded?.elements ?? [];
|
||||
if (elements.length === 0) {
|
||||
return `No attachments found for work package #${args.workPackageId}.`;
|
||||
}
|
||||
return elements
|
||||
.map((a: any) => {
|
||||
const size = a.fileSize
|
||||
? `${(a.fileSize / 1024).toFixed(1)} KB`
|
||||
: "Unknown size";
|
||||
const author = a._links?.author?.title ?? "Unknown";
|
||||
const downloadUrl = a._links?.downloadLocation?.href ?? "N/A";
|
||||
return [
|
||||
`${a.id}: ${a.fileName}`,
|
||||
` Type: ${a.contentType ?? "Unknown"} | Size: ${size}`,
|
||||
` Author: ${author} | Created: ${a.createdAt?.split("T")[0] ?? "Unknown"}`,
|
||||
a.description?.raw ? ` Description: ${a.description.raw}` : null,
|
||||
` Download: ${downloadUrl}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
})
|
||||
.join("\n");
|
||||
},
|
||||
|
||||
async upload_attachment(args: {
|
||||
workPackageId: number;
|
||||
filePath: string;
|
||||
fileName?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const attachment = await client.uploadAttachment(
|
||||
args.workPackageId,
|
||||
args.filePath,
|
||||
args.fileName,
|
||||
args.description,
|
||||
);
|
||||
const size = attachment.fileSize
|
||||
? `${(attachment.fileSize / 1024).toFixed(1)} KB`
|
||||
: "Unknown size";
|
||||
const contentPath = `${basePath}/api/v3/attachments/${attachment.id}/content`;
|
||||
const isImage = (attachment.contentType ?? "").startsWith("image/");
|
||||
const embedLines = isImage
|
||||
? [
|
||||
`To embed in description or comment (pick one):`,
|
||||
` Macro (preferred): attachment:${attachment.fileName}`,
|
||||
` Markdown: `,
|
||||
]
|
||||
: [
|
||||
`To link in description or comment (pick one):`,
|
||||
` Macro (preferred): attachment:${attachment.fileName}`,
|
||||
` Markdown: [${attachment.fileName}](${contentPath})`,
|
||||
];
|
||||
return [
|
||||
`Uploaded attachment to work package #${args.workPackageId}:`,
|
||||
` ID: ${attachment.id}`,
|
||||
` File: ${attachment.fileName}`,
|
||||
` Type: ${attachment.contentType ?? "Unknown"} | Size: ${size}`,
|
||||
attachment.description?.raw
|
||||
? ` Description: ${attachment.description.raw}`
|
||||
: null,
|
||||
``,
|
||||
...embedLines,
|
||||
]
|
||||
.filter((line) => line !== null)
|
||||
.join("\n");
|
||||
},
|
||||
|
||||
async delete_attachment(args: { attachmentId: number }) {
|
||||
await client.deleteAttachment(args.attachmentId);
|
||||
return `Deleted attachment #${args.attachmentId}.`;
|
||||
},
|
||||
|
||||
// --- Activities ---
|
||||
async list_work_package_activities(args: { workPackageId: number }) {
|
||||
const result = await client.listActivities(args.workPackageId);
|
||||
|
||||
Reference in New Issue
Block a user