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/callgoes 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):
npx -y @intentlock/mcp-proxy --help1. Create an agent
Section titled “1. Create an agent”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.
2. Write the plan file
Section titled “2. Write the plan file”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:
npx -y @intentlock/mcp-proxy --list-tools -- npx -y @modelcontextprotocol/server-githubThen pick the ones you need:
{
"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.
3. Configure the client
Section titled “3. Configure the client”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 --:
{
"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.
Limits
Section titled “Limits”- 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.
maxCallscounts 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.