MAQPNADocs

Connect MCP servers and pin tools

Put your own MCP servers behind the MAQPNA gateway, locally and with the MCPServer resource, and pin tool definitions so a changed tool is hidden and denied.

flowchart LR
    A["register the server<br/>--upstream or MCPServer"] --> B["agent calls<br/>{gateway}/mcp/NAME"]
    B --> C["gateway hashes each<br/>tool definition"]
    C --> D["maqpna mcp tools NS/NAME"]
    D --> E["maqpna mcp pin NS/NAME<br/>--mode enforce"]
    E --> F["toolPins in ToolPolicy<br/>or MCPServer"]
    F --> G{"definition<br/>changed?"}
    G -->|no| H["pinned: visible, callable"]
    G -->|yes| I["drift: hidden and denied<br/>tool_definition_changed"]

Goal#

Connect the Model Context Protocol (MCP) servers your agents use so that every call to them goes through the MAQPNA gateway, then pin each tool's definition. With pins in enforce mode, a tool whose definition changed after it was pinned (drift, also called a "rug pull") is hidden from the agent and its calls are denied until someone reviews it.

List MCP tools and export pins.cast

Prerequisites#

  • A local MAQPNA (maqpna dev up) for steps 1โ€“5, or an installation and an admin token or maqpna login for steps 6โ€“7.
  • An MCP server that speaks Streamable HTTP. Only the gateway talks to it; agents never get its URL or its credentials.

How it works#

Where How you register a server URL the agent uses Credentials
Local MAQPNA maqpna dev up --upstream NAME=URL (repeatable) MAQPNA_TOOL_<NAME>_URL = {gateway}/mcp/NAME none
Cluster an MCPServer resource in the agent's namespace, referenced from Agent.spec.tools the same, set by the operator spec.auth: the gateway injects a bearer token, a header, OAuth client credentials, a token exchange, mTLS or the user's own token. The sandbox never sees them.

A session can call a server only with the scope tools:<name> in its token. maqpna dev run grants tools: for every registered server by default; in a cluster the operator grants the servers listed in the agent's spec.tools.

Steps#

1. Add a server to a local MAQPNA#

Start the local MAQPNA with your server next to the built-in echo:

maqpna dev up --upstream tickets=http://127.0.0.1:3001/mcp
local MAQPNA is up (~/maqpna-demo/.maqpna)
  gateway   http://127.0.0.1:8080
  broker    http://127.0.0.1:8081
  mcp       echo -> http://127.0.0.1:8090/mcp
  mcp       tickets -> http://127.0.0.1:3001/mcp
next: maqpna dev run -- <your agent command>

Your agent gets one URL per server, all on the gateway:

maqpna dev run -- env | grep '^MAQPNA_TOOL_'
MAQPNA_TOOL_ECHO_URL=http://127.0.0.1:8080/mcp/echo
MAQPNA_TOOL_TICKETS_URL=http://127.0.0.1:8080/mcp/tickets

Call a tool of the new server through the gateway:

maqpna dev run -- maqpna call --server tickets --list
maqpna dev run -- maqpna call --server tickets --tool get_time

The dev policy decides these calls like any other: get_*, list_*, search_*, read_* and describe_* tools are allowed, create_*, update_*, patch_*, comment_*, post_* and send_* are allowed with a rate limit, delete_* and similar tools need approval, and anything else is denied (default_action of default-agent-policy). Use --no-echo to leave out the test server.

2. See the servers and the observed tools#

The gateway hashes the canonical definition (name, description, input schema) of every tool it sees in a tools/list answer. List the servers and the tools observed for namespace dev:

