Skip to content
Documentation menu

Documentation

Wrap your agent

MakerChecker does not ask you to re-platform. You wrap the tools your agent already calls, and each call passes through one checkpoint: check, run, record. This page is the reference for every integration surface, with a real code block for each: LangChain, the Claude Agent SDK, a generic wrapper, and Python.

Licensing and install

The SDK and connectors are Apache-2.0 and live in the repository under packages/. They are not on npm or PyPI yet. To use them, clone the repository and build the packages. The import paths below use the real package names (@makerchecker/sdk, makerchecker).

Before you start

Every wrapper does the same three things in order, inside the tool call: checkthe proposal against the agent's role and grants, run the original tool if the check passes, and record the outcome to the audit chain. A denied check throws before the tool body ever runs.

You need two things in place first:

  1. A running server. The control plane listens on port 3000. The fastest way to get one is docker compose up, which seeds the demo and prints an admin and an officer API key once at boot. The Quickstart walks through this.
  2. An open proxy session. A session is the unit of work that ties a run of governed calls together in the audit. You open one, wrap and invoke tools against it, then close it.

The session lifecycle is the spine that every connector shares. It is the same five calls regardless of framework:

session lifecycle

const mc = createClient({ baseUrl, apiKey });
const { session } = await mc.proxy.openSession({ label, externalRef });
// ... wrap tools, then invoke them ...
await mc.proxy.closeSession(session.id);
const verdict = await mc.audit.verify();

The apiKey is a Bearer token in the form mk_... printed at boot. If you run the server with MAKERCHECKER_AUTH_DISABLED=1, you can omit it. A GovernContext carries the three identifiers each check needs: { sessionId, agentName, skillRef }.

LangChain

governLangChainTool(client, context, tool) takes a real LangChain StructuredTool and returns a DynamicStructuredTool with the same name, description, and schema. The LLM tool spec is byte-for-byte identical, so the governed tool drops into the same ToolNode or agent executor with no other change.

To govern a whole toolkit at once, use governToolkit(client, { sessionId, agentName }, tools, skillRefs). The skillRefs argument is either a function (tool) => skillRef or a record mapping each tool name to its skill reference.

import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { createClient } from "@makerchecker/sdk";
import {
governLangChainTool,
governToolkit,
GovernanceDeniedError,
} from "@makerchecker/connector-langchain";
const mc = createClient({
baseUrl: process.env.MAKERCHECKER_URL ?? "http://localhost:3000",
apiKey: process.env.MAKERCHECKER_API_KEY,
});
// A real LangChain tool you already have. Body is a stand-in.
const matchTxns = tool(
async ({ statement }) => ({ matched: statement.length, exceptions: 1 }),
{
name: "match_txns",
description: "Match statement transactions against the ledger.",
schema: z.object({ statement: z.array(z.string()) }),
},
);
const { session } = await mc.proxy.openSession({
label: "governed-langchain-demo",
externalRef: "langchain-thread-1",
});
// Wrap one tool. Name, description, and schema are preserved, so the
// governed tool drops into the same ToolNode / agent executor.
const governedMatch = governLangChainTool(
mc,
{ sessionId: session.id, agentName: "recon-preparer", skillRef: "txn-match@1" },
matchTxns,
);
// The agent calls it normally. A deny throws before the tool runs.
const out = await governedMatch.invoke({ statement: ["t1", "t2", "t3"] });
// Wrap a whole toolkit at once. Each tool needs a skillRef; an unmapped
// tool fails closed (throws) rather than running ungoverned.
const governed = governToolkit(
mc,
{ sessionId: session.id, agentName: "recon-preparer" },
[matchTxns],
{ match_txns: "txn-match@1" },
);
await mc.proxy.closeSession(session.id);
const verdict = await mc.audit.verify();

The toolkit fails closed

Every tool passed to governToolkit must resolve to a skillRef. If one is unmapped, the call throws rather than running that tool ungoverned. A gap in the map is a denied tool, never a silent bypass.

Claude Agent SDK

governClaudeTool(client, context, name, description, inputSchema, handler) returns a normal SdkMcpToolDefinition. It preserves the name, description, and input schema, so the agent's tool spec is unchanged.

Drop the result into createSdkMcpServer({ tools: [...] }) alongside your ungoverned tools. The agent invokes it through the SDK as normal; you can also call tool.handler(args, extra) directly, which is how the demo proves the governance path without a model call.

