Skip to content

feat(tasks): implement attach subcommand with multipart file upload - #57

Open
nico-fioretti wants to merge 1 commit into
democratize-technology:mainfrom
nico-fioretti:feat/attach-file-upload
Open

feat(tasks): implement attach subcommand with multipart file upload#57
nico-fioretti wants to merge 1 commit into
democratize-technology:mainfrom
nico-fioretti:feat/attach-file-upload

Conversation

@nico-fioretti

Copy link
Copy Markdown

Summary

The attach subcommand of vikunja_tasks was already declared in the schema but handleAttach() only threw NOT_IMPLEMENTED. This PR ships the real implementation against the Vikunja REST endpoint PUT /tasks/{id}/attachments (multipart/form-data, field name files).

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 to basename(filePath) or attachment.bin. Any directory component injected via filename is stripped before upload.
  • handleAttach(args, authManager):
    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 responses; 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, rewire the dispatch from handleAttach() to handleAttach(args, authManager). The inline stub is removed.

Tests

  • tests/tools/tasks/attach.test.ts13 unit tests, all passing. Cover: missing id; zero / negative id; missing filePath + fileContent; empty decoded fileContent; filePath read with basename fallback; missing-path error; base64 decode with default filename; explicit filename; filePath precedence over fileContent; directory-component stripping in filename; trailing-slash stripping on apiUrl; HTTP non-OK status + body propagation; network error wrapping.
  • tests/tools/tasks.test.ts — existing integration test updated: assertion changed from File attachments are not supported in the current MCP context to attach 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 .txt and 6594-byte .xml → both uploaded, visible in the UI, byte-exact (mime auto-detected).
  • filePath pointing to a missing path → clear error: attach: cannot read filePath ...: ENOENT: no such file or directory.

Compatibility notes

  • The handler signature changes from sync-throw to async; observable behavior is identical (.rejects.toThrow(...) keeps working for the error path).
  • additionalProperties on the tool schema is unchanged — the three new fields are additive and optional. Existing callers that don't pass them are unaffected.
  • The MCP server now needs network reachability to the Vikunja host (it already did for every other tool).

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.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant