MCP Server
The MCP server is in Preview, like the execution API it is built on: it can change or be removed at any time. Today:
- Its one tool, the release of a SAP purchase requisition, cannot be enabled yet. Until the procurement taxonomy is loaded,
tools/listlists onlyget_execution, and a call to the tool answers that it is unknown. - It runs only against Mandaitor's hosted sandboxes.
- Only registered agents connect. A person signing in through an MCP client, such as a desktop assistant, is not supported yet.
Mandaitor speaks the Model Context Protocol. An agent's MCP client connects to it as to any remote MCP server. tools/list shows the tools the agent's mandates could back, and each tools/call asks Mandaitor to carry out one business operation: an execution, decided, approved when a mandate says so, written and read back exactly as POST /executions does it. The agent never holds the credential to the target system.
Connect
The server is at https://api.mandaitor.io/v1/mcp, over Streamable HTTP. It speaks MCP 2025-11-25, 2025-06-18 and 2025-03-26.
Your agent connects as itself. Register it in the Agent Identity Registry with its own public key, whose private key you hold: public_key_jwk, an EC key on P-256 (ES256) or P-384 (ES384). An agent registered without one gets a key Mandaitor holds, which it cannot sign with. Bind the agent to a mandate whose delegate it is. A standard MCP client then finds out how to authenticate on its own:
- Its first request is answered
401, and theWWW-Authenticateheader names the server's protected resource metadata (RFC 9728):https://api.mandaitor.io/.well-known/oauth-protected-resource/v1/mcp. - That names the authorization server,
https://api.mandaitor.io/v1, whose metadata (RFC 8414) is athttps://api.mandaitor.io/.well-known/oauth-authorization-server/v1. - The client asks
POST /mcp/tokenfor a session with the client credentials grant, authenticated by a JWT it signs with the agent's key (private_key_jwt). It names the MCP server as theresource(RFC 8707), and the session is issued for the MCP server only.
With the official TypeScript SDK, that is configuration only:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { PrivateKeyJwtProvider } from "@modelcontextprotocol/sdk/client/auth-extensions.js";
const authProvider = new PrivateKeyJwtProvider({
clientId: "did:web:agents.mandaitor.io:agt_01HXYZ", // the agent's DID
privateKey: process.env.AGENT_PRIVATE_KEY_PEM!, // its registry key, PKCS #8
algorithm: "ES256",
expectedIssuer: "https://api.mandaitor.io/v1",
});
const transport = new StreamableHTTPClientTransport(
new URL("https://api.mandaitor.io/v1/mcp"),
{ authProvider },
);
const client = new Client({ name: "procurement-agent", version: "1.0.0" });
await client.connect(transport);
const { tools } = await client.listTools();
const result = await client.callTool({
name: "sap_purchase_requisition_release",
arguments: { purchase_requisition: "10000124", purpose: "Q4 retrofit" },
});
A standard client's session lasts 15 minutes. When a request is answered 401, the client asks for a new one. A session ends sooner when the agent is suspended or revoked. A session issued for the MCP server is refused by the rest of the API, and one issued for the API is refused here.
A client that can bind its session to a key of its own, with DPoP (Sessions bound to a key), may: the server accepts both kinds. A bound session lasts an hour, and a token taken from it is no use without the key.
Tools
tools/list shows the tools your tenant enabled whose business action a mandate naming the agent could back now. The mandate must be active, created or approved by a person, unexpired, and must carry only constraints executions read. Last comes get_execution, the server's own tool.
A tool's name is its id with each dot an underscore: sap.purchase_requisition.release is sap_purchase_requisition_release. Its input schema is the tool's arguments, with an optional purpose: a sentence on why, shown to the person who approves the operation, if one has to. The purpose never decides anything.
The list shows what could be done, not a decision. Calling a tool the list leaves out is decided like any request: it is denied, and the denial is recorded.
Calling a tool
Each call is one execution. The call waits up to about 20 seconds for it to settle, then answers. The answer's text says what happened, and its structuredContent is the execution as GET /executions/{id} returns it, signed receipt included.
| The execution | The answer | isError |
|---|---|---|
SUCCEEDED | The result, and the receipt in the structured content. | false |
DENIED | Why, with the state reason, e.g. NO_COVERING_MANDATE. Nothing was done. | true |
FAILED | Why: rejected, expired, refused by the target, or never sent. | true |
AWAITING_APPROVAL | Answered at once: a person of your tenant has to approve it, until a deadline. Check it later with get_execution. | false |
| Still running after about 20 seconds | Its state and id. Check it later with get_execution. | false |
UNKNOWN | The write may or may not have happened. Do not repeat it; Mandaitor reads it back until it knows, or a person settles it. | false |
get_execution takes an execution_id and answers the same way. It waits up to about 20 seconds while the execution is still being carried out. It reads only the agent's own executions.
Repeats. A call carries no idempotency key. So a repeat of the same call, the same tool with the same arguments from the same agent, answers with the same execution while it runs or waits for an approval, and for 10 minutes after it ends. A model that times out and calls again therefore never writes twice and never asks for a second approval. After those 10 minutes, the same call is a new request, decided afresh.
Errors
| Situation | Answer |
|---|---|
| No session, or one expired, ended or issued for the API | HTTP 401, with WWW-Authenticate naming the metadata |
| A session bound to a key, sent without a valid proof | HTTP 401, with a DPoP challenge naming the error and the metadata |
| A browser page on an origin Mandaitor does not serve | HTTP 403 |
GET or DELETE on the server | HTTP 405: it offers no stream and keeps no MCP session |
Not one JSON-RPC message, a batch, or an MCP-Protocol-Version it does not speak | HTTP 400 |
| An unknown tool, or one your tenant has not enabled | JSON-RPC error -32602, the same for both |
| Arguments that do not fit the tool | A tool error naming what is wrong |
More than 30 executions a minute from one agent, counting its POST /executions requests (get_execution does not count); a target that cannot be read; or an audit log that cannot be written | A tool error saying to try again, and when. Nothing was decided. |
| More than 10 requests a second to the MCP server, from every agent together | HTTP 429 from the gateway. Wait a moment and send it again |
Not yet
- No sign-in for people. An MCP client used by a person cannot connect yet. The authorization server's metadata names an authorization endpoint because clients require one, and it answers that no browser flow exists.
- No streams. Every request is answered with one JSON object; there is no progress while a call waits, and no stream to open.
- Tools only. No resources or prompts.
- Bearer sessions for standard clients. The official MCP SDKs do not bind a session to a key, so whoever holds one of their sessions can use it until it ends, within 15 minutes. Keep it as secret as the agent's key.
The MCP Authorization Server page describes /mcp/token in full, and the older /mcp/authorize, which decides without acting and will be retired now that the MCP server exists.