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
Connecting a server
- 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
Set authentication
Bearer token or none. Tokens are encrypted at rest; store them in Secrets and reference them so rotation is one edit. - 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
Allow-list it on an agent
In the Agent Builder, enablemcp_call_tooland choose which servers this agent may reach. An agent with no allow-list entry cannot call any server.
Every field on a server
| Field | Values | Notes |
|---|---|---|
| Name | text | How the agent refers to it — this exact string goes in server_name on a call, and in the agent allow-list. |
endpoint | https URL | Streamable HTTP MCP endpoint. Must be reachable from where the app runs. |
description | text | For humans browsing the list. |
auth_type | none | token | Bearer token or anonymous. |
auth_token | text | Encrypted at rest, never returned to the browser after saving. Prefer a Secrets reference. |
status | connected / disconnected | Read-only, set by the last test. |
tools_count | number | Read-only — how many tools the server advertised. Zero after a successful connect is the signal described below. |
last_ping | timestamp | Read-only. |
The server Name is an identifier, not a label
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
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
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:
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.
greetabove advertises one required string argument callednamepurely because of its signature. - Write the decorator with parentheses.
@mcp.tool()works on every FastMCP version; bare@mcp.toolneeds 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) andmcp, the official SDK — whosemcp.server.fastmcp.FastMCPis a different class with the same name. The runner duck-types across them, sofrom mcp.server.fastmcp import FastMCPis 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_indexand theagentswarmshelper 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:
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
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 samemcp_call_toolpath as any other server. Nothing is exposed to the internet. If your instance setsBLOCK_PRIVATE_NETWORK_FETCH, registration refuses untilPUBLIC_APP_URLnames an address the app can call itself on — the alternative would be a registration that looks fine and fails on every agent call. - 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:
{
"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:
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"}'| Detail | Value |
|---|---|
| Transport | Streamable HTTP, POST only |
| GET | 405 by design — there is no server-to-client stream, and clients fall back to POST |
| Protocol revision | 2025-06-18 |
| Session | Mcp-Session-Id is returned on the response and echoed back on subsequent calls |
| Ending a session | DELETE to the same URL |
| Key format | mcps_ + 32 characters, shown once at creation |
A 404 is the answer to several different questions
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-Ida 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 theinitializedandcancellednotifications. 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
Egress filtering is instance-wide
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.