Skip to content

Connect an external capability through MCP

At the end of this tutorial a model in Core looks up tickets through the MCP server of Noordwind's ticket system. You have registered the server, chosen which of its tools you allow, and given them to a request and to an agent.

Building blocks MCP: Core uses an MCP server. Tools: toolsets and allowed destinations. Keys, capabilities and policy
You need a key with the capability tooling (and agents for step 6); the address of an MCP server and a token for it; an administrator of your organisation, or a key with management, for step 1; for the trace in the check, trace storage must be on for your organisation (Read a trace)
Builds on nothing. Step 6 uses the agent from Build your first agent if you have it

MCP (Model Context Protocol) is a standard through which a server offers tools. Choose this way when the system you want to connect already has an MCP server: you then get all of its tools, with their descriptions and schemas, in one registration. When the system only has an HTTP API, an API connector is shorter.

The examples use https://mcp.tickets.example.com/ with the tools search_tickets, get_ticket and close_ticket. Replace the address with that of your own server.

1. Allow the host

Core reads the server's tool list and later calls its tools. Both are outbound calls, and an organisation reaches no host until it allows one. In the console: open Settings, the tab Access & policy, choose Add a host on the card Allowed hosts, enter mcp.tickets.example.com and choose Allow host.

With a key that has management:

curl -s -X POST "$CORE_URL/organisation/allowed-hosts" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"host": "mcp.tickets.example.com"}'

You do this first so that the first read after registering succeeds straight away.

2. Register the server

curl -s -X POST "$CORE_URL/mcp_servers" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"name": "tickets",
         "url": "https://mcp.tickets.example.com/",
         "auth": {"type": "bearer", "secret": "Bearer <the server token>"}}'

The answer is 201 (abridged):

{
  "id": "mcp_6Tq1...",
  "name": "tickets",
  "allowed_tools": null,
  "last_synced_at": null,
  "last_sync_error": null,
  "tool_count": 0,
  "egress": [{ "host": "mcp.tickets.example.com", "allowed": true, "blocked": false }]
}
export SERVER_ID=mcp_6Tq1...

The name tickets returns in every tool name (mcp__tickets__<tool>) and does not change afterwards. You keep the token with the registration: Core sends it when reading the tool list and on every call, and never returns it.

3. Wait for the first read

Core reads the tool list in the background shortly after you register.

curl -s "$CORE_URL/mcp_servers/$SERVER_ID" -H "Authorization: Bearer $CORE_KEY"

Repeat this until last_synced_at holds a time. When last_sync_error is then null and tool_count is greater than zero, the server has been read. A server without a successful read offers a turn no tools. You ask for a new read with POST /mcp_servers/$SERVER_ID/sync, which answers 202.

4. Choose which tools you allow

curl -s "$CORE_URL/mcp_servers/$SERVER_ID/tools" -H "Authorization: Bearer $CORE_KEY"
{
  "items": [
    { "name": "search_tickets", "description": "Searches tickets by keyword or customer.", "offered": true },
    { "name": "get_ticket", "description": "Reads one ticket.", "offered": true },
    { "name": "close_ticket", "description": "Closes a ticket.", "offered": true }
  ]
}

While allowed_tools is null, the registration offers every tool of the server, including a tool the server adds later. For an assistant that only needs to read, that is too wide:

curl -s -X PATCH "$CORE_URL/mcp_servers/$SERVER_ID" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"allowed_tools": ["search_tickets", "get_ticket"]}'

close_ticket is then offered: false and reaches no turn at all. You narrow at the registration and not per request because the server is somebody else's: what it offers tomorrow is not yours to decide.

In the console you do steps 2 to 4 under MCP servers.

5. Offer the tools on a request

Every registered server is also a toolset of its own tools. You name the set, or one tool:

curl -s -X POST "$CORE_URL/responses" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"model": "<a model id>",
         "tools": [{"type": "toolset", "name": "mcp__tickets"}],
         "input": "Which tickets are open about damaged orders?"}'

You name one tool with {"type": "tool", "name": "mcp__tickets__search_tickets"}. The toolset moves with what the registration allows; a single tool name is fixed. Use the set when you mean the whole server and the single name when a turn may do exactly one thing.

6. Give the tools to an agent

curl -s -X POST "$CORE_URL/agents/$AGENT_ID/versions" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"model": "<a model id>",
         "instructions": "You are the support assistant of Noordwind. Answer briefly. Look up existing tickets before you say anything about the status of a report.",
         "temperature": 0.2,
         "tools": [{"type": "toolset", "name": "mcp__tickets"}]}'

A version is the whole definition. When your agent already has other tools from earlier tutorials, include them in the same tools list.

Check that it works

The answer to step 5 (abridged) holds the call to the MCP tool:

{
  "status": "completed",
  "output": [
    { "type": "mcp_call", "name": "mcp__tickets__search_tickets", "server_label": "mcp__tickets",
      "arguments": "{\"query\": \"damaged\"}", "status": "completed", "output": "..." },
    { "type": "message", "role": "assistant",
      "content": [{ "type": "output_text", "text": "Two tickets are open: ..." }] }
  ]
}

The arguments and the output depend on your server. GET /tools?source=mcp shows the MCP tools your key can offer; close_ticket is no longer among them. The trace of the turn (GET /responses/{id}/trace) shows the execution under tool_calls, with source: "mcp".

If it does not work

You see Cause Fix
404 on POST /mcp_servers the capability tooling is off on your key or in your organisation's ceiling ask your administrator to switch it on (Capabilities)
last_sync_error is filled Core could not read the server, and the sentence says why: the host is not allowed, the address is unreachable, the server refused the credential with 401 or 403, it listed more than 1,000 tools or more than 2 MiB of tool definitions, two of its tools reach one name, or the address is one of your own toolsets on this Core check egress on GET /mcp_servers/{id}, fix the cause and send POST .../sync
the model gets no tools the server has not been read yet, or allowed_tools excludes them steps 3 and 4
404 on POST /responses the tool name or the toolset does not exist for your key read the names with GET /tools?source=mcp
400 with tools or tools.N.name in error.param two tools on the request have the same name for the model; tools.N.name is a tool of your own named exactly like a tool Core runs in the same request offer each tool once, or rename your own
"Tool endpoint refused the credential." in the answer the server answered the call with 401 or 403 check the credential stored with the server
"Denied by policy." in the answer the server's host is not allowed, or a rule over Destinations of your key excludes it check egress on GET /mcp_servers/{id} and repeat step 1
the host is on your list and the call is still refused an organisation above yours does not permit the host (egress then reads allowed: false), or it restricts regions the host is not served in ask the administrator of the organisation above to permit the host (Policy)
the model does not get a tool that is offered: true your key's policy does not allow the tool; Core then does not offer it read the rules over Tools with GET /key

Read on

Next tutorial: Build an agent that combines knowledge and tools.