Skip to content

feat(plugins): MCP plugin management, agent injection, and custom MCP import#261

Open
lucas77778 wants to merge 22 commits into
masterfrom
chenyu/code-387
Open

feat(plugins): MCP plugin management, agent injection, and custom MCP import#261
lucas77778 wants to merge 22 commits into
masterfrom
chenyu/code-387

Conversation

@lucas77778

Copy link
Copy Markdown
Member

Builds out the Connector/plugin batch on the daemon and clients: a redesigned plugin schema, MCP injection into agent sessions, the Tools settings surface, and user-imported custom MCP servers.

What's in here

Schema redesign (CODE-382) — a plugin composes 1..N MCP servers (McpPluginDescriptor.servers[]), and connector association moved from a per-unit binding to a global per-service serviceBindings map. Secrets never cross the wire (masked public projections).

MCP capability + warnings (CODE-385)AGENT_MCP_CAPABLE lives in @linkcode/schema (engine and clients read one truth); the resolver skips declaredly-expired credentials (expired-credential); plugin warnings are kept on LiveSession and replayed on session.attach; client-core folds them into ConversationViewModel.pluginWarnings.

Data plane + Tools UI (CODE-386 / CODE-387) — workbench view models (pluginUnitViews / pluginServiceViews / customServerViews), and a card-based Tools tab in desktop + webview: capability units with enablement + per-server status badges.

MCP injection into agents (CODE-93) — claude / codex / opencode now consume StartOptions.mcpServers (each via its own SDK channel; pi excluded by the capability matrix). Verified live on claude 2.1.218 — an injected stdio MCP server's tool was called end to end.

Custom MCP import (CODE-407) — users add their own MCP servers on the Tools tab. Credentials are inline and masked to key-only on config.get; the resolver injects enabled ones, sharing the capability gate and client-name precedence; name-uniqueness is enforced at write time, at resolution, and mirrored in the dev mock.

Also merges master (telemetry batch, chat-card refactor, CODE-388/391 data-plane work). Wire protocol 43 → 48 — daemon and every client must rebuild/restart together.

Verification

pnpm check:ci clean, 1846 vitest passing. Live E2E: MCP injection (catalog + custom) drove a real claude session end to end; the Tools tab add/mask/remove flow verified in webview via Playwright. Two adversarial review passes (workflow) — confirmed findings fixed (expired-credential injection, fold dedup, dev-mock name-collision parity, resolver dedup defense-in-depth).

Notes for review

  • Merge conflict resolution for the parallel wire bumps is recorded in packages/foundation/schema/src/wire/message.ts comments (44 collision → 46, then 47/48).
  • Custom MCP v1 editing = toggle + remove (engine already supports wholesale server replacement for a future edit form).
  • Follow-ups recorded on the issues: codex MCP approval/elicitation server-requests unhandled (CODE-93); managed/broker path still stubbed pending CODE-96/94/340.

… service

A plugin now composes one or more MCP servers; each server names its
service and resolves its credential through the global service->binding
map instead of a per-unit connector reference. Wire protocol 45.
Master's 44 (CODE-388/391) and this branch's 44/45 (CODE-382) were
parallel bumps with distinct schemas; the merged schema takes 46.
…n warnings

AGENT_MCP_CAPABLE replaces the resolver-private agent set so clients
read the same truth. The resolver skips a declaredly expired local
credential with the new expired-credential warning reason (wire 47),
and start diagnostics now live on LiveSession so session.attach
replays them to late attachers.
Deduped by unit+service+reason so attach replays never stack while
distinct reasons for one dependency all stay visible.
Pure derivation from catalog + public config, mirroring the resolver's
verdict vocabulary; the dev mock now fails invalid connector operations
with the daemon's sanitized public message.
Card list of catalog units on a new Tools tab in both apps: localized
label/description, enable switch writing PluginConfigSet.units, and
per-server status badges derived from the plugin view model.
…ns (CODE-93)

