10 KiB
OpenAPI-Driven MCP Support Design
Context
Memos previously had an MCP server, but it was removed in commit 2a4638b3 and should not be used as the baseline for this work. GitHub issue #6022 reported that some old MCP tools returned a bare JSON array in result.structuredContent, which strict MCP clients reject because structuredContent must be an object.
The new MCP support should start from zero and use the generated OpenAPI document at proto/gen/openapi.yaml as the source of truth. The OpenAPI file already describes the public REST gateway operations generated from protobuf definitions, including operation IDs, descriptions, parameters, request schemas, and response schemas.
Goals
- Add MCP support back through a new implementation that is mechanically tied to
proto/gen/openapi.yaml. - Expose a standard MCP Streamable HTTP endpoint at the
/mcppath. - Register tools only; do not add MCP resources or prompts in the first version.
- Expose a curated memo-focused toolset derived from OpenAPI operations, not every API endpoint.
- Execute MCP tool calls through the existing API contract instead of duplicating store or service logic.
- Return object-shaped
structuredContentfor every tool result. - Keep authentication and authorization behavior aligned with the existing API.
Non-Goals
- Reviving or adapting the removed MCP package design.
- Adding custom MCP route aliases, readonly endpoints, or toolset filtering headers.
- Exposing every operation in
proto/gen/openapi.yamlas an MCP tool. - Adding MCP tools for admin settings, users, identity providers, webhooks, personal access tokens, authentication, share-link management, AI transcription, or bulk deletion.
- Adding
list_tagsorsearch_memosunless matching proto/API operations are added and OpenAPI is regenerated. - Hand-editing generated OpenAPI or generated protobuf outputs.
Recommended Approach
Build a new server/router/mcp package that parses proto/gen/openapi.yaml at startup, selects a curated allowlist of operation IDs, and converts those selected OpenAPI operations into MCP tools. Tool calls map MCP arguments into the selected operation's path parameters, query parameters, and JSON body, then execute the corresponding /api/v1/... HTTP request through the existing Echo/gRPC-Gateway route in-process.
This approach keeps OpenAPI as the authoritative contract while avoiding the usability and safety problems of exposing a large API-mirrored tool surface.
Architecture
MCP Service
Create a new MCP service package under server/router/mcp. server.NewServer creates the existing APIV1Service, registers the normal file, RSS, and API routes, then registers a new MCP service against the same Echo server.
The service exposes one MCP endpoint path:
/mcp
The implementation must support standard Streamable HTTP client messages on POST /mcp. It may also support GET /mcp and DELETE /mcp if the chosen MCP transport implementation requires those methods for standards-compliant streaming or session cleanup.
The first version advertises only the MCP tools capability. It does not advertise prompts or resources.
OpenAPI Operation Registry
At startup, the MCP service loads proto/gen/openapi.yaml and builds an operation registry keyed by operationId. Each parsed operation stores:
- operation ID
- HTTP method
- OpenAPI route template
- description
- path parameters
- query parameters
- JSON request body schema
- HTTP 200 JSON response schema
The parser should fail fast during service construction if any curated operation ID is missing or cannot be converted into a valid MCP tool schema.
Tool Names
Tool names are derived from OpenAPI operationId values and normalized for MCP clients. The exact naming convention should be deterministic and tested. A practical convention is lower snake case without the Service suffix in the subject:
MemoService_ListMemos -> memo_list_memos
AttachmentService_GetAttachment -> attachment_get_attachment
The OpenAPI operationId remains stored in tool metadata so tests and future diagnostics can prove which OpenAPI operation produced each MCP tool.
Tool Schemas
Each MCP tool input schema is an object built from:
- OpenAPI path parameters
- OpenAPI query parameters
- JSON request body fields, when present
Required path parameters stay required. Required request bodies stay required. Optional query parameters stay optional. The schema should preserve OpenAPI descriptions and primitive types where possible.
Each MCP tool output schema is the OpenAPI HTTP 200 JSON response schema. For empty 200 responses with no JSON schema, the MCP output schema is:
{ "type": "object", "properties": { "ok": { "type": "boolean" } } }
Tool Scope
The first version exposes these curated OpenAPI operations:
MemoService_ListMemosMemoService_CreateMemoMemoService_GetMemoMemoService_UpdateMemoMemoService_DeleteMemoMemoService_ListMemoCommentsMemoService_CreateMemoCommentMemoService_ListMemoAttachmentsMemoService_SetMemoAttachmentsMemoService_ListMemoReactionsMemoService_UpsertMemoReactionMemoService_DeleteMemoReactionMemoService_ListMemoRelationsMemoService_SetMemoRelationsAttachmentService_ListAttachmentsAttachmentService_GetAttachmentAttachmentService_DeleteAttachment
Excluded in the first version:
- auth sign-in, sign-out, and refresh
- user management
- personal access token management
- identity provider management
- webhooks
- instance settings
- share-link management
- AI transcription
- bulk delete operations
- any operation not present in generated OpenAPI
Data Flow
Startup
server.NewServercreates the existingAPIV1Service.- The new
MCPServiceloadsproto/gen/openapi.yaml. - The OpenAPI parser builds the operation registry.
- The curated operation allowlist selects supported operations.
- Each selected operation is registered as an MCP tool with input schema, output schema, description, annotations, and operation metadata.
Tool Call
- The client sends a standard MCP
tools/callrequest to/mcp. - The MCP server validates the tool name and arguments against the OpenAPI-derived input schema.
- The adapter substitutes path parameters into the OpenAPI route template.
- The adapter encodes query parameters into the query string.
- The adapter marshals request body arguments as JSON when the operation has a request body.
- The adapter forwards the caller's
Authorizationheader and executes the matching/api/v1/...request through the existing Echo handler in-process. - The API response JSON is decoded into
map[string]any. - The MCP result returns a compact JSON text fallback plus object-shaped
structuredContent.
Result Shape
Every MCP result must use object-shaped structuredContent.
Rules:
- If the API response is a JSON object, return it unchanged.
- If the API response is empty, return
{ "ok": true }. - If an unexpected raw JSON array appears, wrap it as
{ "result": [...] }. - If an unexpected scalar appears, wrap it as
{ "result": value }.
This directly addresses issue #6022 by preventing collection tools from returning bare arrays.
Authentication And Origin Safety
The MCP endpoint accepts the same bearer credentials as the API:
Authorization: Bearer <PAT-or-access-token>
The MCP adapter forwards the bearer header to the in-process API request. Public API operations can work without authentication when the API allows them. Mutating operations require authentication because the API already enforces that behavior.
For browser-origin safety, /mcp rejects cross-origin browser requests unless the Origin header is same-origin or matches the configured instance URL. Requests without an Origin header are allowed because desktop MCP clients commonly omit it.
Errors
The MCP server should convert failures into MCP tool errors:
- Invalid MCP arguments: concise validation message.
- API
401or403: preserve the API message where available. - API
404: report that the resource was not found. - Other API errors: include the HTTP status code and decoded API message.
- Internal OpenAPI or adapter errors: log server-side details and return a concise tool error.
Adapter errors should not bypass MCP result formatting unless the underlying MCP framework requires protocol-level errors for invalid protocol messages.
Tool Annotations
Tool annotations are derived from HTTP methods:
GET: read-only, non-destructive, idempotent.POST: mutating unless the operation is explicitly known to be read-only.PATCH: mutating, non-idempotent by default.DELETE: destructive and idempotent by default.
These annotations are hints for clients and do not replace API authorization.
Testing
OpenAPI Parsing Tests
Tests should verify:
- every curated operation ID exists in
proto/gen/openapi.yaml - every selected tool has an object input schema
- every selected tool has an object output schema
- selected operations do not include admin, auth, webhook, identity provider, personal access token, instance setting, share-link, AI transcription, or bulk-delete operations
- tool names are deterministic and unique
MCP Protocol Tests
Using an Echo test server and JSON-RPC requests, tests should verify:
initializesucceedstools/listreturns only curated OpenAPI-derived tools- tool definitions include input and output schemas
- no prompts or resources capabilities are advertised
- collection tool calls return object-shaped
structuredContent, never a bare array
Adapter Tests
Representative adapter tests should cover:
GET /api/v1/memos?pageSize=...POST /api/v1/memosGET /api/v1/memos/{memo}PATCH /api/v1/memos/{memo}DELETE /api/v1/memos/{memo}GET /api/v1/memos/{memo}/commentsPOST /api/v1/memos/{memo}/reactions
Before finishing implementation, run:
go test ./server/router/mcp/... ./server/router/api/v1/... ./server/...
Implementation Notes
- Do not hand-edit
proto/gen/openapi.yaml. - If a needed MCP tool is not represented by OpenAPI, add or adjust the proto/API surface first, run
cd proto && buf generate, then derive the MCP tool from the regenerated OpenAPI. - Prefer existing API auth and gateway behavior over duplicating authorization logic in the MCP adapter.
- Keep the first version intentionally small. Additional OpenAPI-derived tools can be added by extending the curated allowlist and tests.