feat(coding-agent): first-party MCP extension - #279
Conversation
Adds packages/coding-agent/extensions/prime-mcp, a client-only MCP
adapter exposing a single token-efficient `mcp` proxy tool (list/
describe/call) plus optional directTools promotion. Supports stdio and
HTTP servers with lazy connect, idle disconnect, and reconnect on
transport errors. Config resolves from .mcp.json, .prime/agent/mcp.json,
and ~/.prime/agent/mcp.json with ${VAR} expansion for headers and env.
Includes docs/mcp.md, README pointer, CHANGELOG entry, example config,
and tests backed by an in-memory mock MCP server.
Co-authored-by: Cursor <cursoragent@cursor.com>
Addresses review findings on the MCP extension: - Guard against leaking an in-flight connect when disconnect/shutdown races a pending connection, and tear down via the connecting map. - Track active operations so the idle timer never closes a connection with a call in flight. - Terminate streamable-HTTP sessions on close so idle disconnects do not accumulate abandoned server-side sessions. - Resolve MCP config once (first session cwd) since promoted directTools register globally and cannot be unregistered; documented. - Render tool structuredContent when no text content is returned. - Validate headers/env values are strings at config load instead of failing at connect time. - Warn instead of silently dropping directTools whose names collide. Co-authored-by: Cursor <cursoragent@cursor.com>
Second review pass on the MCP extension: - Track active operations per connection instance so a reconnect or a finishing concurrent call can no longer close a connection that another in-flight call is using; share one retry helper across listTools and callTool so discovery also recovers from a dead transport. - Only honor a directTools entry when the file that declared it also won the referenced server's definition, preventing a low-trust project file from hijacking a global auto-promotion into spawning a command. - Make config loading resilient: skip unreadable/malformed files with a warning instead of failing the whole load, treat ENOTDIR like a missing file, and reject servers that define both command and url. - Resolve config via a shared promise so concurrent session starts don't race, and retry a failed load on a later session. - Keep live connection errors out of the persisted warning list so /mcp status reflects current state. - Preserve non-text MCP result content: pass image blocks through as image content and render resource links/embedded resources instead of dropping them; include structuredContent in error messages. Co-authored-by: Cursor <cursoragent@cursor.com>
Third review pass on the MCP extension: - Give each connect its own AbortController so disconnect/shutdown can cancel a hung connector immediately instead of blocking on its timeout, and so one caller's abort can't tear down a connection others await. - Retry path now closes only the specific dead connection instance, so a failing call no longer cancels a parallel call's freshly reconnected client on the same server. - Terminate HTTP sessions on the connect-timeout path too. - Preserve MCP content block order, surface images embedded in resource blocks as image content, dedupe resource uris, and accept scalar structuredContent. - Reject reserved server names (prototype pollution) and non-string cwd; show config warnings in /mcp status even when no servers load. Co-authored-by: Cursor <cursoragent@cursor.com>
Fourth review pass: - Defer closing a retired MCP connection until its last in-flight operation drains. A call hitting a dead transport now evicts and reconnects without closing the shared client out from under a parallel sibling call. - Bound HTTP session termination with a timeout and always close the client in a finally, so a server that accepts the session DELETE but never replies can't wedge session shutdown (which awaits disconnectAll). Co-authored-by: Cursor <cursoragent@cursor.com>
Fifth review pass: - Cap promoted direct tool names at the 64-char provider limit, appending a stable hash so distinct long refs stay unique instead of getting the whole model request rejected. - Resolve sanitized direct-tool name collisions in favor of the higher- precedence config file (project over global) rather than insertion order. - Bound the rendered error summary so a tool returning a huge error payload can't flood the transcript past the output cap. - Let a cancelled call stop waiting on an in-flight connect without aborting the shared connection other callers still await; abort the connector's own connect on timeout so it can't leak a late server-side HTTP session. - Treat inherited Object keys (e.g. toString) as unknown servers. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@cursor macroscope found stuff. please address the findings and respond/resolve the threads. |
|
Implemented and pushed review fixes on Fixed:
Verified:
I updated the PR registration. I could not post/resolve the GitHub review threads directly: no GitHub comment/review MCP tool is available, and Environment note: |
| if (directToolRefs.length > 0) { | ||
| await registerDirectTools( | ||
| pi, | ||
| manager, | ||
| directToolRefs, | ||
| (message) => failed.push(message), | ||
| promoted, | ||
| ctx.signal, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🟡 Medium prime-mcp/index.ts:144
/mcp reconnect <server> re-runs registerDirectTools() with the full directToolRefs list, so reconnecting one server triggers describeTool() calls against every other direct-tool server. This lazily connects to unrelated servers and, if any of them is down, reports a failure even though the targeted server reconnected successfully. Consider filtering directToolRefs to only refs whose server matches the reconnect target.
if (directToolRefs.length > 0) {
- await registerDirectTools(
- pi,
- manager,
- directToolRefs,
- (message) => failed.push(message),
- promoted,
- ctx.signal,
- );
+ const refsForTarget = directToolRefs.filter((ref) => ref.split("/")[0] === name);
+ if (refsForTarget.length > 0) {
+ await registerDirectTools(
+ pi,
+ manager,
+ refsForTarget,
+ (message) => failed.push(message),
+ promoted,
+ ctx.signal,
+ );
+ }🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/extensions/prime-mcp/index.ts around lines 144-153:
`/mcp reconnect <server>` re-runs `registerDirectTools()` with the full `directToolRefs` list, so reconnecting one server triggers `describeTool()` calls against every other direct-tool server. This lazily connects to unrelated servers and, if any of them is down, reports a failure even though the targeted server reconnected successfully. Consider filtering `directToolRefs` to only refs whose server matches the reconnect target.
| * Begin an operation: ensure a live connection and disarm idle disconnect so | ||
| * the timer can never close a connection out from under an in-flight call. | ||
| */ | ||
| private async begin(name: string, signal?: AbortSignal): Promise<Connection> { |
There was a problem hiding this comment.
🟡 Medium prime-mcp/manager.ts:220
In begin(), waitForConnect() resolves with a Connection, but between the await resuming and connection.active += 1 executing, a concurrent disconnect(name) can evict that connection (sees active === 0) and call closeClient(), closing the transport. begin() then increments active on the stale, closed object and returns it, so the caller's listTools/callTool runs against a closed client and fails spuriously during shutdown/reconnect races. Consider re-reading the current connection from this.connections.get(name) after await and reconnecting if it was evicted while waiting.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/extensions/prime-mcp/manager.ts around line 220:
In `begin()`, `waitForConnect()` resolves with a `Connection`, but between the `await` resuming and `connection.active += 1` executing, a concurrent `disconnect(name)` can evict that connection (sees `active === 0`) and call `closeClient()`, closing the transport. `begin()` then increments `active` on the stale, closed object and returns it, so the caller's `listTools`/`callTool` runs against a closed client and fails spuriously during shutdown/reconnect races. Consider re-reading the current connection from `this.connections.get(name)` after `await` and reconnecting if it was evicted while waiting.
| async setConfig(config: McpConfig): Promise<void> { | ||
| await this.disconnectAll(); | ||
| this.config = config; | ||
| this.idleTimeoutMs = this.options.idleTimeoutMs ?? config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS; | ||
| } |
There was a problem hiding this comment.
🟡 Medium prime-mcp/manager.ts:100
setConfig() awaits disconnectAll() before assigning this.config, so a concurrent listTools/callTool started during that window still sees the old config. Its begin() → connect() call is not in the snapshot disconnectAll() took, so it is never aborted, and when it resolves it writes the old client into this.connections. Later calls under the new config then silently talk to the previous server for the same name. Replace this.config before disconnecting so concurrent operations started during the teardown window reject against the new config.
- await this.disconnectAll();
- this.config = config;
+ this.config = config;
+ await this.disconnectAll();🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/extensions/prime-mcp/manager.ts around lines 100-104:
`setConfig()` awaits `disconnectAll()` before assigning `this.config`, so a concurrent `listTools`/`callTool` started during that window still sees the old config. Its `begin()` → `connect()` call is not in the snapshot `disconnectAll()` took, so it is never aborted, and when it resolves it writes the old client into `this.connections`. Later calls under the new config then silently talk to the previous server for the same name. Replace `this.config` before disconnecting so concurrent operations started during the teardown window reject against the new config.
|
To follow the RLM paradigm, MCP should be implemented as an IPython RLM skill rather than a Prime Agent extension. This was added in #280. |
Core tools now properly use the cwd passed to createAgentSession(). Added tool factory functions for SDK users who specify custom cwd with explicit tools. Fixes PrimeIntellect-ai#279
Records, without editing the now-false text away, that sections 9 and 4 went stale five hours after they were written. The correction matters more than the content: this file exists to stop sessions trusting notes over GitHub, and it caught its own author. - main is 8d2139c. Between 16:12Z and 21:54Z the fleet merged PrimeIntellect-ai#279 (the PrimeIntellect-ai#58 alert-bridge race, FIXED — stop carrying it as a standing exception), PrimeIntellect-ai#278 (AGENTS.md invariants), PrimeIntellect-ai#283 (repo cleanup), and PrimeIntellect-ai#284, which delivered the last brief and closed PrimeIntellect-ai#165 with a keyword. - Section 3's routing conclusion was confirmed by events: PrimeIntellect-ai#271 was delivered by the Mac maker fleet via auto-dispatch, exactly as argued, and the remote session correctly declined to open a second lane. - Flags issue-state drift: PrimeIntellect-ai#271, PrimeIntellect-ai#276 and PrimeIntellect-ai#274 are delivered and merged yet still open, because a title reference is not a closing keyword. That is the mirror image of the hazard the V2 CLAUDE.md documents, and it leaves open-work disagreeing with main. Operator action, named as such. - Records the residual PrimeIntellect-ai#284 deferred on stated grounds (PrimeIntellect-ai#286), which is a known open edge on the LIVE path. - States the next slice: S4 / PrimeIntellect-ai#236, the first whose exit criteria need a real broker order. Certification stays 0/12; the system has never placed a trade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5B7QM1QLQuWSBMCxiCzS6
…e-notes lesson (#13) * docs(spx-v2): verification pass, queue state, and self-refinement record No code written this session — a verification pass over already-delivered work plus the queue-state answer. Records, so future sessions do not repeat them: - The audit-challenge / V1-coverage / rag-tot-cot-challenge / corrected-input deliverable ALREADY EXISTS (AUDIT_CHALLENGE sections A-D and PRIME_AGENT_INPUT_SPX_V2). An operator prompt has now asked for it in at least two sessions; redoing it is inventing work. - Verified queue state from GitHub: PrimeIntellect-ai#266/PR PrimeIntellect-ai#268 merged (and PrimeIntellect-ai#263 with it, now main f64029a); PrimeIntellect-ai#265/PR PrimeIntellect-ai#269 and PrimeIntellect-ai#264/PR PrimeIntellect-ai#270 open with CI in flight; PrimeIntellect-ai#272 and PrimeIntellect-ai#271 filed, unstarted, no lane. - Errors and corrections: settle elapsed time from GitHub workflow-run timestamps, never the container clock; add_repo push access was classifier-denied so a remote session may hold read-only and cannot push; register_repo_root denial falls back to reading CLAUDE.md directly. - MATS/superpowers/routing settled empirically with the exact commands used, so the search is not repeated: they are Mac-harness resident, and PrimeIntellect-ai#272/PrimeIntellect-ai#271 already carry auto-dispatch, which is what routes them to the maker fleet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5B7QM1QLQuWSBMCxiCzS6 * docs(spx-v2): implementation-session addendum — access, setup, CI outage, self-review Appends the second half of the session to the notes: implementing PrimeIntellect-ai#272 after the operator corrected two access assumptions. The corrections matter more than the code: - push DOES work; "I cannot push" was inferred from add_repo's access label rather than tested. A dry-run push proved it. Also: the refspec push form is classifier-denied while `git push -u origin <branch>` succeeds. - this host is not the MacBook (uname, no /Users, no ~/.prime). Also records the environment setup the Makefile assumes (venv before v2-install, ruff 0.15.22 via python -m, seeding the gitignored account.yaml, and proving PYTHONPATH beats editable installs in a worktree), the method that diagnosed the repo-wide CI outage in two calls (zero recorded steps, then the same workflow red on main), and two test defects self-review caught before pushing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5B7QM1QLQuWSBMCxiCzS6 * docs(spx-v2): final queue state — PrimeIntellect-ai#273 merged, only PrimeIntellect-ai#271 remains Closes the record for this session. - All five briefed PRs merged (PrimeIntellect-ai#268, PrimeIntellect-ai#263, PrimeIntellect-ai#270, PrimeIntellect-ai#269, PrimeIntellect-ai#273); main is c84855d. Issues PrimeIntellect-ai#266 and PrimeIntellect-ai#272 closed by their PRs. PrimeIntellect-ai#271 is the only open brief and was never authorised, so never started. Runtime testing is unblocked. - The CI outage (13:51Z-15:47Z) was account-level and hit main identically; recovery was visible as `changes` taking 9s with real steps instead of 2s with none. Nothing in the diff ever needed changing. - Records the scope misjudgement worth carrying forward: a Codex P1 mapped directly to an acceptance checkbox I had deferred as out of scope. When a finding maps to an acceptance criterion it is in scope by definition. - Records the auto-merge hazard: squash composes the commit message from the PR body, so a body left stale after a review round writes false claims into main permanently. Rewrite the body before merge; keep corrections visible. - Records a published test claim that had not been executed, and the rule that follows from it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5B7QM1QLQuWSBMCxiCzS6 * docs(spx-v2): correct the queue state — the last brief landed while idle Records, without editing the now-false text away, that sections 9 and 4 went stale five hours after they were written. The correction matters more than the content: this file exists to stop sessions trusting notes over GitHub, and it caught its own author. - main is 8d2139c. Between 16:12Z and 21:54Z the fleet merged PrimeIntellect-ai#279 (the PrimeIntellect-ai#58 alert-bridge race, FIXED — stop carrying it as a standing exception), PrimeIntellect-ai#278 (AGENTS.md invariants), PrimeIntellect-ai#283 (repo cleanup), and PrimeIntellect-ai#284, which delivered the last brief and closed PrimeIntellect-ai#165 with a keyword. - Section 3's routing conclusion was confirmed by events: PrimeIntellect-ai#271 was delivered by the Mac maker fleet via auto-dispatch, exactly as argued, and the remote session correctly declined to open a second lane. - Flags issue-state drift: PrimeIntellect-ai#271, PrimeIntellect-ai#276 and PrimeIntellect-ai#274 are delivered and merged yet still open, because a title reference is not a closing keyword. That is the mirror image of the hazard the V2 CLAUDE.md documents, and it leaves open-work disagreeing with main. Operator action, named as such. - Records the residual PrimeIntellect-ai#284 deferred on stated grounds (PrimeIntellect-ai#286), which is a known open edge on the LIVE path. - States the next slice: S4 / PrimeIntellect-ai#236, the first whose exit criteria need a real broker order. Certification stays 0/12; the system has never placed a trade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5B7QM1QLQuWSBMCxiCzS6 --------- Co-authored-by: Claude <noreply@anthropic.com>


Summary
packages/coding-agent/extensions/prime-mcp/— a first-party MCP client shipped as apipackage. A singlemcpproxy tool (list/describe/call) keeps the prompt small (~200 tokens) instead of loading every server's tool defs.directToolspromotes hot tools to first-classregisterToolentries with their real schemas. Promotions are trust-filtered so a lower-precedence repo config can't hijack a server a higher-trust config opted to auto-promote.<cwd>/.mcp.json><cwd>/.prime/agent/mcp.json>~/.prime/agent/mcp.json. Malformed files warn and are skipped rather than failing the whole load.${VAR}expansion inenv/headers./mcpslash commands (status, tools, reconnect), example config,docs/mcp.md, README pointer, and a CHANGELOG entry.Test plan
npm run checkcleanprime-agent -e ./packages/coding-agent/extensions/prime-mcpwith an example serverScope notes (v1)
prime-agent install ./packages/coding-agent/extensions/prime-mcp); not auto-enabled in default settings and not bundled into the published coding-agent package.mcp__<server>__<tool>(capped at the 64-char provider limit with a stable hash). There is no extension API to detect collisions with other extensions' tools, so the prefix is the guard.prime-swarm alignment
MCP is agent-to-tool (see prime-swarm
docs/interconnect.md); the implementation stays in the agent image and only tool names cross the swarm wire.Made with Cursor
Note
Add first-party MCP client extension to the coding agent
prime-mcpextension package that connects the coding agent to external MCP servers via a singlemcpproxy tool, allowing the model to list servers/tools, inspect schemas, and invoke tools.McpManagerwith lazy connect, idle disconnect via configurable timeout, single retry on transport error, and graceful shutdown across sessions..mcp.json,.prime/agent/mcp.json, or~/.prime/agent/mcp.jsonwith precedence-based merging; supports both stdio and HTTP transports with${VAR}env expansion.directToolsconfig, with sanitized names, collision detection, and schema passthrough./mcpslash command for status, tool listing, and per-server reconnect operations.📊 Macroscope summarized 4ab8720. 13 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.