claude: Options.mcpServers record (spawn-time --mcp-config; merges with
the user's own MCP config). codex: thread/start|resume
config.mcp_servers (no type tag, headers->http_headers; startup
failures surface as recoverable errors). opencode: per-session spawn
Config.mcp (local/remote, env->environment, explicit enabled).

Live-verified on claude 2.1.218: an injected stdio MCP server's tool
was called end to end through a LinkCode session.
Adds a BYO MCP tier alongside the catalog: CustomMcpServer ({id,
enabled, server}) in the plugin config, injected into agent sessions
through the same resolved StartOptions.mcpServers path. Credentials are
inline and opaque to LinkCode — the public projection masks env/header
values to key lists, and a custom name colliding with a catalog server
is rejected at write time. The resolver shares the capability gate and
client-name precedence with catalog servers; plugin-warning gained a
custom-server source. A Tools-tab section imports/toggles/removes them.
Wire protocol 48.

Live-verified on claude 2.1.218: a custom stdio server imported through
the SDK was injected and its tool called end to end; the webview form
adds a server, shows only the masked credential key, and removes it.
…path

Adversarial review found the name-collision invariant lived only in the
engine's config.set guard: the dev-mock reimplementation skipped it
(breaking engine parity), and nothing defended a bad-state config.json
at resolution. The resolver now skips a custom server whose name is
already taken (client / catalog / earlier custom) so it can never
silently clobber another in the adapter's name-keyed MCP config, and
the dev-mock now rejects colliding names like the daemon.
Copilot AI review requested due to automatic review settings July 23, 2026 08:10
@linear-code

linear-code Bot commented Jul 23, 2026

Copy link
Copy Markdown

CODE-387

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown

Greptile Summary

Adds daemon-backed MCP plugin management and injects resolved catalog and custom servers into supported agent sessions.

  • Redesigns the plugin schema around multi-server units, service bindings, masked public configuration, and protocol version 48.
  • Adds MCP translation for Claude Code, Codex, and OpenCode plus session warning replay.
  • Adds shared desktop and webview Tools settings for enabling plugins and importing custom stdio or HTTP servers.

Confidence Score: 4/5

The custom MCP import path should be fixed before merging because it can silently remove entered credentials and rejects names accepted by the shared server schema.

Malformed environment-variable or header rows are ignored during form conversion, so an imported server can be stored without credentials the user entered and then fail when injected.

packages/client/workbench/src/settings/plugins/custom-server-dialog.tsx also prevents importing schema-valid dotted names.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex prepared a focused real-code UI reproduction harness that imports the actual CustomServerDialog and a Playwright submission script, but the run could not complete within the step budget due to an early harness resolution issue.
  • The harness error blocking the initial browser run was identified and corrected just before the execution limit was reached, enabling the final run attempt to proceed.
  • Before and after captures of the real Tools page were created: the pre-import state showed an empty Custom MCP section, and the post-import view emphasized the weather-helper card with its stdio metadata and credential metadata; the Playwright run exited with code 0 and its traces, script, browser evidence, build output, and server log were attached.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
packages/host/engine/src/session/start-options-resolver.ts Resolves enabled catalog and custom MCP servers with capability checks, binding diagnostics, credential expiry handling, and collision defenses.
packages/host/agent-adapter/src/native/claude-code.ts Translates resolved stdio and HTTP MCP descriptors into Claude Agent SDK startup configuration.
packages/host/agent-adapter/src/native/codex/adapter.ts Translates MCP descriptors into Codex app-server thread configuration and reports server startup failures.
packages/host/agent-adapter/src/native/opencode/adapter.ts Injects enabled local and remote MCP definitions through OpenCode's session configuration channel.
packages/client/workbench/src/settings/plugins/custom-server-dialog.tsx Implements custom MCP import, but silently drops malformed credential lines and rejects server names accepted by the shared schema.
apps/daemon/src/config.ts Loads and persists plugin state and local credentials while preserving other config fields and enforcing mode 0600.
packages/foundation/schema/src/model/plugin.ts Defines catalog, connector, service-binding, custom-server, and masked public plugin contracts.
packages/foundation/schema/src/wire/message.ts Advances the lockstep wire protocol version for the new plugin configuration and warning payloads.

Sequence Diagram

sequenceDiagram
  participant U as Tools UI
  participant C as Client SDK
  participant D as Daemon config
  participant R as MCP resolver
  participant A as Agent adapter
  U->>C: Add or enable MCP server
  C->>D: config.set
  D-->>C: Masked config projection
  C-->>U: Refreshed Tools state
  R->>D: Read plugin configuration
  R->>R: Resolve bindings, credentials, capability, and names
  R->>A: StartOptions.mcpServers
  A->>A: Translate to provider-specific MCP configuration
Loading

Reviews (1): Last reviewed commit: "fix(plugins): enforce custom-server name..." | Re-trigger Greptile

Comment on lines +55 to +57
const at = trimmed.indexOf(separator);
if (at <= 0) continue;
pairs[trimmed.slice(0, at).trim()] = trimmed.slice(at + 1).trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Credential lines are silently discarded

When a user enters a nonblank environment-variable or HTTP-header line without the expected separator, parsePairs silently omits it and saves the server without that credential, causing authentication or startup failures with no validation feedback.

Comment on lines +31 to +34
name: z
.string()
.trim()
.regex(/^[\w-]+$/, 'name'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 UI rejects schema-valid server names

The dialog restricts names to word characters and hyphens even though McpServerSchema accepts arbitrary strings, preventing users from importing otherwise valid names such as my.server through the Tools UI.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

2 participants