@rebindit/mcp-server
v0.1.4
Published
Stdio MCP server that lets a coding agent take part in the ReBind feedback loop
Downloads
638
Readme
@rebindit/mcp-server
A stdio MCP server that lets a coding agent take part in the ReBind loop:
feedback → agent claims a task → reports progress → produces a revision → a human approvesThe agent runs this locally; it talks to your Convex deployment over HTTP.
Configuration
Two environment variables, both required — the server refuses to start without them:
CONVEX_URL— your Convex deployment URL, e.g.https://acute-llama-123.convex.cloud.npx convex devprints it.REBIND_AGENT_TOKEN— the agent JWT issued byagentCredentials.create(see "Getting a token" below). Every backend call authenticates as that credential; the guard seam (ADR 0002) uniformly denies unauthenticated calls, so without a valid token every tool call fails.
Getting a token
A token is minted by the agentCredentials:create Convex action — one credential per
agent installation, workspace-scoped, exp 90 days. It requires the deployment to have
AGENT_JWT_ISSUER and AGENT_JWT_PRIVATE_KEY set (see convex/lib/agentJwt.ts for the
key format and rotation procedure).
Mint from the app: Workspace settings → ReBind MCP server (#371). Any workspace member may mint; the token is shown exactly once — the backend stores only the credential row, never the token — and the same card emits a ready-to-paste config per editor with the token and deployment URL already inline. Revoke from the same card (the creator or any workspace owner); revocation takes effect on the credential's next call.
Without a running dashboard (bootstrap, headless), the Convex dashboard's function runner still works:
- Open your deployment in the Convex dashboard → Functions →
agentCredentials:create. - Choose "Act as a user" and supply an identity whose
subjectmatches yourworkspaceMembers.subject(your Clerk user id). Any workspace member may register a credential. - Run it with
{ workspaceId: "<your workspace id>", label: "claude-code" }and copy the returnedtoken. - Export it as
REBIND_AGENT_TOKENwhere the MCP server runs.
Prefer real environment indirection (a secrets manager, your shell profile) over pasting
the token into a config file. .mcp.json is gitignored in this repo for exactly this
reason — but a token in any file on disk is still a token in a file on disk.
Where to keep it
Where the client expands env-var references, the config below writes one — spelled
"${REBIND_AGENT_TOKEN}" in a .mcp.json, "${env:REBIND_AGENT_TOKEN}" for Cursor and
VS Code — rather than the token itself, which is what keeps the file free of the secret:
the file can be committed and the token is stored once per machine instead of once per
project. (Claude Desktop expands nothing — see its section.) Two ways to set the variable,
and the difference is not security (both land unencrypted somewhere your own processes can
read) but when it exists:
| how you set it | lives in | you must |
| --- | --- | --- |
| setx (Windows) / a shell-profile export | the user environment | nothing, ever |
| a .env.local sourced per session | that file | source it before every launch |
The per-session loader has one hole worth knowing: launch the client from a desktop shortcut
or app icon rather than the shell you sourced in, and the variable is simply absent, so the
server exits with the two-variable refusal. The user-environment form has no such gap. On
Windows, setx only reaches new processes — reopen the terminal after setting it.
Neither applies to Claude Desktop, which expands no ${…} references at all: there the
literal token goes in the file, and the file is the secret. A leaked token revokes as one
workspace-scoped credential (agentCredentials.revoke), which is what keeps the stakes
proportionate.
Rotating a token
Yes, tokens expire — 90 days, hard. exp is stamped at mint time
(AGENT_TOKEN_TTL_SECONDS, ADR 0002 #13) and there is no renewal: no refresh call, no
sliding window, no grace period. On day 91 the agent's next call is refused — twice over:
the JWT verifier rejects the aged exp, and the guard independently refuses a credential row
whose stored expiresAt has passed (#782), so a fresh signature cannot revive an old row.
There is also no rotate mutation, deliberately. Rotating is revoke + mint, and the
result is a different credential — the JWT's sub is the credential row's id, so a new
token is a new row with a new subject, not the same row re-signed. Expect the card's list
to grow one revoked entry per rotation; that is the audit trail working, not clutter.
Order matters, and which order depends on why you are rotating:
| why | order | why that order | | --- | --- | --- | | routine (90 days approaching) | mint → paste → restart → then revoke the old | revocation takes effect on the credential's next call, so the old token keeps working during the swap and the agent never has a dead window | | the token leaked (pasted into a chat, a commit, a screenshot) | revoke first, then mint | the dead window is the point — accept it |
The steps, once you know which order you are in:
Mint a fresh credential in Workspace settings → ReBind MCP server, labelled so you can tell it from the one it replaces. Copy it now — it is shown exactly once.
Put it where the config expects it, which is not the same place for everybody:
- Config with the token inline (
"REBIND_AGENT_TOKEN": "eyJ…") — what the connect surface emits, and the only form Claude Desktop can use — edit the file. - Config with a reference (
"${REBIND_AGENT_TOKEN}","${env:REBIND_AGENT_TOKEN}") — set the variable instead and leave the file alone. On Windows that issetx REBIND_AGENT_TOKEN "<new-token>"or the User-scoped[Environment]::SetEnvironmentVariable(...).
Check which one you have before doing either. Setting a variable that no config reads changes nothing, and the failure looks identical to a wrong token.
- Config with the token inline (
Fully quit and reopen the client — not a reconnect. A user-environment variable reaches only newly launched processes, so a client that was already running keeps the old value however many times it re-spawns the server.
Revoke the old credential (routine order) and confirm the agent still answers.
Knowing it is time
The Coding-agents card tells you (#563). Each credential stores the exp its token was
actually signed with, and the row states where that leaves it:
| badge | meaning |
| --- | --- |
| live | working; the expiry date is shown beside it |
| expires soon | inside 14 days — mint the replacement now, while the old one still works |
| expired | every call from it is refused |
| revoked | someone revoked it; wins over expired, because that is what actually happened |
Fourteen days, because rotation is not instant: the routine order is mint → paste → restart → revoke, and the person reading this card is routinely not the person who has to paste.
What a dead token looks like
Expired and revoked are indistinguishable to the agent: both resolve to the same
forbidden sentinel (guardSentinels.denied), and that is on purpose — the guard layer
does not narrate why an identity failed. So the tool call fails the same way whether the
token aged out, was revoked, was never valid, or names a credential in another workspace.
Which means: if calls stop working and you did not revoke anything, check the mint date
before you go looking for a config bug. And distinguish that from the two failures that
look similar but are not authorization at all — a stale CONVEX_URL (the MCP server points
at the wrong deployment while the dashboard looks fine) and a missing variable (the server
refuses at startup with its two-variable message rather than failing per call).
Pointing an agent at it
The package is published to npm (#485), so an agent's config runs it with npx -y
@rebindit/mcp-server — no checkout of this repo needed, only Node.js 20+. bunx and
pnpm dlx work too, with different flags — see “Swapping the runner” below. The dashboard's
connect surface (Workspace settings → ReBind MCP server) emits exactly these forms with the
deployment URL and token already substituted. Contributors working in this repo can keep
the checkout form instead: pnpm --filter @rebindit/mcp-server start (requires pnpm install
and codegen at the root).
The short way in: one command, or one click
Three of the five editors below can install this without a config file at all, and the dashboard's connect surface offers whichever is the shortest for the editor you pick (#832). A config file is always still one disclosure behind it — a vendor's CLI flag or URL scheme can be renamed and a dead deeplink opens nothing, so the pasted file stays the arm that cannot rot.
Claude Code takes the whole server on one line, run inside the project the agent works in:
claude mcp add rebind -e CONVEX_URL=https://acute-llama-123.convex.cloud -e REBIND_AGENT_TOKEN=<token> -- npx -y @rebindit/mcp-serveradd, not add-json: add-json needs the config quoted as one shell argument and the
quoting that survives differs between bash, cmd.exe and PowerShell, while add with --
takes plain words that paste into any of the three. Nothing in the line needs quoting — a
Convex URL and a JWT draw on no characters a shell treats specially. The default local
scope files it in your own config against that directory, so nothing lands in the repo
and there is no .gitignore to remember. On Windows, insert cmd /c after the -- for the
reason the next section gives. The token is in the line: clear your shell history afterwards
on a shared machine.
Cursor and VS Code each answer a URL. The dashboard renders them as an Add to
Cursor / Add to VS Code button rather than a string to assemble by hand:
cursor://anysphere.cursor-deeplink/mcp/install?name=rebind&config=<base64 of the server
entry> and vscode:mcp/install?<percent-encoded JSON of the entry plus its name>. Both are
handed to the app on that machine and go nowhere near the network.
Claude Desktop and T3 Code have neither, so for those the config file below is the route, not a fallback.
Windows: npx needs cmd /c
Every snippet below spells "command": "npx", which is correct on macOS and Linux and does
not start on Windows. MCP clients spawn the server without a shell, and Windows npx is
really npx.cmd — measured on Node 24.11.1:
| command / args | result |
| --- | --- |
| "npx" | ENOENT — spawn does not try the .cmd extension |
| "npx.cmd" | throws EINVAL — Node refuses to spawn .cmd/.bat without a shell |
| "cmd", ["/c", "npx", …] | starts |
The second row is the point: spelling the extension is the fix everyone reaches for second,
and Node's 2024 batch-file hardening (CVE-2024-27980) closed it. cmd /c is the only
form that works, and it is harmless to leave in place on a machine that did not need it —
so on a mixed team, prefer it.
{
"mcpServers": {
"rebind": {
"command": "cmd",
"args": ["/c", "npx", "-y", "@rebindit/mcp-server"],
"env": {
"CONVEX_URL": "https://acute-llama-123.convex.cloud",
"REBIND_AGENT_TOKEN": "${REBIND_AGENT_TOKEN}"
}
}
}
}Clients disagree about whether they wrap the spawn for you — the MCP TypeScript SDK routes
through cross-spawn since v1.8.0 and rewrites the command to cmd.exe /d /s /c, which is
why bare npx measurably works in Claude Code today while VS Code and Copilot CLI still
fail on it. Which clients wrap is a moving target; cmd /c passes through cross-spawn
unchanged, so it is correct either way. The dashboard's connect surface offers a
macOS/Linux–Windows toggle and emits this cmd /c form on its Windows arm (#489).
Swapping the runner: bunx, pnpm dlx
Every snippet below spells npx because it is the one runner a plain Node install already
has. The package is bin-only and carries no runtime pin, so any npm-registry runner starts
it — but the flags are not interchangeable, and the runner has to exist on the machine that
spawns the server. That machine is yours, not this repo's: this repo's pnpm-only rule
(CLAUDE.md) governs building ReBind and does not reach your MCP client's config.
| runner | command / args | why |
| --- | --- | --- |
| npm | "npx", ["-y", "@rebindit/mcp-server"] | -y answers the "install it?" prompt that would otherwise hang a client with no stdin |
| bun | "bunx", ["@rebindit/mcp-server"] | no -y — bunx has no such flag and reads it as the package to run (bun 1.3.14). It installs into a shared global cache without asking, so there is no prompt to answer |
| pnpm | "pnpm", ["dlx", "@rebindit/mcp-server"] | no -y either; dlx never prompts |
Carrying -y across from an npx snippet is the failure worth naming, because it does not
look wrong: ["-y", "@rebindit/mcp-server"] under bunx tries to fetch a package called
-y.
On Windows, wrap all three in cmd /c. The rule is not "npx is npx.cmd" — that is
only the case we measured first. It is that a config generated on one machine cannot know how
the runner was installed on another:
| runner | from its own installer | from npm i -g |
| --- | --- | --- |
| npx | npx.cmd — needs the wrapper | — |
| bunx | bunx.exe — starts bare (measured, Node 24.11.1 + bun 1.3.14: spawn("bunx", …) exits 0 where spawn("npx", …) is ENOENT) | a .cmd shim — needs the wrapper |
| pnpm | pnpm.exe — starts bare | pnpm.cmd — needs the wrapper |
cmd /c is required in half those cells and inert in the other half, so it is the only arm
that is correct without first asking a question nobody can answer remotely.
Pinning a version — "@rebindit/[email protected]" — is a real choice, not noise. Unpinned,
every client launch takes whatever is newest on npm; pinned, an upgrade is something you do on
purpose. Pin if the config is shared with a team.
The dashboard's connect surface offers all three as a third ButtonGroup beside the editor
and OS ones, so nobody has to hand-translate the table above. The five fenced blocks below
stay npm-only, and tools/mcp-config-drift.ts holds only those against
src/launch-config.ts — the bun and pnpm arms are proved by src/launch-config.test.ts
instead, which asserts each runner's whole argv rather than that it merely contains the
package name.
Claude Code / any .mcp.json
{
"mcpServers": {
"rebind": {
"command": "npx",
"args": ["-y", "@rebindit/mcp-server"],
"env": {
"CONVEX_URL": "https://acute-llama-123.convex.cloud",
"REBIND_AGENT_TOKEN": "${REBIND_AGENT_TOKEN}"
}
}
}
}Claude Desktop (claude_desktop_config.json)
Claude Desktop does not inherit your shell, so if npx is not found, spell its absolute
path. It also does not expand ${…} references, so the token value has to be pasted in —
treat that file as a secret.
{
"mcpServers": {
"rebind": {
"command": "npx",
"args": ["-y", "@rebindit/mcp-server"],
"env": {
"CONVEX_URL": "https://acute-llama-123.convex.cloud",
"REBIND_AGENT_TOKEN": "<paste the issued token>"
}
}
}
}Cursor
Same shape as Claude Code's, in ~/.cursor/mcp.json (all projects) or <project>/.cursor/mcp.json
(one project). Cursor supports config interpolation,
so on a machine you control you can keep the token in your environment rather than in the
file. The dashboard's generated snippet inlines the token instead — the right default for a
machine we don't control, with the gitignored file itself treated as the secret.
{
"mcpServers": {
"rebind": {
"command": "npx",
"args": ["-y", "@rebindit/mcp-server"],
"env": {
"CONVEX_URL": "https://acute-llama-123.convex.cloud",
"REBIND_AGENT_TOKEN": "${env:REBIND_AGENT_TOKEN}"
}
}
}
}VS Code
.vscode/mcp.json in the workspace you're coding in. The top-level key is servers, not
mcpServers, and VS Code expands ${env:…}, so the token can stay in your environment:
{
"servers": {
"rebind": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@rebindit/mcp-server"],
"env": {
"CONVEX_URL": "https://acute-llama-123.convex.cloud",
"REBIND_AGENT_TOKEN": "${env:REBIND_AGENT_TOKEN}"
}
}
}
}T3 Code
No separate T3 Code entry is required: configure MCP through the agent provider that T3 Code launches. Claude-backed sessions load Claude's user, project, and local settings, as established by t3code#1334; configure other providers in their own supported format. T3 Code then surfaces the resulting ReBind tool calls in its UI. For a setup that must be available to every T3 thread, prefer the provider's user-level config.
Run it by hand:
CONVEX_URL=https://acute-llama-123.convex.cloud \
REBIND_AGENT_TOKEN=$REBIND_AGENT_TOKEN \
npx -y @rebindit/mcp-serverTools
| Tool | Use it to |
| ----------------- | ------------------------------------------------------------ |
| list_feedback | Find open work in a project (by slug, optionally by status). |
| list_projects | Discover the projects your credential can see, with their slugs. No arguments. |
| get_project | Resolve a slug to the project's repository and production URLs. |
| get_feedback | Read one item with its comments, revisions, latest task, and tags. |
| get_screenshot_evidence | Get a short-lived URL for the screenshot ReBind captured at pin time. |
| get_attachment_evidence | Get a short-lived URL for the image the reviewer pasted into the note. |
| claim_task | Take on an item; returns the agentTaskId later calls need. |
| report_progress | Heartbeat shown live in the dashboard. |
| finish_task | Close a task that never opened a revision; safe replay otherwise. |
| ask_reviewer | Stuck on what only the pin's author knows: ask them, and end your task. |
| start_revision | Open a revision attempt; returns revisionAttemptId. |
| finish_revision | End linked work in one call: revision, task, and hand-off together. |
| add_comment | Ask the client something. Always attributed to an agent. |
Evidence: what an agent sees, and what it does not
get_screenshot_evidence and get_attachment_evidence return a short-lived URL (about a
minute), one per provenance — the screenshot ReBind captured at pin time, and the image the
reviewer pasted into the note. The agent fetches the URL itself. ReBind does not stream bytes
through this server, and it does not describe the image for you: there is no ReBind-side model,
and a generated description would be lossy in the one direction that matters.
The client owns this, per review session. A session whose owner has turned evidence sharing
off answers denied on both tools. That is a settled answer, not a mistake and not something a
human needs to fix — work from the text, and use add_comment if you genuinely cannot tell what
to change. null is different: it means this item carries nothing in that column, which is
normal. not_found means the item is gone or outside your credential's reach.
Each URL ReBind issues is recorded on the item's activity trail — which item, which credential, which provenance. It records that access was issued; ReBind cannot observe whether you fetched it.
What this server deliberately cannot do
There is no approve tool. Approval is a human decision, and an agent must not sign off on its
own work. The furthest an agent can take a feedback item is awaiting_approval, via
finish_revision. This is enforced server-side too: feedback status moves are checked against
allowedFeedbackTransitions in @rebindit/contracts, and an illegal move comes back as a refusal
(see Notes below), not a crash.
There is no tool that changes what evidence you receive. The per-session switch behind
get_screenshot_evidence / get_attachment_evidence is human-only and set from the dashboard.
A control over what an agent receives that the agent could write would not be a control.
Nothing here returns image bytes. The two evidence tools return a URL and nothing else, so every read carries its own expiry rather than becoming an open pipe through this transport.
Publishing
pnpm --filter @rebindit/mcp-server build bundles src/main.ts into a self-contained
dist/main.js (esbuild): the workspace-internal code — @rebindit/contracts and the
generated api reference, which at runtime is Convex's anyApi string plumbing — is
inlined, while convex, zod, and @modelcontextprotocol/server stay ordinary npm
dependencies. That is why @rebindit/contracts sits in devDependencies: it is unpublished,
so it must be baked in at build time, never left as an installable dependency. The version
has one spelling — package.json's, which server.ts imports for the MCP handshake. The
release procedure — org, 2FA, bump, publish — is docs/runbooks/mcp-server-npm-publish.md.
Notes
- Running from a checkout requires codegen to have run (
pnpm codegenat the repo root) — the typed Convexapicomes fromconvex/_generated. The published bundle carries its own copy, baked in at build time. - A failed call comes back as an
isErrortool result, never a killed server, classified into one of five kinds —refusal,denied,not_found,unavailable,server_bug— rather than the raw error message: only aConvexError'sdatasurvives production redaction, so a plainError's message does not reach this server intact, and the classifier reads what actually arrives instead of assuming it does. The server states the five kinds once, in its MCPinstructionssent at connect (agent-surface.ts'sfailureKindsInstructions).
