Custom gateway
Introduction
Section titled “Introduction”A custom gateway is a process in an image you build that publishes many utilities (command-line tools, local HTTP services, stdio MCP servers) as one MCP server. An administrator registers it in Connect, and it becomes an ordinary provider named gw-<slug>: it is enabled per tenant, its tools appear in connections_search, users and organisations connect accounts to it, and every call is checked by the sharing policy and written to the audit log.
Connect reaches a gateway in one of two ways:
| Transport | Who starts the connection | Use when |
|---|---|---|
url |
Connect calls the gateway’s public URL | The gateway runs somewhere Connect can reach, such as a server. |
worker |
The gateway polls Connect | The gateway runs on a machine behind NAT, such as your laptop, and has no public URL. |
The contract is packages/chat-protocol/src/connect-gateway.ts. The gateway process that implements it is apps/connect-gateway.
Registering a url gateway
Section titled “Registering a url gateway”Call the admin API with an administrator session or token. The slug becomes the provider id gw-<slug>; it takes a-z, 0-9 and -, starts with a letter, and is at most 31 characters.
curl -X POST "$CONNECT_URL/admin/connections/gateways" \ -H "Authorization: Bearer $CONNECT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "slug": "my-tools", "name": "My tools", "transport": "url", "url": "https://gateway.example.com", "token": "a-long-random-token-of-at-least-16-characters" }'token is the bearer token the gateway expects (GATEWAY_TOKEN on the gateway). Connect stores it sealed in the vault and never returns it. url must be https, except on localhost and 127.0.0.1.
Connect reads the gateway’s manifest and tools right away. If the gateway is not reachable yet, the registration still succeeds with no tools; fix the gateway and call refresh:
curl -X POST "$CONNECT_URL/admin/connections/gateways/gw-my-tools/refresh" \ -H "Authorization: Bearer $CONNECT_TOKEN"Every request to the gateway carries Authorization: Bearer <token>. Connect calls GET <url>/manifest and POST <url>/mcp (streamable HTTP, stateless).
Registering a worker gateway
Section titled “Registering a worker gateway”curl -X POST "$CONNECT_URL/admin/connections/gateways" \ -H "Authorization: Bearer $CONNECT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "slug": "home", "name": "Home tools", "transport": "worker" }'The answer holds the worker token once:
{ "gateway": { "provider": "gw-home", "transport": "worker", "toolCount": 0 }, "workerToken": "gwk_..." }Run the gateway image with the token:
docker run --rm \ -e GATEWAY_MODE=worker \ -e CONNECT_URL=https://connect.example.com \ -e CONNECT_WORKER_TOKEN=gwk_... \ my-gateway-imageThe worker announces its manifest and tools with every poll, so the provider’s tool list follows the image without a refresh.
What users see
Section titled “What users see”The manifest lists the environment variables each utility needs. Connect turns them into the fields of the provider’s credential:
| Manifest variable | Field in the connect form |
|---|---|
secret: true (the default) |
A masked field. |
secret: false |
A plain text field. |
provided: true |
Optional: the gateway has a value of its own, and a value entered here replaces it for that account. |
| any other | Required. |
Each tool becomes gw-<slug>__<tool>. A tool whose MCP annotations say readOnlyHint: true has read access; every other tool has write access. Its JSON Schema is shown to the model as the gateway published it, and the gateway validates the arguments.
The administrator then enables the provider under Admin → Apps, and users or the organisation connect an account, as for any provider with several fields.
The credential values are sent with each call, never stored by the gateway: in the x-connect-env header as base64url JSON for a url gateway, and in the env of the job for a worker. They are not written to logs and are scrubbed from tool errors when the field is secret.
Admin routes
Section titled “Admin routes”All routes need a role in CONNECTIONS_ADMIN_ROLES and act on the caller’s tenant. Every change is written to the audit log as config_changed.
| Route | Purpose |
|---|---|
GET /admin/connections/gateways |
{ gateways }: each with provider, transport, url, hasToken, manifest, toolCount, lastSeenAt. |
POST /admin/connections/gateways |
Register. Body slug, name, description?, transport, and for url also url and token. Answers 201 with { gateway, workerToken? }. 409 when the slug is taken. |
PUT /admin/connections/gateways/:provider |
Change name, description, url or token. The slug and the transport cannot change. An absent token keeps the stored one. |
DELETE /admin/connections/gateways/:provider |
Remove. 204, or 409 while the provider is enabled. |
POST /admin/connections/gateways/:provider/refresh |
url gateways: read the manifest and tools again. |
POST /admin/connections/gateways/:provider/worker-token |
worker gateways: issue a new token, shown once. |
A gateway belongs to the tenant that registered it. No other tenant sees its provider or can run its tools.
The worker API
Section titled “The worker API”A worker authenticates with Authorization: Bearer <worker token> and nothing else: these routes take no user session. Each token belongs to one gateway of one tenant, and any other token is answered 401.
POST /gateway/worker/poll
{ "manifest": { "name": "my-tools", "version": "1", "env": [{ "name": "TELEGRAM_API_ID", "secret": false, "provided": false }] }, "tools": [{ "name": "telegram_fetch", "description": "Read recent messages", "inputSchema": { "type": "object" }, "annotations": { "readOnlyHint": true } }], "waitMs": 25000}Connect stores the manifest and tools, then holds the request for up to waitMs (at most 30 000, default 25 000). It answers:
200with one job when a tool call arrives.204when none came.
{ "id": "5d0f...", "tool": "telegram_fetch", "arguments": { "chat": "general" }, "env": { "TELEGRAM_API_ID": "123456" }, "deadline": "2026-06-01T12:00:27.000Z"}Poll again straight away after either answer. Poll with several requests at once to run jobs in parallel; each job goes to one request.
Return a result
Section titled “Return a result”POST /gateway/worker/jobs/:id/result
{ "content": [{ "type": "text", "text": "{\"messages\":[]}" }], "isError": false }This is the MCP tools/call result. Connect answers 204. It answers 404 when the job is unknown, belongs to another gateway, or is past its deadline: a result sent late is refused.
Timeouts and offline workers
Section titled “Timeouts and offline workers”| Situation | The tool call fails with |
|---|---|
| No worker polled in the last 60 seconds | The gateway worker is offline, immediately. |
A job is not taken or not answered before its deadline (just under CONNECTIONS_TOOL_TIMEOUT_MS) |
The gateway worker did not answer in time. |
Connect keeps the queue of a worker gateway in the memory of one process, so run one Connect instance for it.
Next steps
Section titled “Next steps”- Pilotoo Connect for running the service.
- Agent tools for how the model finds and calls a gateway tool.
- Server integration for the other admin routes and error codes.