Integrate & ship

MCP servers

The Model Context Protocol is a standard way for a system to expose its tools to any agent. Connect one and its tools become available to yours.

Open Configure → MCP Servers. Once a server is connected and allow-listed on an agent, the agent can discover its tools and call them.

Why it works this way

Before MCP, every integration was bespoke: someone wrote a wrapper per system, per platform. MCP inverts it — the system that owns a capability describes its own tools once, and any MCP-speaking agent can use them. The practical benefit is that the team owning the ticketing system owns its tool definitions, instead of you guessing at their API.

Connecting a server

  1. 1

    Add the endpoint

    An HTTP(S) URL speaking Streamable HTTP MCP. It must be reachable from wherever the app runs — a server on a private network won't be reachable from a hosted instance.
  2. 2

    Set authentication

    Bearer token or none. Tokens are encrypted at rest; store them in Secrets and reference them so rotation is one edit.
  3. 3

    Test the connection

    The page lists the tools the server advertises. An empty list means it connected but exposes nothing — usually an auth scope problem.
  4. 4

    Allow-list it on an agent

    In the Agent Builder, enable mcp_call_tool and choose which servers this agent may reach. An agent with no allow-list entry cannot call any server.

Every field on a server

FieldValuesNotes
NametextHow the agent refers to it — this exact string goes in server_name on a call, and in the agent allow-list.
endpointhttps URLStreamable HTTP MCP endpoint. Must be reachable from where the app runs.
descriptiontextFor humans browsing the list.
auth_typenone | tokenBearer token or anonymous.
auth_tokentextEncrypted at rest, never returned to the browser after saving. Prefer a Secrets reference.
statusconnected / disconnectedRead-only, set by the last test.
tools_countnumberRead-only — how many tools the server advertised. Zero after a successful connect is the signal described below.
last_pingtimestampRead-only.

The server Name is an identifier, not a label

An agent allow-lists servers by name, and mcp_call_tool takes server_name. Renaming a server after agents reference it breaks those references silently — the agent simply finds no server and reports it cannot do the thing.

How an agent uses it

Three tools, used in sequence:

list_mcp_servers
Which servers this agent may reach.
mcp_list_tools
What one server offers, with argument schemas.
mcp_call_tool
Invoke a named tool on a named server with arguments.

Discovery calls are not treated as sources — only mcp_call_tool contributes to the Sources shown under an answer, where it appears as the remote tool name and the server it came from.

Security

  • Allow-lists are per agent. Connecting a server workspace-wide does not expose it to every agent; each one must be granted it explicitly.
  • Outbound requests are guarded. Endpoints resolving to private or link-local addresses — including cloud metadata services — are refused, so a malicious or mistyped endpoint can't be used to reach inside your network.
  • Tokens are encrypted at rest and never returned to the browser after saving.
  • Calls are traced. Every invocation appears in the run trace with its arguments and result.

A remote tool can act

Unlike retrieval, an MCP tool may change something — file a ticket, send a message, update a record. An agent deciding to call it is a model decision. For anything consequential, put a human approval node in front of it in a swarm rather than trusting the prompt to hold.

Building your own MCP server

Everything above is about connecting to a server somebody else runs. Build → MCP Builder is the other direction: you write the server in Python with FastMCP, it runs on the same sandboxed kernel the Developer workspace uses, and it is reachable over Streamable HTTP at /api/mcp/s/<slug>.

Why it works this way

The alternative is standing a service up somewhere else and wiring it back in by URL — which means a second deployment, a second set of credentials, and a second place to reason about who may call what. Here the source, the sandbox, the keys and the audit trail are one thing.

What the runner expects

A module-level FastMCP instance named mcp (server and app also work), with @mcp.tool() functions. Do not call mcp.run() — the platform serves the object for you, and calling it yourself deadlocks startup. This is a complete server; it deploys as written:

python
from fastmcp import FastMCP

mcp = FastMCP("my-server")


