The Chalk MCP Gateway is a control plane for the external Model Context Protocol (MCP) servers your agents use. Instead of pointing each agent at a handful of MCP servers directly — each with its own URL, credentials, and trust assumptions — you register those servers once with the gateway. The gateway aggregates their tools, resources, and prompts behind a single connection, holds the credentials, enforces who may call what, and records every operation.

The MCP Gateway is distinct from the MCP Server. The MCP Server exposes Chalk itself to agents (run queries, execute Chalk SQL, inspect your deployment). The MCP Gateway sits in front of other MCP servers and governs your agents’ access to them.


​
Deploying the gateway

The gateway runs as a service in your Chalk environment, alongside the engine. Chalk deploys it the first time you register an MCP server in an environment, and adds it to that environment’s default resource group. Registering the first server takes a few minutes while the gateway starts. If the gateway is still starting when the request times out, registration fails with MCP Gateway is still starting; try the connection again shortly. Register the server again.

Registering the first server requires the deploy.create permission, which the built-in Admin and Owner roles have. Chalk turns on the MCP Gateway for each environment. If MCP Gateway does not appear in the dashboard, contact Chalk support.

To change the gateway’s resource request after it is deployed, edit the MCP Gateway service in Infrastructure → Resources. See Resource Configuration for how services and resource groups work.


​
Registering MCP servers

Register and manage backend servers from MCP Gateway → MCP servers in the Chalk dashboard. Each server has:

FieldDescription
NameA unique name for the backend.
TransportHTTP for a remote server (set a URL), or stdio for a local process (set a command and args).
CredentialThe credential used to connect (see Credentials).

Once a server is registered and connected, the dashboard shows its connection status and lets you browse everything it exposes: its tools (with input schemas), resources (with URI, MIME type, and description), and prompts (with arguments). You can also call a tool directly from the UI to verify it works.


​
Credentials

Credentials are stored with the gateway, not with the agents, so secrets never have to be distributed to clients. The gateway supports several credential kinds to match how each backend authenticates:

KindUse for
Bearer tokenA server that accepts a static Authorization: Bearer token.
HeadersA server that needs one or more custom HTTP headers.
Environment variablesA stdio server that reads secrets from its environment.
OAuth (user)A server behind an OAuth 2.0 user flow (authorize/token URLs, client ID/secret, scopes).
OAuth (DCR)A server that supports OAuth Dynamic Client Registration (issuer + scopes).

For OAuth-backed servers, each user links their own account, so calls run with that user’s identity rather than a shared credential. Callers that are not users, such as service tokens, cannot link an account or call these servers.


​
Connecting an agent

Agents connect to the gateway over MCP’s Streamable HTTP transport at your Chalk API host:

https://<your-chalk-api-host>/v1/mcp

For Chalk’s hosted metadata plane this is https://api.chalk.ai/v1/mcp. Every request carries X-Chalk-Env-Id, set to the environment id that chalk config reports, and one of two credentials:

CredentialHeadersLifetime
Access tokenAuthorization: Bearer <token>, where the token is the output of chalk token getUp to 8 hours
Client credentialsX-Chalk-Client-Id and X-Chalk-Client-Secret, from chalk config or a service tokenUntil the credentials are revoked

Run chalk login once before chalk token get or chalk config. Both return credentials that identify you, so the gateway applies your permissions and its authorization policies to every call the agent makes. When a request carries an Authorization header, the gateway uses it and ignores the client credential headers.

The MCP Gateway → MCP servers page in the Chalk dashboard has an Add to… menu that generates this configuration for the current environment, for Claude Code, Codex, Cursor, Zed, and stdio-based clients. For Claude Code, the generated commands are:

export CHALK_USER_TOKEN="$(chalk token get)"

claude mcp add --transport http chalk-gateway 'https://<your-chalk-api-host>/v1/mcp' \
  --header "Authorization: Bearer $CHALK_USER_TOKEN" \
  --header 'X-Chalk-Env-Id: <environment-id>'

Confirm the gateway answers before you configure a client by sending it an MCP initialize request:

