MAQPNADocs

Call tools over MCP

List and call tools through the MAQPNA gateway from Python, TypeScript, Go or raw JSON-RPC over HTTP, read the result, and work with pinned tools.

A tool is a function an MCP server offers over the Model Context Protocol (MCP). Your agent never connects to the MCP server itself. It sends each request to the MAQPNA gateway at {gateway}/mcp/{server}, which checks it, forwards it with the real credential, and returns the result or a decision. This page shows the same three steps in every language: list the tools, call one, read the result.

sequenceDiagram
    participant A as Agent
    participant G as Gateway /mcp/echo
    participant S as MCP server echo
    A->>G: tools/list
    G->>S: tools/list (gateway credential)
    S-->>G: all tools
    G-->>A: tools this session may call<br/>(filtered by policy and pins)
    A->>G: tools/call echo {text: "hi"}
    G->>G: kill switch, DLP, scope, pin, policy, budget
    G->>S: tools/call
    S-->>G: result
    G->>G: DLP on the result, taint, meter, audit
    G-->>A: result

Before you start#

  • A local MAQPNA: maqpna dev up (see Set up your environment). The examples call the test server echo.
  • To call your own MCP server locally, add it: maqpna dev up --upstream github=http://127.0.0.1:9000/mcp. It is then MAQPNA_TOOL_GITHUB_URL and server github in policy rules.
  • The SDK for your language: pip install kosa8-maqpna or npm install @kosa8/maqpna (Go: from the source tree; see Python, TypeScript, Go).

Step 1: List the tools#

from maqpna import MaqpnaClient

client = MaqpnaClient.from_env()
echo = client.tools("echo")
for tool in echo.list_tools():
    print(tool.name, "-", tool.description)
import { MaqpnaClient } from "@kosa8/maqpna";

const client = MaqpnaClient.fromEnv();
const echo = client.tools("echo");
for (const t of await echo.listTools()) console.log(t.name, "-", t.description);
client, err := maqpna.NewFromEnv(ctx)
if err != nil {
    return err
}
tools, err := client.ListTools(ctx, "echo") // follows nextCursor
if err != nil {
    return err
}
for _, t := range tools {
    fmt.Println(t.Name, "-", t.Description)
}
curl -sS "$MAQPNA_TOOL_ECHO_URL" \
  -H "Authorization: Bearer $MAQPNA_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Run it under the local stack:

maqpna dev run -- python list_tools.py

The list is filtered for this session: tools that policy would never let it call, and tools whose definition drifted from its pin, are left out. A missing tool is a policy question, not a bug in your code.

Step 2: Call a tool#

result = echo.call("echo", text="hello")                   # keyword arguments
result = echo.call_tool("echo", {"text": "hello"})          # or a dict, with per-call options
print(result.text)
const result = await echo.call("echo", { text: "hello" });
console.log(result.text);
res, err := client.CallTool(ctx, "echo", "echo", map[string]any{"text": "hello"})
if err != nil {
    return err
}
fmt.Println(res.Text())
curl -sS "$MAQPNA_TOOL_ECHO_URL" \
  -H "Authorization: Bearer $MAQPNA_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}'
{"jsonrpc":"2.0","id":2,"result":{"content":[{"text":"hi","type":"text"}],"isError":false}}

For a quick test without code, use maqpna call: maqpna dev run -- maqpna call --server echo --tool echo --arg text=hello. It exits 0 on a result, 3 when the gateway refuses the call or the tool reports isError.

Step 3: Read the result#

A result has MCP content blocks, optional structured content and an error flag. isError: true means the tool ran and reported a failure: it is a result, not an exception, and it was recorded as allow.

Field Python ToolResult TypeScript ToolResult Go *ToolResult
Joined text content .text .text .Text()
Content blocks .content .content .Content
structuredContent .structured .structured .Structured, .DecodeStructured(&v)
isError .is_error .isError .IsError

A refused call is not a result. The SDKs raise or return a typed error (PolicyDenied, ApprovalPending, Unauthorized, ...), and curl gets a JSON-RPC error:

{"jsonrpc":"2.0","id":3,"error":{"code":-32001,"message":"denied by policy baseline-guardrails rule never-touch-system-namespaces: system namespaces are off-limits to agents","data":{"detail":"system namespaces are off-limits to agents","domain":"maqpna.com","policy":"baseline-guardrails","reason":"policy_denied","rule":"never-touch-system-namespaces"}}}

