toolgate
v1.1.1
Published
Optimistic execution middleware for autonomous agents — let reads pass, intercept actions, approve at the end. Supports MCP servers and custom local/hosted tools.
Maintainers
Readme
ToolGate
Execution middleware for autonomous agents — intercept, approve, execute.
Every tool call your agent makes goes through ToolGate. Reads pass through instantly. Writes, sends, deletes, and executes are intercepted, return a phantom response so the agent keeps running, and queue for human approval. Approved actions execute for real — even after the agent process has stopped.
Agent calls tool ──▶ ToolGate classifies ──▶ READ? ──▶ Execute immediately, record result
│
└──▶ ACTION? ──▶ Return phantom, store params
┊
Agent finishes ──▶ Session in dashboard
┊
From anywhere ──▶ Approve / Edit / Reject
(executes for real)Works with MCP servers, custom local tool functions, and hosted tool endpoints.
Install
npm install toolgateTwo ways to use ToolGate
1 — MCP servers
Wrap any MCP server. ToolGate auto-discovers tools, classifies them, and intercepts actions.
import { mcpToolGate, autoConfigFromMCP } from "toolgate";
const config = await autoConfigFromMCP([
{ name: "gmail", url: "https://gmail-mcp.example.com", headers: { "Authorization": "Bearer ya29.xxx" } },
{ name: "github", url: "https://github-mcp.example.com", headers: { "X-API-Key": "ghp_xxx" } },
]);
const gate = mcpToolGate(mcpExecutor, { apiKey: "tg_live_...", agentName: "inbox-agent", ...config });
gate.describe("Read inbox and draft replies to urgent emails");
const emails = await gate.proxy("gmail_listMessages", { maxResults: 10 }); // ✅ READ — runs immediately
const draft = await gate.proxy("gmail_sendMessage", { to: "...", body: "..." }); // 🛑 SEND — intercepted
await gate.finalize();
// Session appears in dashboard. Click Approve & Execute to send the real email.2 — Custom tools
No MCP server needed. Define your own tools as plain async functions.
import { ToolGate } from "toolgate";
const gate = new ToolGate(executor, { apiKey: "tg_live_..." });
// Wrap a map of functions — returns proxied versions to give your agent
const tools = gate.wrapTools({
readFile: async (p) => fs.readFile(p.path, "utf8"), // auto-classified as READ
writeFile: async (p) => fs.writeFile(p.path, p.content), // auto-classified as ACTION
deleteFile: async (p) => fs.unlink(p.path), // auto-classified as ACTION
}, {
readFile: { kind: "read", description: "Read a file from disk" }, // force read
});
// Give `tools` to your agent — ToolGate handles classification and interception
const content = await tools.readFile({ path: "config.json" }); // ✅ executes immediately
await tools.writeFile({ path: "out.txt", content: "hello" }); // 🛑 intercepted → approval
await gate.finalize();
await gate.approveAll(); // or let the dashboard handle itYou can also pass tools directly in config:
import { ToolGate, ToolDef } from "toolgate";
const gate = new ToolGate(executor, {
apiKey: "tg_live_...",
tools: [
{ name: "readFile", description: "Read a file from disk", kind: "read", fn: readFileFn },
{ name: "writeFile", description: "Write content to a file", fn: writeFileFn },
{ name: "deleteFile",description: "Delete a file", fn: deleteFileFn },
],
});Mixing MCP + custom tools
Both work in the same gate instance:
const gate = new ToolGate(mcpExecutor, {
apiKey: "tg_live_...",
mcpServers: [{ name: "gmail", url: "...", tools: [...] }],
tools: [
{ name: "readFile", description: "Read a local file", kind: "read", fn: readFileFn },
],
});Local vs Hosted
| | Local (free) | Hosted (paid) |
|---|---|---|
| Custom tools | Provide fn — called in-process after approval | Provide endpoint — dashboard POSTs to your URL after approval |
| MCP servers | Local / stdio MCP | ToolGate hosted MCP proxy |
| Agent must be running for execution? | Yes | No — deferred execution, agent can be offline |
Hosted custom tool endpoint
const gate = new ToolGate(executor, {
apiKey: "tg_live_...",
tools: [
{
name: "sendReport",
description: "Email a weekly summary report",
endpoint: "https://myapp.com/hooks/send_report",
headers: { "Authorization": "Bearer secret" },
},
],
});When the human approves in the dashboard, ToolGate POSTs to your endpoint:
POST https://myapp.com/hooks/send_report
Content-Type: application/json
Authorization: Bearer secret
{ "tool": "sendReport", "params": { ... } }No agent process needs to be running. The result is saved back to the session.
Classification
ToolGate automatically classifies every tool call as a read (passthrough) or action (intercept) by analyzing the tool name and description:
list*,get*,search*,fetch*,read*→ read (passthrough)send*,create*,delete*,update*,write*,post*→ action (intercepted)- Unknown → intercepted (safe default)
Override classification:
// In ToolDef
{ name: "myTool", description: "...", kind: "read" } // always passthrough
{ name: "myTool", description: "...", kind: "action" } // always intercepted
// In config
const gate = new ToolGate(executor, {
readTools: ["alwaysPassThrough"],
actionTools: ["alwaysIntercept"],
classifier: (name, params) => ({ intent: "write", isPassthrough: false, confidence: 1, reason: "custom" }),
});Getting Your API Key
- Sign up at toolgate.dev
- Go to API Keys → + New Key
- Copy the key — no database setup required
Programmatic Approval
If the agent is still running and you want to handle approval in-process:
await gate.finalize();
// Approve everything
await gate.approveAll();
// Or per-action
await gate.executeApproval({
sessionId: gate.sessionId,
approvedAt: Date.now(),
decisions: new Map([
["action-id-1", { action: "approve" }],
["action-id-2", { action: "reject" }],
["action-id-3", { action: "edit", actionId: "action-id-3", newParams: { to: "[email protected]" } }],
]),
});Inspection
gate.reads; // all reads that executed (with results)
gate.pending; // all intercepted actions
gate.currentSession; // full session snapshot
gate.summary(); // pretty-printed CLI summary — shows [local-tool] / [hosted-tool] / [local-mcp] / [hosted-mcp]API Reference
new ToolGate(executor, config)
| Config field | Type | Description |
|-------------------|------------------------------------------------|-------------|
| apiKey | string | ToolGate API key — required for dashboard sync |
| agentName | string | Display name shown in dashboard |
| tools | ToolDef[] | Custom tool definitions (local or hosted) |
| mcpServers | MCPServerDef[] | MCP server definitions |
| readTools | string[] | Tool names that always passthrough |
| actionTools | string[] | Tool names that always intercept |
| classifier | (name, params) => ToolClassification | Custom classifier override |
| phantomResponse | (tool: ToolCall) => unknown | Custom phantom response factory |
ToolDef
| Field | Type | Description |
|---------------|--------------------------------------------|-------------|
| name | string | Must match what your agent calls |
| description | string | Used by the classifier heuristic |
| kind | "read" \| "action" | Force classification (omit to auto-classify) |
| fn | (params) => Promise<unknown> | Local: called in-process after approval (free) |
| endpoint | string | Hosted: dashboard POSTs here after approval (paid) |
| headers | Record<string, string> | Auth headers for hosted endpoint |
MCPServerDef
| Field | Type | Description |
|-----------|--------------------------|-------------|
| name | string | Display name |
| url | string | MCP server HTTP endpoint |
| tools | MCPToolDef[] | Tool list (omit to use autoConfigFromMCP) |
| headers | Record<string, string> | Auth headers sent on every call |
mcpToolGate(executor, config)
Convenience factory — equivalent to new ToolGate(executor, config). Useful when working exclusively with MCP servers.
autoConfigFromMCP(servers)
Calls tools/list on each server and returns a ToolGateConfig. Accepts headers per server.
Instance methods
| Method | Description |
|-----------------------------|-------------|
| .proxy | Executor to hand to your agent |
| .wrapTools(fns, overrides?) | Wrap a { name: fn } map — returns proxied versions |
| .describe(text) | Set a task description shown in the dashboard |
| .finalize() | End session, persist to dashboard |
| .approveAll() | Approve + execute all pending actions locally |
| .rejectAll() | Reject all pending actions |
| .executeApproval(result) | Execute with per-action decisions |
| .summary() | Pretty-printed session summary |
License
MIT
