toolgate
v1.0.4
Published
Optimistic execution middleware for autonomous agents — let reads pass, intercept actions, approve at the end.
Downloads
485
Maintainers
Readme
🛡️ ToolGate
Optimistic execution middleware for autonomous agents.
Let your agent run freely — reads execute instantly, every other action (writes, deletes, sends, deploys) is silently intercepted, recorded, and held for human approval. The agent never knows the difference.
Agent calls tool ──▶ ToolGate classifies ──▶ READ? ──▶ Execute for real
│
└──▶ ACTION? ──▶ Return phantom result
Save params to queue
┊
Agent finishes ──▶ Human reviews all actions
┊
Approve ──▶ Execute for real
Reject ──▶ Discard
Edit ──▶ Execute with new paramsInstall
npm install toolgateQuick Start — 5 Lines
import { ToolGate, cliApproval } from "toolgate";
// Your real tool executor (what the agent normally calls)
async function execute(tool: string, params: Record<string, unknown>) {
// call your APIs, MCP servers, SDKs, whatever
}
const gate = new ToolGate(execute, {
apiKey: "tg_live_your_api_key_here", // get one from the dashboard
agentName: "my-agent",
onApprovalNeeded: cliApproval, // interactive terminal approval
});
// Give your agent gate.proxy instead of the real executor
await myAgent.run({
tools: gate.proxy,
task: "Organize my inbox and draft replies",
});
// When done, finalize — triggers the approval flow
await gate.finalize();That's it. Your agent runs as normal. Reads pass through. Actions are queued. You approve at the end. Sessions automatically appear in your dashboard.
Getting Your API Key
- Sign up at the ToolGate Dashboard
- Go to API Keys and click + New Key
- Copy the key (shown only once) and use it in your code:
const gate = new ToolGate(execute, {
apiKey: "tg_live_a1b2c3d4e5f6...",
agentName: "my-agent",
});That's all you need — no Supabase credentials, no database setup. The library handles everything automatically.
How Classification Works
ToolGate uses a multi-layer classifier to determine if a tool call is a read (passthrough) or an action (intercept):
- Explicit overrides —
readTools: ["getUser"]/actionTools: ["deleteUser"] - Custom classifier — provide your own function
- Name pattern matching —
get*,list*,search*→ read.create*,send*,delete*→ action - MCP description analysis — parses tool descriptions for read vs. mutate signals
- HTTP method hints — if params contain
method: "GET"→ read,method: "POST"→ action - Safe default — unknown tools are intercepted (never accidentally executes a write)
Explicit Tool Lists
const gate = new ToolGate(execute, {
apiKey: "tg_live_...",
readTools: ["getEmails", "searchContacts", "listFiles"],
actionTools: ["sendEmail", "deleteFile", "createIssue"],
});Custom Classifier
const gate = new ToolGate(execute, {
apiKey: "tg_live_...",
classifier: (toolName, params) => {
if (toolName.startsWith("db.query")) {
return { intent: "read", isPassthrough: true, confidence: 1, reason: "DB query" };
}
// fall through to default
return classifyTool(toolName, params);
},
});MCP Integration
ToolGate auto-classifies MCP tools by analyzing their names and descriptions:
import { mcpToolGate, autoConfigFromMCP } from "toolgate";
// Option 1: Provide tool definitions upfront
const gate = mcpToolGate(mcpExecutor, {
apiKey: "tg_live_...",
mcpServers: [
{
name: "gmail",
url: "https://gmail.mcp.example.com/sse",
tools: [
{ name: "gmail_listMessages", description: "List messages in the inbox" },
{ name: "gmail_sendMessage", description: "Send a new email message" },
{ name: "gmail_trashMessage", description: "Move a message to trash" },
],
},
],
});
// Option 2: Auto-discover from running MCP servers
const config = await autoConfigFromMCP([
{ name: "gmail", url: "https://gmail.mcp.example.com/sse" },
{ name: "github", url: "https://github.mcp.example.com/sse" },
]);
const gate = new ToolGate(mcpExecutor, { apiKey: "tg_live_...", ...config });Wrapping an Existing Tool Map
If your agent uses a { toolName: function } map:
const realTools = {
getUser: async (p) => db.users.find(p.id),
createUser: async (p) => db.users.insert(p),
sendEmail: async (p) => mailer.send(p),
};
const gate = new ToolGate(
async (name, params) => realTools[name](params),
{ apiKey: "tg_live_...", agentName: "user-manager" }
);
// Or use the convenience wrapper:
const proxiedTools = gate.wrapTools(realTools);
await agent.run({ tools: proxiedTools });
await gate.finalize();Phantom Responses
When an action is intercepted, the agent receives a convincing phantom response so it continues working normally:
| Intent | Default Phantom Response |
|----------|-------------------------------------------------------|
| create | { success: true, id: "phantom_abc123" } |
| update | { success: true, message: "Updated successfully" } |
| delete | { success: true, message: "Deleted successfully" } |
| send | { success: true, messageId: "msg_xyz789" } |
Custom phantom responses:
const gate = new ToolGate(execute, {
apiKey: "tg_live_...",
phantomResponse: (tool) => {
if (tool.name === "createIssue") {
return { id: 99999, url: "https://github.com/org/repo/issues/99999", title: tool.params.title };
}
return { ok: true };
},
});Approval Methods
1. CLI (built-in)
import { cliApproval } from "toolgate";
const gate = new ToolGate(execute, {
apiKey: "tg_live_...",
onApprovalNeeded: cliApproval,
});2. Programmatic
const gate = new ToolGate(execute, { apiKey: "tg_live_..." });
await agent.run({ tools: gate.proxy });
const request = await gate.finalize();
// Inspect what the agent wants to do
console.log(gate.summary());
// Approve everything
await gate.approveAll();
// Or reject everything
await gate.rejectAll();
// Or decide 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]" } }],
]),
});3. Dashboard (built-in web UI)
Use the dashboardApproval helper — your agent will wait while you approve/reject in the web dashboard:
import { ToolGate, dashboardApproval } from "toolgate";
const gate = new ToolGate(execute, {
...dashboardApproval({ apiKey: "tg_live_..." }),
agentName: "deploy-bot",
});
gate.describe("Deploy v2.3.1 to production");
await agent.run({ tools: gate.proxy });
await gate.finalize();
// → Agent pauses. Open the dashboard, review and approve actions.
// → Once approved, ToolGate executes the real calls.Cloud Persistence
Pass your API key and sessions are automatically persisted to the cloud and visible in your dashboard:
const gate = new ToolGate(execute, {
apiKey: "tg_live_your_key_here",
agentName: "inbox-agent",
});Every session, read, and pending action is synced in real time. No database setup needed.
Inspection
gate.reads; // all reads that were executed
gate.pending; // all intercepted actions
gate.currentSession; // full session snapshot
gate.summary(); // pretty-printed CLI summaryDashboard
The ToolGate Dashboard shows all your agent sessions in real time. Sign up, generate an API key, and your agents automatically report sessions here.
Features:
- 🔐 Multi-user auth (email + password)
- 🔑 API key management (generate, revoke, copy)
- 📊 Real-time session monitoring
- ✅ Approve, reject, or edit pending actions
- 🔒 User-scoped data isolation (you only see your agents)
Getting started:
- Sign up at toolgate.dev
- Go to API Keys → + New Key
- Copy the key into your code — done
Full Example: Autonomous Email Agent
import { ToolGate, cliApproval } from "toolgate";
// Simulated tool executor
async function execute(tool: string, params: Record<string, unknown>) {
switch (tool) {
case "listEmails": return [{ id: 1, from: "[email protected]", subject: "Q3 Report" }];
case "readEmail": return { id: 1, body: "Please review the Q3 numbers." };
case "sendEmail": return { messageId: "real_123" };
case "archiveEmail": return { success: true };
default: return { error: "unknown tool" };
}
}
const gate = new ToolGate(execute, {
apiKey: "tg_live_...",
agentName: "email-assistant",
onApprovalNeeded: cliApproval,
});
gate.describe("Read inbox, draft replies, archive processed emails");
// Simulate what an autonomous agent would do:
const emails = await gate.proxy("listEmails", {}); // ✅ READ — executes
const detail = await gate.proxy("readEmail", { id: 1 }); // ✅ READ — executes
const sent = await gate.proxy("sendEmail", { // 🛑 SEND — intercepted
to: "[email protected]",
subject: "Re: Q3 Report",
body: "Reviewed — numbers look solid. Ship it.",
});
const archived = await gate.proxy("archiveEmail", { id: 1 }); // 🛑 DELETE — intercepted
// Agent thinks both worked. Now finalize:
await gate.finalize();
// → CLI shows: 2 reads executed, 2 actions pending approval
// → You approve/reject/edit each oneAPI Reference
new ToolGate(executor, config?)
| Config Field | Type | Description |
|-------------------|----------------------------------------------|------------------------------------------|
| apiKey | string | Your ToolGate API key (recommended) |
| classifier | (name, params) => ToolClassification | Custom classification function |
| readTools | string[] | Always-passthrough tool names |
| actionTools | string[] | Always-intercept tool names |
| phantomResponse | (tool: ToolCall) => unknown | Custom phantom result generator |
| onApprovalNeeded| (req: ApprovalRequest) => Promise<Result> | Callback when agent finishes |
| agentName | string | Display name for the agent |
| mcpServers | MCPServerDef[] | MCP server + tool definitions |
| supabaseUrl | string | (deprecated) Supabase project URL |
| supabaseKey | string | (deprecated) Supabase anon key |
Instance Methods
| Method | Returns | Description |
|----------------------|----------------------------|----------------------------------------------|
| .proxy | RealExecutor | The proxied executor to give to your agent |
| .wrapTools(map) | Same tool map shape | Wraps a { name: fn } tool map |
| .describe(text) | this | Set task description |
| .finalize() | Promise<ApprovalRequest> | End session, trigger approval |
| .approveAll() | Promise<Map> | Approve + execute all pending actions |
| .rejectAll() | void | Reject all pending actions |
| .executeApproval() | Promise<Map> | Execute with per-action decisions |
| .summary() | string | Pretty-printed session summary |
License
MIT
