Skip to content

MCP proxy

Put Intentlock in front of any stdio MCP server, so Claude Desktop, Cursor or Claude Code only use the tools you planned.

intentlock-mcp sits between an MCP client (Claude Desktop, Cursor, Claude Code) and an MCP server that runs over stdio. The client starts the proxy instead of the server; the proxy starts the server and checks every tool call against a plan and the agent’s policy:

  • the client only sees the tools named in your plan file;
  • every tools/call goes through the gate: allowed calls are forwarded, blocked calls return an error result with the reason;
  • risky calls wait for an approver in the inbox;
  • every decision lands in the audit log.

It runs with npx, so there is nothing to install (Node 22 or newer):

Terminal
npx -y @intentlock/mcp-proxy --help

One MCP setup is one agent (for example claude-desktop-github). Create it and an API key in the inbox under Agents, or with the CLI. Its policy is where holds and argument limits go.

MCP clients never send a task, so the plan comes from a file you write once: which upstream tools this setup may use, and with which arguments. List the server’s tools first:

Terminal
npx -y @intentlock/mcp-proxy --list-tools -- npx -y @modelcontextprotocol/server-github

Then pick the ones you need:

github-plan.json
{
  "goal": "Triage issues in acme/app",
  "steps": [
    { "tool": "list_issues", "args": { "owner": { "eq": "acme" }, "repo": { "eq": "app" } }, "maxCalls": 200 },
    { "tool": "get_issue", "args": { "owner": { "eq": "acme" }, "repo": { "eq": "app" } }, "maxCalls": 200 },
    { "tool": "add_issue_comment", "args": { "owner": { "eq": "acme" }, "repo": { "eq": "app" } }, "maxCalls": 50 }
  ]
}

Steps use the format from Plans and tokens, without version and user. Tool names must match the server’s names exactly. At most 50 steps.

Claude Desktop (claude_desktop_config.json), Cursor (.cursor/mcp.json) and Claude Code (.mcp.json) use the same shape. Replace the server’s entry with the proxy, and put the original command after --:

claude_desktop_config.json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": [
        "-y", "@intentlock/mcp-proxy",
        "--plan", "/home/sam/github-plan.json",
        "--",
        "npx", "-y", "@modelcontextprotocol/server-github"
      ],
      "env": {
        "INTENTLOCK_URL": "https://intentlock.acme.dev/api",
        "INTENTLOCK_API_KEY": "il_live_...",
        "INTENTLOCK_USER": "sam@acme.dev",
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."
      }
    }
  }
}
Variable Meaning
INTENTLOCK_URL Your Intentlock API URL.
INTENTLOCK_API_KEY The agent’s API key. Never passed to the upstream server.
INTENTLOCK_USER The team member the client acts for (or --user). Must be an active member.
INTENTLOCK_OUTBOX_DIR Where decisions are queued before upload. Default ~/.intentlock/outbox.

Every other variable is passed through to the upstream server. The proxy writes its logs to stderr, which most clients show in their MCP log.

MCP clients give up on a tool call after about a minute, so the proxy does not wait for an approval. A held call returns at once with a message telling the model to ask you to approve it in the inbox and then make the same call again. When the model repeats exactly the same call (same tool, same arguments):

  • approved: the call runs once;
  • rejected: it is blocked, and the model is told not to retry;
  • still pending: the same message again;
  • expired: it is checked again, which usually creates a new hold.

An approval only ever runs the call that was approved. A call with different arguments is checked on its own.

  • Tools only. Resources, prompts and sampling from the upstream server are not passed through.
  • Stdio servers only. Remote (HTTP) MCP servers are not supported yet.
  • The plan is fixed. The model cannot add tools. Change the file and restart the client to change it.
  • maxCalls counts per signed plan. The proxy refreshes its token while the client is open. After more than an hour idle it signs the plan file again, which starts new counters.
  • It fails closed. If the Intentlock server cannot be reached, calls are blocked.