@mcp.tool()
def greet(name: str) -> str:
    """Return a friendly greeting for the given name."""
    return f"Hello, {name}!"


@mcp.tool()
def word_count(text: str) -> dict:
    """Count words and characters in a block of text."""
    words = text.split()
    return {"words": len(words), "characters": len(text)}

That is the Hello world template, and the other two — wrap an HTTP API, search a knowledge base — are equally complete. Start from one rather than an empty file.

  • Type hints become the input schema and the docstring becomes the description the calling model reads. Both are worth writing carefully — they are how a model decides whether your tool is the right one. greet above advertises one required string argument called name purely because of its signature.
  • Write the decorator with parentheses. @mcp.tool() works on every FastMCP version; bare @mcp.tool needs 2.11 or newer and fails to load on older images.
  • Either SDK works. The image ships both fastmcp (the standalone 2.x package most examples use) and mcp, the official SDK — whose mcp.server.fastmcp.FastMCP is a different class with the same name. The runner duck-types across them, so from mcp.server.fastmcp import FastMCP is equally valid.
  • Extra packages go in the Deploy tab, one per line, and are installed at container start. httpx, pydantic, pandas, numpy, langchain, langgraph, llama_index and the agentswarms helper are already there.

Using a secret, and reaching a real API

Bind secrets on the Deploy tab as ENV_NAME={{secret:NAME}} and read them with os.environ. They are resolved only when the container starts and arrive over an authenticated call, so they never appear in the stored configuration, the database, or the logs. The Wrap an HTTP API template is the shape:

python
import os

import httpx
from fastmcp import FastMCP

mcp = FastMCP("http-api")

BASE_URL = os.environ.get("API_BASE_URL", "https://api.example.com")
API_TOKEN = os.environ.get("API_TOKEN", "")


def _client() -> httpx.Client:
    headers = {"Authorization": f"Bearer {API_TOKEN}"} if API_TOKEN else {}
    return httpx.Client(base_url=BASE_URL, headers=headers, timeout=20, trust_env=True)


@mcp.tool()
def get_customer(customer_id: str) -> dict:
    """Fetch one customer record by id."""
    with _client() as c:
        r = c.get(f"/customers/{customer_id}")
        r.raise_for_status()
        return r.json()

On the Deploy tab that server needs two bindings — API_BASE_URL=https://api.internal.example.com and API_TOKEN={{secret:INTERNAL_API_TOKEN}} — with INTERNAL_API_TOKEN stored once in Secrets. Rotating it later is one edit there and a redeploy here.

Add the host to the egress allow-list first

trust_env=True is what routes the request through the sandbox proxy. If api.internal.example.com is not on the instance allow-list the call is refused by the proxy, not by the remote server — so the error you see will not mention the remote host at all. An administrator adds it under Admin → Developer runtime.

Cold starts and keep-warm

By default a server scales to zero: no container exists until the first call, which pays a few seconds of start-up, and it stops again after its idle timeout. Turn on Keep warm for latency-sensitive servers — it holds a container permanently, which is why it is off by default.

Who can call it

  1. 1

    Your agents (internal)

    Toggle it on under Access and the server is registered in your connected-servers list. Agents and swarms here can call it through the same mcp_call_tool path as any other server. Nothing is exposed to the internet. If your instance sets BLOCK_PRIVATE_NETWORK_FETCH, registration refuses until PUBLIC_APP_URL names an address the app can call itself on — the alternative would be a registration that looks fine and fails on every agent call.
  2. 2

    Anyone with a key (public)

    Expose publicly, then mint a key. External MCP clients call /api/mcp/s/<slug> with Authorization: Bearer mcps_… . Keys are stored hashed, can expire, can be limited to specific tools and source addresses, and are revoked rather than deleted so the usage trail survives.

While a server is not exposed publicly, its endpoint answers 404 to everything except your own agents — the same response an unknown slug gets, so probing cannot tell the difference.

