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.

The examples on this page use the tables of a sample invoicing app (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 app
POST /{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:

FieldValue
URLhttps://<your API host>/<your app uid>/mcp
HeaderAuthorization: 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
initializeReturns 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/initializedAcknowledged, with no response body.
pingReturns {}.
tools/listThe key's toolbox.
tools/callRun one tool.
resources/listReturns 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:

Annotationtrue means
readOnlyHintThe tool changes nothing: a list, a read, an aggregate.
destructiveHintIt may overwrite or delete, not only add. A create is false; an update or delete is true.
idempotentHintRepeating the call with the same arguments has no further effect.
openWorldHintIt 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/riskLevellow, moderate or high. Anything above low halts for approval.
authoric.ai/externaltrue on a tool from a connected tool server.
authoric.ai/mcpServerThat 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.

modeTables 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_tableOne tool per table, with an operation argument listing only the operations the agent holds. Fewer entries for a client with a small tool budget.
compactFive 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 are application/json. A client that accepts only text/event-stream gets the same response as a single server-sent event, then the stream closes. A body of notifications only is answered 202 with no content.
  • Changes through tools/list. GET /{appUid}/mcp answers 405; call tools/list again 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.

What's next

On this page