feat(mcp): support bundled text and attachment ingest

Allow one multipart request to stage body text and repeated files as a Bundle with bundle-level metadata. Preserve Bundle roles through preview, job launch, prompts, cleanup, and text-note ingestion.\n\nValidated with the full unittest suite (188 tests).
This commit is contained in:
Codex
2026-07-16 14:07:58 +00:00
parent 4c5454da90
commit a2b4bc393b
5 changed files with 443 additions and 18 deletions
+8 -3
View File
@@ -475,20 +475,25 @@ This adds write-capable MCP tools to the same `wiki-file-assets` server and bear
The managed service config sets the OpenCode model server-side. By default ingest uses `OBSIDIAN_MCP_INGEST_OPENCODE_MODEL=openai/gpt-5.5`, `OBSIDIAN_MCP_INGEST_OPENCODE_VARIANT=high`, and an `OBSIDIAN_MCP_INGEST_JOB_TIMEOUT_SECONDS=1800` deadline. It also writes `OBSIDIAN_MCP_PUBLIC_BASE_URL` from the detected remote address so ticket responses can include a complete upload URL; set it manually when the service is behind a tunnel, reverse proxy, or custom DNS name. If the upload endpoint is exposed through a different public URL than `/mcp`, set `OBSIDIAN_MCP_INGEST_PUBLIC_UPLOAD_URL` explicitly and it will override the derived upload URL in capabilities, upload instructions, tickets, and `show-config`. Change these values in `.runtime/obsidian-wiki-mcp.env` on the server if needed; remote MCP clients do not choose the model per call.
For short text thoughts, notes, findings, or pasted context, call `ingest_text_note(text, title?, project?, doc_type_hint?, note?, start_job?)`. It uses `doc_type_hint="thought"` by default and returns the created upload plus the queued job when `start_job=true`.
For short text thoughts, notes, findings, or pasted context, call `ingest_text_note(text, title?, project?, doc_type_hint?, note?, start_job?)`. It uses `doc_type_hint="thought"` by default and returns a one-item Bundle, the created upload, and the queued job when `start_job=true`.
For binary files and larger source documents, remote agents should use upload tickets by default: call `create_ingest_upload_ticket(start_job=false)`, upload with the returned `upload_url` and `X-Obsidian-Wiki-Upload-Token`, call `preview_upload_metadata(upload_ids)`, then call `start_ingest_job(upload_ids)` when the preview looks correct:
For binary files and larger source documents, remote agents should use upload tickets by default: call `create_ingest_upload_ticket(start_job=false)`, upload with the returned `upload_url` and `X-Obsidian-Wiki-Upload-Token`, call `preview_upload_metadata(upload_ids)`, then call `start_ingest_job(upload_ids)` when the preview looks correct. The same multipart POST can carry `body_text` plus repeated `files`; the body becomes the Bundle's primary Markdown source and the files become attachments:
```bash
curl -H "X-Obsidian-Wiki-Upload-Token: <upload-token>" \
-F "body_text=Mail or meeting body text" \
-F "body_title=Customer quality thread" \
-F "files=@drawing.pdf" \
-F "files=@report.xlsx" \
-F "source_type=email_thread" \
-F "label=Customer quality thread" \
-F "project=bjd" \
-F "doc_type_hint=drawing" \
-F "start_job=false" \
<upload_url>
```
The endpoint accepts repeated `files` fields plus optional `project`, `doc_type_hint`, `note`, and `start_job` fields. Multipart files are parsed as streams into spooled storage instead of buffering the whole request in memory, and `OBSIDIAN_MCP_INGEST_MAX_FILE_BYTES` is enforced while each file part is being parsed. `OBSIDIAN_MCP_INGEST_MAX_FILES_PER_JOB` limits the count. With `OBSIDIAN_MCP_INGEST_MAX_REQUEST_BYTES=0`, the route derives its total request-body cap from per-file size times maximum file count plus multipart overhead; a positive value adds a stricter total cap. The stricter upload-ticket limits still apply. Ticket-bound `project`, `doc_type_hint`, and `note` values cannot be replaced by form data, and a ticket created with `start_job=false` cannot be escalated to start a job by sending `start_job=true` in the form. Oversized requests return HTTP `413`. The endpoint still accepts the same bearer token as `/mcp` for local/admin compatibility, but remote agents should prefer single-use upload tickets. Upload tickets are consumed by authorized `POST` attempts, including malformed multipart or oversized uploads, but not by accidental `GET`/wrong-method requests. When an upload-ticket POST fails after the ticket is consumed, the JSON error includes `ticket_consumed: true` and `retry_action: "create_new_upload_ticket"`, so the remote agent should request a fresh ticket before retrying. The upload route enforces configured `--allowed-host` and `--allowed-origin` checks. Base64 file upload is intentionally not exposed as an MCP tool because large JSON arguments are fragile in many agent tool-call implementations. Duplicate detection is exact sha256 matching only; it does not replace human review for near-duplicate or renamed source files.
The endpoint accepts optional `body_text`, `body_title`, `bundle_id`, `source_type`, `label`, `project`, `doc_type_hint`, `note`, and `start_job` fields plus repeated `files` fields. At least a body or one file is required. The body counts toward `OBSIDIAN_MCP_INGEST_MAX_FILES_PER_JOB`, and the server generates `bundle_id` when it is omitted. Multipart files are parsed as streams into spooled storage instead of buffering the whole request in memory, and `OBSIDIAN_MCP_INGEST_MAX_FILE_BYTES` is enforced while each file part is being parsed. With `OBSIDIAN_MCP_INGEST_MAX_REQUEST_BYTES=0`, the route derives its total request-body cap from per-file size times maximum item count plus multipart overhead; a positive value adds a stricter total cap. The stricter upload-ticket limits still apply. Ticket-bound `bundle_id`, `source_type`, `label`, `project`, `doc_type_hint`, and `note` values cannot be replaced by form data, and a ticket created with `start_job=false` cannot be escalated to start a job by sending `start_job=true` in the form. Oversized requests return HTTP `413`. The endpoint still accepts the same bearer token as `/mcp` for local/admin compatibility, but remote agents should prefer single-use upload tickets. Upload tickets are consumed by authorized `POST` attempts, including malformed multipart or oversized uploads, but not by accidental `GET`/wrong-method requests. When an upload-ticket POST fails after the ticket is consumed, the JSON error includes `ticket_consumed: true` and `retry_action: "create_new_upload_ticket"`, so the remote agent should request a fresh ticket before retrying. The upload route enforces configured `--allowed-host` and `--allowed-origin` checks. Base64 file upload is intentionally not exposed as an MCP tool because large JSON arguments are fragile in many agent tool-call implementations. Duplicate detection is exact sha256 matching only; it does not replace human review for near-duplicate or renamed source files.
Run `check_ingest_opencode_readiness` before the first remote upload. Use `run_smoke_test: true` when you want OpenCode itself to prove it can load the project `wiki-ingest` skill; otherwise the static preflight is also run automatically before every `start_ingest_job`.