feat: add upload_attachment MCP tool
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s
Add support for uploading file attachments to Grist documents: - GristClient.upload_attachment() method using multipart/form-data - upload_attachment tool function with base64 decoding and MIME detection - Tool registration in server.py - Comprehensive unit tests (7 new tests) Returns attachment ID for linking to records via update_records. Bumps version to 1.3.0.
This commit is contained in:
@@ -116,6 +116,39 @@ class GristClient:
|
||||
"""Delete records by ID."""
|
||||
await self._request("POST", f"/tables/{table}/data/delete", json=record_ids)
|
||||
|
||||
async def upload_attachment(
|
||||
self,
|
||||
filename: str,
|
||||
content: bytes,
|
||||
content_type: str = "application/octet-stream",
|
||||
) -> dict:
|
||||
"""Upload a file attachment. Returns attachment metadata.
|
||||
|
||||
Args:
|
||||
filename: Name for the uploaded file.
|
||||
content: File content as bytes.
|
||||
content_type: MIME type of the file.
|
||||
|
||||
Returns:
|
||||
Dict with attachment_id, filename, and size_bytes.
|
||||
"""
|
||||
files = {"upload": (filename, content, content_type)}
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
response = await client.post(
|
||||
f"{self._base_url}/attachments",
|
||||
headers=self._headers,
|
||||
files=files,
|
||||
)
|
||||
response.raise_for_status()
|
||||
# Grist returns list of attachment IDs
|
||||
attachment_ids = response.json()
|
||||
return {
|
||||
"attachment_id": attachment_ids[0],
|
||||
"filename": filename,
|
||||
"size_bytes": len(content),
|
||||
}
|
||||
|
||||
# Schema operations
|
||||
|
||||
async def create_table(self, table_id: str, columns: list[dict]) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@ from grist_mcp.tools.read import sql_query as _sql_query
|
||||
from grist_mcp.tools.write import add_records as _add_records
|
||||
from grist_mcp.tools.write import update_records as _update_records
|
||||
from grist_mcp.tools.write import delete_records as _delete_records
|
||||
from grist_mcp.tools.write import upload_attachment as _upload_attachment
|
||||
from grist_mcp.tools.schema import create_table as _create_table
|
||||
from grist_mcp.tools.schema import add_column as _add_column
|
||||
from grist_mcp.tools.schema import modify_column as _modify_column
|
||||
@@ -218,6 +219,32 @@ def create_server(
|
||||
"required": ["document", "table", "column_id"],
|
||||
},
|
||||
),
|
||||
Tool(
|
||||
name="upload_attachment",
|
||||
description="Upload a file attachment to a Grist document. Returns attachment ID for linking to records via update_records.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"document": {
|
||||
"type": "string",
|
||||
"description": "Document name",
|
||||
},
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "Filename with extension (e.g., 'invoice.pdf')",
|
||||
},
|
||||
"content_base64": {
|
||||
"type": "string",
|
||||
"description": "File content as base64-encoded string",
|
||||
},
|
||||
"content_type": {
|
||||
"type": "string",
|
||||
"description": "MIME type (optional, auto-detected from filename)",
|
||||
},
|
||||
},
|
||||
"required": ["document", "filename", "content_base64"],
|
||||
},
|
||||
),
|
||||
Tool(
|
||||
name="get_proxy_documentation",
|
||||
description="Get complete documentation for the HTTP proxy API",
|
||||
@@ -324,6 +351,12 @@ def create_server(
|
||||
_current_agent, auth, arguments["document"], arguments["table"],
|
||||
arguments["column_id"],
|
||||
)
|
||||
elif name == "upload_attachment":
|
||||
result = await _upload_attachment(
|
||||
_current_agent, auth, arguments["document"],
|
||||
arguments["filename"], arguments["content_base64"],
|
||||
content_type=arguments.get("content_type"),
|
||||
)
|
||||
elif name == "get_proxy_documentation":
|
||||
result = await _get_proxy_documentation()
|
||||
elif name == "request_session_token":
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Write tools - create, update, delete records."""
|
||||
"""Write tools - create, update, delete records, upload attachments."""
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
|
||||
from grist_mcp.auth import Agent, Authenticator, Permission
|
||||
from grist_mcp.grist_client import GristClient
|
||||
@@ -59,3 +62,50 @@ async def delete_records(
|
||||
|
||||
await client.delete_records(table, record_ids)
|
||||
return {"deleted": True}
|
||||
|
||||
|
||||
async def upload_attachment(
|
||||
agent: Agent,
|
||||
auth: Authenticator,
|
||||
document: str,
|
||||
filename: str,
|
||||
content_base64: str,
|
||||
content_type: str | None = None,
|
||||
client: GristClient | None = None,
|
||||
) -> dict:
|
||||
"""Upload a file attachment to a document.
|
||||
|
||||
Args:
|
||||
agent: The authenticated agent.
|
||||
auth: Authenticator for permission checks.
|
||||
document: Document name.
|
||||
filename: Filename with extension.
|
||||
content_base64: File content as base64-encoded string.
|
||||
content_type: MIME type (auto-detected from filename if omitted).
|
||||
client: Optional GristClient instance.
|
||||
|
||||
Returns:
|
||||
Dict with attachment_id, filename, and size_bytes.
|
||||
|
||||
Raises:
|
||||
ValueError: If content_base64 is not valid base64.
|
||||
"""
|
||||
auth.authorize(agent, document, Permission.WRITE)
|
||||
|
||||
# Decode base64 content
|
||||
try:
|
||||
content = base64.b64decode(content_base64)
|
||||
except Exception:
|
||||
raise ValueError("Invalid base64 encoding")
|
||||
|
||||
# Auto-detect MIME type if not provided
|
||||
if content_type is None:
|
||||
content_type, _ = mimetypes.guess_type(filename)
|
||||
if content_type is None:
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
if client is None:
|
||||
doc = auth.get_document(document)
|
||||
client = GristClient(doc)
|
||||
|
||||
return await client.upload_attachment(filename, content, content_type)
|
||||
|
||||
Reference in New Issue
Block a user