TypeScript SDK
Call tools and models through the MAQPNA gateway from Node.js, and handle denials, approvals and results.
The MAQPNA TypeScript SDK (@maqpna/sdk) gives Node.js agents the same API as the Python SDK: governed Model Context Protocol (MCP) tool calls through the MAQPNA gateway, typed errors for every governance outcome, approval polling and result reporting.
flowchart LR
A[Agent code] -->|server.call| S["@maqpna/sdk"]
S -->|tools/call + session token| G[MAQPNA gateway]
G -->|allowed| R[ToolResult]
G -->|-32001| D[throws PolicyDenied]
G -->|-32002, async mode| P[throws ApprovalPending]
G -->|-32003| U[throws Unauthorized]
- ESM only, Node.js 20.3 or later. It uses the built-in
fetchand has no runtime dependencies. - MCP Streamable HTTP, with JSON or incrementally parsed server-sent event (SSE) responses.
- The token file is re-read on every request. After a
401the SDK retries once.
Install#
cd sdk/typescript
npm ci && npm run build # compiles src/ to dist/
npm pack # writes maqpna-sdk-0.1.0.tgz
cd ~/my-agent
npm install ../maqpna/sdk/typescript/maqpna-sdk-0.1.0.tgz
Configuration#
MaqpnaClient.fromEnv() reads the same variables as the Python SDK:
| Variable | Use |
|---|---|
MAQPNA_GATEWAY_URL |
Gateway base URL. Required: without it the constructor throws ConfigError. |
MAQPNA_TOKEN_FILE |
Session token file, default /var/run/maqpna/token, re-read on every request. |
MAQPNA_TOKEN |
Static token for local dev (maqpna dev run sets it). |
MAQPNA_SESSION |
The session name. |
MAQPNA_TOOL_<NAME>_URL |
Gateway URL of each MCP server. |
MAQPNA_MODEL_ENDPOINT, MAQPNA_MODEL_NAME |
The governed model route and its model. |
MAQPNA_ASYNC_APPROVALS |
1, true or yes: every call in async approval mode. |
MAQPNA_MCP_PROTOCOL |
auto (default), 2026-07-28, 2025-11-25 or 2025-06-18. |
TRACEPARENT, TRACESTATE |
W3C trace context. |
MAQPNA_BOOTSTRAP_URL (+ _DIR, _TOKEN_FILE) |
Warm-pool pods: use await MaqpnaClient.fromEnvAsync() to wait for the session. |
Pass overrides as the second argument, MaqpnaClient.fromEnv(process.env, { timeoutMs: 600_000 }), or construct it directly with new MaqpnaClient({ gatewayUrl, token, tokenFile, session, asyncApprovals, timeoutMs, fetch, protocol }). timeoutMs (default 120000) applies per HTTP request.
List and call tools#
import { MaqpnaClient, PolicyDenied, ApprovalPending } from "@maqpna/sdk";
const client = MaqpnaClient.fromEnv();
const echo = client.tools("echo");
console.log("tool servers:", client.availableToolServers());
for (const t of await echo.listTools()) console.log("tool:", t.name);
const res = await echo.call("echo", { text: "hello from TypeScript" });
console.log("echo ->", res.text);
try {
await echo.call("delete_resource", { id: "db-1", namespace: "kube-system" });
} catch (e) {
if (e instanceof PolicyDenied) console.log("denied:", e.rule, e.reason);
else throw e;
}
try {
await echo.call("delete_resource", { id: "db-1", namespace: "dev" }, { asyncMode: true });
} catch (e) {
if (e instanceof ApprovalPending) console.log("pending approval:", e.approvalId);
else throw e;
}
const report = await client.reportResult("Succeeded", "echo ok; kube-system delete denied");
console.log("reported", report.result, "auditSeq", report.auditSeq);
maqpna dev up
maqpna dev run -- node agent.mjs
maqpna dev: session dev-3af8dec0 (agent coder, namespace dev, on behalf of you@localhost)
tool servers: [ 'echo' ]
tool: echo
tool: get_time
tool: delete_resource
echo -> hello from TypeScript
denied: never-touch-system-namespaces policy_denied
pending approval: apr_2619815504c9bb5070d18daa
reported Succeeded auditSeq 11
maqpna dev: dev-3af8dec0: 1 allow, 1 deny, cost $0.0000 (maqpna dev timeline --last)
| Call | Does |
|---|---|
client.tools(name) |
A ToolServer for MCP server name. |
await server.listTools() |
Tool[] (name, description, inputSchema), filtered by policy. |
await server.call(name, args, opts) |
A ToolResult (.text, .content, .structured, .isError). opts: asyncMode, approvalId, traceparent, signal. |
client.availableToolServers() |
Server names from MAQPNA_TOOL_*_URL. |
Model calls#
The model route is OpenAI-compatible. Use the official openai package with the session token as the API key:
import OpenAI from "openai";
const llm = new OpenAI({ baseURL: client.modelEndpoint("stub"), apiKey: await client.token() });
const reply = await llm.chat.completions.create(
{ model: client.modelName() ?? "stub", messages: [{ role: "user", content: "hi" }] },
{ headers: await client.openaiHeaders() }, // fresh token per request; it rotates in place
);
client.modelEndpoint() without an argument returns MAQPNA_MODEL_ENDPOINT and throws ConfigError when it is not set. maqpna dev run does not set it, so locally pass the route name or use OPENAI_BASE_URL, which maqpna dev run exports when the stack runs with --stub-llm. Errors from the route are OpenAI-style, with error.type and error.reason (see Error and reason codes).
Denials and approvals#
The behaviour matches the Python SDK:
- Sync (default): the gateway holds the call until an approver decides. A denial throws
PolicyDeniedwithreasonapproval_deniedorapproval_expired. KeeptimeoutMsabove the gateway's approval timeout if you rely on this. - Async:
{ asyncMode: true }per call, orasyncApprovals: true/MAQPNA_ASYNC_APPROVALS=1for all calls. A held call throwsApprovalPendingwithapprovalIdat once.
waitForApproval sends the call in async mode and polls until someone decides:
import { waitForApproval, PolicyDenied, ApprovalTimeout } from "@maqpna/sdk";
const res = await waitForApproval(echo, "delete_resource", { id: "db-1", namespace: "dev" }, {
pollIntervalMs: 2000,
timeoutMs: 120_000,
onPending: (p) => console.log(`waiting for approval ${p.approvalId}`),
});
Options: pollIntervalMs (default 5000), timeoutMs (default 900000), onPending, signal, approvalId (resume an approval queued earlier). Approve or deny it with maqpna approvals, the console or MAQPNA Desk.
Errors#
All extend MaqpnaError.
| Error | When | Fields |
|---|---|---|
PolicyDenied |
JSON-RPC -32001 |
rule, reason, data, code |
ApprovalPending |
JSON-RPC -32002 (async mode) or an MCP 2026-07-28 input_required approval |
approvalId, requestState, data |
Unauthorized |
JSON-RPC -32003, or a 401 after one retry |
data |
ApprovalTimeout |
waitForApproval gave up |
approvalId, waitedMs |
InputRequired |
MCP 2026-07-28 input_required from an upstream server |
inputRequests, requestState |
HeaderMismatch, UnsupportedProtocolVersion |
-32020, -32022 |
supported |
RPCError |
Any other JSON-RPC error | code, rpcMessage, data |
TransportError |
Gateway unreachable, or not a JSON-RPC answer | status, body |
ConfigError |
Missing MAQPNA_GATEWAY_URL, unreadable token file, no model route |
— |
reportResult throws RangeError for a bad result or a summary over 1024 bytes (and HTTP 413), TypeError for HTTP 400, and PolicyDenied when data loss prevention (DLP) blocks the summary (422) or the session is revoked. It retries a 503 three times, honouring Retry-After.
Streaming#
The SDK parses text/event-stream responses from the gateway incrementally and resolves with the matching JSON-RPC response; it does not surface progress notifications. For streamed model output, pass stream: true to the OpenAI client on the model route. The SDK also exports its SSE parser (parseSSE, iterSSE, SSEParser) if you need one.
Report the result#
import { truncateUtf8 } from "@maqpna/sdk";
await client.reportResult("Succeeded", truncateUtf8(finalAnswer)); // at most 1024 UTF-8 bytes
Trace context#
const client = MaqpnaClient.fromEnv(process.env, { traceparent: () => currentTraceparent() });
await client.tools("github").call("get_issue", { number: 1 }, { traceparent: "00-…-01" });
Examples and tests#
sdk/typescript/test/runs the SDK against an in-memory gateway passed in through thefetchoption (npm test).- For the Vercel AI SDK and the OpenAI Agents SDK, see Framework adapters.
Next: Go SDK, Error and reason codes.