feat: Add upload_file_attachment MCP tool for agent file uploads
Build and Push Outline MCP Docker Image / build (push) Successful in 8s

Adds a new MCP tool that accepts base64-encoded file content and handles
the full attachment workflow (register → upload → return embedMarkdown).
This makes file uploads accessible to MCP clients like Hermes that can
only interact via registered tools.

Also updates the API reference resource with embedding format docs.
This commit is contained in:
2026-05-25 03:20:57 +00:00
parent 176e1f1040
commit b27b2d207b
+133 -11
View File
@@ -1,5 +1,6 @@
import os
import json
import base64
import logging
import mimetypes
import contextlib
@@ -227,6 +228,120 @@ async def outline_api_call(method: str, params: Any = None) -> str:
return json.dumps(result, indent=2)
@mcp.tool()
async def upload_file_attachment(
document_id: str,
filename: str,
content_base64: str,
content_type: str = "",
) -> str:
"""Upload a file attachment to an Outline document.
Accepts base64-encoded file content, uploads it to Outline's storage,
and returns metadata including ready-to-embed markdown.
To embed the attachment in the document body, use the returned
embedMarkdown value with documents.update (append: true).
Args:
document_id: The UUID of the destination document
filename: The filename including extension (e.g., 'report.pdf', 'screenshot.png')
content_base64: The file content encoded as a base64 string
content_type: Optional MIME type (e.g., 'application/pdf'). Auto-detected from filename if omitted.
Returns:
JSON with attachment id, url, size, and embedMarkdown for inserting into a document
"""
try:
file_bytes = base64.b64decode(content_base64)
except Exception as e:
return json.dumps({"ok": False, "error": f"Invalid base64 content: {e}"})
size = len(file_bytes)
MAX_UPLOAD_SIZE = int(os.getenv("MAX_UPLOAD_SIZE", "104857600"))
if size > MAX_UPLOAD_SIZE:
return json.dumps({"ok": False, "error": f"File too large ({size} bytes, limit {MAX_UPLOAD_SIZE})"})
if size == 0:
return json.dumps({"ok": False, "error": "File content is empty"})
if not content_type:
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
create_response = await outline_client.call("attachments.create", {
"name": filename,
"documentId": document_id,
"size": size,
"contentType": content_type,
})
if not create_response.get("ok"):
return json.dumps({
"ok": False,
"error": f"attachments.create failed: {create_response.get('error')}",
})
response_data = create_response.get("data", {})
upload_url = response_data.get("uploadUrl", "")
form_fields = response_data.get("form", {})
attachment_data = response_data.get("attachment", {})
if not upload_url:
return json.dumps({"ok": False, "error": "Outline did not return an uploadUrl"})
if upload_url.startswith("/"):
upload_url = f"{OUTLINE_API_URL}{upload_url}"
try:
if form_fields:
files = {"file": (filename, file_bytes, content_type)}
async with httpx.AsyncClient() as client:
resp = await client.post(
upload_url,
data=form_fields,
files=files,
headers={"Authorization": f"Bearer {OUTLINE_API_TOKEN}"},
timeout=600.0,
)
resp.raise_for_status()
else:
async with httpx.AsyncClient() as client:
resp = await client.put(
upload_url,
content=file_bytes,
headers={
"Content-Type": content_type,
"Content-Length": str(size),
},
timeout=600.0,
)
resp.raise_for_status()
except Exception as e:
att_id = attachment_data.get("id")
if att_id:
try:
await outline_client.call("attachments.delete", {"id": att_id})
except Exception:
pass
return json.dumps({"ok": False, "error": f"Storage upload failed: {e}"})
att_name = attachment_data.get("name", filename)
att_size = attachment_data.get("size", size)
att_url = attachment_data.get("url", "")
embed_markdown = f"[{att_name} {att_size}]({att_url})"
return json.dumps({
"ok": True,
"data": {
"id": attachment_data.get("id"),
"name": att_name,
"size": att_size,
"contentType": attachment_data.get("contentType", content_type),
"url": att_url,
"embedMarkdown": embed_markdown,
},
}, indent=2)
# --- API Reference Resource ---
API_REFERENCE = """# Outline API Reference
@@ -487,6 +602,7 @@ Delete file operation. Params: id (required)
### attachments.create
Create attachment upload URL. Params: name (required), documentId (required), size (required), contentType (required)
Note: This only registers metadata. Use the upload_file_attachment tool instead for a complete upload.
### attachments.delete
Delete attachment. Params: id (required)
@@ -494,17 +610,23 @@ Delete attachment. Params: id (required)
### attachments.redirect
Get attachment URL. Params: id (required)
### Custom HTTP Gateway Upload
Rather than manually performing the multi-step attachments.create + PUT process, you can stream files directly through the MCP server's built-in HTTP streaming gateway:
- **Endpoint**: `POST /upload`
- **Query Parameters**:
- `documentId` (required): The UUID of the destination Outline document.
- `name` (required): The filename of the attachment.
- **Headers**:
- `Content-Length` (required): Total size of the file in bytes.
- `Content-Type` (optional): The MIME type of the file (auto-detected if omitted).
- **Body**: Raw binary stream of the file content.
- **Response**: JSON containing the created attachment's details, including its public access `url` on success.
### Uploading Files (Preferred Method)
Use the `upload_file_attachment` MCP tool to upload files. It handles the full workflow:
1. Registers the attachment with Outline
2. Uploads file content to storage (S3 or local)
3. Returns embedMarkdown ready to insert into a document
Example workflow to attach a file to a document:
1. Call upload_file_attachment(document_id, filename, content_base64)
2. Get embedMarkdown from the response
3. Call documents.update with append=true and text=embedMarkdown
### Embedding Attachments in Document Markdown
Outline renders attachment cards using this specific markdown syntax:
[filename.ext filesize](/api/attachments.redirect?id=UUID)
Where filesize is the size in bytes. The upload_file_attachment tool returns this
as the embedMarkdown field. Always use the relative /api/attachments.redirect path,
not absolute URLs, for Outline to render the attachment card UI.
---