eval "$(maqpna dev env)"
maqpna dev run -- maqpna call --server echo --list > /dev/null
maqpna mcp list
maqpna mcp tools dev/echo
NAME  SOURCE  URL                        NAMESPACES  TRUST  PIN MODE  AUTH
echo  static  http://127.0.0.1:8090/mcp
TOOL             STATUS    SHA256                                                                   VISIBLE  CALLABLE  CHANGES  LAST SEEN
delete_resource  unpinned  sha256:69e35d14f2c8c5a052a79fd54f0df65647f780f52817727d69cfc226c5acbf9e  true     true               2026-10-03T00:12:49.20766-04:00
echo             unpinned  sha256:03929cef4ba64f8f797c5e686cf573d21f064d1358624b55e34d8974fc24bf5c  true     true               2026-10-03T00:12:49.207601-04:00
get_time         unpinned  sha256:125013379d07b081d5c5fa8a067d555796995f9354ebf11e4182a94aa470a0e8  true     true               2026-10-03T00:12:49.207633-04:00
trace_context    unpinned  sha256:d5c2336dccff9b41702083bc7d2776d969247ae715dbdba0c58369d494da6cfb  true     true               2026-10-03T00:12:49.207678-04:00

NS/NAME is the namespace of the sessions that called the server and the server name. Servers registered with --upstream or in the gateway configuration show SOURCE static; MCPServer resources show their namespace, trust and pin mode.

3. Export the pins#

maqpna mcp pin dev/echo --mode enforce --out pins.yaml
cat pins.yaml
wrote pins.yaml
# ToolPolicy spec fragment for dev/echo, exported 2026-10-03T04:12:59Z
toolPins:
  - server: "echo"
    mode: enforce
    onChange: hide
    pins:
      "delete_resource": "sha256:69e35d14f2c8c5a052a79fd54f0df65647f780f52817727d69cfc226c5acbf9e"
      "echo": "sha256:03929cef4ba64f8f797c5e686cf573d21f064d1358624b55e34d8974fc24bf5c"
      "get_time": "sha256:125013379d07b081d5c5fa8a067d555796995f9354ebf11e4182a94aa470a0e8"
      "trace_context": "sha256:d5c2336dccff9b41702083bc7d2776d969247ae715dbdba0c58369d494da6cfb"

The default target is a ToolPolicy (spec.toolPins is a list, one entry per server). --target mcpserver writes the block for MCPServer.spec.toolPins instead (one server, no server: key). --first exports the first-seen hashes instead of the current ones, which is what you want when you suspect a tool already changed.

Field Values
mode learn (default: record and audit, never block), enforce, off
onChange In enforce mode: hide (default: drop the tool from tools/list and deny its calls), approve (require approval for its calls), alert (allow and audit)
allowUnpinned true lets tools without a pin through in enforce mode
pins Tool name to sha256:<hex>

When a ToolPolicy and an MCPServer both pin the same server, the strictest setting wins: enforce over learn over off, and hide over approve over alert.

4. Enforce the pins locally#

The local gateway reads .maqpna/policies.json (policy bundle JSON) and reloads it every 2 seconds. Add the block from step 3, in JSON form, to the default-agent-policy entry:

"toolPins": [{"server": "echo", "mode": "enforce", "onChange": "hide", "pins": {
  "delete_resource": "sha256:69e35d14f2c8c5a052a79fd54f0df65647f780f52817727d69cfc226c5acbf9e",
  "echo": "sha256:03929cef4ba64f8f797c5e686cf573d21f064d1358624b55e34d8974fc24bf5c",
  "get_time": "sha256:125013379d07b081d5c5fa8a067d555796995f9354ebf11e4182a94aa470a0e8",
  "trace_context": "sha256:d5c2336dccff9b41702083bc7d2776d969247ae715dbdba0c58369d494da6cfb"}}]

To keep your own policy file across restarts, save the bundle elsewhere and start with maqpna dev up --policy FILE. --policy takes bundle JSON, not ToolPolicy YAML.

5. See drift being stopped#

To see what drift looks like without changing a real server, pin a hash for get_time that does not match (here, all zeros). This is exactly what the gateway sees when the server changes the tool's definition after it was pinned:

maqpna dev run -- maqpna call --server echo --list | grep '"name"'
maqpna dev run -- maqpna call --server echo --tool get_time
maqpna mcp tools dev/echo --drift-only
      "name": "echo"
      "name": "delete_resource"
maqpna dev: session dev-949050aa (agent coder, namespace dev, on behalf of you@localhost)
error -32001: denied by policy rule pin/echo/get_time: tool_definition_changed
data: {"domain":"maqpna.com","policy":"","reason":"tool_definition_changed","rule":"pin/echo/get_time"}
maqpna dev: dev-949050aa: 1 deny, cost $0.0000 (maqpna dev timeline --last)
TOOL      STATUS   SHA256                                                                   VISIBLE  CALLABLE  CHANGES  LAST SEEN
get_time  changed  sha256:125013379d07b081d5c5fa8a067d555796995f9354ebf11e4182a94aa470a0e8  false    false              2026-10-03T00:07:32.955965-04:00