Pointing an external client at it

Once a server is exposed and you hold a key, it is an ordinary Streamable HTTP MCP endpoint. Most clients take a URL and a header:

json
{
  "mcpServers": {
    "my-server": {
      "type": "http",
      "url": "https://your-instance.example.com/api/mcp/s/my-server-a1b2c3",
      "headers": {
        "Authorization": "Bearer mcps_xxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}

The slug carries a random suffix, so copy it from the server's page rather than assuming it matches the name. To check a key without a client at all:

bash
curl -sS https://your-instance.example.com/api/mcp/s/my-server-a1b2c3 \
  -H 'Authorization: Bearer mcps_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
DetailValue
TransportStreamable HTTP, POST only
GET405 by design — there is no server-to-client stream, and clients fall back to POST
Protocol revision2025-06-18
SessionMcp-Session-Id is returned on the response and echoed back on subsequent calls
Ending a sessionDELETE to the same URL
Key formatmcps_ + 32 characters, shown once at creation

A 404 is the answer to several different questions

An unknown slug, a server that is not exposed publicly, and a session id that has expired all return 404. That is deliberate — probing should not be able to tell a private server from one that does not exist — so when a client that used to work starts getting 404, check whether the key was revoked or public access was turned off before suspecting the URL.

How a built server is contained

  • Same sandbox as notebooks. Non-root, read-only root filesystem, all capabilities dropped, no-new-privileges, CPU/memory/PID limits, and no route off the box except the filtering egress proxy.
  • No credential of yours reaches your code. The caller's Authorization header is consumed at the edge and never forwarded, and the sandbox holds only a short-lived capability token — never a provider key or your Supabase session.
  • Sessions are translated. The Mcp-Session-Id a caller receives is ours, bound to the key that opened it; the server's own session id never leaves the platform.
  • Only tool methods are forwarded. initialize, tools/list, tools/call, ping, and the initialized and cancelled notifications. Anything else is refused at the edge — an allow-list, not a deny-list, because MCP keeps growing (resources, prompts, sampling, roots, elicitation) and several of those let a server ask the client to do something. A future protocol method cannot become reachable because a dependency was upgraded.
  • Tool changes need re-approval. A redeploy that changes any tool name, description or schema blocks calls until you review it — see below.

Why a changed tool description blocks calls

Tool descriptions are instructions the calling model reads. A server that is trusted and then quietly rewrites "look up an order" into something that also forwards the result elsewhere is a real attack, not a hypothetical one. So a deploy that moves the tool fingerprint parks the server until a human approves the new list.

Egress filtering is instance-wide

The allow-list that decides which hosts a sandbox may reach is a single shared proxy, so it applies to every server and every notebook on the instance — there is no per-server egress isolation. If your server needs a host, an administrator adds it under Admin → Developer runtime, and every other sandbox gains that host too.

When a build won't start

Deploy fails immediately
Read the Logs tab — it is the container's stdout and stderr, with bound secret values scrubbed. A missing package and a syntax error both surface there.
No MCP server found
The runner could not find a module-level FastMCP instance. Name it `mcp`, and make sure it is created at import time rather than inside a function.
Deploy hangs then times out
Usually a call to mcp.run() in your own code. The platform serves the object; your file should only define it.
A network call is refused
The host is not on the instance egress allow-list. Add it under Admin → Developer runtime — the refusal comes from the proxy, not from the remote server.

Troubleshooting a connected server

Connects but lists no tools
Authenticated as a principal with no tool scope, or the server exposes tools only after an initialisation step it didn't complete.
Agent never calls it
Not allow-listed on that agent, or the tool descriptions are too vague for the model to match against the question. Descriptions come from the server — improve them there.
Refused endpoint
The URL resolves to a private address. Expose it on a reachable host, or run the app where it can see it.
Times out
Long-running remote tools exceed the call budget. Make the remote tool return quickly and poll, rather than blocking.