Skip to main content
Glama

toolgate

An MCP gateway on Cloudflare Workers that sits between AI agents and the tools they call. Each agent gets its own key and its own policy: which tools it can see, what arguments it can pass, how often it can call, and which calls wait for a person to approve them. Everything a tool returns is checked for secrets and personal data, scanned for text that tries to give the agent instructions, and fenced off as untrusted before the agent sees it. Every call leaves a row in a hash-chained audit log, and an operator can switch any agent off.

The tools behind it are demo backends: a support desk in D1, feature flags in KV, files in R2, and a web fetch limited to hosts the policy lists. The seed data includes a ticket with a prompt injection in it, one with a leaked AWS key and password, and one with a customer's phone and card number (all well-known documentation examples, not real ones).

Quickstart

Needs Node 22 or newer and pnpm.

pnpm install
pnpm dev                     # local D1 + wrangler dev on :8787
open http://localhost:8787   # admin token: local-admin

Press "Load the demo" in the console to create two agents and copy their keys. Then point any MCP client at it:

{
  "mcpServers": {
    "toolgate": {
      "url": "http://localhost:8787/mcp",
      "headers": { "Authorization": "Bearer tg_..." }
    }
  }
}

pnpm test runs the suite inside workerd, with an MCP client talking to the Worker over streamable HTTP. pnpm deploy applies the migrations and deploys; set the D1, KV and R2 ids in wrangler.jsonc, and wrangler secret put ADMIN_TOKEN first.

Related MCP server: Dvarapala

How it works

agent ──► /mcp ──► key ──► MCP server built for that agent ──► tools/call
                                                                 │
          kill switch ─► policy ─► argument rules ─► rate limits ─► approval? ─► tool ─► filter ─► fence ─► agent
                                                                                                 │
                                                                            every step ─► audit chain

Built on the Agents SDK and MCP v2. /mcp is createMcpHandler from agents/mcp with an SDK v2 McpServer. It is stateless: each request is checked against its key and then served by a server built for that agent, holding only the tools its policy allows. A tool an agent may not use isn't refused at call time; it isn't in tools/list, and calling it by name is a protocol error. Clients on the 2025 protocol work too, through the SDK's stateless fallback.

Keys. An agent key is tg_ plus 32 base32 characters (160 random bits). Only its SHA-256 is stored, and the console shows it once. Revoking a key refuses its next request.

Policy. JSON, validated with zod before it is saved. It names the tools an agent may use, and per tool: argument rules (oneOf, pattern, prefix, min, max, maxLength), a per-minute limit, and whether to ask for approval. A policy that names a tool or argument that doesn't exist is rejected, so a typo can't silently allow or constrain nothing. A path with a prefix rule can't contain . or .. segments, so docs/../secrets doesn't get past docs/.

Rate limits and budgets. One Durable Object per agent holds token buckets (one for all its calls, one per limited tool) and a count of calls today. The check and the take happen in one synchronous method, so 25 concurrent calls against a bucket of 10 get exactly 10.

Approval. Calls that change something (comment on or close a ticket, set a flag, write a file) wait for a person by default. The first call returns pending_approval with an id and runs nothing. The id is a hash of the agent, the tool and the arguments, so asking again finds the same request instead of queueing a second one. When a person approves, the call runs right away; whoever moves the row from approved to running in D1 runs it, and everyone after gets the stored result. So an approved write runs once, however often the agent retries. A declined call tells the agent not to retry. A request nobody answers in 24 hours expires. If the agent is switched off between the request and the yes, the approved call doesn't run.

Output filter. Runs on every string in every tool result, never on numbers, so ids and counts aren't mangled.

  • Secrets are always redacted: private key blocks, AWS, GitHub and Slack tokens, sk_live_ style API keys, JWTs, toolgate keys, and password: ... style assignments (the label is kept).

  • Emails, phone numbers and card numbers that pass the Luhn check are redacted unless the policy sets "pii": "allow".

  • Zero-width, bidi and Unicode tag characters are stripped and flagged.

  • Text that looks like instructions is flagged: "ignore previous instructions", role changes, chat markup, telling the agent to call a tool, asking for data to be sent somewhere. With "onInjection": "block" the output is withheld instead.

  • The result is wrapped in <tool_output trust="untrusted">, with any closing tag inside the data broken up so the data can't end the fence early. Flagged output gets a note in front saying what was found.

Kill switch. Switching an agent off writes a KV key in a namespace of its own (CONTROL). No tool can reach that namespace, so an agent with flags_set can't switch itself back on.