curl -sS https://<your-chalk-api-host>/v1/mcp \
  -H "Authorization: Bearer $(chalk token get)" \
  -H "X-Chalk-Env-Id: <environment-id>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

A running gateway returns a JSON-RPC response whose result contains protocolVersion, capabilities, and serverInfo. A 401 means the credentials are missing, expired, or rejected. A response reading MCP gateway is not available for this environment means no gateway is deployed in that environment.

​
Connecting with a service token

To run an agent that no person is logged in for, such as a CI job or a scheduled task, connect it with a service token’s client credentials. Create the token as described in Creating a service token in the dashboard, and give it only the permissions the agent needs.

For Claude Code, add the gateway to the project’s .mcp.json:

{
  "mcpServers": {
    "chalk-gateway": {
      "type": "http",
      "url": "https://<your-chalk-api-host>/v1/mcp",
      "headers": {
        "X-Chalk-Client-Id": "${CHALK_CLIENT_ID}",
        "X-Chalk-Client-Secret": "${CHALK_CLIENT_SECRET}",
        "X-Chalk-Env-Id": "<environment-id>"
      }
    }
  }
}

Claude Code expands ${...} references in .mcp.json when it starts, so the secret stays out of the file. Export CHALK_CLIENT_ID and CHALK_CLIENT_SECRET in the environment the agent runs in.

A call made with a service token runs as that token, not as any user:

  • The gateway applies the token’s permissions.
  • In a policy, input.auth.kind is service_token and input.auth.subject is the token’s ID. The audit log records that ID as the agent.
  • The token cannot call a server whose credential is OAuth (user) or OAuth (DCR). A tool call returns the error backend '<name>' requires a linked user account; service tokens cannot call it.
  • Client credentials stay valid until you revoke the token, so store the secret the way you store other long-lived secrets.

Create one service token per agent. The audit log then names the agent behind each call, and revoking a token disconnects only that agent.


​
Authorization policies

Policies decide which users and agents may call which tools, and with which arguments, independent of the backend itself. You write them in Rego, Open Policy Agent’s policy language. A policy has a name, its Rego source, and an enabled flag, so you can keep a policy in place and turn it on or off without deleting it.

Beyond per-tool policies, you can scope an agent to a fixed set of allowed backends, so a given agent only reaches the servers it is meant to use.

​
Writing a policy

The gateway evaluates data.chalk.policy.deny. Write your rules in a chalk.policy package, returning a message for each reason a request should be refused:

package chalk.policy

# Only the platform owner may merge pull requests. Replace
# <platform-owner-user-id> with that user's Chalk user ID.
deny contains msg if {
	input.request.method == "tools/call"
	input.gateway.upstream_tool == "merge_pull_request"
	input.auth.subject != "<platform-owner-user-id>"
	msg := sprintf("user %v is not permitted to call merge_pull_request", [input.auth.subject])
}

Policies are deny-only. A request proceeds when no deny rule matches, so a policy lists what to refuse rather than what to permit, and an allow rule has no effect. Each message a rule produces becomes one of the decision’s reasons, and those reasons appear in the audit log.

​
What a policy can read

Every evaluation receives one input document describing the request:

PathDescription
input.auth.subjectWho made the request: a Chalk user ID for a user token, the service token’s ID for a service token, or the mapped agent name for a token from a trusted issuer. For Chalk users it is the user ID, not an email address.
input.auth.kindHow the caller authenticated: user for a Chalk user, service_token for a service token, trusted_issuer for a token from a trusted issuer, or other for any other Chalk identity.
input.auth.scopesThe scopes that user holds.
input.gateway.backendThe registered backend the request is bound for.
input.gateway.upstream_toolThe tool name on that backend, for tool calls.
input.gateway.upstream_uriThe resource URI on that backend, for resource reads.
input.gateway.upstream_promptThe prompt name on that backend, for prompt gets.
input.request.methodtools/call, resources/read, or prompts/get.
input.request.params.argumentsThe call’s arguments, for tool calls and prompt gets.
input.schema_versionThe version of this document’s shape. Currently 1.

