MCP server
Point any MCP client at your app, and the agent's permitted tools appear, governed exactly as everywhere else.
Every app is a Model Context Protocol server. There is no MCP configuration and no tool list to maintain: the tools an MCP client sees are the same policy-derived toolbox the agent itself uses, running through the same execution core, with the same approval halt.
invoices, vendors, payments). Your app has only the tables you create, so put your own table and column names in their place. About the example appPOST /{appUid}/mcp
Authorization: Bearer plak_...One key, one server
An API key is bound to one agent, so a key is an MCP server scoped to that agent. Issue keys for
two agents and a client connected to each gets different reach. Change a role and the tool list
changes for every connected client on its next tools/list.
Connecting a client
Most MCP clients take a URL and a header. The configuration file differs from client to client, but the two values are always the same:
| Field | Value |
|---|---|
| URL | https://<your API host>/<your app uid>/mcp |
| Header | Authorization: Bearer plak_... |
A typical remote-server entry looks like this; check your client's documentation for its exact format:
{
"mcpServers": {
"my-app": {
"url": "https://api.authoric.ai/<your app uid>/mcp",
"headers": { "Authorization": "Bearer plak_..." }
}
}
}The key acts as its agent with nothing added. Give a client the narrowest agent that does the job, and keep the key out of anything you would not trust with the agent itself.
Trying it by hand
# List the tools this key may use
curl -X POST "$AUTHORIC_URL/$APP_UID/mcp" \
-H "Authorization: Bearer $AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'
# Call one
curl -X POST "$AUTHORIC_URL/$APP_UID/mcp" \
-H "Authorization: Bearer $AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "invoices_create",
"arguments": { "values": { "invoice_number": "INV-1042" } }
}
}'Methods
| Method | |
|---|---|
initialize | Returns the server's capabilities. Protocol versions 2025-06-18, 2025-03-26 and 2024-11-05 are supported; the server echoes yours when it is one of them, and otherwise answers with the newest. |
notifications/initialized | Acknowledged, with no response body. |
ping | Returns {}. |
tools/list | The key's toolbox. |
tools/call | Run one tool. |
resources/list | Returns an empty list: an app's MCP server serves tools. |
A batch (an array of messages) is answered with an array of responses. An unknown method is the
standard JSON-RPC error -32601.
What a client is told about each tool
Every tool in tools/list carries a title and the four standard MCP annotations, so a client can
tell reads from writes before it calls anything:
| Annotation | true means |
|---|---|
readOnlyHint | The tool changes nothing: a list, a read, an aggregate. |
destructiveHint | It may overwrite or delete, not only add. A create is false; an update or delete is true. |
idempotentHint | Repeating the call with the same arguments has no further effect. |
openWorldHint | It reaches outside the app: a tool from a connected tool server, or an endpoint backed by your own handler code. |
title appears both on the tool and inside annotations, so clients on older and newer protocol
versions both show it.
Clients commonly run read-only tools freely and ask their user before the others. Those are hints for the client. They change nothing on the server: policy and the approval halt apply to every call, whatever a client does with a hint.
Authoric's own facts about a tool travel in _meta, under the authoric.ai/ prefix, never as
extra keys inside annotations:
| Key | |
|---|---|
authoric.ai/riskLevel | low, moderate or high. Anything above low halts for approval. |
authoric.ai/external | true on a tool from a connected tool server. |
authoric.ai/mcpServer | That server's alias in this app. |
A connected server's own annotations are passed on as it reported them, with one exception: a
read-only claim is dropped when the risk you set on review is above low. Its tools are titled
with the alias, for example Create refund (billing).
{
"name": "invoices_delete",
"title": "Delete a row from invoices",
"description": "Delete a row by its uid from the \"invoices\" table.",
"inputSchema": {
"type": "object",
"properties": { "uid": { "type": "string" }, "deleteStrategy": { "type": "string" } },
"required": ["uid"]
},
"annotations": {
"title": "Delete a row from invoices",
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": true,
"openWorldHint": false
},
"_meta": { "authoric.ai/riskLevel": "high" }
}Tool shapes
How your tables appear as tools is chosen per request with ?mode= on the endpoint URL. Your
endpoints and tool-server tools are always one tool each, under their own names.
mode | Tables appear as |
|---|---|
expanded (default) | One tool per table and operation: invoices_list, invoices_create, … Each has a schema built from the table's columns. |
per_table | One tool per table, with an operation argument listing only the operations the agent holds. Fewer entries for a client with a small tool budget. |
compact | Five generic tools (listRecords, getRecord, …) that take the table as an argument. |
What tools/call returns
A call that ran:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{ "type": "text", "text": "{\"uid\":\"0199f1d5-…\",\"invoice_number\":\"INV-1042\"}" }],
"isError": false,
"structuredContent": { "uid": "0199f1d5-…", "invoice_number": "INV-1042" },
"_meta": { "status": "executed" }
}
}A call that was refused or failed has isError: true, the reason in content, and
_meta.status of denied or failed, with _meta.code when there is a stable error code.
Halts over MCP
A tool rated above low behaves here exactly as it does everywhere else: it does not run. An MCP
call is answered straight away rather than held open, so the answer is a halt, not an error:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{ "type": "text", "text": "Approval required (risk: high). …" }],
"isError": false,
"structuredContent": { "status": "pending_approval", "runUid": "0199f1d4-0a2e-7d61-9b3c-6f8e1a2d5c90" },
"_meta": { "status": "pending_approval", "runUid": "0199f1d4-0a2e-7d61-9b3c-6f8e1a2d5c90" }
}
}The step waits in the app's approvals inbox like any other. Report to your user that the action
needs approval, and check the outcome afterward with
GET /agent/runs/{runUid} using the same key. Connecting
over MCP never relaxes the boundary: an MCP client is an outside caller, and an outside caller can
only tighten it.
Transport notes
- Streamable HTTP, over
POST. Responses areapplication/json. A client that accepts onlytext/event-streamgets the same response as a single server-sent event, then the stream closes. A body of notifications only is answered202with no content. - Changes through
tools/list.GET /{appUid}/mcpanswers405; calltools/listagain to pick up a change. - Stateless. The key is checked, and the agent's toolbox derived, on every request. There is no session to expire or leak.
- Limits. Calls count against your key's rate limit and your organization's concurrent run limit while they execute.