pi-as-mcp
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., "@pi-as-mcpask me which branch to work on, then run the tests and fix failures"
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.
pi-as-mcp
A one-shot MCP bridge that gives a remote agent two things on your machine:
Pi's coding tools — read, write, edit, ls, grep, find, bash, powershell.
A human channel — the agent talks to you through tool calls, not through the host platform's chat UI.
Built for the Arena case: the agent runs in the cloud, you point it at this bridge, and it works on your computer. Talking through tool calls keeps one model and one context across the whole session and avoids the platform's chat round-trip.
There are no sessions and no persistence. This process is meant to live for one sitting; "state" is a single in-memory message list. That goes for authorization too: a client that outlives a restart simply authorizes again.
The command is pimcp (the project's short name). Run it from the directory the agent
should own.
→ Agent index: AGENTS.md — read that before making non-trivial changes.
What it exposes
Tool | Source | Notes |
| Pi, verbatim | Descriptions, JSON schemas and prompt metadata come from Pi at runtime |
| this repo | Agent → human, returns immediately |
| this repo | Ask and block until answered |
| this repo | Block until the human says anything |
| this repo | Returns the operating prompt verbatim |
The full operating prompt is also sent as the MCP instructions field on initialize,
and exposed as an MCP prompt named pi_as_mcp_instructions.
Related MCP server: AgentGo
Architecture
Arena / any MCP client (cloud)
│ Streamable HTTP + Bearer (or stdio)
▼
pi-as-mcp ──────────────► Pi tool definitions (imported, not copied)
│ ▲ │
│ │ └─► your filesystem / shell, confined to --root
│ └── OAuth 2.1 (--oauth): discovery, registration, PKCE, tokens
│
└─► human channel (in-memory) ──SSE──► http://127.0.0.1:8790 (your browser)
▲
└── where an authorization is approved, by handThe bridge is not an agent. It has no model and no loop: the cloud agent is the only agent. This process is a tool surface plus a message queue with exactly one human in it.
Quickstart
npm install
npm link # puts the `pimcp` command on your PATH — once
cd /path/to/workdir # the directory the agent should own
pimcp # server + token + public tunnel + OAuth, prints a paste-ready URL
pimcp --human # ...and the human channel, so the agent can talk to you
pimcp --no-oauth # ...without the authorization server (the client needs the token)The configuration file
~/.pimcp/config.json (moved with PI_AS_MCP_CONFIG) is one place for everything a run can
be steered by. A flag given on the command line beats the file; the file beats the defaults.
{
"human": true,
"approval": { "mode": "always" },
"mcps": {
"github": { "type": "stdio", "command": "npx", "args": ["-y", "some-mcp-server"] },
"remote": { "type": "http", "url": "https://host.example/mcp", "headers": { "Authorization": "Bearer …" } }
},
"tunnel": {
"enabled": true,
"mode": "named", "hostname": "mcp.example.com",
"name": "my-tunnel", "credentialsFile": "C:/.../tunnel-credentials.json"
}
}humanturns the chat channel on (human_say/human_ask/human_waitand the composer), like--human.approvalis one setting for every tool:"always"holds each call —readincluded, and upstream tools too — until you click Allow or Deny in the web UI;"never"lets everything run. It does not depend on the chat channel. (--approvestill works and means"always".)mcpsare other MCP servers, attached as upstreams at startup. Their tools appear with the upstream's name as a prefix (github_list), behind the same approval gate and timeline as the local ones. An entry with"enabled": falsestays configured but disconnected.tunnelpicks the quick tunnel (random hostname, the default) or a named tunnel with your own hostname: givename+credentialsFile(route its DNS once withcloudflared tunnel route dns) ortoken(a dashboard-managed tunnel, whose ingress points athttp://localhost:8777), plushostname.
The file is watched: human, approval and mcps apply to a running bridge without a
restart, and the same controls exist as buttons in the web UI. The tunnel layer is
restart-only — the log says so when it changes.
Steering a running bridge from the web UI
The sidebar has a controls section (loopback UIs, or any UI with a token): switch the sandbox root to another directory, flip the human channel or the approval mode, and connect / disconnect upstream MCP servers or add new ones. Every action writes back to the config file, so an editor and the UI cannot drift apart. A UI that is reachable from off the machine with no token serves none of the control endpoints — the same rule that governs OAuth approvals. Moving the root is deliberately a human-only action: the agent has no tool for it.
pimcp roots the server at the current directory, picks free ports, mints a bearer
token, opens a Cloudflare quick tunnel, waits for cloudflared to register with the edge,
opens the web UI, and prints the MCP URL and token. Ctrl-C stops everything.
pimcp --no-tunnel keeps it on localhost; pimcp --help lists the rest.
OAuth is on by default, so a client that speaks MCP's authorization flow needs nothing
but the printed URL: it discovers the authorization server, asks for access, and you approve
it in the UI. The same startup also mints and prints a client id and secret, for a product
that wants them pasted into it instead (Gemini's "custom connected app"). A client that
cannot do OAuth still has the bearer token. --no-oauth removes the authorization server
entirely.
The chat channel is off unless you pass --human. Without it the agent gets no
human_say/human_ask/human_wait and no <human> section in the prompt. The web UI always
runs: that is where tool calls are approved and authorizations are granted. Without --human
it has no composer and no conversation in it.
Without the wrapper:
# local client (stdio), tools only
node src/index.ts --root /path/to/workdir
# remote client (HTTP). Point Arena at the printed URL.
node src/index.ts --root /path/to/workdir --http --port 8777 --token "$(openssl rand -hex 16)"
# with the chat channel, and an Allow/Deny card on the web UI for every tool call
node src/index.ts --root /path/to/workdir --human --approve
# with an authorization server in front of it (no chat channel needed)
node src/index.ts --root /path/to/workdir --http --oauth \
--issuer https://mcp.example.comNode 22.6+ is required: the sources are TypeScript and run directly via Node's type stripping. There is no build step.
The default tool list is platform-aware: powershell is exposed on Windows only, because
Pi's PowerShell backend throws only available on Windows the moment it is called
anywhere else. Pass --tools read,write,edit,ls,grep,find,bash,powershell to override in
either direction.
Pointing Arena at it
Arena is in the cloud, so it needs a public URL:
cloudflared tunnel --url http://127.0.0.1:8777
# then use: https://<printed-host>/mcp
# header: Authorization: Bearer <your --token>Quick tunnels get a new hostname every restart — re-paste the URL each time. If you get Cloudflare error 1033 / HTTP 530, you are pointing at a tunnel from a previous run.
OAuth
--oauth turns the bridge into its own OAuth 2.1 authorization server, so a client
can be given access without you copying a token into it. The bearer token keeps
working at the same time, for clients that cannot do OAuth. pimcp turns OAuth on by
default; --no-oauth goes back to a token-only bridge.
Which of the two shapes below applies is decided by the client, not by you.
A client that registers itself
A spec-shaped MCP client does this without being asked:
It calls
/mcp, gets401withWWW-Authenticate: Bearer resource_metadata="…".It fetches
/.well-known/oauth-protected-resource/mcp, then/.well-known/oauth-authorization-server.It registers itself at
/register(dynamic client registration, RFC 7591) and sends its browser to/authorizewith a PKCEcode_challenge.That page has no buttons. It names the client and where it wants to redirect, and says to approve on your own machine. It then waits.
The request appears as a card in the bridge UI — client name, redirect target, scope — with Allow and Deny. Nothing happens until you click one.
Allow: the browser is handed a one-shot code and redirected back. The client exchanges it at
/tokenfor an access token (12h) and a refresh token (30d).
A client you configure by hand: Gemini's "custom connected app"
A hosted product asks you for a client id and a client secret, and shows you — behind a
button, in the same dialog — the redirect URI it will use. Such a client never registers
itself and never learns that this bridge's scope is called mcp.
Plain pimcp is enough. It mints a credential pair on every run and prints it:
Client credentials — paste these into the client's own OAuth settings. The redirect URI
is whatever that client tells you it wants; paste it back there, and the first time it
asks, the card on the approval page names it and your Allow is what permits it.
client id pimcp-h05-kLnEyqe0
client secret 1lrdVL8ehejLeu10VhXx-ADW3xWnwFpVM2Hsy8AjpGYPaste the id and secret into the dialog (客户端 ID / 客户端密钥) and continue in it. You do not have to name the redirect URI in advance, and that is not a shortcut: the product tells you the address after the bridge is already running, so there was never a moment at which it could have been registered. When the client finally asks, it waits as a card on the bridge UI like any other request — with the address it named printed on the card, under a line that says what your click is about to mean:
That address was not set up in advance — the client sent it just now, and allowing is what allows it (for this run only).
Allow is what permits that address, and the only thing that does. Approving also records it for the rest of the run, so the same address is not flagged again — but a different address from the same client is flagged again, because the permission is per address, not per client. A client with no secret gets no such path at all: it must use PKCE and one of its registered URIs, since its code has nothing else protecting it.
If you would rather have the strict shape — addresses named up front, anything else refused outright — name them yourself, and the credentials are then printed beside them:
pimcp --oauth-redirect https://oauth-redirect.googleusercontent.com/r/<your-app> client id pimcp-ZhySNFlK2i04
client secret PAootb1Kkdx75D6ATZxTQoAJ2icfeF3RQzY0xTw-rNM
redirects to https://oauth-redirect.googleusercontent.com/r/<your-app>Those credentials are minted fresh on every run, because nothing here is persisted. To keep
a pair across restarts — which is what you want, since the client already holds it — pin
both, with or without --oauth-redirect:
pimcp --oauth-client-id pimcp-gemini --oauth-client-secret "$(openssl rand -base64 32)"What a hand-configured client may do differently, and why:
Skip PKCE. Its client secret is what protects the authorization code — the arrangement RFC 6749 §4.1 described before PKCE existed — and the code only goes to its address or to one you approved. A client without a secret must use PKCE S256: that is what the MCP specification requires, and it is the only thing protecting its code. A public client that skips PKCE is turned away with
invalid_request.Send its secret in the body or as HTTP Basic. Which of the two a hosted client chooses is not this bridge's decision to make.
What it may not do: get a token without your click, or get one at all without its
secret. --oauth-redirect replaces the registration step, not the human.
A scope the bridge does not have is narrowed to mcp rather than refused (RFC 6749
§3.3 permits issuing less than was requested). A hosted client asks for whatever its own
platform taught it to ask for — openid, say — and has no way to find out otherwise. The
consent page states what was asked for and what will actually be granted.
The decision lives on the bridge UI on purpose. The page at /authorize is served
from whatever address the client reached — over a quick tunnel, that is the open internet
— and a button there would be an authorization any stranger who found the URL could click.
The UI is bound to loopback and can carry --ui-token; a page that only you can open is
the only kind that can be allowed to hand away a shell. The page is always up, because
Allow/Deny lives there, and approving a tool call or an authorization should not require
wanting to converse with an agent. Without --human it has no composer and no
human_say/human_ask/human_wait — only the approvals and authorizations.
Requests wait 10 minutes for you and then expire; at most 8 may be pending at once. Everything is in memory: tokens, registered clients and pending requests all die with the process, so a restart means the client authorizes again.
Reachability is the whole game. The authorization server advertises exactly one issuer
(--issuer, or the quick-tunnel hostname, or http://127.0.0.1:<port>), and a client checks
it against the address it actually reached. So the URL you give the client has to be the URL
the bridge is reachable at, and it must be https — except for a loopback address, which is
allowed unencrypted for local use. The bridge logs the issuer it chose, and warns when that is
a loopback address a remote client cannot use. A stable Zero Trust hostname is a better fit
here than a quick tunnel, whose hostname changes every restart.
The human UI
http://127.0.0.1:8790 is a single self-contained page — no CDN, no build step, and the
markdown and LaTeX libraries are vendored in src/human/vendor/, so it works with the
network off. It always runs. With --human it carries the composer and the conversation; without it,
it is only where approvals and authorizations are decided.
One timeline. Agent messages, your messages, and every tool call the agent runs on this machine, in the order they happened. Tool rows are compact (
bash · echo hi · 42 ms · ✓); click one to expand its output. Failed calls expand themselves. The toolbar hides all tool rows if you only want the conversation.A runtime info panel (
☰ info): sandbox root, exposed tools, the approval mode, the deny list, the wait window, uptime, the MCP endpoint, and whether the tokens are set. Token values are never shown.A countdown.
human_ask/human_waitblock for--block-ms(90s by default) and then returnno_message. The header shows the seconds left so you know whether to hurry.Alerts.
🔔asks for notification permission and turns on a short chime — different tones for a question, adone, and an error — plus a flashing tab title. Off by default.Agent messages render as markdown and LaTeX (
$…$,$$…$$,\(...\),\[...\]). They come from a remote model, so the HTML they produce is reduced to a tag allowlist before it touches the page: raw HTML is displayed, never executed.One bubble style. Messages carry a category, but it is not printed on them — a chat does not have message types. The category still picks the alert sound.
Authorization cards. With
--oauth, a client asking for access appears above the timeline — not in it, because it is not something the agent said — with Allow and Deny. It stays until you answer or it expires in 10 minutes, and it chimes like a question. The client's name is shown as text, never as markup: it chose that name itself.Tool approval cards. With
approval: "always"(or--approve), each tool call appears above the timeline with Allow and Deny, and the agent waits for your click. A chat reply does not approve anything. No answer within--block-msis a refusal, and so are a Deny, a full queue, or a shutdown.What ran on this machine. Tool calls are listed whenever the UI is up. Watching the agent work while you decide whether to trust it is the point of having the page there at all.
ui.html is read on every request, so changing it costs a browser refresh, not a restart of
the bridge. Changes to src/*.ts still need a restart.
The queue
Whatever you type while the agent is busy is queued, and its next human_ask/human_wait takes
all of it at once — oldest first, each line stamped [n/total · HH:MM:SS]. A single message
arrives verbatim. So three short lines are read as the one thought they are, and a retraction
arrives together with the message it retracts instead of a round trip later.
There is no settle window: the first message wakes a blocked call immediately, and anything typed after that waits for the next call. There is no cap on the queue, and nothing is persisted, so a restart clears it.
Configuration
Flags take precedence over environment variables. Environment variables all carry the
PI_AS_MCP_ prefix, after the project's name. The defaults below are the server's;
pimcp deliberately differs on one of them (it turns --oauth on) and says so in the
table row.
Flag | Env | Default | Meaning |
|
| cwd | Working directory. Every path argument resolves against it; a path outside it is allowed. |
|
| off | Expose the chat channel: |
|
| 7 on Linux, 8 on Windows | Comma-separated subset to expose. |
|
| none | Approval mode |
|
| none | Paths whose calls always wait for Allow or Deny in the human UI, in every approval mode: it escalates, it never refuses. Relative entries resolve against |
|
| 90000 | Max time |
|
| off | Serve Streamable HTTP instead of stdio. |
|
| 8777 / 127.0.0.1 | HTTP listener. A port already in use is a hard error, not a silent fallback. |
|
| none | Bearer token. Set this if you expose the port, unless you use |
|
| off (on in | Serve an OAuth 2.1 authorization server in front of |
|
| the tunnel hostname, else | The URL clients reach, and the identity the authorization server advertises. Only with |
|
| none | The addresses a hand-configured client may return to (comma-separated), named before it ever asks. Without this the address is not known in advance: the first one the client sends is shown on the approval card, and your click is what allows it. Only with |
|
| minted per run | Use this credential pair instead of minting one, so a restart does not invalidate a pair a client already holds. Both together, and only with |
|
| 8790 / 127.0.0.1 | The web UI. Always runs. A non-loopback |
|
| none | Token for the UI. Required if you bind the UI to anything but loopback (the bridge will not start otherwise). With |
Project instructions and skills
Project instructions. AGENTS.md (or CLAUDE.md, where a directory has no AGENTS.md) goes into the instructions the agent is given. The bridge reads ~/.pimcp/AGENTS.md first, then each directory from the git root down to --root. Nearer files come last and take precedence. A file over 64 KB is cut and marked. An edit reaches the agent once, as a notice on its next tool result, and get_instructions returns the current text.
Skills. A skill is a directory with a SKILL.md whose frontmatter has a name and a description, both required. The bridge reads skills from, least specific first:
its built-in skills (
builtin-skills/, which holdspimcp: how to configure the bridge itself);~/.agents/skills;.agents/skills,.claude/skillsand.pi/skillsin each directory from the git root down to--root.
A nearer directory wins over a farther one, and a project skill wins over a global one with the same name. The agent is given each skill's name, description and address, skill://<name>/SKILL.md, and reads it with read. Files inside a skill are skill://<name>/<file>. Those addresses are read-only and cannot leave their skill directory, symlinks included. A skill that breaks these rules is skipped, and the reason goes to stderr once. Frontmatter is read one key: value per line, and a description over 1024 characters is cut.
The bridge only reads these directories. The built-in skills are served by the bridge alone and are never copied into another agent's directory.
Security
This bridge hands a remote model a shell on your machine. It is remote code execution by design, so:
Run it in a container or VM, with only one work directory mounted.
--rootonly decides what a relative path argument resolves against: nothing confines a path, so every file the process can read is reachable, its own config file included.The shell guard refuses a short list of destructive command lines in every approval mode: a recursive delete of a root, a home or the working directory, a disk format, a shutdown, a boot-configuration or registry-hive wipe. It reads the text of a command, so it catches mistakes, not a determined caller. The deny list applies to the file tools' path arguments only, and it escalates rather than refuses: a call that touches one waits for Allow or Deny in every approval mode.
bashandpowershellare never path-checked, so a deny-listed path is reachable through a shell without a card. The deny list is empty unless you set--deny.Always set
--tokenwhen using--http, unless you are using--oauth. Without either, the port is unauthenticated and the bridge logs a warning.The web UI is the approval console — for tool calls, and with
--oauth, for authorizations. Keep it on loopback, or set--ui-token. A non-loopback--ui-hostwith no token is refused at startup, because whoever could reach that page could approve their own access.A hand-configured client's secret is a credential you now have to keep. It is printed once per run unless you pin it with
--oauth-client-id/--oauth-client-secret; anyone who reads it can ask for access, but still cannot get a token without your click.Approving an address you were not expecting is the one decision worth slowing down on. A confidential client's callback address only has to be approved by you once, and the card says so in as many words — but if you did not just start that client, the address on the card is somebody else's, and Allow sends them a code. Their secret is still required to turn it into a token, which is why this is a warning and not a refusal; a client with no secret gets no such path at all.
An OAuth access token is a shell until it expires (12h, refreshable for 30 days or until the process ends). Denying a request that has not been answered yet is the only way to refuse — once granted, the way to take it back is to restart the bridge. Keep the run short; that is what this process is for.
Use
--approveif you want a gate on every call. Every tool call then blocks until you click Allow or Deny on the web UI — all of them,readincluded, upstream MCP tools too: one setting, no per-tool list to keep complete. It fails closed: no answer means denied. It does not need--human, and a chat reply does not count as an approval.Rotate the token per sitting. For one-shot use, quick tunnels that change hostname are fine — they expire on their own.
Tool output (file contents, command output) transits the remote platform and may be logged there.
How Pi's prompt and tools were ported
Both are taken from @earendil-works/pi-coding-agent@1.0.0 (MIT) at runtime, so they
cannot drift:
Tools. The package's root export drags in the TUI and native image codecs, and it has no subpath export for the tool modules. So
src/pi-tools.tsresolves the package directory and importsdist/core/tools/index.jsdirectly, then callscreateAllToolDefinitions(cwd, …). Only that module graph loads. Descriptions, JSON schemas,promptSnippetandpromptGuidelinesare used as-is; TypeBox emits plain JSON Schema, which is what MCP wants.Prompt.
buildSystemPromptis not exported, sosrc/prompt.tsreproduces Pi's algorithm fromcore/system-prompt.ts: the preamble stays raw, every other section is rendered as<name>…</name>, and the rules list is built bybuildRules()— per-tool guidelines in tool order, then global guidelines, then Pi's two closing rules.
Adapted deliberately:
The preamble says you reach a remote machine through a bridge rather than claiming to run inside Pi.
Pi's
docsandskillssections are dropped: they point at Pi's local documentation and there is no Pi session here.The
<human>section is new. It replaces humanchat's per-call "MANDATORY RULES" block, which was appended to every tool result; the rules are stated once, in the prompt — with one deliberate exception. Ano_messageresult leads with a plain sentence telling the model to call again, because stating the rule only in the prompt did not hold a real client agent in the wait loop. See the gotcha below.Shell session variables (
PI_SESSION_ID,PI_MODEL, …) are switched off viaexposeSessionEnvironment: false, since there is no session; this also drops the guideline telling the model to read them.
Shipping it to somebody else
npm run package produces a self-contained directory that runs on a machine with no
Node, no npm, no cloudflared, and no network:
npm run package # for the machine you are on
npm run package:win # dist/win32-x64 — cross-buildable from anywhere
npm run package:linux # dist/linux-x64dist/win32-x64/
pimcp.cmd double-click launcher (Windows)
pimcp.sh launcher (Linux/macOS)
pimcp.mjs the launcher that runs inside the package
runtime/ the Node runtime — no npm/npx/corepack
bin/ cloudflared, vendored
app/
index.mjs the bridge *and Pi's tools*, bundled — ~3.1MB
node_modules/ only Pi's lazy image-resize wasm — everything else is in the
bundle
ui.html the human UI page
vendor/ marked + KaTeX, vendoredThe recipient runs it from the directory the agent should own:
pimcp.cmd # Windows: tunnel + localhost, prints both URLs
./pimcp.sh # Linux
./pimcp.sh --human --approve # with the human channel and an approval gateEvery flag the bridge understands works unchanged. On startup it prints an MCP URL you
can paste into your client from anywhere on the internet — and because --oauth is on by
default, a client that speaks the authorization flow needs nothing else. A bearer token is
printed too, for clients that cannot do OAuth. That is the whole "I run it on my machine, you
connect from yours" flow. --no-tunnel keeps it on localhost, --no-oauth drops the
authorization server.
Four details worth knowing:
Pi's tools are bundled into
index.mjs, but only in the artifact.src/pi-tools.tsresolves Pi's tool entry at runtime, because Pi'sexportsmap has no subpath fordist/core/tools/*and Node ignores tsconfigpaths— so a static import cannot work in the checkout. When bundling,scripts/package.mjsrewrites that expression into a literal import ofsrc/pi-tools-entry.ts, a seam whose tsconfig alias points at the installed package. esbuild can follow a literal, so Pi's ~265KB of tool source gets inlined and the artifact no longer needs its 143MBnode_modules. Descriptions and schemas still come from the installed version, so they cannot drift.Nothing but one wasm file is left in
node_modules, and that is deliberate. esbuild inlines every static import — Pi's tools, the MCP SDK, and express, which--oauthbrings in with the SDK's authorization router — so the SDK's 5.6MB copy beside the bundle is gone; a packaged build with that directory deleted still served initialize, read, bash, human_say and a whole OAuth flow. What remains is@silvia-odwyer/photon-node, the 2.2MB wasm Pi onlyimport()s when an image actually needs resizing, because a bundled wasm blob cannot be lazily loaded. It is copied to the name the bundle asks for rather than the nested path npm installed it at: Node resolves a bare specifier starting fromapp/index.mjs, and the nested location is not on that path — which is how it was found.cloudflared is vendored, so a tunnel needs nothing installed. It is copied from a local install when there is one (including from inside WSL when cross-building for Linux), and downloaded from the GitHub release otherwise. Without it the package still runs, just without a public URL, and the launcher says so.
Quick tunnels rotate their hostname on every restart. The URL is printed each time, so re-paste it. For a stable hostname, run a named tunnel or a Zero Trust hostname instead — the bridge only ever needs
http://127.0.0.1:<port>/mcp.
Two packaging traps that cost real time, both found by running the artifact rather than reading the build log:
The bundle is ESM, so its CJS dependencies have no
require. esbuild's interop shim throwsDynamic require of "child_process" is not supportedthe moment Pi's bash tool pulls incross-spawn. AcreateRequirebanner fixes it;format: "cjs"cannot be used becausesrc/index.tsawaits the listener at top level.Externals must list both spellings of a builtin. A CJS dependency writes
require("child_process"), and esbuild does not normalise it to thenode:form before matchingexternal, so"node:*"alone silently misses it.No single-file
.exe. A Node SEA would still need a runtime and the UI assets beside it, and the bridge has to readui.htmlper request. A directory with a launcher is the honest shape.
What actually takes up the space
Measured on the Linux package, and the Windows one differs only in ratio:
Part | Size | Why |
| 123 MB | The Node binary itself. Only ~41MB is |
| 27–39 MB | A statically linked Go binary, vendored so a tunnel needs nothing installed. |
| 6 MB |
|
npm, npx, corepack, share/ and lib/node_modules are deleted from the runtime: the
bridge spawns node on its own bundle and never runs a package manager. Deleting bin/
alone is not enough — that only removes the launchers, while the 17MB of npm itself lives in
lib/node_modules.
Tests
npm test # config defaults, stdio round-trip, HTTP round-trip + approval gate,
# the OAuth flow end to end, and both UI probes
npm run typecheck
npm run accept # end-to-end human acceptance; you reply in the browserThe probes spawn the real server and assert on real MCP traffic, including that a path outside
the root is resolved rather than refused, that a deny-listed path is marked for approval, that
an unapproved shell command never runs, and that an authorization
code cannot be replayed, a wrong PKCE verifier is refused, and a denied request grants
nothing. scripts/oauth-probe.mjs then runs two more servers, because the shape of the
client decides the shape of the flow. One is a hosted product's "custom connected app" — a
pre-registered confidential client, no --human — driven with no PKCE at all, once with the
secret in the body and once as HTTP Basic. The other is the default, the shape plain pimcp
produces: credentials printed, no redirect URI known in advance, an address that arrives late
and is allowed only by the click on its card, and a public client refused outright for the
same address.
Gotchas
Blocking over a tunnel.
human_ask/human_waithold the HTTP request open. Cloudflare drops a silent origin after ~100s, which is why the default cap is 90s. Raising--block-mspast that risks a 524; the call also emits MCP progress notifications every 20s to keep the stream warm when the client supplies a progress token.no_messageis not an error. It means the human did not answer within the window. The prompt instructs the model to callhuman_waitagain. A model that ignores this will end its turn early.One blocking call at a time. A second concurrent
human_ask/human_waitreturns immediately withno_messagerather than queueing.Stateless HTTP. A fresh
Server+ transport is created per request. Reusing one transport makes every request afterinitializefail with HTTP 500.bashon Windows needs a real bash onPATH; usepowershellotherwise.Do not judge a tunnel by knocking on its URL. A local VPN or proxy (fake-IP TUN) can leave your own machine unable to reach its own public URL —
ECONNRESET, HTTP 000 from curl, orENOTFOUNDwhen the proxy also intercepts DNS — while the tunnel is perfectly healthy for remote clients. The reliable signal is cloudflared loggingRegistered tunnel connection;pimcpwaits for exactly that and then reports the local probe as advisory only.A model will happily end its turn on a
no_message. Observed with a real client agent: it calledhuman_waitonce, readno_message, and ended the conversation — silently dropping the human, which is the exact failure this bridge exists to prevent. Prompt-only instruction was not enough, so ano_messageresult now leads with an imperative sentence and the<human>section names the failure mode outright ("your turn must not end while you are waiting"). If you touch either, re-test with a real client agent —npm testcannot catch this.Port 8765 is taken by AgentDock on this machine, which is why the HTTP default is 8777.
The advertised issuer carries a trailing slash. The SDK writes both the issuer and the resource metadata from
URL.href, so a bare origin comes out ashttps://host/. That is fine as long as the two documents agree — a client resolves the metadata endpoint fromauthorization_servers[0]and compares the issuer it finds against exactly that string — which is whatscripts/oauth-probe.mjschecks. Do not "fix" one of them by hand.The web UI always runs, and that is not the same as
--human. The UI is where Allow/Deny lives;--humanis whether the agent can talk to you. An earlier design required--humanfor--oauth, which made a hosted client impossible to add: an authorization needs a click, not a conversation.PKCE is required from a public client and optional for one with a secret. The MCP specification requires S256 from every client, and the SDK's own
/tokenhandler enforces it (code_verifier: z.string()), which a client such as Gemini's connector may never send.src/oauth-http.tstherefore serves/authorizeand/tokenitself, mounted beforemcpAuthRouterso its versions are shadowed — which is why nothing in them may callnext(). Discovery,/registerand/revokeare still the SDK's.A scope this bridge does not have is narrowed, not refused. A hosted client asks for whatever its platform taught it (
openid,email) and cannot learn otherwise; RFC 6749 §3.3 allows issuing less than requested. What must never happen is granting more, and the probe asserts the scope on the token, not just on the page.A redirect URI nobody registered is decided by the human, not by the bridge. The credentials are printed before any address is known, and a hosted product only shows you its callback address afterwards — so refusing an unregistered address would make that client impossible, and accepting it silently would mean nobody ever looked. It is accepted and printed in full on the same card as Allow/Deny, and only a confidential client gets this: without a secret the exact match is all that keeps a code from being delivered elsewhere. Approving records the address for the rest of the run, so a repeat is not flagged again.
Authorization endpoints are rate-limited, and the tunnel sets
X-Forwarded-For. Express refuses to key the limiter on a header it has not been told to trust, which shows up as noise or a 500 that only happens behind the tunnel.src/index.tssetstrust proxy 1for that one hop, and the OAuth probe sends the header so this cannot regress silently.Packaging used to depend on which shell started it. Extracting the Node archive goes through
tar, and Git Bash puts MSYS GNU tar first onPATH— which readsD:\…as a remote host (Cannot execute remote shell, thenCannot connect to D: resolve failed) and cannot read a.zipat all (This does not look like a tar archive). PowerShell worked by accident, becausetarthere is Windows' bsdtar.scripts/package.mjsnow names the extractor (%SystemRoot%\System32\tar.exeon Windows) and passesdist/-relative paths, sonpm run packagebehaves the same in Git Bash, PowerShell and cmd. If you see "does not look like a tar archive" on a zip you know is intact, that is this.
History
This repository previously contained HumanChat MCP, a Python human-in-the-loop MCP
server (broker + TUI + Web UI). It was replaced by what later became pi-as-mcp; that code
was removed from the working tree. The old implementation, including the edits that were
uncommitted at the time of removal, is preserved at tag humanchat-final:
git show humanchat-final --stat
git checkout humanchat-final -- src/humanchat/webui_assets.py # recover a single fileThe project was called pi-mcp until it was published, and was renamed to pi-as-mcp to
match the repository. The command stayed pimcp, and the environment variables moved from
PI_MCP_* to PI_AS_MCP_* — there are no aliases for the old names.
The former pi-mcp/ subdirectory was promoted to the repository root; git log --follow
crosses that move.
Available Tools
8 toolsbashA
Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | Shell command to execute | |
| timeout | No | Timeout in seconds (optional, no default timeout) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does disclose important output behavior: stdout/stderr return, truncation limits (2000 lines or 50KB), temp-file fallback, and optional timeout. However, it omits safety-critical traits such as destructive potential, permission requirements, and that commands run with the user's privileges.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four sentences, front-loaded with the core action and followed by output handling details. Every sentence contributes useful information; no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
No output schema exists, so the description appropriately explains return values and truncation behavior. For a complex arbitrary-command tool with no annotations, it is mostly complete but lacks safety/permission context.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents both parameters, including that timeout is optional with no default. The description adds no syntax or format details beyond the schema, so baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('Execute') and resource ('bash command') and scope ('current working directory'). It is clear what the tool does, but it does not explicitly distinguish itself from sibling tools like read, grep, or write, so an agent must infer when general shell execution is preferable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides no when-to-use or when-not guidance. It does not say to prefer read/ls/grep/find for file inspection or to reserve bash for arbitrary commands, leaving alternative selection entirely to the agent's inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
editB
Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Path to the file to edit (relative or absolute) | |
| edits | Yes | One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It usefully discloses that edits are matched against the original file rather than incrementally, and that oldText must be unique and non-overlapping — real behavioral context. However, it says nothing about failure behavior, atomicity, or what happens if a match is not found, which matters for a mutation tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences, front-loaded with the core action and followed by the constraints an agent most often gets wrong. Dense but each sentence addresses a real failure mode; no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a write tool with no annotations and no output schema, the description covers the matching semantics well but leaves gaps around error/failure reporting and permission requirements. Adequate but not complete for a mutation operation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both parameters are already documented in the schema, including the uniqueness/non-overlap constraint on oldText. The description largely restates those constraints rather than adding new meaning beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Edit a single file') plus the mechanism ('exact text replacement'), which distinguishes it from the sibling 'write' tool that presumably replaces whole files. It stops short of naming 'write' explicitly, so differentiation requires some inference.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The constraints ('merge overlapping edits', 'do not include large unchanged regions') imply the intended use case for targeted edits, but there is no explicit statement of when to choose edit over write, read, or bash. Usage is implied rather than stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
findA
Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to 1000 results or 50KB (whichever is hit first).
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | Directory to search in (default: current directory) | |
| limit | No | Maximum number of results (default: 1000) | |
| pattern | Yes | Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts' |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations provided, so description carries the burden. It discloses key behavioral traits: respects .gitignore, returns paths relative to search directory, and truncation limits of 1000 results or 50KB. This is valuable beyond the schema. Missing: whether it follows symlinks, searches recursively, or permission behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three compact sentences, each carrying distinct information: what it does, return format, and truncation behavior. Front-loaded with the core action and zero waste.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple 3-param read tool with no output schema and full schema coverage, the description covers purpose, return format, gitignore behavior, and truncation limits. It's nearly complete, though could mention error behavior or recursion scope.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents all three parameters with examples. The description mentions glob pattern and search directory context but adds no syntax or format details beyond the schema. Baseline 3 is correct.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource (search for files) and the mechanism (glob pattern). It distinguishes itself from grep (content search) implicitly by saying 'files' and 'glob', though it doesn't explicitly name siblings like grep or ls.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage (search by glob pattern) but offers no explicit when-to-use guidance versus alternatives like ls, grep, or bash. An agent can infer it finds files by name, but no exclusions or conditions are stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_instructionsA
Return the operating instructions for this bridge verbatim, plus the live status of attached upstream MCP servers. Call once at the start of a session, and again when a tool you expect is missing or an upstream is reported unavailable.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the behavioral burden itself, and it does state what comes back (instructions verbatim plus live upstream status) and when the result is worth refreshing. It stops short of describing output format, size, or whether the upstream status check has any cost/latency, so it is not fully rich, but it is well beyond a bare restatement.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, zero filler, and correctly front-loaded: the what comes first, the when second. Every clause earns its place and nothing redundant is repeated from structured fields.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter, no-output-schema tool, the description covers everything an agent needs: what is returned, that the return includes live state, and the two situations that warrant calling it. No meaningful gap remains for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so there is nothing to document and the baseline for empty parameter sets applies. The description correctly implies a no-argument call by never referencing any input.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Return the operating instructions for this bridge verbatim') and adds the secondary payload ('live status of attached upstream MCP servers'). This is unambiguous and easily distinguished from the filesystem-ish siblings (read, ls, grep, find, bash) since none of them retrieve bridge instructions.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit call timing: 'once at the start of a session' and again 'when a tool you expect is missing or an upstream is reported unavailable.' This is a rare case where both the when-to-use and the re-invocation trigger are spelled out, leaving nothing to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
grepA
Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to 100 matches or 50KB (whichever is hit first). Long lines are truncated to 500 chars.
| Name | Required | Description | Default |
|---|---|---|---|
| glob | No | Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts' | |
| path | No | Directory or file to search (default: current directory) | |
| limit | No | Maximum number of matches to return (default: 100) | |
| context | No | Number of lines to show before and after each match (default: 0) | |
| literal | No | Treat pattern as literal string instead of regex (default: false) | |
| pattern | Yes | Search pattern (regex or literal string) | |
| ignoreCase | No | Case-insensitive search (default: false) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so well: it discloses the return shape (matching lines with paths and line numbers), a .gitignore constraint, and two concrete truncation limits (100 matches / 50KB, long lines at 500 chars). It omits any note on read-only nature and whether truncation is silent, keeping it just short of a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four tight sentences, front-loaded with purpose and followed by return format and limits. Every sentence carries distinct, non-redundant information with no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a search tool with no output schema, the description adequately covers return format and truncation limits, which is the main thing an agent needs. It could be complete with a brief note on read-only behavior or ordering of results, but nothing critical is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all seven parameters are already documented in the schema. The description adds the output truncation rules that contextualize the `limit` parameter but does not explain any parameter directly, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Search file contents for a pattern'), which distinguishes it from siblings like find (file names) and read (whole files). It does not explicitly name any sibling as an alternative, so it falls short of the top tier.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: an agent can infer this is for content search vs. find for name search, but the description never states when to prefer grep over read, find, or bash. No explicit alternatives or exclusions are given.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lsA
List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to 500 entries or 50KB (whichever is hit first).
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | Directory to list (default: current directory) | |
| limit | No | Maximum number of entries to return (default: 500) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses sort order, the '/' directory suffix, inclusion of dotfiles, and a concrete truncation policy (500 entries or 50KB, whichever first). It omits error behavior for nonexistent paths and permission failures, so not a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, each carrying distinct information (purpose, return format, truncation limit) with no redundancy or filler. Front-loaded with the core action.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a two-parameter, no-annotation, no-output-schema primitive, this is nearly complete: it describes the return shape that the missing output schema would otherwise carry. It lacks only edge-case behavior (missing directory, permissions).
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents both path and limit, establishing a baseline of 3. The description only indirectly reinforces the limit parameter via the truncation note, adding no new syntax or format detail beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List directory contents'), which is unambiguous against siblings like read, grep, and write. It does not explicitly differentiate itself from find, which is the closest sibling (recursive search vs. single-directory listing), so it stops short of 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the purpose but there is no explicit when-to-use or when-not-to-use guidance, and no mention of alternatives such as find for recursive traversal. Adequate for an obvious primitive, but no routing information.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
readA
Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to 2000 lines or 50KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Path to the file to read (relative or absolute) | |
| limit | No | Maximum number of lines to read | |
| offset | No | Line number to start reading from (1-indexed) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden and does a solid job on a key trait: it discloses truncation limits (2000 lines or 50KB, whichever first) and that images arrive as attachments. It doesn't mention error behavior for missing/unreadable paths or binary-file handling, but the critical agent-facing behavior (silent truncation) is well disclosed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four sentences, front-loaded with the core purpose, then format support, then the truncation constraint that matters most for correct use. Every sentence carries information; no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a three-parameter read tool with no output schema and no annotations, the description covers purpose, supported types, truncation behavior, and pagination strategy — enough to invoke correctly. Missing only edge-case behavior (errors, empty files), which is a minor gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3, and the description earns an extra point by explaining the offset/limit interaction and the recommended paging workflow beyond the schema's one-line field docs. It doesn't fully resolve what happens if offset exceeds file length, but it adds genuine semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("Read the contents of a file") and enumerates supported media (text files and images), so the agent knows exactly what the tool does. However, it never distinguishes itself from siblings like bash (e.g., cat) or grep, so an agent gets no routing signal between them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides real usage guidance for one dimension — "Use offset/limit for large files" and "continue with offset until complete" — which tells the agent how to page through a large file. But it gives no when-to-use/when-not guidance relative to siblings (grep for search, ls for listing, write/edit for mutation), leaving the agent to infer the boundaries.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
writeA
Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Path to the file to write (relative or absolute) | |
| content | Yes | Content to write to the file |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full behavioral burden and does well: it discloses that the file is created if missing, overwritten if present, and parent directories are automatically created. The overwrite behavior is important destructive context. It still omits permissions, atomicity, and return behavior, keeping it from a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three short sentences, front-loaded with the core action, and every sentence adds useful information. There is no filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple two-parameter write tool with no annotations and no output schema, the description covers the key side effects: creation, overwriting, and parent-directory creation. It lacks explicit usage routing against edit, but for the core invocation context it is nearly complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both parameters are already documented in the input schema. The description adds no additional parameter meaning, formatting, path constraints, or encoding details. The baseline of 3 is appropriate when the schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description gives a specific verb and resource: 'Write content to a file.' It also clarifies create/overwrite behavior, which helps distinguish it from a pure edit tool. However, it does not explicitly name the sibling tool edit or contrast the two, so it stops short of full sibling differentiation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description states what the tool does behaviorally but gives no explicit when-to-use guidance or alternatives. It does not say to use this for full-file writes and edit for partial modifications, nor does it mention prerequisites or exclusions. An agent must infer the appropriate usage context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
8 tool updates
v0.3.0- First observed
bash - First observed
edit - First observed
find - First observed
get_instructions - First observed
grep - First observed
ls - First observed
read - First observed
write
TDQS
Scored across 8 tools
Each tool targets a distinct operation: read/ls/grep/find cover inspection, write/edit cover mutation, bash executes, and get_instructions is meta. The only mild overlap is that bash can replicate grep/find/read behavior, and write vs edit both modify files, but descriptions clearly delineate overwrite versus exact-text replacement.
Seven of eight tools use terse, all-lowercase Unix-style names (read, ls, grep, find, bash, write, edit), which is internally consistent. The single deviation is get_instructions, which switches to a snake_case verb_noun form, breaking the otherwise uniform pattern.
Eight tools is a well-scoped size for a filesystem/shell bridge, with each tool earning its place across read, search, write, and execute operations. No redundant or filler tools are present.
The surface covers the full read/write/edit/list/search/execute lifecycle, which is strong for this domain. Explicit delete/move/copy tools are absent, but bash provides a workaround, so this is a minor gap rather than a blocking one.
Maintenance
Related MCP Connectors
Work on your own computers from Claude or ChatGPT: run commands, edit and search files.
Connect ChatGPT, Claude, Cursor or VS Code to your Windows PC: files, terminal, browser, apps.
Use your Mac, Windows or Linux computer from ChatGPT, Claude or Codex: files, commands, documents.
Persistent cloud workspaces for AI agents: run commands, edit files, use git and a browser.
Related MCP Servers
- AlicenseAqualityBmaintenanceEnables an AI agent to run local tools on the user's own machine via stdio, including command execution, workspace file read/write, and system status checks.52MIT
- AlicenseNot gradedqualityBmaintenanceEnables an AI assistant to delegate coding tasks to local Codex or Claude Code CLIs, monitor their progress, read results, continue sessions, and browse the files in user-approved project workspaces. It exposes these capabilities over Streamable HTTP through a Cloudflare tunnel or locally over stdio, so a remote or cloud client can drive agents running on the user's own machine.70 npm1MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI chatbots to act like coding agents on your own computer by executing code, editing files, running terminal commands, and performing Git operations through a secure cloud bridge. Supports token-based pairing, path sandboxing, and configurable approval for destructive actions.MIT
- FlicenseNot gradedqualityAmaintenanceConnects web-based AI chat clients such as Claude, ChatGPT, and Grok to a local machine through a single OAuth-gated MCP endpoint so they can browse, read, write, and edit files, run whitelisted shell commands, search content, use Git, query SQLite, drive browsers and DevTools, and manage background tasks, ports, and clipboard. It also exposes localhost and LAN services via a protected HTTP client, letting remote or mobile clients debug and operate local projects without granting unrestricted shell access.-