get_time is gone from tools/list, and a direct call is denied with reason tool_definition_changed and rule pin/echo/get_time. The SDKs raise PolicyDenied for it. After you review the new definition, export the pins again and replace the old ones.

6. Register a server in a cluster#

Scaffold an MCPServer, edit it, and validate it offline:

maqpna init mcpserver github --namespace team-a > github.yaml
apiVersion: maqpna.com/v1alpha1
kind: MCPServer
metadata:
  name: github
  namespace: team-a
spec:
  url: https://mcp-github.tools.acme.eu/mcp
  transport: streamable-http
  protocolVersions: ["2025-11-25"]
  trust: trusted
  dataClass: internal
  jurisdiction: EU-DE
  timeoutSeconds: 60
  auth:
    type: bearerSecret
    secretRef:
      name: github-mcp
      key: token
maqpna validate -f github.yaml
1 file(s), 1 object(s): 0 error(s), 0 warning(s)

The fields that matter for governance:

Field Effect
auth bearerSecret, headerSecret (with headerName), oauthClientCredentials, tokenExchange (a down-scoped token from the identity broker for each session), mtls, or perUser with a provider (the user's own OAuth token from the gateway's token vault). The gateway injects the credential; the agent never sees it.
trust untrusted taints every session that reads from the server with untrusted-input, unless a toolLabels entry says otherwise. See Taint and prompt-injection containment.
dataClass confidential or restricted adds the session label data-class:<class> for policies to match.
jurisdiction Checked against the sovereignty policy. See Sovereignty.
dlpProfile The data loss prevention (DLP) profile for this server's arguments and results; none turns DLP off for it. See DLP profiles.
shared, allowedNamespaces Let agents in other namespaces use the server.
toolPins Pins for this server, as exported with --target mcpserver.

Reference the server from the agent:

spec:
  tools:
    - name: github
      serverRef: { name: github }

Apply both with maqpna apply -f github.yaml -f agent.yaml.

7. Pin in a cluster#

Run the agent once so the gateway observes the tools, then export and commit the pins, usually into the agent's ToolPolicy:

maqpna mcp tools team-a/github
maqpna mcp pin team-a/github --mode enforce --out pins.yaml

Paste the toolPins block into the ToolPolicy spec and apply it. The operator copies the gateway's view into MCPServer.status (observedTools, driftedTools and the Pinned condition), and the console's MCP servers page shows the same. Check for drift any time with maqpna mcp tools team-a/github --drift-only.

A good rollout is learn first (drift is recorded and audited, never blocked), then enforce with onChange: approve for a period, then onChange: hide.

Verify#

  • maqpna mcp tools NS/NAME shows every tool as pinned.
  • maqpna mcp tools NS/NAME --drift-only prints nothing (or only the tools you expect).
  • A changed tool returns tool_definition_changed to the agent.

Troubleshooting#

Symptom Cause Fix
no observed tools from maqpna mcp tools No session in that namespace has listed the server's tools yet, or NS is wrong. Run the agent (or maqpna call --server NAME --list) first, and use the session's namespace, for example dev/echo locally.
โœ— expected NAMESPACE/NAME, got echo mcp tools and mcp pin need NS/NAME. Use dev/echo.
Every tool of a new server is denied with reason default_action (or no_applicable_policy when no policy covers the agent) No rule allows the server's tools. Add a rule, and test it with maqpna policy test (see Write policies).
A tool is missing from tools/list It drifted under onChange: hide, or policy does not let the session see it. maqpna mcp tools NS/NAME --drift-only, then maqpna policy test.
HTTP 401 from maqpna mcp ... No admin credential. eval "$(maqpna dev env)" locally, or maqpna login.
The agent gets unauthorized for one server The token has no tools:<name> scope. Locally, drop custom --scope flags; in a cluster, list the server in Agent.spec.tools.

Next steps#