Skip to content

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:

  1. 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 a 401 pointing at the same document.
  2. The client registers itself with that server, which accepts registration from anyone and decides what the client may be.
  3. The client runs an authorization code flow with PKCE against that server, in your browser, and you approve it by name the first time.
  4. 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 with no_model_configured.
  • An API connector is sent the request's id where a turn sends the response's, in the Idempotency-Key header and in the body's response_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.

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 a permit of kind tool that matches them gives them. Read the current rules with GET /key.
  • A core__ tool runs one level deep. The nested turn a core__create_response call starts is offered no core__ tool of its own, and naming one there is 400 on tools: "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 for background: true and 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_response through it starts a turn one level deeper. Core counts those levels, whichever way the call travelled: an agent as a tool, a core__ tool or a registration at Core's own address. A turn at the fifth level is refused every such call. The model reads This turn is too deep in a chain of calls to start another. as a failed tool result (call_depth_exceeded in 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 with call_depth_exceeded without the call being made. Your operator sets the number with CORE_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_invite and core__remove_organisation_admin — answers 503 in 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.