How to handle each one is in Handle decisions in code.

The HTTP contract#

If you write your own client, this is what the gateway expects on POST /mcp/{server}:

Header Required Value
Authorization Yes Bearer <session token>. Without it: HTTP 401, -32003, missing bearer token.
Content-Type Yes application/json
Accept Recommended application/json, text/event-stream. The gateway streams server-sent events (SSE) through when the MCP server answers with them.
MCP-Protocol-Version No 2026-07-28, 2025-11-25, 2025-06-18 or 2025-03-26. Another value gets -32022 with data.supported.
Mcp-Session-Id No Returned by initialize; send it back if your client keeps an MCP session.
X-Maqpna-Async No 1: do not wait for an approval, return -32002 at once.
X-Maqpna-Approval-Id No Retry a held call after it was approved.
traceparent, tracestate No W3C trace context; the gateway continues your trace.

Do not send X-Maqpna-* identity headers such as X-Maqpna-Agent: the gateway strips client-supplied ones and sets its own from the verified token. The initialize handshake is optional; the SDKs send it on the first request. Batches are accepted and every message is governed on its own.

A real initialize exchange with the local echo server:

POST /mcp/echo HTTP/1.1
Authorization: Bearer <session token>
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-11-25

{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}
HTTP/1.1 200 OK
Content-Type: application/json
Mcp-Session-Id: 964702112364e54eee25a30772de7773

{"id":0,"jsonrpc":"2.0","result":{"capabilities":{"tools":{"listChanged":false}},"protocolVersion":"2025-11-25","serverInfo":{"name":"maqpna-mcp-echo","version":"0.2.0"}}}

The SDKs pick the protocol revision with MAQPNA_MCP_PROTOCOL (auto by default, falling back on -32022).

Pinned tools#

Tool pinning protects you from a tool whose definition changes after you reviewed it (drift). The gateway records a hash of each tool's definition; a pin is that hash. With pins in enforce mode, a drifted, unpinned or unseen tool is hidden from tools/list and denied with tool_definition_changed, tool_unpinned or tool_definition_unverified.

See what the gateway has observed for a server:

eval "$(maqpna dev env)"
maqpna mcp tools dev/echo
TOOL             STATUS    SHA256                                                                   VISIBLE  CALLABLE  CHANGES  LAST SEEN
delete_resource  unpinned  sha256:69e35d14f2c8c5a052a79fd54f0df65647f780f52817727d69cfc226c5acbf9e  true     true               2026-10-18T17:31:42.147251-04:00
echo             unpinned  sha256:03929cef4ba64f8f797c5e686cf573d21f064d1358624b55e34d8974fc24bf5c  true     true               2026-10-18T17:31:42.147174-04:00

Export them as a toolPins block to paste into your ToolPolicy (or an MCPServer with --target mcpserver):

maqpna mcp pin dev/echo --mode enforce --out pins.yaml
toolPins:
  - server: "echo"
    mode: enforce
    onChange: hide
    pins:
      "delete_resource": "sha256:69e35d14f2c8c5a052a79fd54f0df65647f780f52817727d69cfc226c5acbf9e"
      "echo": "sha256:03929cef4ba64f8f797c5e686cf573d21f064d1358624b55e34d8974fc24bf5c"

In your code, treat tool_definition_changed as "stop and tell a human": the tool you planned to call is not the one you reviewed. maqpna mcp tools NS/NAME --drift-only lists only drifted tools. More in MCP servers and tool pinning.

What can go wrong#

Symptom Reason Fix
Tool missing from the list Policy or a pin hides it. maqpna policy test --gateway "$MAQPNA_GATEWAY_URL" --ns dev --agent coder --server echo --tool NAME; maqpna mcp tools dev/echo.
HTTP 404 unknown_server No server of that name for this namespace. Check the name with maqpna mcp list; locally add --upstream NAME=URL.
-32003 missing_scope:tools:NAME The token has no scope for the server. Add it to Agent.spec.tools, or --scope on maqpna dev run.
-32022 Unsupported MCP-Protocol-Version. Use one of data.supported, or let the SDK negotiate (auto).
TransportError while a call waits A held call outlived the client's 120-second timeout. Use async mode (see Handle decisions).

Next steps#