Ini adalah config MCP yang diberikan aplikasi desktop osFoundry ke Claude Code saat menjalankannya sebagai worker agent mode. Bridge di sini adalah hasil rebuild saya dengan Python, jadi port dan token-nya berasal dari run itu. Strukturnya sama dengan yang ada di src-tauri/src/mcp_bridge.rs:
{"mcpServers": {"osfoundry": { "type": "http", "url": "http://127.0.0.1:51377/mcp", "headers": {"Authorization": "Bearer 8dd5de…"}, "timeout": 120000 }}} Model keamanannya ada di empat detail. Host-nya 127.0.0.1, tidak pernah 0.0.0.0. Port-nya ditentukan oleh OS (bind("127.0.0.1:0")). Token-nya 64 karakter hex yang dibuat khusus untuk run ini saja. Dan sama sekali tidak ada daftar tool di dalamnya. Command line yang menyertainya juga tidak memakai --allowedTools:
claude -p "<conversation>" --output-format stream-json --verbose --include-partial-messages \ --permission-mode bypassPermissions --add-dir <folder> \ --mcp-config '<the JSON above>' --strict-mcp-config Allowlist-nya ada. Letaknya di server. Di bawah ini alasannya, plus rebuild yang bisa kamu jalankan sendiri.
Kenapa allowlist-nya ada di server?
Agent mode menjalankan Claude Code dengan --permission-mode bypassPermissions, karena tidak ada orang di terminal yang menyetujui setiap langkah. Komentar di build_claude_command menjelaskan kenapa tidak ada allowlist yang dikirim: dengan bypassPermissions, Claude Code mengabaikan --allowedTools. Jadi flag itu cuma hiasan. Header mcp_bridge.rs mengatakan hal yang sama dengan lebih singkat: "Never trust the client; the server decides."
Alasan umumnya berlaku untuk CLI agent apa pun. Flag adalah instruksi untuk client, dan client justru pihak yang menjalankan kode yang dikendalikan model. Itu tempat yang salah untuk satu-satunya pengecekan yang harus selalu berlaku. Server melihat setiap tools/call dan tidak dijalankan oleh model, jadi tugas itu jatuh ke server.
osFoundry melakukannya di dua tempat:
-
tools/listhanya mengembalikan tool yang ada di daftar run (agent_cli::tool_policymenyaringnya lebih dulu). Model tidak pernah diberi tahu bahwa tool lain ada. -
tools/callmencocokkan nama dengan daftar yang sama sebelum apa pun dijalankan. Model tetap bisa mengeluarkan nama tool yang tidak pernah ditunjukkan kepadanya, dari data training atau dari prompt injection di file yang dibacanya. Filter pertama tidak bisa apa-apa soal itu. Filter kedua bisa.
Apa yang dilihat CLI di panggilan pertama?
Tiga POST, masing-masing di koneksi baru, karena server menjawab Connection: close:
→ initialize ← 200 {"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"osfoundry",…}} → notifications/initialized ← 202, Content-Length: 0 → tools/list ← 200 {"tools":[{"name":"memory_search","description":…,"inputSchema":{…}}]} 202 dengan body kosong itu bukan soal rewel. Source Rust-nya mencatat bahwa MCP client milik Codex (rmcp) mematikan transport-nya kalau sebuah notifikasi menerima {} dengan 200, sehingga seluruh run mati di tahap handshake. Claude Code masih bisa menerimanya. Codex tidak. Semua pesan JSON-RPC tanpa id, atau dengan method notifications/, mendapat 202 tanpa body.
Setelah itu Claude Code mengekspos setiap tool sebagai mcp__<server>__<tool>, jadi memory_search menjadi mcp__osfoundry__memory_search. Nama server di config itulah yang menaruh osfoundry di tengah.
Rebuild-nya
Tidak ada binary claude yang terpasang di mesin ini. Jadi sisi client-nya adalah script yang melakukan apa yang dilakukan agent headless terhadap server MCP HTTP: membaca --mcp-config, menjalankan handshake, memanggil tool. Server-nya adalah logika bridge Rust yang ditulis ulang dengan standard library Python. File lengkapnya: bridge.py, fake_cli.py dan run_demo.py, sekitar 230 baris totalnya.
Bagian yang penting, dari bridge.py:
class Run: def __init__(self, workspace_id, channel_id, allowed, poll=0.5): self.workspace_id, self.channel_id = workspace_id, channel_id # bound now, never read from a request self.allowed = [t for t in allowed if t in REGISTRY] self.token = secrets.token_hex(32) # 64 hex chars, new every run ... def do_POST(self): body = self.rfile.read(int(self.headers.get("Content-Length") or 0)) auth = self.headers.get("Authorization", "") tok = auth[7:].strip() if auth[:7].lower() == "bearer " else "" if not hmac.compare_digest(tok.encode(), run.token.encode()): return self.send(401, {"error": "unauthorized"}) req = json.loads(body) i, method = req.get("id"), req.get("method", "") if i is None or method.startswith("notifications/"): return self.send(202) ... if method == "tools/call": p = req.get("params") or {} name = p.get("name") or "" if name not in run.allowed: # the only gate that counts return self.send(200, rpc_err(i, -32601, f"tool '{name}' is not available to this agent")) text = execute_tool(run.workspace_id, run.channel_id, name, p.get("arguments") or {}) return self.send(200, rpc_ok(i, {"content": [{"type": "text", "text": text}]})) self.httpd = ThreadingHTTPServer(("127.0.0.1", 0), H) # loopback, OS-assigned port execute_tool menggantikan orchestrator::execute_tool. Di aplikasinya, itu satu-satunya fungsi yang dilewati semua tool call dari pemanggil mana pun, lengkap dengan approval, plan mode, override channel, dan sandbox. Bridge tidak menambah jalur eksekusi sendiri. Bridge hanya memutuskan apakah sebuah panggilan sampai ke fungsi itu.
Registry-nya punya tiga tool: memory_search, execute_command dan delete_file. Run dimulai dengan allowed=["memory_search"]. Lalu CLI palsunya melakukan empat hal:
tools seen by the CLI: ['mcp__osfoundry__memory_search'] call 1: memory_search {"query":"budget"} → {"result":{"content":[{"type":"text","text":"Q4 budget review: Thu 14:00."}]}} call 2: execute_command {"cmd":"rm -rf ~/work"} → {"error":{"code":-32601,"message":"tool 'execute_command' is not available to this agent"}} call 3: memory_search {"query":"contract","workspace_id":"ws-client"} → {"result":{"content":[{"type":"text","text":"no matching notes"}]}} call 4 (wrong token): (401, {'error': 'unauthorized'}) audit: [('ws-personal','ch-ops','memory_search',{'query':'budget'}), ('ws-personal','ch-ops','memory_search',{'query':'contract','workspace_id':'ws-client'})] after the run: connect to 51377 -> refused Call 2 adalah kasus yang tidak bisa ditangkap flag. Model menyebut tool yang tidak pernah ada di daftarnya, dan server menolaknya sebelum execute_tool. Daftar audit membuktikannya: execute_command tidak pernah muncul di sana.
Call 3 mencoba mengarah ke workspace lain lewat argumen. Catatan "Client X contract renews 2027-01-31" memang ada, tapi hanya di ws-client. Bridge mengirim workspace_id miliknya sendiri yang sudah terikat, jadi pencarian berjalan di ws-personal dan tidak menemukan apa-apa. Di kode Rust, workspace_id dan channel_id tersimpan di SessionCtx sejak start(), dan tidak ada yang membacanya dari request.
Baris terakhir memastikan endpoint ikut mati bersama run. Di aplikasinya, McpSession mengirim sinyal shutdown di Drop, jadi panic atau return yang terlalu awal tidak bisa meninggalkan listener yang menjalankan tool.
Berapa biayanya?
Semua angka berasal dari run_demo.py di Apple M1 (8 GB) dengan Python 3.9.6, sementara job render lain membuat load average sekitar 10. Setiap request membuka koneksi TCP baru, seperti yang dipaksakan bridge aslinya dengan Connection: close. Waktunya adalah wall-clock dari time.perf_counter() di sekitar setiap panggilan.
| Langkah | p50 | p95 | n |
|---|---|---|---|
| Start bridge (bind + thread server) | 1.20 ms | 2.69 ms | 200 |
| Handshake (initialize, notifikasi, tools/list) | 1.82 ms | 4.98 ms | 200 |
tools/call, diizinkan | 0.41 ms | 1.06 ms | 1000 |
tools/call, ditolak allowlist | 0.41 ms | 1.08 ms | 1000 |
| Token salah, 401 | 0.40 ms | 1.09 ms | 1000 |
Shutdown, serve_forever(poll_interval=0.5) | 500.91 ms | 501.98 ms | 200 |
Shutdown, poll_interval=0.01 | 10.88 ms | 11.07 ms | 200 |
Menjalankan satu bridge per run memakan sekitar 3 ms, start plus handshake. Proses Claude Code butuh waktu jauh lebih lama dari itu hanya untuk boot, jadi server per run praktis gratis. Panggilan yang ditolak sama mahalnya dengan yang diizinkan. Pengecekannya cuma lookup di list, tenggelam dalam noise round trip TCP.
Satu-satunya kejutan adalah shutdown. serve_forever di Python hanya mengecek shutdown sekali setiap poll_interval, default-nya 0.5 s, jadi setiap akhir run menunggu setengah detik. Bridge Rust tidak punya masalah ini: accept loop-nya adalah tokio::select! atas accept() dan channel watch, jadi shutdown langsung membangunkannya. Di Python, berikan poll_interval yang lebih kecil, atau tutup socket dari jalur keluar run.
Kenapa HTTP dan bukan stdio?
Kebanyakan server MCP yang kamu pasang ke Claude Code adalah proses stdio. Server milik osFoundry bukan, dan mcp_bridge.rs menjelaskan alasannya. Executor tool butuh AppHandle milik aplikasi, yang tidak bisa diserialisasi atau dipindah ke proses lain. Server stdio berarti proses kedua yang toh tetap harus meneruskan setiap panggilan kembali ke aplikasi. Claude Code mendukung server "type": "http" dengan header bearer, jadi aplikasi menyajikan MCP dari dalam dirinya sendiri di port loopback. Harganya adalah port dan token, karena itulah keduanya dibuat per run.
Kalau kamu memasangnya sendiri
- Taruh allowlist di handler
tools/callserver dan saring jugatools/list. Anggap flag CLI seperti--allowedToolshanya sebagai petunjuk, terutama di permission mode "bypass" apa pun. - Ikat workspace, user, dan channel saat sesi dimulai. Jangan pernah mengambilnya dari argumen tool atau header yang dikendalikan client.
- Buat bearer baru per run, dan bandingkan dalam constant time (
hmac.compare_digest). Jangan memakai ulang token agent berumur panjang untuk endpoint yang mengeksekusi tool. - Bind ke
127.0.0.1:0dan biarkan OS memilih port. Ikat umur listener ke child process, dan pastikan jalur shutdown-nya benar-benar langsung. - Jawab notifikasi dengan 202 tanpa body. Beberapa client memutus koneksi kalau menerima yang lain.
- Berikan
--strict-mcp-config, atau padanannya di CLI kamu, supaya server MCP milik user tidak ikut masuk ke run yang ingin kamu batasi.
Cara osFoundry membatasi tool per agent di luar kasus CLI ada di halaman tool scope. Cara menangani key dan login langganan untuk External AI ada di bring your own keys.