Audit chain. One Durable Object appends every entry in order. Each row's hash covers its fields and the hash of the row before it. The object keeps the chain in its own SQLite and copies rows to D1 in order, retrying from an alarm if D1 is down. GET /api/audit/verify re-hashes the D1 copy from the first row and compares the end with the head the object holds. It reports the first row that was edited, deleted, reordered or forged, and whether rows were cut off the end. Arguments in the log have secrets and PII redacted.

API

Agents use POST /mcp with Authorization: Bearer tg_.... Everything under /api needs Authorization: Bearer <ADMIN_TOKEN>.

GET /api/overview

agents with policy, key count, today's usage, waiting approvals, kill state; the tool list

POST /api/principals

{name, policy?}

GET /api/principals/:id

one agent with its keys and usage

PUT /api/principals/:id/policy

{policy, by}; 422 with the errors if it doesn't validate

POST /api/principals/:id/keys

issue a key (shown once)

POST /api/principals/:id/keys/:keyId/revoke

POST /api/principals/:id/kill

{by, reason?}

POST /api/principals/:id/revive

{by}

POST /api/principals/:id/check

{tool, args}: what would happen, without running anything or using up limits

GET /api/approvals?status=

POST /api/approvals/:id

{approved, by}; an approved call runs now

GET /api/audit?principal=&before=&limit=

newest first

GET /api/audit/verify

re-check the whole chain

POST /api/seed

demo agents, tickets, files and flags; new keys are returned once

Measured vs Claimed

Claim

Value

How measured

Date

Approved write retried 5 times at once

runs once, all 5 get the stored result

test/approvals.test.ts

2026-10-20

Concurrent calls against a 10-token bucket

10 of 25 allowed

test/meter.test.ts

2026-10-20

Concurrent audit appends

30 appends, seq 1 to 30 with no gaps, chain verifies

test/audit.test.ts

2026-10-20

Tampering found by verify

an edited row, a deleted row, a cut tail and a forged last row, each found

test/audit.test.ts

2026-10-20

Seeded injection ticket

flagged on 3 patterns, withheld under onInjection: block

test/gate.test.ts, and over curl against wrangler dev

2026-10-20

Status

Works: keys, per-agent tool lists, policy validation, argument rules, rate limits and daily budgets, approvals, the output filter, the kill switch, the audit chain and its verifier, all four backends, and the console. 38 tests cover the filter, policy parsing and argument rules, the fetch allowlist, the meter, the audit chain and tampering, and the gate end to end through a real MCP client (allowed and hidden tools, prefix escapes, redaction, flagged and blocked injection, rate limits, the kill switch, approvals approved, declined, expired and replayed, keys and revocation, policy changes, dry runs). It was also run under wrangler dev with curl: 2025-protocol initialize, tools/list, the injection ticket, a real web_fetch to developers.cloudflare.com and a pending write.

Limits:

  • The injection scan is a list of patterns. It catches the common phrasings and markup, and someone who reads the source can write around it. The fence and the note matter more than the scan; neither stops a model that decides to follow the text anyway.

  • PII detection is regex based: emails, phone numbers in common formats, and Luhn-valid card numbers. Names and addresses go through.

  • The kill switch is a KV read, and KV is eventually consistent, so switching an agent off can take up to about 60 seconds to reach every location.

  • If a Worker dies between claiming an approved call and storing its result, the approval stays running and nobody runs it again. That's the safe way round for a write, but it needs a person to look.

  • One audit Durable Object serializes every append, which caps throughput at what one object can hash and write. Fine for a team's agents; a large fleet would need a chain per agent.

  • web_fetch doesn't follow redirects, returns text only, and cuts bodies at 100 KB.

  • The console has a single shared admin token. Put Cloudflare Access in front of it before giving it a public URL.

  • It hasn't been deployed.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables controlled AI-agent access to enterprise-shaped tools with a deny-by-default gated write path, human approval, dry-run execution, and append-only audit logging.
    1
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides permission gates and tamper-evident audit logging for AI agent tool executions, with declarative policies, consent ladders, and hash-chained verification.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to securely invoke tools by enforcing identity proof, capability verification, and risk scoring on every request, blocking unsafe calls before they execute.
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A local proxy in front of any MCP server for developers on Claude Code or Cursor: see every tool call your agent makes, hold the risky ones for your approval, and keep a secret-redacted journal that is tamper-evident with an external anchor.
    76 npm
    1
    Apache 2.0