feat(mcp): model-free document upload via signed URL (#1378)

* feat(mcp): model-free document upload via signed URL (#748)

Adds gnubok_create_document_upload + gnubok_complete_document_upload so
document bytes reach storage through a short-lived signed PUT URL and
never pass through the model context. Fixes silent base64 corruption on
real-size PDFs and the context blowup on batch uploads.

- pending/ staage keys with TTL cleanup; completion validates magic
  bytes + SHA-256, moves bytes to the WORM key and adopts the reserved
  UUID as document id, making retries and concurrent completions
  idempotent
- legacy gnubok_upload_document kept for clients without file access,
  description now points to the signed-URL pair; shared mime resolution
  and inbox-item creation extracted
- both new tools mapped in TOOL_SCOPE_MAP (transactions:write) and
  MCP_TOOL_CAPABILITY_MAP (ai) so the paywall and scope gates hold
- payload guard ceiling 58.5K to 59K after trimming the create tool's
  outputSchema to upload_id/upload_url/expires_at

Fixes #748

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): satisfy capability-map lock and phantom-column scanner

The exact-entries lock in capability-maps.test.ts now includes the
signed-URL pair as dispatch-only AI tools, and the inbox insert uses a
literal payload (explicit UUID instead of a conditional spread) so the
no-phantom-columns scanner can resolve every column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-03 18:41:05 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 00d4c8a49e
commit 5d7952a01e
9 changed files with 946 additions and 85 deletions
@@ -15,19 +15,27 @@ import {
/**
* MCP tools that invoke a paid capability directly (no stage→commit round-trip),
* so they are gated at DISPATCH only and have no commit-time (operation-map)
* counterpart. gnubok_upload_document runs Bedrock OCR inline via
* extractInvoiceFields: it never stages a pending_operation.
* counterpart. The document upload tools run Bedrock OCR inline via
* extractInvoiceFields: they never stage a pending_operation.
*/
const DISPATCH_ONLY_MCP_TOOLS = new Set<string>(['gnubok_upload_document'])
const DISPATCH_ONLY_MCP_TOOLS = new Set<string>([
'gnubok_upload_document',
'gnubok_create_document_upload',
'gnubok_complete_document_upload',
])
describe('MCP_TOOL_CAPABILITY_MAP', () => {
it('gates exactly the paid MCP tools (3 external-service staging tools + the AI OCR tool)', () => {
it('gates exactly the paid MCP tools (3 external-service staging tools + the AI OCR tools)', () => {
expect(MCP_TOOL_CAPABILITY_MAP).toEqual({
gnubok_send_invoice: CAPABILITY.email_send,
gnubok_vat_declaration_submit: CAPABILITY.skatteverket,
gnubok_agi_submit: CAPABILITY.skatteverket,
// Dispatch-only AI tool: inline Bedrock OCR, no staged operation.
// Dispatch-only AI tools: inline Bedrock OCR, no staged operation. The
// signed-URL pair is gated at create AND complete so a free-tier key can
// neither reserve nor finalize a paid extraction.
gnubok_upload_document: CAPABILITY.ai,
gnubok_create_document_upload: CAPABILITY.ai,
gnubok_complete_document_upload: CAPABILITY.ai,
})
})
+7 -5
View File
@@ -65,17 +65,19 @@ export const PAID_CAPABILITIES: readonly CapabilityKey[] = [
* read/local SKV tools (generate_agi, vat_declaration_validate/status, agi_status)
* stay free: the §4 carve-out forbids blocking a statutory filing obligation.
*
* gnubok_upload_document invokes AI (Bedrock document OCR via
* extractInvoiceFields), so it is gated on CAPABILITY.ai: the same paywall the
* HTTP inbox upload/attach/retry paths enforce. Without this entry a free-tier
* API key (incl. the claude.ai connector's minted gnubok_sk_ key) could trigger
* paid AI extraction. bank_sync has no MCP tool (bank sync is cron/HTTP only).
* The document upload tools invoke AI (Bedrock document OCR via
* extractInvoiceFields), so they are gated on CAPABILITY.ai: the same paywall
* the HTTP inbox upload/attach/retry paths enforce. Without these entries a
* free-tier API key could trigger paid AI extraction. bank_sync has no MCP
* tool (bank sync is cron/HTTP only).
*/
export const MCP_TOOL_CAPABILITY_MAP: Readonly<Partial<Record<string, CapabilityKey>>> = {
gnubok_send_invoice: CAPABILITY.email_send,
gnubok_vat_declaration_submit: CAPABILITY.skatteverket,
gnubok_agi_submit: CAPABILITY.skatteverket,
// AI document OCR (Bedrock): the inbox's paid extraction, reachable via MCP.
gnubok_create_document_upload: CAPABILITY.ai,
gnubok_complete_document_upload: CAPABILITY.ai,
gnubok_upload_document: CAPABILITY.ai,
} as const