allowed_tools and tools_namespace behave
identically, and GET /v1/integrations/{id}/mcp-tools lists them the same way.
See MCP servers for all of that. This page is the
parts that are only true of a server we did not write the client for.
Connecting one
Two fields, and only two:url is required, headers is not.
mcp_config is refused — custom_mcp: unknown config field 'transport' — rather than accepted and ignored.
A token whose root is not secrets — a typo, or a spelling borrowed from another
platform — is refused at save rather than sent to your server as literal braces:
{{secrets.NAME}} resolves in the url as well as in header values, which is how
a server whose path carries a tenant token stays out of the config. No other
template root is available here — there is no userdata, no vars, and nothing
per-call. One integration is one set of credentials, so a server that needs
different credentials per caller needs an integration per caller.
There is no OAuth for a custom server. We do not run an authorization flow against
a URL you supply and we do not perform dynamic client registration; the credential
is whatever static value your headers carry.
The URL is checked before we use it
The URL must behttp or https with a host, and its host is resolved and
checked before any request is made. A private, loopback, link-local, reserved,
multicast or carrier-NAT address is refused, which rules out the metadata
endpoints cloud providers expose on internal addresses.
That check runs where the server is used, not at create, so this is what a
customer actually hits — on GET /v1/integrations/{id}/mcp-tools:
http tool operation and webhook delivery.
What your server has to support
We connect with LiveKit’s MCP client over HTTP, and the transport is chosen from the URL path:
So
https://mcp.example.com/mcp speaks Streamable HTTP and
https://mcp.example.com/sse speaks the SSE transport. There is no field to
override it — the path is the switch. If your server only implements one of the
two, give us a URL whose path matches it.
Other things about the connection worth designing against:
- HTTP/2 is offered and negotiated over ALPN. A server that only speaks HTTP/1.1 gets HTTP/1.1; nothing breaks either way.
- Redirects are followed.
- Every request has a five-second budget — the tool listing at connect and each tool call after it. A caller is waiting out every one of them, so a tool that cannot answer in five seconds should not be a tool.
- A server that is down does not fail the call. The agent starts without your server’s tools and the failure is logged. From the model’s side the tools are simply not in its list, and it will improvise around a prompt that still talks about them.
Regex patterns in your schemas are dropped
One transformation happens to your tool schemas on the way to the model, and it is worth knowing because it silently weakens them. Apattern that uses lookahead or lookbehind — (?=, (?!, (?<=, (?<! —
is removed from the schema, wherever it appears, before the tools are sent.
OpenAI compiles JSON Schema pattern with a regex engine that has no lookaround
and rejects the entire request when it meets one. Tools go up as a single array,
so one such pattern from one server would kill every turn the agent takes,
including every other tool in the request.
The keyword is dropped and the tool is kept, because pattern only tells the
model what shape to emit — and the alternative is an agent that answers nothing at
all. Patterns without lookaround are left alone.
So do not rely on a pattern to validate an argument. Validate it in your
server and return a message the model can act on. That is where the check belongs
regardless: a schema keyword is a hint to a language model, not an input guard.
Testing it
GET /v1/integrations/{integration_id}/mcp-tools connects to your server and
lists what it exposes. It is a live call, so it is also the connectivity test —
run it before you attach the integration to anything.
Namespacing: the one default that differs
A hosted provider’s tools are prefixed with the provider key —tavily_search, asana_create_task. A custom server gets no prefix at all,
because there is no provider name to derive one from. Its tools reach the model
under exactly the names your server publishes.
That is often what you want, and it is a real hazard when it is not: a server
publishing search, get, create puts three of the most generic names in the
industry into the model’s tool list, next to your own tools and anything else
attached. Set tools_namespace on the integration and they become crm_search,
crm_get, crm_create.
"" is a real value meaning “the server’s own names”, not an unset field — so on
a PATCH, omitting the field keeps the current namespace and sending "" clears
it. The rules in full are on
namespacing.
Defining one inline
An agent config can carry a server definition instead of an integration id:custom_mcp integration and the
entry is rewritten to {"integration_id": "…"} in the stored config. Read the
config back and you will find the id. It is a convenience for building an agent
and its server in one request, not a second kind of attachment: the integration it
made appears on the Integrations page, which is where its tool approval lives and
where you go to rotate the credential.
allowed_tools and tools_namespace may be set inline and carry over to the
integration. The name must be free — an existing integration with that name is a
409 telling you to reference it by id or rename this one.
A worked example
Your server exposessearch_customers, get_order and refund_order. You want
the agent to look people up and read orders, but never issue a refund on its own.
1
Store the credential
Create a workspace secret named
NORTHWIND_MCP_TOKEN holding the bearer token
your server expects. Nothing about the next step creates it for you.2
Create the integration
3
Prove the connection and read the names
GET /v1/integrations/{id}/mcp-tools should come back with three tools, whose
exposed_name values are crm_search_customers, crm_get_order and
crm_refund_order.4
Approve only two
PATCH the integration with the tools’ own names — approval matches those,
not the exposed ones:refund_order is now not in the model’s list at all, on every agent attached
to this integration, from their next turn. There is nothing to republish.5
Attach and publish
Add
{"integration_id": "…"} to the agent’s mcps and publish. The
attachment is config, so it takes a publish; the approval above is not, and
narrowing it later is immediate.Write the prompt against the exposed names: crm_search_customers, not
search_customers.Next
MCP servers
Attaching, approving and namespacing — the rules every integration shares.
Secrets
Where
{{secrets.NAME}} resolves from, and why headers need one.Tools
Building a tool here instead, when the capability is one HTTP call.