MAQPNADocs

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 fetch and 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 401 the 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 PolicyDenied with reason approval_denied or approval_expired. Keep timeoutMs above the gateway's approval timeout if you rely on this.
  • Async: { asyncMode: true } per call, or asyncApprovals: true / MAQPNA_ASYNC_APPROVALS=1 for all calls. A held call throws ApprovalPending with approvalId at 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 the fetch option (npm test).
  • For the Vercel AI SDK and the OpenAI Agents SDK, see Framework adapters.

Next: Go SDK, Error and reason codes.