Use Core from an MCP client¶
The concept behind MCP is explained in MCP.
An MCP client can call Core's API operations as tools. Connect a coding assistant or your own agent to use them. Core applies the same access checks and records the same activity as it does for an HTTP call.
Choose the endpoint for your credentials:
| Endpoint | Credentials | Available tools |
|---|---|---|
POST /api/v1/mcp |
API key | Operations your key may perform. With management, you can also manage organisations. |
POST /api/v1/mcp/toolsets/{name} |
API key | One of your own toolsets: your connectors, MCP servers, knowledge and the rest, run by Core (below). |
POST /api/v1/management/mcp |
Sign in as an organisation administrator | Management operations for organisations you can access. |
API keys work only on the first two endpoints; administrator tokens work only on the third. All
three use streamable HTTP without an MCP session. GET and DELETE return 405: there is no
separate stream to resume or session to end.
Every operation is a tool¶
Tool names are the operation's own, in the shape <verb>_<noun>: create_skill, list_prompts,
search_index, create_response, rotate_key. The arguments are the operation's path parameters,
query parameters and JSON body, in one schema. Where the HTTP call takes an Idempotency-Key
header, the tool takes an idempotency-key argument.
You are shown the tools your credential can actually use. On the key endpoint that means the
operations your key's capabilities admit: a key without knowledge is not offered
search_index. The list follows the API, so an operation added to Core is a tool without anything
being configured.
Six kinds of operation are deliberately left out. A tool call cannot carry what the first four need; the last two are left out because of what they answer or write:
| Left out | Why |
|---|---|
uploading a file (POST /files and its management twin) |
a tool call has no way to send bytes |
reading bytes (/files/{id}/content, /artifacts/{id}/content, a skill resource, the audit export) |
a tool result cannot carry a file |
GET /api/health, GET /api/ready, the OAuth callback |
they take no credential, so they belong to none of the three endpoints |
| the MCP endpoints themselves | they are this |
requesting a view address (POST .../view) and creating, listing or reading a share link (POST and GET .../links), on a file, an artifact or a page |
the answer is an address that opens a file without a credential, and a model that read one could pass it to any other tool (Write a document). Revoking a link answers no address and is a tool |
delivering a product's own audit events (POST /audit/events) |
only the product's own backend writes the trail in the product's name, never a model (Deliver your product's own events) |
Use HTTP for those. The ordinary path is to upload a file over HTTP and then name its id in a tool call — see Files and artifacts through the API.
Connect with an API key¶
The key endpoint takes your key secret as a bearer token on every request, exactly as the HTTP API does. There is no sign-in and no token exchange.
Claude Code:
claude mcp add --transport http artific-core https://<your Core address>/api/v1/mcp \
--header "Authorization: Bearer ack_..."
Cursor, in .cursor/mcp.json:
{
"mcpServers": {
"artific-core": {
"url": "https://<your Core address>/api/v1/mcp",
"headers": { "Authorization": "Bearer ack_..." }
}
}
}
Claude Desktop adds a remote server as a custom connector, which takes a URL and signs in with OAuth. It has no field for a header, so an API key cannot be presented from it. Use a client that can send a header, or connect to the management endpoint as a person instead.
Treat the configuration file as a credential store: it holds the key secret in clear text. A connection you no longer want ends by rotating or revoking the key.
A refusal arrives before any tool list is built, and it is the same refusal the HTTP API gives:
401 for a secret Core does not know, 403 for a token of the wrong kind, 409 for an
organisation that is not active. Each is a problem document with a title and a detail.
Connect as an administrator¶
POST /api/v1/management/mcp is one endpoint for the whole platform: no organisation in the
address, so one connection administers all the organisations you may reach. Start with
list_tenants to find them; every other tool names one by its tenant_id.
What you may do is what you may do in the console, and the tools do not hide it: you reach the
organisations you administer and everything below them — their keys, libraries, usage and audit
records. A tool naming an organisation outside that reach answers the same 404 the console would
show. The list itself is not filtered: tools for what only your operator may do — the deployment's
price list, the platform's outbox — are in it as well, and refuse when you call them.
Your client signs the person in rather than holding a secret:
- The client asks Core where to sign in, at
GET /.well-known/oauth-protected-resource/api/v1/management/mcp. That document names the endpoint and the one authorization server that issues tokens for it — the same sign-in the console uses. A client that connects without a token gets a401pointing at the same document. - The client registers itself with that server, which accepts registration from anyone and decides what the client may be.
- The client runs an authorization code flow with PKCE against that server, in your browser, and you approve it by name the first time.
- The token it receives carries your own roles and organisations. Core issues nothing and stores nothing about the client.
So there is nothing to configure and no client id to ask your operator for. What the client is
allowed to be is not its own choice either: it may sign a person in and nothing else, it holds no
secret, and its callback has to be a loopback address, its own application scheme, https, or —
because the sign-in server cannot separate the two — a plain http address. A client that insists
on anything else, a wildcard callback among them, is refused at registration rather than later.
Claude Code:
claude mcp add --transport http artific-core-management \
https://<your Core address>/api/v1/management/mcp
Then run /mcp in Claude Code and choose to authenticate; the browser opens for the sign-in.
Cursor, in .cursor/mcp.json — no headers, because the sign-in supplies the token:
{
"mcpServers": {
"artific-core-management": {
"url": "https://<your Core address>/api/v1/management/mcp"
}
}
}
Claude Desktop: add the same address as a custom connector and sign in when it asks.
The key and management endpoints name themselves in the connection, artific-core and
artific-core-management, so a
client that has both configured tells them apart.
Offer a toolset as an MCP server¶
The key endpoint and the management endpoint offer Core's own API. POST /api/v1/mcp/toolsets/{name} offers your
own tools: one toolset — your API connectors, the tools of the MCP
servers you registered, your knowledge and memory indexes, your agents, the builtins, and Core's
own operations where you grouped them in — as a server an agent somewhere else can connect to. Nothing new is
deployed and nothing is stored: the endpoint is the toolset you already have.
{name} is the toolset's own name, the same name a request's {"type": "toolset", "name": …}
writes. Every registered MCP server is also a toolset without you creating one, under
mcp__<server name>. The credential is your key's secret, and the key needs the tooling
capability; without it the endpoint answers 404, exactly as an address that does not exist.
Claude Code:
claude mcp add --transport http librarian \
https://<your Core address>/api/v1/mcp/toolsets/librarian \
--header "Authorization: Bearer ack_..."
Cursor, in .cursor/mcp.json:
{
"mcpServers": {
"librarian": {
"url": "https://<your Core address>/api/v1/mcp/toolsets/librarian",
"headers": { "Authorization": "Bearer ack_..." }
}
}
}
What is listed is what a turn naming that toolset would be offered, decided once and the same
way: the globs expand through your catalogue, a member your key's capabilities do not admit is
left out, and your policy's rules of kind tool remove what they do not permit. A toolset you do not have answers 404, and
a group that expands to more than the 128 tools a turn may carry answers 422 — name what the
agent needs rather than everything. A key whose policy requires an agent
(settings.agents_required) is served no toolset at all: the endpoint answers
403 before any tool is listed or called, because a call here runs outside every agent.
What is not served: builtin__subagent, because there is no turn here for it to be a subagent
of, and builtin__load_skill, which belongs to a response that links skills, and an agent whose
version says background.mode: "always", which only runs as a task of a turn. Everything else your
toolset names is.
An agent in the toolset is listed as agent__<agent> with its prompt's variables and input
as arguments. Calling it runs one turn of that agent, checked as POST /responses naming the agent
would be, and answers the agent's answer, or Agent could not be run. as an error when the turn
could not start; at your
monthly cap the result is an error carrying the
cap's sentence. That turn leaves its own response id and usage rows besides the call's
tool_invocation.
A call runs the tool, under the same rules a turn runs it under: your key's policy decides per
call, egress is checked the same way, the tool gets the same time budget, and a refusal is the
same sentence — Denied by policy., Unknown tool., Tool timed out., Tool failed. Bytes a
tool produced come back in the answer itself, an image as an image and anything else as an
embedded file — if your key has the artifacts capability, and up to the 25 MiB an artifact may
be, per file and for the answer together; over that you get a note saying how many bytes were
left out. Nothing is stored, because an artifact belongs to a response and a direct call has none.
Three things work differently here than in a turn, because there is no turn:
- A tool that writes something derived from your call — an index's
memory_write— stores it as untrusted content. Nothing here reviewed the text, so it never comes back as a trusted instruction in a later turn. - A knowledge tool whose pipeline has a step with a model of its own really calls
that model, and that call is not traced — the same as
POST /knowledge/query. A step that names no model of its own has none to fall back on, since the turn's model is what it would have used, and the run fails withno_model_configured. - An API connector is sent the request's id where a turn sends the
response's, in the
Idempotency-Keyheader and in the body'sresponse_id. It is still one id per call; what your callee cannot recognise is a retry, because retrying is a new request.
What a call leaves behind. One tool_invocation row in Usage and the policy
decisions in your audit trail, attributed to the key that called and carrying no response id —
there is no response. For the same reason there is no trace row: the console's traces are
turns, and this call is not one. Look for it in usage and audit instead.
Many tools through two¶
Add ?tool_search=router to the key endpoint or a toolset's address, and the server lists two
tools instead of all of them: builtin__tool_search, which finds tools by describing what the
agent needs and answers each one's name, description and parameters, and builtin__tool_run,
which runs one by name with its arguments. Use it when an agent would otherwise be handed more
tools than it can choose between well, such as Core's own API with several hundred operations:
claude mcp add --transport http artific-core \
"https://<your Core address>/api/v1/mcp?tool_search=router" \
--header "Authorization: Bearer ack_..."
A search only ever finds a tool the server would have listed, and a run is decided, run and billed
as a call of that tool is. Your key needs tooling.tool_search.enabled, and your organisation must
allow tool search; otherwise the address answers 422 naming tool_search before any tool is
listed. A search embeds what the agent asked for with your organisation's embedding model, billed
to your key, and ranks by keyword alone once the monthly cap is reached. How the router works is on
The tool router.
Give an agent the platform¶
The same key-side operations are available to a turn running inside Core, without any MCP client at
all. They are tools named core__<tool> — core__create_skill, core__search_index — and you
offer them like any other tool:
{
"model": "<a model id>",
"input": "Write a skill for triaging refund requests and store it.",
"tools": [{ "type": "tool", "name": "core__create_skill" }]
}
A core__ tool runs as the key whose turn it is, so it reaches exactly what that key reaches over
HTTP and nothing more. That includes administering the organisation when the key has the
management capability: core__create_organisation, core__register_organisation_key,
core__update_own_organisation and the rest of them. A key without management is offered none
of them. What such a key may do, and where, is on Manage organisations through the API.
Group what an agent needs into a toolset and attach it to a request or to an agent version:
curl -s -X POST "$CORE_URL/toolsets" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "librarian", "description": "Maintains skills and prompts.",
"tools": ["core__*_skill", "core__list_skills", "core__list_prompts"]}'
Name what the agent needs rather than everything. core__* expands to every key-side operation,
which is more than a hundred and thirty — above the 128 tools one turn may carry — so a request
offering it is refused with 400 and "This request offers more tools than a turn may carry."
A long tool list also costs the model budget before it reads your input.
These limits apply on top of the toolset:
- Your key's policy decides what it may run at all. One rule,
{"effect": "forbid", "kind": "tool", "match": {"names": ["core__*"]}}, takes these tools away from one key, and apermitof kindtoolthat matches them gives them. Read the current rules withGET /key. - A
core__tool runs one level deep. The nested turn acore__create_responsecall starts is offered nocore__tool of its own, and naming one there is400ontools:"core__ tools run on Core's own request path, one level deep: not inside a turn Core itself started."When you want a long call into Core from inside a turn, ask that nested call forbackground: trueand poll it. - A chain of turns calling turns stops five deep. You may register this deployment's own MCP
endpoint as an MCP server or point an API connector at
its API, and a turn that calls
create_responsethrough it starts a turn one level deeper. Core counts those levels, whichever way the call travelled: an agent as a tool, acore__tool or a registration at Core's own address. A turn at the fifth level is refused every such call. The model readsThis turn is too deep in a chain of calls to start another.as a failed tool result (call_depth_exceededin the trace), the turn goes on and answers, and nothing is charged for the refused call. A call that is started as a task is the one exception: the turn starts the task, which counts as the tool's invocation, and the task then fails withcall_depth_exceededwithout the call being made. Your operator sets the number withCORE_TOOL_CALL_DEPTH_MAX. The count follows the address your operator gave this deployment: a call that comes back to Core through a server of your own, or through another address of the same deployment, starts at level one again. - A turn Core runs for you is not that. A background run, a
scheduled turn and an evaluation step are offered the same
core__tools your own request is, under the same key and the same capabilities, so an agent that works when you call it works when the schedule fires it. One exception: administering people —core__list_own_organisation_admins,core__list_organisation_admins,core__invite_organisation_admin,core__resend_organisation_admin_inviteandcore__remove_organisation_admin— answers503in a turn Core runs for you, and stays a call your own application makes.
GET /tools?source=core lists these tools with the argument schema the model sees, the same way it
lists every other source.
What a tool call answers¶
A successful call answers the operation's own JSON, both as structured content and as text. A 204
answers an empty object.
Anything else is an error result carrying the problem document's own words —
"<title>: <detail> (<status>)" — never a protocol error. So a 404 from a capability that is
switched off reads to the model exactly as it reads to an HTTP caller, and the turn carries on.
Three answers are Core's own sentence rather than the operation's, because there is nothing else to report:
| What happened | What you are told |
|---|---|
| the operation answers something other than JSON | That operation answers with a … body, which a tool result cannot carry. |
| the operation failed while answering | That operation failed while answering. The failure is in Core's own log. |
| a streamed turn stopped before it finished | The turn's stream ended without a result. It was cut short rather than completed… |
A request that is not a valid MCP message is answered before any tool runs, as a JSON-RPC error
with id: null: -32700 with 400 for a body that is not JSON, -32600 with 400 for a body
that is not a single JSON-RPC 2.0 message or whose id is neither a string nor an integer, and
-32600 with 413 for a body over 1 MiB.
When a client calls create_response with stream: true and sends a progress token, each piece of
the model's text arrives as a progress notification while the turn runs, and the finished response
object is the result — the same object the unstreamed call returns. Without a progress token the
result is identical; nothing is sent while it runs.
Usage and audit records are written by the operation itself, attributed to the same key or the same person as the equivalent HTTP call. There is no separate MCP record to read: find a call in Usage and in the console's audit trail where you would find any other.
Everything a tool answers is data, not instruction. A result is your organisation's own content and a refusal quotes the model's arguments back at it; Core marks both untrusted, the same as every other tool result.