🤖 mcp
amadan.net/rastrillo/rastrillo/mcp
An app's operations as MCP tools, behind the JSON API's gate. MCP is the guide: mounting, connecting a client, and writing descriptors by hand.
New and Config
func New(config Config, descriptors ...op.Descriptor) (http.Handler, error)
type Config struct {
Gate api.Gate
Ctx func(*http.Request) *rastrillo.Ctx
Authorize func(ctx context.Context, actor rastrillo.Actor, operation rastrillo.Operation) error
Guard op.ResponseGuard
Guarded bool
Name, Version string
Instructions string
MaxResult int
}
New builds the MCP endpoint, with one tool per descriptor. Mount it
at /api/v1/mcp.
Gate is the API tree's, from api.Config.Gate(). Ctx builds the
app's Ctx for a request, and MCP sets Actor and Authorize on a
copy of it. Authorize is required: pass op.DefaultPolicy to keep the
default. Guard checks what an answer discloses as it goes out, and
Guarded requires one. Name and Version are the server's in the MCP
handshake. Instructions follows the framework's own instructions to
the agent. MaxResult caps one tool result in bytes; zero or less means
2 MiB.
New returns an error when:
CtxorAuthorizeis nil, orGate.Validatefails;Guardis set andGate.Verifyisn't anapi.Rechecker;Guardedis set and there is noGuard, or noRechecker;- a descriptor has an empty
ID, an invalidAccess, anInputthat isn't an object schema, aResultthat is neither an object schema nor exactly{"type":"null"}, or a nilInvoke; - a descriptor fails
op.CheckFlags; - a tool name doesn't match
^[a-z][a-z0-9_]{0,63}$, or two descriptors have the same tool name; - the SDK refuses a tool.
The handler runs every request behind Gate.Wrap, so a missing or bad
token gets the API's 401 or 503, and every answer has
Cache-Control: no-store. Then:
- It answers only POST. Any other method, GET included, is 405 with
Allow: POST. A batch is 400, andsubscriptions/listenis 404, each with a JSON-RPC error. - Send
Accept: application/json, text/event-streamandContent-Type: application/jsonwith each POST. WithoutAccept, the SDK returns a plain-text 400. WithoutContent-Type, it returns a plain-text 415. These are not JSON-RPC errors. - A message whose
idis a number is refused unless the number is a plain integer, with no fraction or exponent, within ±9007199254740991. The SDK would round any other number, and the answer would carry a different id. The refusal is 400 with JSON-RPC error -32600 and a null id, and nothing runs. A stringidis never refused. - Each request gets its own stateless server, which answers plain JSON and never advertises list changes.
tools/listshows only the toolsGate.Admitandop.Checkallow the caller, without the agent-session check, which a list never needs. Onlyop.Forbiddenorop.NotFoundhides a tool. Any other error fails the list as a JSON-RPC internal error, with a ref in its data.- A tool call runs
Gate.Admitandop.CheckbeforeInvoke, so a hand-writtenInvokethat skipsop.Checkis still checked. A refusal is a tool error holding the API's error envelope, fromapi.Refusal. A panic in a call is aninternaltool error with a ref. So is a result that isn't an object, includingnull, when the tool'sResultis an object schema. - With a
Rechecker, the gate checks the answer again as it goes out. For a call, it rerunsAdmitandop.Checkfor the operation that ran on every answer. It runsGuardonly when the call returned a successful result. For the list, it reruns them for every tool that was listed, then runsGuardwith a nil descriptor and the listed descriptors as the result.
New raises Gate.MaxResponse, when it is lower, to three times
MaxResult plus 64 KiB. A result goes out twice, as
structuredContent and as the text block, and escaping can make the
text longer.
ToolName
func ToolName(id string) string
The tool an operation is served as: bookmarks.create is
bookmarks_create.
Read this page as markdown — exact, unstyled, and cheap for an agent to fetch.