Framework adapters
Plug MAQPNA-governed tools into LangGraph, LangChain, the OpenAI Agents SDK, the Claude Agent SDK, CrewAI and the Vercel AI SDK.
The SDK adapters connect the MAQPNA gateway to an agent framework. Each one turns the gateway's decisions into something the framework already understands, so a denied call or a call held for approval never crashes the agent: the model sees a tool error and can change its plan, or the framework's own pause for human input stops the run.
flowchart TD
F[Framework tool call] --> A[MAQPNA adapter]
A --> G[MAQPNA gateway]
G -->|allowed| R[Tool result]
G -->|-32001 denied| E["Tool error text: denied by policy … rule=… reason=…"]
G -->|-32003 unauthorized| U["Tool error text: not authorized to use server"]
G -->|-32002 approval pending| M{approvals mode}
M -->|error| T["Tool error text: approval pending: id=…"]
M -->|wait| W[Block and poll until a person decides]
M -->|interrupt| I[Framework pause: interrupt, needs_approval, needsApproval]
Approval modes#
Every adapter that wraps tools takes an approvals setting (Python approvals=, TypeScript approvals:):
| Mode | Behaviour |
|---|---|
error (default) |
The call goes out in async mode. The model gets approval pending: id=… as tool output. The approval ID is remembered per call (server, tool and arguments), so a retry of the same call resumes that approval instead of queueing a new one. |
wait |
The tool call blocks and polls wait_for_approval until a person approves or denies it. |
interrupt |
The framework's own pause (interrupt() in LangGraph, needs_approval / needsApproval in the OpenAI Agents SDK and the Vercel AI SDK) stops the run for that call. After you resume it, the tool polls the MAQPNA approval and returns the result. |
The person still decides in MAQPNA (the console, MAQPNA Desk or maqpna approvals). The framework pause only tells your application to wait. See Human approvals.
Python#
Install the extra for your framework from a checkout (the SDK is not on PyPI yet):
pip install "./sdk/python[langgraph]" # or langchain, openai-agents, claude-agent-sdk, crewai
Each adapter imports its framework only when used, so maqpna itself works without any of them.
LangGraph#
maqpna.integrations.langgraph, extra langgraph.
from langgraph.types import Command
from maqpna import MaqpnaClient
from maqpna.integrations.langgraph import invoke_and_report, tool_node
client = MaqpnaClient.from_env()
tools = tool_node(client.tools("echo")) # use as the graph's "tools" node
graph = builder.compile(checkpointer=MemorySaver())
out = invoke_and_report(client, graph, {"messages": [("user", task)]}, config)
while "__interrupt__" in out: # a call is held for approval
print(out["__interrupt__"][0].value["approval_id"])
out = invoke_and_report(client, graph, Command(resume=True), config)
| API | Notes |
|---|---|
tool_node(server_or_servers, prefix=None) |
A ToolNode of governed tools. With several servers, tools are named <server>__<tool>. |
governed_tools(server) |
The same tools as a list of LangChain StructuredTools. |
| Approvals | -32002 becomes interrupt({"type": "maqpna_approval", "approval_id", "server", "tool", "message"}). Resume with Command(resume=True) to poll the approval, with False (or {"approved": False}) to give up, or with {"approval_id": …} when you resume in another process. |
invoke_and_report(client, graph, input, config), ainvoke_and_report |
Reports Succeeded with the final message, or Failed with the exception. A paused graph is not reported. |
LangChain#
maqpna.integrations.langchain, extra langchain.
from maqpna.integrations.langchain import as_tools
tools = as_tools(client.tools("github"), prefix=True, wait_for_approvals=False)
Each Model Context Protocol (MCP) tool becomes a StructuredTool with the tool's JSON schema. Denials, pending approvals and authorisation failures raise a ToolException, which the model sees as tool output. wait_for_approvals=True polls until a person decides instead.
OpenAI Agents SDK#
maqpna.integrations.openai_agents, extra openai-agents. Two ways to give an agent MAQPNA tools:
from agents import Agent
from maqpna.integrations.openai_agents import function_tools, mcp_server
# 1. Function tools with governance mapping
agent = Agent(name="reviewer", tools=function_tools(client.tools("echo"), approvals="interrupt"))
# 2. Native MCP: the Agents SDK talks MCP to the gateway itself
async with mcp_server(client, "echo") as echo:
agent = Agent(name="reviewer", mcp_servers=[echo])
| API | Notes |
|---|---|
function_tools(server, approvals="error"\|"wait"\|"interrupt", prefix=False) |
One FunctionTool per tool (strict_json_schema=False). With interrupt, the tool's needs_approval hook makes the next attempt of a held call an Agents SDK interruption; after state.approve(...) the tool polls the MAQPNA approval. |
mcp_server(client, server), streamable_http_params(client, server) |
An MCPServerStreamableHttp whose HTTP auth reads the session token on every request. Its session timeout is raised to the client timeout. Approvals are synchronous on this path. |
Claude Agent SDK#
maqpna.integrations.claude_agent_sdk, extra claude-agent-sdk.
from claude_agent_sdk import ClaudeAgentOptions
from maqpna.integrations.claude_agent_sdk import MaqpnaPermissions, query_and_report, sdk_mcp_server
perms = MaqpnaPermissions()
servers = {"echo": sdk_mcp_server(client, "echo", permissions=perms)}
options = ClaudeAgentOptions(mcp_servers=servers, hooks=perms.hooks())
async for message in query_and_report(client, "check the cluster", options):
...
| API | Notes |
|---|---|
sdk_mcp_server(client, server, permissions=) |
An in-process MCP server that forwards each call through the SDK with async approvals: a held call returns at once, and a retry resumes the approval. |
mcp_servers(client), mcp_server_config(client, server) |
{"<server>": {"type": "http", "url", "headers"}} for Claude Code to call the gateway directly. It reads the headers once, so build the options per run. Approvals are synchronous on this path. |
MaqpnaPermissions().hooks(), .can_use_tool |
PreToolUse answers deny for a call the gateway denied (-32001, -32003) and ask for one with a pending approval (-32002). PostToolUse hooks record the outcomes. |
query_and_report(client, prompt, options), report_from_message(client, msg) |
Reports the final ResultMessage (Failed for error results). |
CrewAI#
maqpna.integrations.crewai, extra crewai.
from crewai import Agent
from maqpna.integrations.crewai import as_tools
reviewer = Agent(role="reviewer", goal="…", backstory="…", tools=as_tools(client.tools("github")))
Each tool is a CrewAI BaseTool with an args_schema built from the MCP input schema. Denials and pending approvals come back as tool output text. approvals takes "error" or "wait"; CrewAI has no interrupt mode.
TypeScript#
The TypeScript adapters import no framework. They return the objects the framework expects, or call a factory you pass in.
Vercel AI SDK#
import { createMCPClient } from "@ai-sdk/mcp";
import { jsonSchema } from "ai";
import { MaqpnaClient, aiSdkMcpTransport, aiSdkTools } from "@maqpna/sdk";
const client = MaqpnaClient.fromEnv();
// MCP: the AI SDK's MCP client talks to the gateway (approvals are synchronous)
const mcp = await createMCPClient({ transport: await aiSdkMcpTransport(client, "github") });
// Tools with governance mapping
const tools = await aiSdkTools(client.tools("github"), { jsonSchema, approvals: "interrupt" });
aiSdkTools options: jsonSchema, approvals, prefix, approvalTimeoutMs, approvalPollIntervalMs, pending. With interrupt, needsApproval returns true for a call with a pending MAQPNA approval, so the AI SDK emits a tool-approval request. With ai@5, use experimental_createMCPClient from ai.
OpenAI Agents SDK#
import { Agent, MCPServerStreamableHttp, tool } from "@openai/agents";
import { openaiAgentsMcpServerOptions, openaiAgentsTools } from "@maqpna/sdk";
const github = new MCPServerStreamableHttp(await openaiAgentsMcpServerOptions(client, "github"));
const agent = new Agent({
name: "reviewer",
tools: await openaiAgentsTools(client.tools("github"), { tool, approvals: "interrupt" }),
});
Other frameworks#
authFetch(client) (a fetch that sets the current token on every request), governedCall, invokeText, reportRun and PendingApprovals are the building blocks the adapters use. Python has the same in maqpna.integrations._common (governed_call, invoke_text, report_run, PendingApprovals).
Run reporting from adapters#
Adapters report the run when it ends and never raise from reporting. The summary is cut to 1024 UTF-8 bytes; a summary that data loss prevention (DLP) blocks is retried once without it, and other failures are logged. Summaries never contain tool arguments.
Templates#
Three ready-to-run agents use these adapters. Each runs unchanged under maqpna dev up --stub-llm, with no API key:
| Template | Path | Uses |
|---|---|---|
| LangGraph | examples-agents/langgraph/ |
tool_node, invoke_and_report, interrupt() for approvals |
| OpenAI Agents SDK | examples-agents/openai-agents/ |
function_tools |
| Claude Agent SDK | examples-agents/claude-agent-sdk/ |
sdk_mcp_server, MaqpnaPermissions, query_and_report; offline mode against the stub |
Each has a Dockerfile and manifests/ (Agent, ToolPolicy, AgentSession). Walk through them in Use a framework template.