feat(tasks): implement attach subcommand with multipart file upload - #57
Open
nico-fioretti wants to merge 1 commit into
Open
feat(tasks): implement attach subcommand with multipart file upload#57nico-fioretti wants to merge 1 commit into
nico-fioretti wants to merge 1 commit into
Conversation
The attach subcommand of vikunja_tasks was declared in the schema but
handleAttach() threw NOT_IMPLEMENTED. This change implements the real
upload against Vikunja PUT /tasks/{id}/attachments (multipart/form-data,
field name files), using Node 22 native fetch/FormData/Blob, so no new
dependencies.
New module src/tools/tasks/attach.ts exports:
attachSchemaFields: 3 optional zod fields spread into the tool schema
- filePath: string, absolute path readable by the MCP server process
- fileContent: string, base64-encoded contents (for clients that run
on a different host than the server)
- filename: string, optional; defaults to basename(filePath) or
attachment.bin. Any directory component injected via filename is
stripped before upload.
handleAttach(args, authManager): async handler that
1. Validates id is a positive number.
2. Reads bytes from filePath (fs.readFileSync) OR decodes base64
fileContent. filePath wins when both are provided.
3. Rejects an empty decoded buffer with an explanatory error.
4. Builds FormData and PUTs to {apiUrl}/tasks/{id}/attachments
with Authorization: Bearer of the active session.
5. Propagates the HTTP status and body on non-OK response, wraps
network errors with an attach: prefix.
6. Returns an MCP text response with a JSON summary
(taskId, filename, bytes, source) and the raw backend response.
src/tools/tasks/index.ts: import the module, spread attachSchemaFields
into the zod schema, and rewire the dispatch from handleAttach() to
handleAttach(args, authManager). The inline NOT_IMPLEMENTED stub is
removed.
Tests:
- tests/tools/tasks/attach.test.ts: 13 unit tests covering missing
id, zero/negative id, missing filePath and fileContent, empty
decoded fileContent, filePath read + basename default, missing
filePath, base64 decode + default filename, explicit filename,
filePath precedence, directory-component stripping, trailing
slashes in apiUrl, HTTP non-OK propagation, network error wrap.
- tests/tools/tasks.test.ts: existing integration test updated from
asserting File attachments are not supported to the new validation
message attach requires filePath or fileContent.
End-to-end verified on a live Vikunja instance via the MCP wrapper:
- fileContent base64 (19 bytes txt and 6594 bytes XML) → uploaded
and visible in the UI, byte-exact.
- filePath pointing to a missing path → error message
attach: cannot read filePath ...: ENOENT: no such file or directory.
Lint and typecheck clean.
This was referenced Jul 16, 2026
netadvanced
referenced
this pull request
in netadvanced/vikunja-mcp-ng
Jul 17, 2026
Resolve trivial additive import conflict in tasks/index.ts: keep both the attach imports (#57) and the setTaskBucket import (#17 Kanban). Subcommand enum, schema fields, and case routing for both auto-merged cleanly. Claude-Session: https://claude.ai/code/session_016D2e7m4RD4YLiVN9Dvp2Vf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
attachsubcommand ofvikunja_taskswas already declared in the schema buthandleAttach()only threwNOT_IMPLEMENTED. This PR ships the real implementation against the Vikunja REST endpointPUT /tasks/{id}/attachments(multipart/form-data, field namefiles).Uses Node 22 native
fetch/FormData/Blob, so no new dependencies.What's in it
New module
src/tools/tasks/attach.ts:attachSchemaFields— three optional zod fields spread into the tool schema:filePath: absolute path readable by the MCP server process.fileContent: base64-encoded contents (useful when the MCP client runs on a different host than the server).filename: optional; defaults tobasename(filePath)orattachment.bin. Any directory component injected viafilenameis stripped before upload.handleAttach(args, authManager):idis a positive number.filePath(fs.readFileSync) OR decodes base64fileContent.filePathwins when both are provided.FormDataandPUTs to{apiUrl}/tasks/{id}/attachmentswithAuthorization: Bearerof the active session.attach:prefix.taskId,filename,bytes,source) and the raw backend response.src/tools/tasks/index.ts: import the module, spreadattachSchemaFieldsinto the zod schema, rewire the dispatch fromhandleAttach()tohandleAttach(args, authManager). The inline stub is removed.Tests
tests/tools/tasks/attach.test.ts— 13 unit tests, all passing. Cover: missingid; zero / negativeid; missingfilePath+fileContent; empty decodedfileContent;filePathread with basename fallback; missing-path error; base64 decode with default filename; explicit filename;filePathprecedence overfileContent; directory-component stripping infilename; trailing-slash stripping onapiUrl; HTTP non-OK status + body propagation; network error wrapping.tests/tools/tasks.test.ts— existing integration test updated: assertion changed fromFile attachments are not supported in the current MCP contexttoattach requires filePath or fileContent(same call shape).Lint + typecheck clean. No regressions vs. upstream main baseline (touch points isolated to the attach dispatch).
Verified end-to-end
Against a live Vikunja instance via the MCP wrapper:
fileContent(base64) for 19-byte.txtand 6594-byte.xml→ both uploaded, visible in the UI, byte-exact (mime auto-detected).filePathpointing to a missing path → clear error:attach: cannot read filePath ...: ENOENT: no such file or directory.Compatibility notes
async; observable behavior is identical (.rejects.toThrow(...)keeps working for the error path).additionalPropertieson the tool schema is unchanged — the three new fields are additive andoptional. Existing callers that don't pass them are unaffected.