An ERP MCP server is a business system's actions published over the Model Context Protocol, so that any compatible AI agent can find out what the system can do and do it. Concretely, it is one HTTPS endpoint that answers two JSON-RPC methods: tools/list, which returns the tools available to the caller with a name, a description and a JSON Schema for the inputs, and tools/call, which runs one of them and returns a result the model can read. Claude, ChatGPT, Cursor, VS Code and other clients speak the protocol, so the server is written once and every agent can use it.
For an ERP the two things that matter are not in the transport. The first is that the tool list and every call are scoped to the person the agent represents, which the protocol supports through OAuth and per-request authorisation but leaves the server to enforce. The second is that each tool has to correspond to a business transaction that is safe to retry, refuse or ask about, because a model will do all three.
A server that turns a system into tools
The Model Context Protocol has three roles. A host is an AI application such as Claude or ChatGPT. Inside the host, a client is created per server and talks to that server only. A server is a service that offers context and capabilities to the host through a small number of primitives. The specification's design principles say servers should be easy to build, composable, and unable to see the whole conversation or into other servers; the host keeps the conversation and enforces consent, the server sees only the calls addressed to it.
An ERP MCP server is therefore not the ERP. It is the ERP's action surface, expressed as tools with typed inputs, served at one URL. The interesting design decision is what a tool is. A weather server has one; a business system has hundreds, and the useful ones are actions a person could take (create an invoice, record a payment, move a deal, reserve stock) rather than rows in tables. Sois's workspace server, for instance, publishes tools with names such as createInvoice, recordPayment, searchContacts and examineContact, grouped by module, and the exact list a caller receives depends on who they are.
Tools, resources and prompts
The specification defines three server primitives, and they differ in who controls their use. Tools are model-controlled: the model decides when to call one. Resources are application-driven: the host decides what context to attach, often with the user picking from a list. Prompts are user-controlled templates. An ERP needs the first, may benefit from the second, and rarely needs the third.
| Primitive | Who invokes it | Shape | In an ERP |
|---|---|---|---|
| Tools | The model, via tools/call | Name, description, inputSchema, optional outputSchema and annotations; result has content, optional structuredContent and isError | Every action: search, create, update, send, approve, reconcile |
| Resources | The host or the user, via resources/read | A URI with a MIME type; text or binary contents; optional templates and change subscriptions | Reference documents, a customer statement, a report; useful but not where the work happens |
| Prompts | The user, via prompts/get | A named message template with arguments | Occasionally, for a month-end routine; most ERP servers omit them |
Claude's Messages API connector and OpenAI's Responses API support tools only, which is one more reason to put the ERP's substance into tools.
Two features of tool results matter for a business system. A tool may declare an outputSchema and return structuredContent that conforms to it, alongside the text a model reads, so an integration can use the result without parsing prose. And a tool that fails for a business reason (an invoice in the wrong state, a date in the past, a permission missing) returns a normal result with isError: true and an explanation, rather than a protocol error, so the model can correct its input and try again. Protocol errors are reserved for malformed requests and unknown tools.
The transport and the current specification
Two transports are standard. Stdio is for a server the client launches as a local process, which is how desktop tools such as file access work. Streamable HTTP is for remote servers and is what an ERP uses: the server exposes one endpoint that accepts an HTTP POST per JSON-RPC message and answers with either a JSON object or a Server-Sent Events stream scoped to that request, so a long call can send progress before its final result. The earlier HTTP with SSE transport is deprecated.
The current revision, 2026-07-28, changed the transport in a way that matters for anyone deploying a server behind a load balancer. It removed protocol-level sessions: there is no longer an initialize handshake or a session identifier header, every request carries its protocol version and client capabilities in its own metadata, and a server that needs state across calls returns an explicit handle the model passes back as an argument. Servers written against the 2025-11-25 revision, which did use sessions, keep working because clients are required to detect the older era and fall back; a new server should not adopt sessions.
Authorisation: who the call is for
For HTTP transports the specification defines an OAuth 2.1 flow. The server is a resource server and must publish protected resource metadata (RFC 9728) naming its authorisation server; when a request arrives without a token it answers 401 with a WWW-Authenticate header pointing at that metadata and, ideally, the least scope needed. The client discovers the authorisation server's endpoints, identifies itself (client ID metadata documents are the recommended route; dynamic registration is retained for compatibility), runs an authorization-code flow with PKCE, and must include the server's canonical URI as the resource parameter so the token is bound to this server alone. The server must validate that audience, must refuse tokens issued for anything else, and must never pass a token through to another service.
The consequence for an ERP is the important one. Because the token identifies a person, the server may vary the result of tools/list by the credentials on the request, and the specification says so explicitly. That is the mechanism for role-based filtering: a finance user's list and a warehouse user's list come from the same server and are different. Insufficient scope at runtime is signalled with a 403 and a scope challenge the client can step up from, though for most business systems the real boundary is the role in the ERP rather than the coarse OAuth scope.
The hard part: permissions and transactions
Everything above can be implemented in an afternoon with an SDK. What separates an ERP MCP server from a demo is the treatment of the two things the protocol leaves to the server: whether a call is allowed, and what a call means.
Permission has to be checked twice. Filtering the tool list stops the model choosing something it should not, which saves tokens and confusion. Checking again when the tool runs is the actual boundary, because a client can send any call it likes. A refusal is best returned as a tool execution error, so the model reads it and reports it, rather than as a transport failure that ends the turn. This is what a refused call looks like from a Sois workspace: a normal result, flagged, with a reason.
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "Tool not permitted for this account: approveInvoice requires the finance approver role."
}
],
"isError": true
}
}A permission refusal returned as a tool execution error. The model learns why and can ask the person to approve instead; nothing was written. Sois also reserves JSON-RPC error codes for unauthorised sessions, insufficient credits, tools not permitted and budget caps reached.
Transactions are the second half. A tool should map to one business transaction with a clear before and after: createInvoice produces a draft with an identifier, recordPayment applies one payment to one invoice, and neither leaves a half-written state if it fails. Models retry, so writes should be safe to repeat or should refuse a repeat with a clear message; the specification's annotations let a server declare a tool as read-only, idempotent or destructive, and clients such as ChatGPT use those hints when deciding whether to ask for confirmation. Where a step needs a decision that is above the agent's authority, the server can return an input-required result asking the person a question through the client, rather than guessing.
The permission layer sits between the endpoint and the modules. The tool list is filtered by the caller's role on the way out, and every call is checked again on the way in, before it reaches a module.
How Sois implements one
A Sois workspace is an MCP server at a single URL, the workspace address followed by /api/mcp. It answers the 401 challenge with protected resource metadata, publishes its authorisation server metadata, requires PKCE, and issues tokens scoped to the workspace; a connector in Claude or ChatGPT completes the sign-in with nothing pasted. A bearer token with an API key is available for scripts that do not do OAuth, with identity and spend held in separate credentials on purpose.
The tool list is generated from the live tool definitions and filtered by role and installed apps, then each call is permission-checked at execution and fails closed. Calls are rate-limited per connection, metered where the workspace's own agent does the reasoning and free of AI charges where the caller's own agent does, capped by a budget the workspace sets, and logged with inputs and results against the person who made them. Apps published to the marketplace add their tools to the same list under the same rules, so a developer's app is operable by an agent the moment it is installed.
Questions people ask
Is an MCP server just a wrapper around a REST API?
Often it is implemented that way, and that is fine. The difference is what it publishes: typed tools a model can discover at runtime, results a model can read and recover from, and per-user OAuth authorisation, none of which a REST API gives an agent on its own.
Which agents can use an ERP MCP server today?
Claude (web, desktop, Cowork, Claude Code and the Messages API connector), ChatGPT in developer mode and the Responses API, Cursor, VS Code and any other client that implements the protocol. The server does not need to know which one is calling.
Does the server need to keep sessions?
Not under the current revision, which removed protocol-level sessions and asks servers to return explicit handles for anything that spans calls. Clients still interoperate with servers on the 2025-11-25 revision, which used a session header, by detecting the older era.
Where does the ERP enforce permissions?
In the server, at execution, on every call. Filtering the tool list is a convenience for the model; the check that matters happens when the tool runs, and a refusal should come back as a readable tool error so nothing is written and the model can explain why.
- Model Context Protocol specification (2026-07-28): tools tool definitions, results, error handling, annotations and the per-request variation of tools/list
- Model Context Protocol specification: Streamable HTTP transport and changelog the single-endpoint transport, the removal of sessions and backward compatibility
- Model Context Protocol specification: authorization OAuth 2.1, protected resource metadata, resource indicators and token rules
- Sois documentation: the workspace MCP server the endpoint, discovery documents, role filtering, limits and error codes as implemented
This article is reviewed when the products it describes change. Next scheduled review: 4 December 2026.
