Skip to content

feat(coding-agent): first-party MCP extension - #279

Closed
Apocrathia wants to merge 7 commits into
mainfrom
feat/prime-mcp-extension
Closed

feat(coding-agent): first-party MCP extension#279
Apocrathia wants to merge 7 commits into
mainfrom
feat/prime-mcp-extension

Conversation

@Apocrathia

@Apocrathia Apocrathia commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds packages/coding-agent/extensions/prime-mcp/ — a first-party MCP client shipped as a pi package. A single mcp proxy tool (list/describe/call) keeps the prompt small (~200 tokens) instead of loading every server's tool defs.
  • Optional directTools promotes hot tools to first-class registerTool entries 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.
  • Lazy connect on first use, refcounted idle disconnect, and a single transparent reconnect on a dead transport. Supports stdio and streamable-HTTP servers.
  • Config resolution with precedence: <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 in env/headers.
  • /mcp slash commands (status, tools, reconnect), example config, docs/mcp.md, README pointer, and a CHANGELOG entry.

Test plan

  • npm run check clean
  • 28 unit tests (config precedence/validation, proxy call path, reconnect, concurrency, content rendering, name caps) — mocked MCP server, no real keys
  • Manual: prime-agent -e ./packages/coding-agent/extensions/prime-mcp with an example server

Scope notes (v1)

  • Ships as a workspace package installed via local path (prime-agent install ./packages/coding-agent/extensions/prime-mcp); not auto-enabled in default settings and not bundled into the published coding-agent package.
  • Client-only — Prime Agent is not exposed as an MCP server.
  • Promoted tools are namespaced 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

  • Adds a new prime-mcp extension package that connects the coding agent to external MCP servers via a single mcp proxy tool, allowing the model to list servers/tools, inspect schemas, and invoke tools.
  • Implements McpManager with lazy connect, idle disconnect via configurable timeout, single retry on transport error, and graceful shutdown across sessions.
  • Reads merged config from .mcp.json, .prime/agent/mcp.json, or ~/.prime/agent/mcp.json with precedence-based merging; supports both stdio and HTTP transports with ${VAR} env expansion.
  • Supports promoting selected MCP tools to first-class agent tools via directTools config, with sanitized names, collision detection, and schema passthrough.
  • Exposes a /mcp slash 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.

Apocrathia and others added 6 commits June 29, 2026 10:21
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>
Comment thread packages/coding-agent/extensions/prime-mcp/content.ts
Comment thread packages/coding-agent/extensions/prime-mcp/manager.ts
Comment thread packages/coding-agent/extensions/prime-mcp/manager.ts Outdated
Comment thread packages/coding-agent/extensions/prime-mcp/manager.ts Outdated
Comment thread packages/coding-agent/extensions/prime-mcp/manager.ts
Comment thread packages/coding-agent/extensions/prime-mcp/direct-tools.ts Outdated
@Apocrathia

Copy link
Copy Markdown
Contributor Author

@cursor macroscope found stuff. please address the findings and respond/resolve the threads.

@cursor

cursor Bot commented Jun 29, 2026

Copy link
Copy Markdown

Implemented and pushed review fixes on feat/prime-mcp-extension.

Fixed:

  • Null/undefined MCP content blocks no longer crash rendering.
  • callTool no longer auto-retries transport errors, avoiding duplicate side effects.
  • listTools still retries once on dead transports.
  • Reconnect failure no longer double-decrements old connection refcounts.
  • /mcp reconnect now passes abort signals through.
  • disconnect() now lets in-flight operations drain before closing.
  • directTools now register a deferred fallback on connection errors and retry/upgrade schema after reconnect.

Verified:

  • npx tsx ../../node_modules/vitest/dist/cli.js --run test/prime-mcp.test.ts passes: 34 tests.
  • npm run check passes.
  • Pre-commit hook reran npm run check cleanly.
  • Working tree clean.
  • Commit: 0e74b42e fix(coding-agent): address prime mcp review findings

I updated the PR registration. I could not post/resolve the GitHub review threads directly: no GitHub comment/review MCP tool is available, and gh is read-only in this environment.

Environment note: node_modules was absent, so I ran npm install from the existing lockfile. For future Cloud agents, consider an env setup agent with: “Install workspace dependencies for prime-agent from package-lock.json so vitest and npm run check work without a per-agent npm install.”

Open in Web Open in Cursor 

Comment on lines +144 to +153
if (directToolRefs.length > 0) {
await registerDirectTools(
pi,
manager,
directToolRefs,
(message) => failed.push(message),
promoted,
ctx.signal,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +100 to +104
async setConfig(config: McpConfig): Promise<void> {
await this.disconnectAll();
this.config = config;
this.idleTimeoutMs = this.options.idleTimeoutMs ?? config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@kevinjosethomas

Copy link
Copy Markdown
Member

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.

@kevinjosethomas
kevinjosethomas deleted the feat/prime-mcp-extension branch July 8, 2026 00:30
zhengr pushed a commit to zhengr/prime-agent that referenced this pull request Aug 8, 2026
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
thomaswillner pushed a commit to thomaswillner/prime-agent that referenced this pull request Aug 29, 2026
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
thomaswillner added a commit to thomaswillner/prime-agent that referenced this pull request Aug 29, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants