deepseek-mcp-bridge
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@deepseek-mcp-bridgeUse deepseek_flash to independently review this code for security vulnerabilities."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ModelBridge
An MCP server that gives a ChatGPT connector (or any other MCP client) a working sub-agent — and, optionally, one that reads files, runs commands, and works a multi-step task to completion on your machine.
It ships pointed at DeepSeek, which is the cheap default rather than the point. The design is model-agnostic: the sub-agent's brain is whatever Anthropic-compatible endpoint you put in DEEPSEEK_BASE_URL, and the harness around it is a swappable part. That is where the name comes from.
中文说明 · Changelog · Contributing · Security
What this is actually for
It puts another working sub-agent inside your ChatGPT conversation — one whose brain can be any model you like.
Be precise about this, because the project gets retold wrong a lot: ChatGPT's own sub-agents (Luna and friends) can already do real work — they read and write files on your machine, run commands, and iterate. This bridge is not filling in a missing capability.
The real constraint sits elsewhere: sub-agent slots only accept OpenAI's own model tiers (Sol / Terra / Luna). An external model cannot be registered as one. So the moment you want a different model doing that work — faster, cheaper, or independent enough to review the implementer — the only door in is an MCP connector.
And an MCP tool is a text interface by default: you call it, you get text back. To let an external model actually work, you have to hand it a whole environment first.
The mechanism matters more than the outcome: it does not plug DeepSeek in, it gives DeepSeek hands. A bridge that only forwards an API hands the host one more model to ask. This one does not — on an agent job it spawns a full agent harness (Claude Code) as a child process, pointed at DeepSeek's Anthropic-compatible endpoint. So the loop, context compaction, prompt caching and tool implementations — everything that makes a model able to work — are an existing, maintained implementation rather than a hand-rolled imitation. What the host gets is therefore a sub-agent that can do the job, not a Q&A endpoint.
And it is exposed as one MCP tool, so any MCP-capable host can call it; the ChatGPT connector is one door in, and so far the only one tested. The harness layer is a swappable part — Claude Code is its current implementation, not the essence of the design.
Native sub-agent (Luna et al.) | This bridge |
Can do the whole job | Can do the whole job too — with a different executor |
Model is an OpenAI tier, and the slot is not swappable | Model is DeepSeek V4.1 Flash (552B MoE, ~1M context); two lines of |
Harness is built into the platform, invisible and fixed | Harness is Claude Code, a swappable part |
Same stack as Sol, shared training preferences | External model, no shared lineage — which is what makes it useful for security review and counter-examples |
About "faster" — an honest note. From my own use the felt difference is two things: fewer steps for the same task, and a higher chance of getting it right the first time. But I have not benchmarked it — I have not run the same task set against Luna and DeepSeek as a controlled comparison. So that is a subjective impression, not performance data. It may also not hold for every task: there is community feedback that non-OpenAI models do worse on
apply_patch-style mechanical edits. Try it on a small slice first; do not switch wholesale.
Measured, not intended: given "read witness.txt, reverse its contents, write them to answer.txt", the sub-agent ran 3 steps over 2 min 25 s; the answer.txt on disk matched the expected string byte for byte, next to a nonce that only a real result can produce. A chat transcript agreeing with itself is not evidence — bytes on disk are.
That measurement shows it really does the work. It does not show it is faster than Luna — the efficiency difference is still an impression, not a comparison.
⚠️ This capability is off by default. With no
DEEPSEEK_ALLOWED_ROOTSset, the bridge can only spend your DeepSeek quota and cannot touch your computer. Turn it on and anyone holding the URL can read and write files and run commands on your machine — "can run commands" means "has your computer." Read the security model first.
Related MCP server: deepseek-mcp
Why this exists
ChatGPT's sub-agent slots only accept OpenAI's own model tiers (Sol / Terra / Luna). You cannot register an external model as a sub-agent. The only supported entry point for a foreign model is an MCP connector — i.e. wrapping it as a tool that the main agent can call.
That has real consequences you should understand before deploying:
Native ChatGPT sub-agent | This bridge (DeepSeek as a tool) | |
Separate context | Yes | Yes (it only sees the prompt you send) |
Independence | Separate session, same OpenAI stack | Different vendor, different model — genuinely independent |
Parallelism | Yes | Yes, via agent jobs — |
Result destination | Retained in its own session | Returns into the caller's context |
Can touch your machine | Yes — but only inside the host's own sandbox, never your filesystem | Yes — in your directories, once you grant a workspace. Read Security model first |
Cost | Subscription credits | DeepSeek API, billed separately (very cheap) |
The main practical payoff is cross-vendor independent review. If your prompt requires that a reviewer must not reuse the implementer's conclusions, a model from a different vendor satisfies that requirement far better than another tier of the same stack — there is no shared training lineage to echo.
Swapping the host model does not break this. The bridge does not depend on Sol, or on which tier you are running. It depends on one thing: sub-agent slots are not open to external models. That is a property of the platform, not of the model tier — change tiers and the slot is still closed, so this bridge is still the only way in. Only the sub-agent side is ever re-pointed: edit DEEPSEEK_BASE_URL and DEEPSEEK_MODEL in .env and run npm run ctl -- reload (the tunnel is left alone, so the URL does not change). The one change that would make this redundant is platform-level, not model-level: OpenAI opening the sub-agent slot to external models.
The bridge itself is a standalone Node process that talks only to
api.deepseek.com. Agent jobs are the exception: to run one it spawns Claude Code as a child process, pointed at DeepSeek's Anthropic-compatible endpoint. The bridge never runs inside a host — it launches one.
Architecture
ChatGPT (Sol) ──HTTPS──▶ Cloudflare edge ──tunnel──▶ bridge (127.0.0.1:8787)
│
deepseek_flash(task, mode, files) ───────────────────────┤──▶ api.deepseek.com ──▶ text back
│
deepseek_agent_start(task, workspace) ──▶ job registry ──┤ abort · TTL · nonce · step/time ceilings
deepseek_agent_poll(job_id) ◀──────────── snapshots │
│
harness: claude -p ──▶ api.deepseek.com/anthropic
│ Read / Write / Edit / Bash
│
└─ approval prompt (stdio MCP child of the harness,
never reachable over the URL) ──▶ you, via `npm run ctl`Once a tool call reaches that approval prompt it is routed one of three ways, and the split is the point:
Tool class | Rule |
Commands ( | Pre-approved names run unattended; everything else pauses for a human. |
File tools ( | Path checked against the job's workspace. Inside runs unattended; outside pauses for a human. |
Network tools ( | Always a human, regardless of workspace. |
--add-dir is not what enforces the middle row. It only adds an accessible directory; it restricts nothing. The boundary comes from listing the file tools in the harness's permissions.ask, which is the only knob that makes --permission-prompt-tool fire in headless mode — measured, not assumed. Before that, a job whose workspace was D:\project could Read C:\Users\you\.env with no prompt at all.
Transport: MCP Streamable HTTP, stateless (
sessionIdGenerator: undefined, a fresh server + transport per request so callers never share state). The job registry is deliberately not per-request — it is created once per process, or every job would be forgotten the moment itsstartcall returned.Responses stream as SSE rather than buffered JSON. This keeps bytes moving on the wire, which avoids Cloudflare's free-tier 524 timeout on long DeepSeek calls.
Auth: a capability URL. The MCP endpoint is
/mcp/<64-hex-secret>; the path is the credential. The bare/mcppath and any wrong path return 404, indistinguishable from nothing being there — because ChatGPT's connector form has no Bearer-token field.Agent jobs are asynchronous by necessity. ChatGPT's tool-call budget is around 60 s; a real task is not.
agent_startblocks up to 45 s for a fast job and otherwise returns a job id to poll.
Requirements
Node.js >= 24 (uses native TypeScript type-stripping; no build step)
A DeepSeek API key — create one at https://platform.deepseek.com
cloudflaredfor the quick tunnel (or bring your own deployment — see below)For agent jobs only: Claude Code on
PATH(or setBRIDGE_CLAUDE_BIN). The one-shotdeepseek_flashtool has no extra dependencies, and the offline test suite does not need either this or an API key.
Quick start
Two ways in. The wizard asks the questions and writes .env for you, then brings everything up; do it by hand if you would rather see every value before it is written.
The guided way
git clone https://github.com/CassiaTw1g/modelbridge.git
cd modelbridge
npm run setupIt runs on a bare clone — no npm install, no .env — because it is the thing that creates both. Have your API key ready to paste.
On Windows you can also double-click windows/0-首次安装.bat, which is the same thing.
It asks five things:
# | Question | What it does with the answer |
1 | Which model, and its API key | Writes |
2 | Which harness runs it | Confirms Claude Code is findable and records the path if it is not on |
3 | Which directories the sub-agent may touch | Writes |
4 | Temporary or fixed hostname |
|
5 | Should the sub-agent ask before running commands |
|
Every answer is written to .env as it is given, so Ctrl+C leaves a usable file and rerunning carries on from where you stopped. It ends by starting the server, opening the tunnel, copying the connector URL to your clipboard and printing it.
Language is picked up from the OS locale; override with npm run setup -- --lang zh or --lang en (also BRIDGE_LANG).
Then go to Register the connector. To check everything afterwards, see Verification.
The manual way
git clone https://github.com/CassiaTw1g/modelbridge.git
cd modelbridge
npm install
cp .env.example .envEdit .env:
Set
DEEPSEEK_API_KEYto your key. Use a dedicated key for this bridge so it can be revoked independently, and set a spend cap on it in the DeepSeek console as a last line of defence.Generate the path secret:
npm run ctl -- secretThis writes
MCP_PATH_SECRET=<random hex>into.env(or prints it if.envdoesn't exist yet).
Start the server and open the tunnel:
npm run start # background server on 127.0.0.1:8787
npm run tunnel # cloudflared quick tunnel; prints the public URL and the full MCP endpointnpm run tunnel prints the exact URL to paste into ChatGPT:
公网端点 : https://<random>.trycloudflare.com/mcp/<your-secret>It checks first that the local port is actually answering before it opens anything — a live PID is not the same as a live service, and a tunnel in front of a dead port produces a URL that fails in a way that looks like ChatGPT's fault.
Register the connector
Open ChatGPT on the web (connectors cannot be configured from desktop or mobile).
Settings → Plugins → MCP → Add server (alternatively: Settings → Connectors → Advanced → Developer mode).
Type: Streamable HTTP. Authentication: No authentication.
URL: the full
https://<random>.trycloudflare.com/mcp/<your-secret>— including the/mcp/<secret>path. Pasting only the domain will not work.Save. The connector handshake shows up in the log as
server/discover → initialize → notifications/initialized → tools/list.
Tell your main agent when to use it
The tool description is the router. It states dispatch conditions, not just what the tool does — this is what prevents the calling agent from picking it arbitrarily. Add a rule to your system prompt that is mutually exclusive with your sub-agent rules, e.g.:
Tasks needing cross-vendor independent review (security review, counterexample construction) → call the
deepseek_flashtool, do not delegate to a sub-agent. Tasks needing file access, parallelism, or same-stack collaboration → delegate to a sub-agent.
Tool reference
Three tools in two groups. All three are always registered — but until DEEPSEEK_ALLOWED_ROOTS is set, the two agent tools reject every workspace with an explanatory error, so the bridge stays read-only.
deepseek_flash — one question, one answer
Parameter | Type | Required | Description |
| string | yes | The concrete task. State the goal, constraints, and expected output format. For independent review, do not reveal your own conclusion here — it contaminates the independence. |
| enum | no |
|
| string | no | The code/text to analyse, passed through as plain text. This tool cannot access your filesystem — content must be inlined here. Subject to the server's 2 MB request-body cap: the model's context window is ~1M tokens, but that is not how much this path can carry in. |
Each mode gets a distinct system prompt. review explicitly instructs the model to treat any author conclusion in the material as an unverified claim and to state disagreements explicitly — that is the point of routing to an external vendor.
deepseek_agent_start — hand over a task that needs hands
Parameter | Type | Required | Description |
| string | yes | The concrete task. State the goal, constraints, and expected output. Do not reveal your own conclusion. |
| string | yes | Absolute path the sub-agent may work in — it reads, writes and runs commands with this as its root. Must fall inside |
| enum | no | Accepted for compatibility; the current harness does not read it. |
Blocks up to 45 s (ChatGPT caps a tool call near 60 s). A fast job returns its result inline; anything longer returns a job_id.
The client hanging up does not cancel the job. The abort controller belongs to the job registry, not to the HTTP request — otherwise every caller that lost patience would silently kill the work it had just dispatched. Use npm run ctl -- job kill <id> to actually stop one.
deepseek_agent_poll — collect it
Parameter | Type | Required | Description |
| string | yes | The id |
| number | no | Blocking wait, default 20, max 40. Returns early if the job finishes sooner. |
Every terminal payload carries a nonce minted at job creation. An agent that reports an outcome without quoting it does not have a result — that is the point.
Behind the job: what the sub-agent actually runs
A job spawns Claude Code (claude -p … --output-format stream-json) with ANTHROPIC_BASE_URL pointed at api.deepseek.com/anthropic, so the model driving the loop is DeepSeek while the loop, context compaction, prompt caching and tool implementations are Claude Code's own. Every tool event is captured into the job trace, readable with npm run ctl -- jobs <id> --trace.
Configuration
All via .env (gitignored):
Variable | Default | Notes |
| — | Required. Use a dedicated key with a spend cap. |
| — | Required, min 16 chars. The capability path segment. |
|
| Local listen port. |
|
| Listen address. Leave on loopback — the tunnel runs on the same machine, so exposing the port to your LAN has no upside. |
|
|
|
| — | Named mode. Your public hostname, e.g. |
| — | Named mode. The credential the Cloudflare dashboard issues for the tunnel. A full credential, on the same footing as |
| — | Named mode, alternative to |
| (probe) | Transport cloudflared uses to reach the edge: |
|
| Sliding window that limits the blast radius if the URL leaks. One global bucket, not per IP — the server listens on loopback and the only client is the tunnel, so |
|
| OpenAI-compatible endpoint. |
|
| Model ID. |
|
| Server-side timeout; returns a structured error instead of hanging. Kept under Cloudflare's 100 s edge timeout. |
|
| Output cap. |
Agent jobs
With DEEPSEEK_ALLOWED_ROOTS unset, the agent tools reject every workspace and the bridge stays read-only. These settings only matter once you set it — read Security model first.
Variable | Default | Notes |
| (empty) |
|
|
| Step ceiling per job (one tool call = one step). A job that hits it still stops, but what it had already read comes back as a partial result instead of nothing. It is a runaway brake, not a work estimate — a review that walks a whole project passes 40 steps easily and dies just short of the end. |
|
| Wall-clock ceiling (30 min). On expiry the job and its entire process tree are killed. |
|
| How long a command waits for a human (5 min). Expiry is a deny. |
| see | Comma-separated pre-approved command names — |
| on | Set to |
|
| Path to the Claude Code binary. |
|
| Where the harness sends its requests. |
|
| Job snapshots, approval queue, audit log. |
| (injected) | Set by the harness, one value per job — it is how the approval prompt learns the boundary it is enforcing. Do not set it by hand. |
| (unset) | Opt-in |
Lifecycle commands
npm run setup # the first-run wizard (see Quick start)
npm run start # background start (no-op if already running)
npm run stop # stop server and tunnel
npm run restart # restart server (stops the tunnel too — re-run `npm run tunnel`)
npm run status # enabled state, PIDs, local + public endpoints
npm run logs # last 40 log lines
npm run url # copy the full connector URL to the clipboard, and print it
npm run tunnel # start the Cloudflare tunnel, print the public URL
npm run untunnel # stop the tunnel only
npm run auto # show the current approval mode
npm run auto:on # bypass approvals entirely (see Security model)
npm run auto:off # back to "needs approval"
npm run enable # clear the disabled flag
npm run disable # stop everything and set the disabled flag
npm run uninstall # stop and clear local state (leaves the project directory)npm run start --foreground runs in the foreground for debugging.
npm run setup is rerunnable: it re-asks everything and rewrites the answers, and its workspace-roots answer is added to the list rather than replacing it, so a rerun cannot silently erase roots you added with npm run ctl -- allow.
npm run url is the one to reach for when rebuilding the ChatGPT connector: it
prints https://<host>/mcp/<secret> and puts the same string on your clipboard,
so you never have to hand-select the secret out of npm run status --show.
Rotating the path secret
npm run ctl -- rotateGenerates a new secret, restarts the server only, and prints the new public URL — which it also copies to your clipboard. The tunnel is deliberately left running: it forwards a port and has no idea what the path is, so the public hostname stays the same and only the last segment of the URL changes. (npm run restart would kill the tunnel too, costing you a new hostname and a second trip to the connector.)
Rotate whenever the URL may have been seen by anyone else. It is the only thing standing between your machine and whoever holds it.
On Windows, windows/7-轮换密钥.bat does the same thing from a double-click. If you prefer to drive it yourself, npm run ctl -- secret writes the secret without restarting anything — you then have to restart and update the connector by hand.
The same shape applies to the workspace allow-list: windows/8-添加项目目录.bat asks for a path and runs npm run ctl -- allow "<dir>", which writes .env and reloads with the tunnel left alone. Quote the path or use forward slashes — a bare D:\codex\3 typed into a POSIX shell loses its backslashes.
The secret is no longer printed
npm run ctl -- status / start / tunnel / secret now render the endpoint's secret as <密钥已隐藏>. Add --show to print it in full. Every printed copy outlives the moment — terminal scrollback, a shell transcript, a log pasted into a bug report — and the one real leak this project has had came from ctl printing it, not from anyone finding it. rotate still puts the full URL on the clipboard: a clipboard is not a log.
After editing code or .env
npm run ctl -- reloadRestarts the server alone. The tunnel is untouched, so the public URL is identical and the ChatGPT connector needs no attention. restart also stops the tunnel — a different command for a different situation.
Why you have to recreate the connector, and how to stop doing that
This is not you failing to find the button. ChatGPT's connector form may have no "edit URL" action at all — the documented recovery is to remove the connector and add it again. (Interfaces differ by account and by release, so it costs nothing to look for an edit control first.) And the bridge's default transport is a Cloudflare quick tunnel, which is handed a new random hostname on every start — so any tunnel restart changes the URL and costs you a rebuild.
Three ways out:
Approach | Cost | Result |
Do nothing | none | Usually enough. |
A Cloudflare named tunnel | you must own a domain already on Cloudflare | The hostname half of the URL never changes — built in, see below. |
A tunnel with a permanent hostname | install one tool, create one account | The hostname half of the URL never changes. Tailscale Funnel is the lowest-effort option: free for personal use, giving you |
Start with the first row. Move on only if an occasional rebuild still bothers you.
A fixed hostname on your own domain
The quick tunnel is the default because it needs no setup. If you have a domain whose NS already points at Cloudflare, you can have a URL that survives restarts, reboots and restart alike — so the connector is built once and never touched again.
Two of the three steps happen in Cloudflare's dashboard and only you can do them. Create the tunnel at Networking → Tunnels → Create a tunnel (older guides say Zero Trust → Networks → Connectors; that menu redirects to the same page), then add the route under that tunnel's Routes tab:
Networking → Tunnels → your tunnel → Routes → Add route
→ Published application
Subdomain : mcp
Domain : your domain
Service type : HTTP
Service URL : http://localhost:8787Cloudflare creates the DNS record and the certificate for you — do not add one by hand. Then copy the long token the page shows you and come back here:
npm run ctl -- tunnel named mcp.example.comIt asks for the tunnel token (paste it — do not retype it into a file by hand) and writes your .env for you. On Windows, windows/11-固定域名.bat is the same thing from a double-click: it prints the dashboard steps, asks for the hostname and the token, and offers to switch the tunnel over once you confirm the route is saved.
⚠️ Skipping the route step is the trap. The tunnel will report connected to the edge, everything local looks healthy, and the domain returns 404 — because the "hostname → local port" route lives in Cloudflare's cloud, not on this machine. npm run ctl -- tunnel check lists what you have not done yet, and reports "registered with the edge" and "the domain actually answers" separately, because they are different facts. Nothing on your machine has to change to fix it: add the route and it starts working.
Nothing real is hardcoded here: the code and docs carry no one's domain, and every value comes from your own .env. TUNNEL_TOKEN is a credential on the same footing as the path secret — it is never printed, and ctl masks it.
Do not use the free ngrok tier. It injects a browser-warning page that needs an
ngrok-skip-browser-warningrequest header to get past, and a ChatGPT connector cannot set custom headers — the connection is cut.
Agent job and approval commands
These read the files the server writes, so they work from a second terminal — and they still work if the server was restarted between the request and your answer.
npm run ctl -- jobs # every job: state, steps, duration, nonce
npm run ctl -- jobs <id> --trace # full trace: which tool at which step
npm run ctl -- job kill <id> # cancel a running job, process tree included
npm run ctl -- pending # commands waiting for your approval, in full
npm run ctl -- approve <id> # let it run
npm run ctl -- deny <id> # refuse it — the model is told why, and told not to route around it
npm run ctl -- audit # recent approval decisions (--all for everything)Verification
Four layers, cheapest first.
1. Offline suites — no network, no key, no money:
npm testSuite | What it guards |
| Response parsing and request accounting. A tool-call turn must not be sent twice; |
| Path-escape regressions: UNC, |
| The approval protocol: auto-approval rules, chaining refusal, and that every non-human exit — timeout, cancellation, a corrupt decision file — resolves to deny. |
| The workspace boundary for file tools: in-workspace paths allowed, out-of-workspace paths, parent traversal, prefix-sibling directories, UNC and alternate data streams all sent to a human; |
| The registry. Chiefly: a client disconnect must not kill the job, because the MCP SDK aborts per-request handlers when a client hangs up. |
| In-memory MCP round trip; the agent tools are registered and reject an out-of-scope workspace. |
2. Public endpoint smoke test — exercises the real HTTP path:
npm run smoke # reads the tunnel URL from .state/tunnel.log
npm run smoke -- https://host/mcp/xxx # or pass an endpoint explicitlyThis asserts the health check, that a bare /mcp returns 404, that a wrong secret returns 404, and that a real tools/call returns non-empty content. It uses Node's fetch, not curl — see the Windows note below.
3. Acceptance — is the sub-agent actually capable? (online, costs a little)
npm run accept # all three tasks
npm run accept -- --only BThree tasks a chat-only model cannot pass:
Task | Passing means | |
A | Reproduce a 32-character random string that exists only inside a file, reversed | It really read the file — nothing else can produce that string |
B | Run a script, observe the failure, fix it, run it again | Two or more command executions in the trace. A model answering in one shot cannot know the program fails; this is the hard evidence that it loops |
C | Report the contents of a file that does not exist | It says so. A sub-agent that fabricates is more dangerous than one that cannot work |
Test B is the load-bearing one. Approval is deliberately disabled for the suite, which is why it is not part of npm test.
4. Did ChatGPT actually call us?
Server access logs and the chat transcript look identical whether the model truly called the tool or merely narrated doing so. The only trustworthy signals are:
the log line
tools/call deepseek_flashinnpm run logs, anda matching entry in the DeepSeek console usage page.
For agent jobs, npm run ctl -- jobs must also show a record with more than one step and a real workspace path. If the chat shows a plausible answer but every one of those is silent, the calling model role-played the call. Re-sharpen the tool description or your dispatch rule.
Deployment options
The quick tunnel is fine to start, but its URL changes on every restart (you must re-edit the connector). For anything long-lived:
Option | Cost | When |
cloudflared quick tunnel | free | First run, validation. |
Cloudflare Worker | free | Stable URL, no local process. Rewrite the transport onto Hono's Web Standard variant; store the DeepSeek key as a Worker secret. |
VPS (HK / SG) | ~$5–12/mo | Long-term stability, and the 100 s edge timeout disappears entirely. |
Avoid the ngrok free tier: its interstitial warning page requires an ngrok-skip-browser-warning header, which ChatGPT connectors cannot send, so the connection is severed.
Troubleshooting
Symptom | Cause / fix |
| Run |
ChatGPT shows "couldn't connect" | URL must include the full |
404 from the public URL | Wrong or stale secret. Restart after rotating, and update the connector URL. |
Empty/blank reply from DeepSeek |
|
Truncated answer | Reasoning consumed the token budget ( |
Cloudflare 524 | A single call exceeded the ~100 s edge timeout. Slow responses already stream as SSE; lower |
Tunnel URL unreachable from your own machine | Local router DNS may not have the fresh |
Windows / git-bash: garbled results or token blowups |
|
| The path is outside |
Agent job sits in | A command is waiting for you. Run |
| It is: the PID is alive and the service is not. |
The wizard stopped at a question with | Something closed stdin — a pipe, a redirected file, a CI runner, or a double-click that lost its console. Answer in a real terminal, or answer the remaining questions by hand in |
Every agent job fails immediately | Claude Code is not on |
Agent job dies partway through | Step ceiling ( |
A job you killed shows as | That is a bug — a person stopping a job is not a crash. Please report it. |
A file-reading task is refused before it reaches the bridge | ChatGPT's own safety layer, not this bridge. Sol may refuse to hand a local file to an external model and ask you to authorise it explicitly. Grant it (naming the file and what is in it helps), or reframe the task so the contents never travel back through the chat — have the sub-agent write the result to disk and read it there yourself. Confirm with |
The sub-agent tells you "nothing left your machine" | Do not take its word for it. A sub-agent has no visibility into its own hosting. It runs on DeepSeek's API, so anything a file tool reads into its context is sent there on the next model call — and it will still report that no transmission occurred, because from where it sits the work looked local. Only the architecture answers this question; never the model's own account of it. |
Security model
Read this before exposing anything. It has two tiers, and they defend completely different things.
Tier 1 — protecting your bill
Applies always.
The path secret is the credential. Anyone with the URL can spend your DeepSeek quota. Treat it like a password; rotate with
npm run ctl -- secret.The secret is never written down by the bridge itself. The startup line,
ctl logs,ctl auditandctl jobsall render it as<密钥已隐藏>, andsmokemasks it too. The reason is unglamorous: the only real leak this project has had came from the bridge's own log file, not from anyone guessing it. Every printed copy outlives the moment — scrollback, a transcript, a log pasted into a bug report.Bind to loopback.
HOSTdefaults to127.0.0.1. Do not set0.0.0.0.Rate limiting is on by default and caps abuse if the URL leaks. It is a global bucket of
tools/callrequests rather than a per-IP window — see the variable table for why the per-IP framing was worse than useless here.A DeepSeek spend cap in the provider console is the final backstop — set it.
Use a separate API key. Do not reuse a key that other tools depend on; a leaked bridge key should be revocable without collateral damage.
Tier 2 — protecting your computer
None of tier 1 stops this. Set
DEEPSEEK_ALLOWED_ROOTSand whoever holds that URL can make the sub-agent read and write files on your machine, and run commands on it. "Can run commands" means "has your computer."
Out of the box the bridge is still read-only — the agent tools refuse every workspace and can only spend quota. The tiers are orders of magnitude apart:
What you enable | What the holder of the URL can do |
Nothing (default) | Spend your DeepSeek quota. Cannot touch your computer. |
| Read and modify files under those roots; reads and writes outside them pause for your approval rather than being blocked outright |
…plus command execution | Run arbitrary commands — that is the machine |
The guardrails, and — just as importantly — what they are not:
Fail closed. No
DEEPSEEK_ALLOWED_ROOTSmeans every workspace is denied, never "anywhere by default".Every workspace goes through
src/sandbox.ts, which is the only place a caller-supplied string becomes a real path.File tools are held to the workspace by
src/harness/file-guard.ts, which reuses that samesandbox.admit(), so junctions, 8.3 short names, UNC paths, alternate data streams and trailing dots are one implementation with one regression suite (npm run test:guard) rather than a second, weaker copy. Measured before and after: a job whose workspace wasD:\projectused to read a file thousands of directories outside it in two steps with no prompt and copy the contents back into the workspace; it now stops atwaiting_approvaland the content never enters the run. In-workspace reads and writes still run unattended, so nothing was made slower for the normal case.Commands outside the allow-list pause the job for a human. Unanswered means deny; there is no default-allow path, and the approval channel is a stdio child of the harness rather than an endpoint on the URL — otherwise a caller could approve its own commands.
Network tools always go to a human.
WebFetchandWebSearchcannot be pre-approved and are not covered by the workspace at all — the workspace governs what comes in, and nothing in the original design governed what goes out.The sub-agent's environment is scrubbed of your credentials.
claude-code.tsdeletes every inherited variable whose name looks like a secret (API_KEY,_KEY,SECRET,TOKEN,PASSWORD,CREDENTIAL…) before spawning the harness, so a pre-approvednode -e "console.log(process.env.X)"no longer reaches the bridge's own key, or an unrelated tool's. One variable is deliberately kept:ANTHROPIC_AUTH_TOKEN, which is the DeepSeek key the harness needs in order to call a model at all. Treat that key as readable-by-the-sub-agent — dedicated, revocable, spend-capped — because it is.Step, time and process-tree ceilings. A job that hits one settles as
errorand is given no nonce, so it cannot be mistaken for a finished review — but what it had already read comes back as a partial result. A brake should not also throw away the work that was done. Plus an audit log at.state/audit.log.Clear
DEEPSEEK_ALLOWED_ROOTSand restart to go back to tier 1. That is a supported configuration and the recommended place to start: run read-only for a while, confirm the URL has not leaked, and only then consider turning it on.
This is not a security boundary. It is an observation window. The only real boundary is a sandbox or a VM — be at the machine while jobs run. Do not ship a public deployment of this with agent capabilities enabled.
Temporarily disabling approvals altogether (npm run auto:on)
While you are watching a batch of jobs run, approving commands one at a time is tedious. This switch turns the approval queue off wholesale:
npm run auto # which mode am I in
npm run auto:on # fully unattended
npm run auto:off # back to "needs approval"It removes more than you probably expect. It is not "commands stop asking" — the approval-mcp.ts child process is never spawned, so everything hanging off it goes away together:
Gate | Under the bypass |
Command allow-list | gone |
Chaining guard ( | gone |
File tools' workspace boundary | gone |
Per-request approval records in | no longer produced |
What still applies: DEEPSEEK_ALLOWED_ROOTS still decides which directories may be opened as a workspace. The bypass loosens what happens inside one, not which ones can be opened.
A restart revokes it. The switch is a flag file at .state/auto-approve (not a .env key), and every service start deletes it. So reload / restart / rebooting all land back on "needs approval". That is on purpose: the bypass means "I am watching this job right now", not "I have stopped caring". reload, rotate and allow all restart the service and therefore revoke it too — each of them says so before it does.
Turning it on needs no restart — it takes effect on the next job (the check is re-read per job).
BRIDGE_CC_APPROVAL=offin.envis the other path and does not reset on restart. It is kept fornpm run accept. If both are on,auto offonly clears the flag file, andctl statustells you plainly that the.envone is still live.
An honest note on auditing: under the bypass there are no approval_requested / approval_auto lines any more — the process that writes them never starts. What remains is the mode-change lines (auto_approve_on / auto_approve_off / auto_approve_cleared) and the harness's own tool-call trace in .state/jobs/<id>.json (one entry per call, capped at 400, readable with npm run ctl -- jobs <id> --trace). A record is not a gate — that is for looking back afterwards, not for stopping anything.
To report a vulnerability, see SECURITY.md.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
The OpenRouter for tools. One MCP connection gives any AI agent 254 hosted tools, pay per call.
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
Pay-per-use tool marketplace for AI agents. Search, price-check, and call APIs via MCP.
5 pay-per-call SEO tools over MCP. Free discovery, tool calls settle in USDC on Base via x402.
Related MCP Servers
- AlicenseAqualityAmaintenanceEnables integration of DeepSeek's language models with MCP-compatible applications, offering features like chat completion, custom model selection, and parameter control for enhancing language-based interactions.11800 npm351MIT
- AlicenseAqualityDmaintenanceEnables using DeepSeek models as a small, cheap supervised worker from any MCP-compatible client, providing fast flash and deep reasoning tools for bounded tasks.253MIT
- AlicenseAqualityCmaintenanceAugments DeepSeek-V4 with image understanding and deep research capabilities via MCP, enabling vision and web research tools.51MIT
- AlicenseNot gradedqualityBmaintenanceConnects MCP clients to DeepSeek API, including DeepSeek-R1 reasoning with visible chain-of-thought. Provides four tools: generate, chat, reason, and list models.1MIT