import { z } from "zod";
import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { createClient } from "@makerchecker/sdk";
import {
governClaudeTool,
GovernanceDeniedError,
} from "@makerchecker/connector-claude-agent";
const mc = createClient({
baseUrl: process.env.MAKERCHECKER_URL ?? "http://localhost:3000",
apiKey: process.env.MAKERCHECKER_API_KEY,
});
const { session } = await mc.proxy.openSession({
label: "governed-claude-agent-demo",
externalRef: "claude-agent-thread-1",
});
const text = (t) => ({ content: [{ type: "text", text: t }] });
// governClaudeTool returns a normal SdkMcpToolDefinition: name,
// description, inputSchema, handler. Drop it into a server unchanged.
const matchTool = governClaudeTool(
mc,
{ sessionId: session.id, agentName: "recon-preparer", skillRef: "txn-match@1" },
"match_txns",
"Match statement transactions against the ledger.",
{ statement: z.array(z.string()) },
async ({ statement }) => text(`matched ${statement.length} transactions`),
);
const server = createSdkMcpServer({ name: "governed-tools", tools: [matchTool] });
// The handler is governed: check (deny throws), run, record.
const out = await matchTool.handler({ statement: ["t1", "t2", "t3"] }, {});
await mc.proxy.closeSession(session.id);

Any framework: the generic wrapper

When there is no dedicated connector for your framework, use governedTool(client, sessionId, agentName, skillRef, fn) from the SDK. It wraps any async function that takes a plain-object input and returns a governed version with the same signature.

This is what the LangChain and Claude connectors are built on. Reach for it for CrewAI, a raw HTTP call, a database write, or any tool the framework-specific wrappers do not cover.

import { createClient, governedTool, GovernanceDeniedError } from "@makerchecker/sdk";
const mc = createClient({
baseUrl: process.env.MAKERCHECKER_URL ?? "http://localhost:3000",
apiKey: process.env.MAKERCHECKER_API_KEY,
});
const { session } = await mc.proxy.openSession({ label: "recon-run" });
// Any async function with a plain-object input works: CrewAI, a raw
// fetch, a database write. The framework stays the executor.
const matchTxns = governedTool(
mc,
session.id,
"recon-preparer",
"txn-match@1",
async ({ statement }) => ({ matched: statement.length, exceptions: 1 }),
);
// check -> deny throws GovernanceDeniedError -> run -> record output.
const out = await matchTxns({ statement: ["t1", "t2", "t3"] });
await mc.proxy.closeSession(session.id);
const verdict = await mc.audit.verify();

Python

The Python SDK mirrors the generic wrapper. create_client(base_url, api_key=None) builds the client, and governed_tool(client, session_id, agent_name, skill_ref, fn) wraps a callable that takes a dict input. It is framework-agnostic: use it inside a CrewAI @tool, a LangChain-Python StructuredTool, or a bare function.

import os
from makerchecker import create_client, governed_tool, GovernanceDeniedError
mc = create_client(
base_url=os.environ.get("MAKERCHECKER_URL", "http://localhost:3000"),
api_key=os.environ.get("MAKERCHECKER_API_KEY"),
)
session = mc.proxy.open_session(label="recon-run")["session"]
# Wrap any callable that takes a dict input. Framework-agnostic:
# CrewAI @tool, a LangChain StructuredTool, or a bare function.
match_txns = governed_tool(
mc, session["id"], "recon-preparer", "txn-match@1",
lambda i: {"matched": len(i["statement"]), "exceptions": 1},
)
# check -> deny raises GovernanceDeniedError -> run -> record.
out = match_txns({"statement": ["t1", "t2", "t3"]})
mc.proxy.close_session(session["id"])
verdict = mc.verify_audit()

Handling denials

A denied check raises GovernanceDeniedError before the tool runs. The error carries two fields: code, a stable machine-readable string, and reason, a human sentence you can show or log. The codes come from the engine and the limits layer.

Catch it and decide what to do: route the action to a human, log it, or hand the reason back to the agent so it can pick a different step. Treat any other thrown error as a real tool failure, not a governance decision.

handling a denial

try {
await governedMatch.invoke({ exceptions: 1 });
} catch (err) {
if (err instanceof GovernanceDeniedError) {
// err.code is machine-readable; err.reason is the human sentence.
console.log(`denied (${err.code}): ${err.reason}`);
// Route to a human, log it, or surface it back to the agent.
} else {
throw err; // a real tool error, not a governance decision.
}
}

Common codes you will see:

CodeWhat it means
skill_not_grantedThe skill is not granted to the agent's role, or not at this version. Deny by default.
sod_violationActing would break segregation of duties. The agent that prepared a case cannot also approve it.
high_risk_requires_gateThe action is high-risk. Through the proxy it is denied outright; in a flow it would wait at an approval gate.

A denial is a recorded event

The deny is not just an exception in your process. The check is appended to the audit chain before the error is thrown, so a refused action leaves the same tamper-evident trace as an allowed one. The seeded demo shows this: recon-preparer holds txn-match@1 and runs, but is denied any approval skill because that role belongs to recon-approver.

Where to go next

  • Quickstart: run the control plane, wrap a tool, watch it get blocked, verify the audit.
  • Concepts and model: roles, skills, grants, risk tiers, segregation of duties, and where each denial code comes from.
  • The audit trail: how each check, run, and denial is chained and how you verify it offline.
Stuck or evaluating for a regulated team?Book a walkthroughOpen an issue on GitHub