Model Platform
Register, secure, and govern the external MCP servers your agents connect to.
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 ChalkSQL, inspect your deployment). The MCP Gateway sits in front of other MCP servers and governs your agents' access to them.
The gateway runs as a service in your Chalk environment, alongside the engine. To deploy it, open the Cloud Resource Configuration pane, add MCP Gateway to a resource group from the Add Config menu, where it is listed under Advanced, set its resource request, and choose Save and Apply Service. See Resource Configuration for how services and resource groups work.
The gateway is unavailable in an environment until the service is added, so deploy it before registering backends.
Register and manage backend servers from MCP gateway → MCP servers in the Chalk dashboard. Each server has:
| Field | Description |
|---|---|
| Name | A unique name for the backend. |
| Transport | HTTP for a remote server (set a URL), or stdio for a local process (set a command and args). |
| Credential | The 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 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:
| Kind | Use for |
|---|---|
| Bearer token | A server that accepts a static Authorization: Bearer token. |
| Headers | A server that needs one or more custom HTTP headers. |
| Environment variables | A 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.
An environment reaches the gateway one of two ways, depending on how it is provisioned. Ask your Chalk contact which applies to yours.
| Provisioning | Endpoint |
|---|---|
| Shared gateway | https://<shared-gateway-host>/mcpeg |
| Gateway deployed in your own environment | https://<environment-id>.<cluster-dns-zone>/mcp |
Your environment id is the one chalk config reports. The DNS zone is the one your Chalk cluster
runs under.
Authenticate with a service token, which you can mint with
chalk token get, and name the environment on every request:
| Header | Value |
|---|---|
Authorization | Bearer <CHALK_ACCESS_TOKEN> |
X-Chalk-Env-Id | Your environment id |
Requests that go directly to an environment’s own gateway need one more header,
X-Chalk-Deployment-Type: mcp-gateway, which selects the gateway among the services running in
that cluster. Without it the request never reaches the gateway and returns a 404.
Confirm the gateway answers before you configure a client:
curl -sS https://<environment-id>.<cluster-dns-zone>/health \
-H "X-Chalk-Env-Id: <environment-id>" \
-H "X-Chalk-Deployment-Type: mcp-gateway"A running gateway reports its status and the backends registered with it:
{"backends":["chalk"],"status":"ok"}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.
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
# Refuse repository deletion for everyone except the platform owner.
deny contains msg if {
input.request.method == "tools/call"
input.gateway.upstream_tool == "delete_repo"
input.auth.subject != "platform-owner@example.com"
msg := sprintf("user %v is not permitted to call delete_repo", [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.
Every evaluation receives one input document describing the request:
| Path | Description |
|---|---|
input.auth.subject | The user making the request. |
input.auth.scopes | The scopes that user holds. |
input.gateway.backend | The registered backend the request is bound for. |
input.gateway.upstream_tool | The tool name on that backend, for tool calls. |
input.gateway.upstream_uri | The resource URI on that backend, for resource reads. |
input.gateway.upstream_prompt | The prompt name on that backend, for prompt gets. |
input.request.method | tools/call, resources/read, or prompts/get. |
input.request.params.arguments | The call’s arguments, for tool calls and prompt gets. |
input.schema_version | The version of this document’s shape. Currently 1. |
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.
Prefer the input.gateway fields over 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.
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])
}Protect a set of resources:
package chalk.policy
deny contains msg if {
input.request.method == "resources/read"
startswith(input.gateway.upstream_uri, "bigquery://project/dataset/private_")
not endswith(input.auth.subject, "@example.com")
msg := sprintf("user %v cannot read private tables", [input.auth.subject])
}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. A policy that fails to deny something you expected usually has a field name wrong, since an unmatched rule fails silently.
The gateway records every operation it handles: tool calls, resource reads, and prompt fetches. The recent audit feed shows, for each one:
| Column | Description |
|---|---|
| Time | When the event was recorded. |
| Agent | The agent, or user, that made the request. |
| Operation | tools/call, resources/read, or prompts/get. |
| Target | The namespaced tool, resource URI, or namespaced prompt the operation addressed. |
| Backend | The registered server the request was bound for. |
| Policy | What your policy recommended: allow, deny, error, or NOT RUN. |
| Outcome | What the gateway did: allow, deny, or error. |
| Policy reason | The messages your policy returned with a deny. Empty on an allow. |
| Arguments | A preview of the call arguments, truncated when long. |
| Duration | How long the call took, in milliseconds. |
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:
deny, Outcome allow: the policy ran in audit mode, which records decisions without
enforcing them.error, Outcome allow: the policy failed to evaluate. The gateway allows the request
rather than blocking on a rule it cannot run.NOT RUN: no policy evaluated the request, which is the case when none is enabled.