To find the ID a rule should compare against, open the policy simulator and choose a team member or service token from its User list. The simulator shows the input.auth.subject value it sends.

Policies cover resource reads and prompt gets as well as tool calls. Match on input.request.method when a rule should apply to only one of them.

Match against the input.gateway fields instead of their equivalents under input.request.params. The gateway namespaces names and encodes URIs before passing them upstream, so a resource read arrives with input.gateway.upstream_uri set to bigquery://project/dataset/table while input.request.params.uri holds chalk-mcp://resource/v1/<backend>/bigquery%3A%2F%2F.... Matching the encoded form works but is easy to get wrong, and a rule that fails to match allows the request.

​
More examples

Restrict a tool by its arguments:

package chalk.policy

# The SQL tool may only run reads.
deny contains msg if {
	input.gateway.upstream_tool == "execute_sql_query"
	not startswith(lower(trim_space(input.request.params.arguments.query)), "select")
	msg := "execute_sql_query is limited to SELECT statements"
}

Require a scope for writes:

package chalk.policy

deny contains msg if {
	input.gateway.upstream_tool in {"create_issue", "update_issue"}
	not "write" in input.auth.scopes
	msg := sprintf("%v requires the write scope", [input.gateway.upstream_tool])
}

Keep a tool for people only:

package chalk.policy

# Only Chalk users may delete branches. Service tokens and other callers may not.
deny contains msg if {
	input.gateway.upstream_tool == "delete_branch"
	input.auth.kind != "user"
	msg := sprintf("%v is limited to users", [input.gateway.upstream_tool])
}

Protect a set of resources:

package chalk.policy

# Only these users may read the private tables.
private_table_readers := {"<user-id-1>", "<user-id-2>"}

deny contains msg if {
	input.request.method == "resources/read"
	startswith(input.gateway.upstream_uri, "bigquery://project/dataset/private_")
	not input.auth.subject in private_table_readers
	msg := sprintf("user %v cannot read private tables", [input.auth.subject])
}

​
Simulating before you enable

Simulate a policy against a hypothetical request before turning it on. Supply a user, scopes, backend, tool, and arguments, and the gateway returns the decision it would reach along with its reasons. The backend, tool, prompt, and resource fields list what your registered MCP servers expose, and the simulator warns when a backend, tool, or prompt you enter is not among them. A policy that fails to deny something you expected usually has a field name or tool name wrong, since an unmatched rule fails silently.


​
Audit log

The gateway records every operation it handles: tool calls, resource reads, and prompt fetches. To see them, open MCP Gateway → Policies in the Chalk dashboard and select the Audit tab. The feed refreshes every 5 seconds, and scrolling down loads older events. For each operation, it shows:

ColumnDescription
TimeWhen the event was recorded.
AgentThe agent, or user, that made the request.
Operationtools/call, resources/read, or prompts/get.
TargetThe namespaced tool, resource URI, or namespaced prompt the operation addressed.
BackendThe registered server the request was bound for.
PolicyWhat your policy recommended: allow, deny, error, or NOT RUN.
OutcomeWhat the gateway did: allow, deny, or error.
Policy reasonThe messages your policy returned with a deny. Empty on an allow.
ArgumentsA preview of the call arguments, truncated when long.
DurationHow long the call took, in milliseconds.

​
Policy and outcome

Policy is what your policy recommended. Outcome is what the gateway did. A request can be allowed despite a deny recommendation, so the two are recorded separately.

Three combinations have specific meanings:

  • Policy deny, Outcome allow: the policy ran in audit mode, which records decisions without enforcing them.
  • Policy error, Outcome allow: the policy failed to evaluate. The gateway allows the request rather than blocking on a rule it cannot run.
  • Policy NOT RUN: no policy evaluated the request, which is the case when none is enabled.

​
See also

  • MCP Server: connect agents to your Chalk deployment over MCP.
  • Model Gateway: OpenAI-compatible LLM routing for your Chalk deployment.