On this page
Overview
MCP — the Model Context Protocol — is how an AI agent picks up tools it did not ship with. Point your agent at this server and it can work your ERP the way a salesperson does: take a customer’s enquiry in the customer’s own words, work out which of your products it means, look the customer up, and draft the offer.
The server is remote and hosted by us, so there is nothing to install or run. It speaks plain HTTP and needs only an API key.
https://erp-agent.com/api/v1/mcp
Everything here sits on the same platform as the REST API: the same organization, the same keys, the same offers.
Connect
In Claude Code, add the server with one command. Anything a registered MCP server can do, your agent can then do in the course of a normal conversation.
claude mcp add erp-agent --transport http \ https://erp-agent.com/api/v1/mcp \ --header "Authorization: Bearer YOUR_API_KEY"
Claude Desktop and most frameworks take a JSON configuration instead. The shape varies slightly between clients, but the three things that matter are always the same: the transport is http, the URL, and the Authorization header.
{
"mcpServers": {
"erp-agent": {
"type": "http",
"url": "https://erp-agent.com/api/v1/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}Any client that speaks MCP works — Claude Code, Claude Desktop, or an agent framework of your own. Once connected, ask your agent to call erp_whoami to confirm which organization the key is bound to before you let it do anything else.
Authentication
Authentication is the same sk_live_… API key as the REST API, sent as a bearer token. One key belongs to exactly one organization and cannot reach another tenant’s data.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
The key must additionally carry the mcp:use scope. Scopes are granted per key by ERP Agent, not self-served: a key that works perfectly well against the REST API may still be missing this one. Without it every tool call comes back as insufficient_scope — if you see that, ask your ERP Agent contact to add the scope to the key.
Keys themselves are issued and scoped by ERP Agent. How they are created, stored and revoked is covered in the API documentation; treat the key as a password and keep it out of anything a user can read.
Transport
The server is stateless. There is no session id to carry between calls, and responses come back as plain JSON on the POST rather than as an SSE stream. That makes it safe to put behind a load balancer and cheap for a client to retry.
| Field | Description |
|---|---|
| Transport | Streamable HTTP. POST JSON-RPC, receive JSON in the response body. |
| Sessions | None. No session id is issued and none is expected. |
| Streaming | No SSE stream. Every response is a complete JSON body. |
| Protocol versions | 2025-06-18, 2025-03-26 and 2024-11-05 are supported. |
Search vs. match
Two tools look superficially alike and are not. Getting this right is most of what separates an agent that works from one that quietly returns the wrong products.
erp_product_search is a catalogue lookup. It answers does this code exist and what does the catalogue hold for these words. It is fast, synchronous and literal: it searches your catalogue and hands back the rows it finds.
erp_product_match is the identification agent. It answers a harder question — which of our products does this customer’s request mean — and it is the same pipeline that produces offers. It handles the customer’s own part numbers, competitor codes, standards and abbreviations, the things that never appear verbatim in your catalogue.
The rule: use erp_product_match whenever the input is a customer’s wording, and erp_product_search only when you already hold a code you trust, or want to see what the catalogue contains. Searching with a customer’s phrasing is the classic mistake; it finds something, and the something is often wrong.
Tool reference
Ten tools. Two of them start an agent run and return an id; the other eight answer immediately.
erp_whoamisyncReturns the organization the key is bound to, its scopes and its rate limit. Takes no arguments. Call it first when wiring up a new client — it is the cheapest way to prove the key and the mcp:use scope are both in place.
erp_product_searchsyncCatalogue lookup: code, name, unit and price. Use it to confirm a code exists or to see which catalogue rows match a few words. This is not identification — see Search vs. match.
| Field | Type | Description |
|---|---|---|
| query | string | Free text to search the catalogue for. |
| code | string | An exact product code to look up. |
| limit | integer | 1–50, default 10. |
erp_customer_searchsyncFind a customer in the ERP. Returns the customer number, name, address, contact and payment terms. Use it to resolve a company name into the number you then pass to erp_offer_create.
| Field | Type | Description |
|---|---|---|
| query | string | Company name to search for. |
| number | string | An exact customer number to look up. |
| limit | integer | 1–50, default 10. |
erp_product_matchasyncRuns the identification agent over a customer’s wording and attachments and returns a match id. Use it when you want the identified product lines but no offer — checking an enquiry, enriching a ticket, answering “do we sell this?” for a list. It leaves no offer behind.
| Field | Type | Description |
|---|---|---|
| text | string | The customer's request in their own words. |
| file_ids | string[] | Ids returned by erp_file_create, for attachments uploaded beforehand. |
| fast_mode | boolean | true trades part of the search for speed. false (default) searches thoroughly. |
| customer_number | string | Optional ERP customer number, so the agent matches in that context. |
| reference | string | Your own identifier, echoed back on every read. |
erp_product_match_getsyncReads a match back: its status, and once the run has completed, the identified lines. Poll this after erp_product_match.
| Field | Type | Description |
|---|---|---|
| match_id (required) | string | The id erp_product_match returned. |
erp_offer_createasyncTurns a request into a priced offer draft and returns an offer id. Use it when the outcome should be something a salesperson can review and send, rather than just a list of products.
| Field | Type | Description |
|---|---|---|
| text | string | The customer's request in their own words. |
| file_ids | string[] | Ids returned by erp_file_create. |
| fast_mode | boolean | true trades part of the search for speed. false (default) searches thoroughly. |
| customer_number | string | Optional ERP customer number. Given one, the agent looks the customer up directly. |
| customer_name | string | Optional hint used when no customer number is supplied. |
| request_type | string | "offer" or "order". |
| reference | string | Your own identifier, echoed back on every read. |
erp_offer_getsyncReads one offer: its status, and once the run has finished, the resolved customer and every line item. Poll this after erp_offer_create.
| Field | Type | Description |
|---|---|---|
| offer_id (required) | string | The id erp_offer_create returned. |
erp_offer_listsyncEvery offer your organization created, newest first. This is the recovery path: if an agent loses track of an id mid-conversation, it can find the run again here.
| Field | Type | Description |
|---|---|---|
| limit | integer | 1–100, default 25. |
| status | string | Return only offers in this status. |
| created_after | string | ISO-8601 timestamp; only offers created after it. |
| cursor | string | The cursor from the previous page. |
erp_match_listsyncThe same listing for matches rather than offers, newest first.
| Field | Type | Description |
|---|---|---|
| limit | integer | How many matches to return. |
| created_after | string | ISO-8601 timestamp; only matches created after it. |
| cursor | string | The cursor from the previous page. |
erp_file_createsyncReserves an attachment and returns an id plus a presigned upload_url and upload_fields. POST the bytes to that URL as multipart/form-data, then pass the id in file_ids. See Files.
| Field | Type | Description |
|---|---|---|
| filename (required) | string | The file name, including its extension. |
| content_type | string | The MIME type of the file. |
| size (required) | integer | Size in bytes. 25 MB is the maximum. |
Asynchronous tools
erp_product_match and erp_offer_create start real work. They return an id immediately and the result arrives minutes later: roughly one to three minutes in fast mode for about 50 lines, three to eight minutes otherwise. Read the result with erp_product_match_get or erp_offer_get.
This is worth telling your agent in its own instructions, because an agent that has not been told will treat the id as a failure and try again. The pattern that works is: call, tell the user roughly how long it takes, then poll rather than re-submit.
User
Here's an enquiry from Sähkötukku. Can you quote it?
"Tarvitsemme 20 kpl 16mm2 kuparikaapelia ja 50 kpl KAA 6x1.5"
Agent → erp_customer_search { "query": "Sähkötukku" }
← { "number": "K-10042", "name": "Sähkötukku Oy", … }
Agent → erp_offer_create {
"text": "Tarvitsemme 20 kpl 16mm2 kuparikaapelia ja 50 kpl KAA 6x1.5",
"customer_number": "K-10042"
}
← { "id": "9f1c0e2a-…", "status": "processing" }
Agent
Started the offer for Sähkötukku Oy — it usually takes a few minutes.
I'll check back.
… a few minutes later …
Agent → erp_offer_get { "offer_id": "9f1c0e2a-…" }
← { "status": "processing" }
… waits, then polls again …
Agent → erp_offer_get { "offer_id": "9f1c0e2a-…" }
← { "status": "completed", "result": { "line_items": [ … ] } }
Agent
Two lines, both matched: MCM-CU16-100 ×20 and KAA-6X15 ×50.
The draft is waiting for approval in the dashboard.Offers are drafts
Nothing an agent does here reaches the end customer, and nothing is booked in the ERP. erp_offer_create produces a draft, and the draft stays a draft until a salesperson opens it in the ERP Agent dashboard and approves it.
This is the property that makes it sane to hand these tools to an agent at all. The worst case of a confused agent is a draft nobody approves, not a wrong price in a customer’s inbox or a bad row in your ERP.
Matching without an offer
erp_product_match runs the same pipeline as an offer and deletes its draft when the run finishes. The practical consequence, stated plainly: while the run is in progress the draft is briefly visible in the dashboard, and then it is gone. Expect to see it there; do not act on it.
Files
Attachments are uploaded out of band, not through the tool call. Ask for an upload with erp_file_create, POST the bytes to the presigned URL it returns, then reference the id in file_ids on erp_product_match or erp_offer_create.
# 1. reserve
erp_file_create {
"filename": "bom.xlsx",
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"size": 18300000
}
{
"id": "1c7a…",
"upload_url": "https://…",
"upload_fields": { "key": "…", "policy": "…", "x-amz-signature": "…" }
}
# 2. POST the bytes as multipart/form-data:
# every upload_fields entry first, file last
# 3. reference the id
erp_offer_create { "file_ids": ["1c7a…"], "customer_number": "K-10042" }A file may be up to 25 MB, and one request may carry up to 20 of them.
Errors
A tool call that is refused comes back as an MCP tool result with isError: true, whose text is the same error envelope the REST API uses. Branch on error.code, never on the message text.
{
"error": {
"type": "invalid_request_error",
"code": "missing_query",
"message": "Provide 'query' or 'code'."
}
}| Field | Description |
|---|---|
| insufficient_scope | The key does not carry mcp:use. Ask your ERP Agent contact to add it. |
| rate_limit_exceeded | Too many requests for this key in the last minute. Back off and retry. |
| too_many_concurrent_runs | This key already has 5 agent runs in progress. Wait for one to finish. |
| erp_not_configured | This organization has no ERP connection set up yet. |
| missing_query | A search tool was called with nothing to search for. |
| unsupported_file_type | The attachment is not a type the agent can read. |
| match_not_found | No such match for this organization. |
| offer_not_found | No such offer for this organization. |
Protocol-level problems are different. An unknown tool name or malformed JSON-RPC comes back as a JSON-RPC error rather than as a tool result — the call never reached a tool, so there is no tool result to carry an envelope.
Limits
Each key is limited to 60 requests per minute by default. Exceeding it returns rate_limit_exceeded.
Separately, one key may have 5 agent runs in progress at a time — erp_product_match and erp_offer_create counted together. Starting a sixth returns too_many_concurrent_runs, so have your agent queue its work rather than firing a batch at once.
Attachments are capped at 25 MB per file and 20 files per request.
Need a higher limit?
Email lauri@erp-agent.com and we’ll set it up.