From 176e1f1040ce5bd3325e4d34a49a7ab5584afca7 Mon Sep 17 00:00:00 2001 From: b3nw Date: Mon, 25 May 2026 02:52:49 +0000 Subject: [PATCH] feat: Add embedMarkdown to upload response and document attachment format The upload response now includes an embedMarkdown field with the correct Outline attachment syntax ([name size](/api/attachments.redirect?id=...)) so callers can insert it directly into documents for native card rendering. IMPLEMENTATION.md updated with storage backend details, auth header, and embedding format documentation. --- IMPLEMENTATION.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++ server.py | 17 +++++++++++---- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 0159820..c78ad56 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -314,6 +314,61 @@ Delete attachment. Params: id (required) ### attachments.redirect Get attachment URL. Params: id (required) +### Custom HTTP Gateway Upload +Instead of manually calling `attachments.create` and executing the subsequent storage upload, you can use the MCP server's integrated HTTP upload proxy. + +This endpoint receives binary content over HTTP, registers the attachment with Outline, and uploads it to the configured storage backend (S3 or local file storage). + +- **Endpoint**: `POST /upload` +- **Query Parameters**: + - `documentId` (required): The UUID of the destination Outline document. + - `name` (required): The filename of the attachment (e.g., `image.png`, `document.pdf`). +- **Headers**: + - `Authorization` (required): `Bearer ` + - `Content-Length` (required): The exact size of the file in bytes. + - `Content-Type` (optional): The MIME type of the file (e.g., `application/pdf`, `image/png`). If omitted, it will be auto-detected by filename extension. +- **Request Body**: The raw file binary content. +- **Storage backends**: Auto-detected from the Outline API response. + - **S3**: PUT to pre-signed URL. + - **Local file storage**: Multipart POST to `/api/files.create` with form fields. +- **Response**: + ```json + { + "ok": true, + "data": { + "id": "attachment-uuid", + "name": "filename.ext", + "size": 12345, + "contentType": "mime/type", + "url": "/api/attachments.redirect?id=attachment-uuid", + "embedMarkdown": "[filename.ext 12345](/api/attachments.redirect?id=attachment-uuid)" + } + } + ``` + +### Embedding Attachments in Documents + +Outline uses a special markdown syntax to render attachment cards (with file icon, name, and download size). The format is: + +``` +[filename.ext filesize](/api/attachments.redirect?id=attachment-uuid) +``` + +Where: +- `filename.ext` is the attachment filename +- `filesize` is the file size in bytes (space-separated, not a label) +- The URL is the **relative** `/api/attachments.redirect?id=...` path + +The `embedMarkdown` field in the upload response provides this string ready to insert into a document body via `documents.update`. + +**Example**: Upload a file and embed it in a document: +``` +1. POST /upload?documentId=doc-uuid&name=report.pdf → get embedMarkdown +2. outline_api_call("documents.update", {"id": "doc-uuid", "text": "...\n\n" + embedMarkdown, "append": true}) +``` + +**Important**: Do NOT use absolute URLs (e.g., `https://docs.example.com/api/attachments.redirect?id=...`) — Outline will not render the attachment card for absolute URLs. Always use the relative path. + --- ## Auth diff --git a/server.py b/server.py index 7c932d1..f041e75 100644 --- a/server.py +++ b/server.py @@ -684,16 +684,25 @@ async def upload_endpoint(request: Request) -> JSONResponse: logger.info("Upload to storage completed successfully") - # 4. Return success and attachment metadata + # 4. Return success with attachment metadata and embedding markdown + att_name = attachment_data.get("name", filename) + att_size = attachment_data.get("size", size) + att_url = attachment_data.get("url", "") + + # Outline renders attachment cards when the link text is "filename size" + # and the href is the relative /api/attachments.redirect path. + embed_markdown = f"[{att_name} {att_size}]({att_url})" + return JSONResponse( { "ok": True, "data": { "id": attachment_data.get("id"), - "name": attachment_data.get("name"), - "size": attachment_data.get("size"), + "name": att_name, + "size": att_size, "contentType": attachment_data.get("contentType"), - "url": attachment_data.get("url"), + "url": att_url, + "embedMarkdown": embed_markdown, }, } )