Mcp
Implements the Model Context Protocol over JSON-RPC 2.0. All operations go through a single POST /mcp endpoint, differentiated by the request body.
- Spec: modelcontextprotocol.io
- MIME:
application/json - Serializer:
McpSerializer - Routing: Body-matched (single endpoint)
import crate.protocols.mcp.policy;
auto crateRouter = router.crateSetup!Mcp;crateRouter.add(userCrate);Generated Tools
Section titled “Generated Tools”All operations use POST /mcp with a JSON-RPC body. The params.name field determines the operation:
| Tool Name | Operation |
|---|---|
list_users | List all |
get_user | Get one |
create_user | Create |
replace_user | Replace |
update_user | Update fields |
delete_user | Delete |
Request Format
Section titled “Request Format”{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_user", "arguments": { "id": "abc123" } }, "id": 1}Base Operations
Section titled “Base Operations”MCP registers additional protocol-level operations via onRouterInit:
| Endpoint | Body Match | Purpose |
|---|---|---|
POST /mcp | method: "initialize" | Protocol handshake |
POST /mcp | method: "tools/list" | List available tools |
POST /mcp | method: "notifications/initialized" | Client notification (202 response) |
POST /mcp | method: "ping" | Ping/pong health check |
POST /mcp | name: "get_upload_instructions" | File upload endpoint discovery |
POST /mcp | name: "get_list_query_options" | Query parameter discovery |
GET /mcp/sse | — | Server-Sent Events stream |
GET /.well-known/oauth-protected-resource/mcp | — | OAuth 2.1 protected-resource metadata (RFC 9728) |
The tools list is populated lazily via onRouterReady after all models are registered.
Discovery Tools
Section titled “Discovery Tools”MCP provides two discovery tools that help AI clients understand what operations are available and how to use them.
get_upload_instructions
Section titled “get_upload_instructions”Returns REST API upload instructions for a resource field, so AI clients can upload files directly via HTTP instead of passing base64 through tool arguments.
Request:
{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_upload_instructions", "arguments": { "model": "picture", "id": "abc123", "field": "picture" } }, "id": 1}Parameters:
| Name | Required | Description |
|---|---|---|
model | yes | Model name (singular, lowercase) |
id | yes | Resource ID to upload to |
field | yes | Field name (e.g. "picture", "image/value") |
Response (upload available):
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "{\"url\":\"http://localhost/pictures/abc123/picture\",\"method\":\"POST\",\"contentType\":\"multipart/form-data\",\"example\":\"curl -X POST 'http://localhost/pictures/abc123/picture' -H 'Authorization: Bearer {token}' -F 'file=@/path/to/file'\"}" }] }}Response (no upload endpoint):
If no REST upload endpoint exists for the given model/field, the response suggests using the base64 tool instead:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "No REST upload endpoint available for picture/avatar. Use the `set_picture_avatar_value` tool with base64-encoded data instead." }] }}get_list_query_options
Section titled “get_list_query_options”Returns the available query/filter parameters for a model’s list endpoint, so AI clients can discover filtering, pagination, and search options.
Request:
{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_list_query_options", "arguments": { "model": "team" } }, "id": 2}Parameters:
| Name | Required | Description |
|---|---|---|
model | yes | Model name (singular, lowercase) |
Response:
Returns a JSON Schema object describing the available query parameters:
{ "jsonrpc": "2.0", "id": 2, "result": { "content": [{ "type": "text", "text": "{\"type\":\"object\",\"properties\":{\"limit\":{\"type\":\"string\",\"description\":\"Max items to return\",\"in\":\"query\"},\"skip\":{\"type\":\"string\",\"description\":\"Items to skip\",\"in\":\"query\"}}}" }] }}The tool description in tools/list is automatically updated with the list of available models.
Notifications
Section titled “Notifications”MCP supports client-initiated notifications and ping requests.
notifications/initialized
Section titled “notifications/initialized”Sent by the client after the initialize handshake completes. This is a fire-and-forget message that returns 202 Accepted with no body.
{ "jsonrpc": "2.0", "method": "notifications/initialized"}Response: 202 Accepted (empty body)
Standard MCP ping request. Returns a JSON-RPC pong response.
{ "jsonrpc": "2.0", "method": "ping", "id": 3}Response:
{ "jsonrpc": "2.0", "id": 3, "result": {}}Request Processing
Section titled “Request Processing”MCP uses two internal middleware layers to bridge JSON-RPC requests into the same format that REST operations expect. This makes all downstream middleware and operations protocol-agnostic.
Context Marker
Section titled “Context Marker”The McpContextMarker middleware runs on every request (@any). It detects MCP requests by checking for the jsonrpc field in the request body and sets two context flags:
_isMcpRequest—truefor all MCP requests_mcpMethod— the JSON-RPC method string (e.g."tools/call","tools/list")
Other middleware can check req.context["_isMcpRequest"] to branch on protocol.
Request Normalization
Section titled “Request Normalization”The McpRequestNormalizer middleware transforms JSON-RPC tool calls into REST-compatible format:
- Extracts the RPC ID and stores it in
req.params["_rpcId"] - Extracts tool arguments from
params.arguments - Unwraps the
datafield for_recordtools (e.g.create_record) - Moves the item
idfrom arguments toreq.params["id"] - Forwards non-reserved arguments to
req.queryas filter/pagination params - Stores base64 data in
req.params["_base64Data"]if present - Rewrites the request body to model-specific format (e.g.
{ "icon": {...} })
Reserved keys (not forwarded to query): id, model, data, fullJsonExport
Partial Updates
Section titled “Partial Updates”An AI client calling update_* sends only the fields it means to change, but the underlying update operation validates a whole record. The normalizer bridges that: given a CrateAccess, it reads the stored item, reduces it to the fields a client may write, and merges the call’s arguments on top.
writableSubset is what does the reducing. It keeps only the declared fields whose canWrite is true, recursing into embedded objects and leaving opaque ones intact:
| Stored field | Kept |
|---|---|
| A declared, writable field | yes |
| A declared, read-only field | no |
| A field not on the model at all | no |
Dropping read-only and undeclared fields matters — they would otherwise reach the update validator as if the client had sent them, and be rejected. The merge is skipped entirely for a non-update_ tool, for a call with no item id, when the normalizer has no crate, and when the stored item no longer exists.
Response Formatting
Section titled “Response Formatting”MCP responses are wrapped in the JSON-RPC tool result envelope. The serializer supports two output modes controlled by summary fields and the fullJsonExport argument.
Summary Mode (default)
Section titled “Summary Mode (default)”When a model has fields marked @isId or @isSummary, read responses return a compact key-value text format:
_id: 000000000000000000000001name: My Team---_id: 000000000000000000000002name: Other TeamThis reduces token usage for AI clients that only need an overview.
Full JSON Mode
Section titled “Full JSON Mode”Pass fullJsonExport: true in the tool arguments to get the complete JSON representation:
{ "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_user", "arguments": { "id": "abc123", "fullJsonExport": true } }, "id": 1}If no summary fields are defined on the model, full JSON is always returned regardless of this flag.
Mutating Operations
Section titled “Mutating Operations”Create, update, replace, and delete operations return a success confirmation instead of the full item:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "create_user succeeded. id: 000000000000000000000001" }] }}Response Mappers
Section titled “Response Mappers”Context mappers registered on operations are applied to response data before serialization. This works for both toResponseItem and toResponseList, allowing field transformation, enrichment, or filtering at the protocol level.
Not Yet Supported
Section titled “Not Yet Supported”- Resources: Only tools are exposed; no MCP resource endpoints
- Prompts: No prompt support
- Sampling: No sampling / LLM model endpoints
- Progress tracking: No progress reporting mechanism
- Logging: No structured logging API
- Cancellation: No request cancellation
Custom Base URL
Section titled “Custom Base URL”auto crateRouter = router.crateSetup!(McpPolicy!(PolicyConfig("/tools")));// Endpoint at POST /tools/mcpOAuth Protected Resource Discovery
Section titled “OAuth Protected Resource Discovery”The policy registers an RFC 9728 metadata document at the well-known path derived from its own endpoint — /.well-known/oauth-protected-resource/mcp for the default base URL, with the configured prefix inserted otherwise. It serves the metadata itself; redirecting the client back to the MCP endpoint would answer “where do I get a token” with “ask the thing you cannot reach”.
{ "resource": "https://example.org/mcp", "issuer": "https://example.org", "authorization_endpoint": "https://example.org/auth/authorize", "token_endpoint": "https://example.org/auth/token", "revocation_endpoint": "https://example.org/auth/revoke", "registration_endpoint": "https://example.org/auth/register", "code_challenge_methods_supported": ["S256"]}Behind a proxy that strips a mount prefix, X-Forwarded-Prefix is added back to the advertised resource so it matches the URL the client actually called. The auth endpoints stay at the origin. See the OAuth2 reference.
OPTIONS /mcp advertises the headers an MCP client needs to send and to read:
| Header | Value |
|---|---|
Access-Control-Allow-Headers | Content-Type, Authorization, If-None-Match, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID |
Access-Control-Expose-Headers | Mcp-Session-Id, MCP-Protocol-Version |
Without the expose list a browser-based client can complete a request but never read the session id back out of it. Operations contribute these through CrateRequest.allowedHeaders and CrateRequest.exposedHeaders, so a custom operation can add its own.
For a cookie-authenticated client, also set isCredentialedOrigin on the router — see Cookie Flows and CORS.
Custom Operations as Tools
Section titled “Custom Operations as Tools”Operations registered with itemOperation and collectionOperation become tools alongside the generated CRUD set:
| Registration | Tool name | Requires an id |
|---|---|---|
itemOperation!("archive") on User | archive_user | yes |
collectionOperation!("search") on User | search | no |
The handler’s first struct parameter (anything but a vibe Json) supplies the tool’s input schema, and the call’s arguments object is deserialized into it. Handlers reply through writeToolResult or writeToolImageResult from crate.protocols.mcp.toolResult, which emit an MCP text or image content block for a JSON-RPC request and plain JSON or raw bytes for a REST call — see Custom Operations.