Skip to content

spike(the-framework): MCP-tools bind for topics (#1121) - #1130

Closed
suleimansh wants to merge 1 commit into
mainfrom
spike/1121-mcp-tools-bind
Closed

spike(the-framework): MCP-tools bind for topics (#1121)#1130
suleimansh wants to merge 1 commit into
mainfrom
spike/1121-mcp-tools-bind

Conversation

@suleimansh

Copy link
Copy Markdown
Contributor

Spike for #1121, part of the #1129 decision. Draft — not for merge. Evaluates the MCP-tools variant of the topic-to-project bind against the await-gate variant (spiked separately).

What it does

A project-less "topic" run (#1120) now gets two MCP tools, so its agent binds to a project by calling a tool instead of parking on a gate:

  • list_projects — returns the registered projects (reads the registry).
  • create_project — takes a path, registers it (addProject), and binds this run to it.

The bind is signalled back to the run over the existing control channel: create_project appends a new { kind: 'bind', projectId } control entry to the run's control.jsonl, the run's watcher folds it to a bind event, and that lands on RunMeta.boundProjectId. Same file-is-the-seam design the dashboard already steers with, so there is no new run<->subprocess IPC.

The worktree re-home / respawn that a real bind implies is out of scope — that is #1122; a comment marks where it goes.

Wiring

  • src/projects-mcp.ts (new) — the server + tools (authored on @gemstack/mcp directly), projectsMcpServers spawn spec, and withProjectsMcp (mirrors withBrowser).
  • src/cli.ts — hidden mcp-projects subcommand (the spawnable server, reads the run cwd from FRAMEWORK_RUN_CWD); folds the server into topic runs' claudeOpts.mcpServers; handles the bind control entry.
  • src/control.ts / src/events.ts / src/store/run-store.ts / src/terminal.ts — the bind control entry, the bind event, RunMeta.boundProjectId, and the terminal line.

No prompt change was needed — MCP advertises the tools itself.

Proof

  • 5 new tests (in-memory registry + real control-channel round-trip + meta fold + spawn spec): green. Suite 1260 pass / 0 fail (+1 skipped), root typecheck clean.
  • Drove the real subprocess over stdio (node dist/bin.js mcp-projects): tools/list returns list_projects, create_project; create_project returns the record with bound: true; the run's control.jsonl gets {"kind":"bind","projectId":"smoke-142xwjl"}.

How it felt (verdict)

Clean:

  • The spawn side is a near-exact copy of the --browser precedent (withBrowser -> withProjectsMcp, one mcpServers entry). The MCP plumbing (--mcp-config temp file, merge-not-replace) already existed, so wiring a second server was trivial.
  • No prompt edit. The tools are self-describing, which is the real ergonomic win over a gate: the agent discovers "I can make a project" without us scripting the moment.
  • The bind signal reuses the control channel verbatim — one new entry kind, one new event, one meta field. It slotted in exactly where handoff-armed already lives.

Awkward:

  • Reuse hit a naming wall. The obvious reuse (@gemstack/mcp-connectors defineConnector) kebab-cases and namespaces tool names, and its id regex rejects _, so it cannot produce create_project verbatim (best it does is projects_create-project). I dropped to @gemstack/mcp McpTool directly to honor the names, which costs a small factory-closure-per-tool. Worth flagging for the decision: if we accept create-project, connectors is even less code.
  • The cross-process callback is genuinely indirect. The tool runs in a separate spawned process from the run; it cannot return anything to the run loop. The only way back is to write the control file and have the run tail it. It works and is consistent with the rest of the framework, but "the tool call updated the run" is two processes and a file, not a function return.
  • The respawn nuance bites, and it is the crux. Binding will (in Topics: re-home a run's cwd/worktree on bind #1122) re-home the run into the project's worktree, which means killing and respawning the run process. So the agent's create_project call returns into a turn that is about to be torn down — there is nothing to continue into mid-turn. The tool's return value is effectively discarded; the side effect (the bind) is what matters. This is a slightly unnatural fit for "the agent calls a tool and uses the result": here the agent calls a tool and then its whole process is replaced. A gate that the orchestrator resolves by respawning models that reality more directly. The tools approach still works (the bind is recorded, Topics: re-home a run's cwd/worktree on bind #1122 picks it up on respawn), but the "tool returns a value the agent reasons about" mental model does not hold at the bind moment.

Recommendation: The MCP-tools path is clean to build and gives the nicest discovery story (no prompt, self-describing, list + create in one surface). Its weak spot is exactly the bind semantics: the respawn means the tool return is throwaway, so we get the ergonomics of "call a tool" without the payoff of "use its result." If the #1129 decision weights agent-facing discoverability, pick tools. If it weights modeling the respawn honestly, the gate is the more faithful shape. My lean: tools for discovery, but only if we are fine treating create_project as fire-and-effect (bind recorded, run respawned by #1122) rather than a value-returning call. Keep the tool return minimal (it will be discarded) and let #1122 own the respawn.

Codex note (not wired here)

On Codex the same server would ride the driver's extraArgs hatch (src/driver/codex.ts): codex exec takes -c config overrides, so a topic run would append
-c mcp_servers.projects.command=<node> -c mcp_servers.projects.args=["<bin>","mcp-projects"] (plus the FRAMEWORK_RUN_CWD / XDG_CONFIG_HOME env). The server binary is agent-agnostic — only the "how the CLI is told about it" differs (Claude's --mcp-config file vs Codex's -c mcp_servers.*). Not built here to keep the spike to one driver.

Give a project-less topic run (#1120) two MCP tools, list_projects and
create_project, so its agent binds to a project by calling a tool rather than
parking on a gate. create_project registers the project and signals the run over
the control channel; the run folds the bind onto RunMeta.boundProjectId. The
worktree re-home / respawn is out of scope (#1122).

Spike for #1121, part of the #1129 decision.
@suleimansh

Copy link
Copy Markdown
Contributor Author

Superseded: per the #1129 recommendation we are going with the gate for the bind (permission model + respawn fit). This MCP-tools spike proved the approach and stays as reference. The real #1121 builds on the gate spike (#1131). Closing.

@suleimansh suleimansh closed this Jul 24, 2026
@suleimansh
suleimansh deleted the spike/1121-mcp-tools-bind branch July 30, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

question Further information is requested the-framework ♻️

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant