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 serverecho. - To call your own MCP server locally, add it:
maqpna dev up --upstream github=http://127.0.0.1:9000/mcp. It is thenMAQPNA_TOOL_GITHUB_URLand servergithubin policy rules. - The SDK for your language:
pip install kosa8-maqpnaornpm 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#
- Call models
- Handle decisions in code
- A governed tool call, every check in order
- Framework adapters for governed tools in LangGraph, CrewAI and others