upload_attachment
Upload a file as a note attachment linked to a party, opportunity, or project by providing base64-encoded data, filename, and MIME type.
Instructions
Upload a file as a new note attachment, linked to a party, opportunity, or project. Provide the file as base64-encoded dataBase64 along with filename and contentType (MIME). Also provide exactly one of partyId / opportunityId / projectId to anchor the note. Optionally pass content to set the note body (defaults to '[attachment]'). Two-step orchestration server-side: bytes upload → token → note creation. Adding to an existing entry is not supported.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| filename | Yes | Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes — a typo in either is accepted and the file is stored as labelled. | |
| contentType | Yes | MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes. | |
| dataBase64 | Yes | File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary. | |
| content | No | Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted. | |
| partyId | No | Link the new note to a party (mutually exclusive with opportunityId / projectId). | |
| opportunityId | No | ||
| projectId | No |
Implementation Reference
- src/tools/attachments.ts:137-179 (handler)The main handler function for upload_attachment. Orchestrates a two-step process: (1) validates inputs (exactly one entity, base64 validity, size limit), uploads bytes via capsulePostBinary to /attachments/upload, (2) creates a note entry with the returned token via capsulePost to /entries, linked to the specified party/opportunity/project.
export async function uploadAttachment(input: z.infer<typeof uploadAttachmentSchema>) { const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean); if (linked.length !== 1) { throw new Error( "upload_attachment: provide exactly one of partyId, opportunityId, or projectId", ); } if (!isValidBase64(input.dataBase64)) { throw new Error( "upload_attachment: dataBase64 is not valid base64 — Node's tolerant decoder would silently produce corrupt bytes. Verify the encoding (RFC 4648, padded with '=' to a multiple of 4 chars).", ); } const decodedBytes = decodedBase64Size(input.dataBase64); if (decodedBytes > HARD_MAX_SIZE_BYTES) { throw new Error( `upload_attachment: decoded file is ${decodedBytes} bytes, exceeding the ${HARD_MAX_SIZE_BYTES} byte attachment limit. Split or shrink the file before uploading.`, ); } // Step 1: upload bytes, receive token. const buffer = Buffer.from(input.dataBase64, "base64"); const uploaded = await capsulePostBinary<{ upload: { token: string } }>( "/attachments/upload", buffer, input.contentType, input.filename, ); const token = uploaded.upload.token; // Step 2: create a note that references the upload token. Capsule // returns the entry with the attachment metadata populated (id, // filename, contentType, size, etc.) once it's been wired in. const entryBody: Record<string, unknown> = { type: "note", content: input.content ?? "[attachment]", attachments: [{ token }], }; if (input.partyId) entryBody["party"] = { id: input.partyId }; if (input.opportunityId) entryBody["opportunity"] = { id: input.opportunityId }; if (input.projectId) entryBody["kase"] = { id: input.projectId }; return capsulePost<{ entry: unknown }>("/entries", { entry: entryBody }); } - src/tools/attachments.ts:82-116 (schema)Zod schema defining the input parameters for upload_attachment: filename (string), contentType (string), dataBase64 (string, max 25MB base64), content (optional string), and exactly one of partyId/opportunityId/projectId (optional ints).
export const uploadAttachmentSchema = z.object({ filename: z .string() .min(1) .describe( "Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes — a typo in either is accepted and the file is stored as labelled.", ), contentType: z .string() .min(1) .describe( "MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes.", ), dataBase64: z .string() .min(1) .max(HARD_MAX_BASE64_CHARS) .describe( "File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary.", ), content: z .string() .optional() .describe( "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted.", ), partyId: z .number() .int() .positive() .optional() .describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."), opportunityId: z.number().int().positive().optional(), projectId: z.number().int().positive().optional(), }); - src/server.ts:820-826 (registration)Registration of the 'upload_attachment' tool in the MCP server via registerTool helper, binding the name, description, schema (uploadAttachmentSchema) and handler (uploadAttachment).
registerTool( server, "upload_attachment", "Upload a file as a new note attachment, linked to a party, opportunity, or project. Provide the file as base64-encoded `dataBase64` along with `filename` and `contentType` (MIME). Also provide exactly one of partyId / opportunityId / projectId to anchor the note. Optionally pass `content` to set the note body (defaults to '[attachment]'). Two-step orchestration server-side: bytes upload → token → note creation. Adding to an existing entry is not supported.", uploadAttachmentSchema, uploadAttachment, ); - src/tools/attachments.ts:123-135 (helper)Helper functions isValidBase64 (validates RFC 4648 base64) and decodedBase64Size (computes decoded byte size from base64 string length), used by the handler for input validation before upload.
function isValidBase64(s: string): boolean { // Strip optional padding then check the alphabet. Length must be a // multiple of 4 once padding is restored. if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false; const len = s.length; if (len % 4 !== 0) return false; return true; } function decodedBase64Size(s: string): number { const padding = s.endsWith("==") ? 2 : s.endsWith("=") ? 1 : 0; return (s.length / 4) * 3 - padding; } - src/server.ts:184-189 (registration)Import of uploadAttachmentSchema and uploadAttachment from src/tools/attachments.ts into the server module where the tool is registered.
import { getAttachmentSchema, getAttachment, uploadAttachmentSchema, uploadAttachment, } from "./tools/attachments.js";