Skip to content

Fix agent host MCP server state and add per-server diagnostics#326384

Open
connor4312 wants to merge 3 commits into
mainfrom
connor/mcp-server-state-diagnostics
Open

Fix agent host MCP server state and add per-server diagnostics#326384
connor4312 wants to merge 3 commits into
mainfrom
connor/mcp-server-state-diagnostics

Conversation

@connor4312

Copy link
Copy Markdown
Member

Problem

Agent-host MCP servers were seeded as Starting before any SDK had attempted to start them, and connection failures were only visible as a coarse status badge with no detail.

Changes

  • Correct the seed state. makeMcpServerCustomization now produces Stopped instead of Starting -- a declared-but-unstarted server has not begun connecting. Comments and parser/scan/provider tests updated accordingly.

  • Optimistic Starting at the real triggers. The Copilot SDK gives no live "starting" signal on initial connect (servers settle inside a blocking init phase, and rpc.mcp.list blocks until settled -- confirmed by an SDK probe and the runtime source). So the workbench drives Starting itself at the two moments the SDK actually (re)starts a server:

    • sending a message (turn start) via _markEnabledMcpServersStarting(), and
    • the explicit disable->enable flip.

    Added McpCustomizationController.markStarting(names) which skips servers already Ready/AuthRequired/Starting. It is not run on every _reconcileMcpServerEnablement pass (that only syncs enablement).

  • Structured lifecycle logging. MCP loaded / statusChanged / inventory events are logged through the agent host's existing OTLP log stream with OtelData attributes (server, status, state, source/transport/plugin metadata, error type), deduplicated by SDK status. Failures log at error with the failure text, which is also preserved in McpServerErrorState.

  • Per-server diagnostics channel. Each (session, MCP server) gets its own hidden Output channel recording lifecycle transitions (deduplicated). "Show Output" reveals it.

  • Encapsulated the channel. showMcpServerLog(sessionResource, serverId) resolves and reveals the channel internally (mirroring authenticateMcpServer); logOutputChannelId was removed from the public IAgentHostMcpServer surface, along with the now-dead getLogOutputChannelId provider plumbing.

No AHP schema/version change: errored state already carries structured ErrorInfo.

Testing

  • npm run typecheck-client, npm run valid-layers-check, and hygiene all clean.
  • New/updated unit tests: markStarting (controller), optimistic-Starting on send (session), the three lifecycle-log tests, the Stopped seed assertions (parser + claude scan), plus existing reconcile/auth-preservation coverage still green.

Copilot AI review requested due to automatic review settings July 17, 2026 18:02
@connor4312
connor4312 enabled auto-merge (squash) July 17, 2026 18:02
@vs-code-engineering

Copy link
Copy Markdown
Contributor

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

@TylerLeonhardt

Matched files:

  • src/vs/platform/agentHost/node/claude/customizations/scan/claudeMcpScan.ts

Copilot AI left a comment

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.

Pull request overview

Corrects MCP lifecycle state reporting and adds structured, per-server diagnostics.

Changes:

  • Seeds unstarted servers as Stopped; marks real restart triggers as Starting.
  • Adds structured lifecycle logging and tests.
  • Adds hidden per-server Output channels and encapsulates channel access.
Show a summary per file
File Description
src/vs/workbench/contrib/mcp/browser/mcpCommands.ts Routes Show Output through the customization service.
src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostCustomizationService.ts Implements per-server diagnostic channels.
src/vs/sessions/services/agentHost/browser/agentHostCustomizationService.ts Removes host-level channel plumbing.
src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts Updates the service test stub.
src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts Removes remote channel lookup.
src/vs/sessions/contrib/providers/agentHost/browser/localAgentHostSessionsProvider.ts Removes local channel lookup.
src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts Removes channel IDs from server projections.
src/vs/sessions/common/agentHostSessionsProvider.ts Simplifies the MCP server interface.
src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts Expects Stopped seed state.
src/vs/platform/agentPlugins/common/pluginParsers.ts Seeds declared servers as Stopped.
src/vs/platform/agentHost/test/node/shared/mcpCustomizationController.test.ts Tests optimistic Starting transitions.
src/vs/platform/agentHost/test/node/customizations/scan/claudeMcpScan.test.ts Updates Claude scan expectations.
src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts Tests starting state and lifecycle logs.
src/vs/platform/agentHost/node/shared/mcpCustomizationController.ts Adds markStarting.
src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts Adds optimistic state and structured logging.
src/vs/platform/agentHost/node/copilot/copilotAgent.ts Updates seed-state documentation.
src/vs/platform/agentHost/node/claude/customizations/scan/claudeMcpScan.ts Updates scan-state documentation.

Review details

  • Files reviewed: 17/17 changed files
  • Comments generated: 6
  • Review effort level: Medium

Comment on lines +156 to +161
// Record into the per-server diagnostics channel on every
// projection (which fires on each state change the UI observes)
// so the channel captures the observable transition history. The
// channel id itself stays internal — reveal it via
// {@link showMcpServerLog}.
this._mcpLogRegistry.record({ id, name: c.name, enabled: c.enabled, state: c.state });
Comment on lines +424 to +425
record(server: { readonly id: string; readonly name: string; readonly enabled: boolean; readonly state: McpServerState }): string {
const channelId = channelIdForMcpServer(server.id);
Comment on lines +467 to +469
function channelIdForMcpServer(serverId: string): string {
return `agentHostMcpServer.${serverId.replace(/[\\/:*?"<>|]/g, '-')}`;
}
Comment on lines +1853 to 1857
// Re-enabling restarts the server. The SDK connects it in the
// background with no live "starting" event, so surface Starting
// optimistically until the trailing refresh settles it.
this._mcpCustomizations.markStarting([server.serverName]);
await this._wrapper.session.rpc.mcp.enable({ serverName: server.serverName });
Comment on lines 1487 to 1489
await this._reconcileMcpServerEnablement();
this._markEnabledMcpServersStarting();
await this._wrapper.session.send({ prompt, attachments: sdkAttachments?.length ? sdkAttachments : undefined });
*/
class AgentHostMcpServerLogRegistry extends Disposable {

private readonly _entries = new Map<string, { readonly logger: ILogger; lastSignature: string | undefined }>();
Discovered MCP servers were seeded as `Starting` before any SDK had
attempted to start them, and failures were only visible as a coarse
status. This corrects the lifecycle state and adds diagnostics.

- Seed `makeMcpServerCustomization` as `Stopped` instead of `Starting`;
  a declared-but-unstarted server has not begun connecting.
- The Copilot SDK emits no live "starting" signal on initial connect
  (servers settle inside a blocking init phase and `rpc.mcp.list`
  blocks until settled), so drive `Starting` optimistically from the
  workbench at the two real start triggers: sending a message
  (`_markEnabledMcpServersStarting`) and the disable->enable flip.
  Added `McpCustomizationController.markStarting` (skips
  Ready/AuthRequired/Starting).
- Log structured MCP lifecycle records (loaded/statusChanged/inventory)
  through the agent host's existing OTLP log stream with `OtelData`
  attributes, deduplicated by SDK status; failures log at error with
  the failure detail preserved in `McpServerErrorState`.
- Give each `(session, MCP server)` its own hidden Output channel that
  records lifecycle transitions; "Show Output" reveals it.
- Encapsulate the channel: `showMcpServerLog(sessionResource, serverId)`
  resolves and reveals internally; removed `logOutputChannelId` from the
  public `IAgentHostMcpServer` surface and the now-dead
  `getLogOutputChannelId` provider plumbing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@connor4312
connor4312 force-pushed the connor/mcp-server-state-diagnostics branch from 6d8af8d to c4610dd Compare July 17, 2026 18:11
@connor4312
connor4312 marked this pull request as draft July 17, 2026 19:47
auto-merge was automatically disabled July 17, 2026 19:47

Pull request was converted to draft

- Record MCP diagnostics from state-change events over a tracked set of
  sessions rather than as a side effect of `getMcpServers`, so a failure
  and a later recovery are both captured even without a UI re-query.
- Key the per-server log registry by the full session resource plus the
  raw customization id (not the UI-facing scoped id, whose authority is
  empty for Agents-window resources and would mix histories across
  sessions).
- Use an injective, filesystem-safe channel id (SHA1 hex of the composite
  key) so distinct servers can't collapse onto one logger/dedup entry.
- Dispose each session's loggers and Output channel registrations when the
  session goes away (session dispose / removal / provisional backend swap),
  fixing the logger/file-handle leak.
- Route the explicit Start/Restart (`startMcpServer`) through `markStarting`
  so it shows Starting during the SDK's blocking reconnect, and always
  reconcile via a trailing inventory refresh (try/finally) so a rejected
  enable can't leave the UI stuck at Starting -- same fix applied to the
  enable path in `_reconcileMcpServerEnablement`.
- Add coverage for the explicit-start optimistic Starting behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@connor4312
connor4312 marked this pull request as ready for review July 17, 2026 20:08
@connor4312

Copy link
Copy Markdown
Member Author

Thanks for the review — addressed all six in 42dc9e0:

  1. record side-effect in getMcpServers / unreliable history — Recording is now driven by onDidChangeCustomizations state-change events over a tracked set of sessions. getMcpServers no longer records per item; it just registers the session for mirroring (and captures the initial snapshot). Once a session is tracked, every subsequent transition is recorded regardless of whether the UI re-queries, so a failure and a later recovery both land in the channel.
  2. server.id not globally unique (empty authority in Agents window) — The registry is now keyed by the full session resource plus the raw customization id, not the UI-facing scoped id.
  3. Non-injective channel id — Channel ids are now agentHostMcpServer.<sha1hex(JSON([sessionKey, rawId]))>. Hex survives the logger service's reserved-character stripping, so distinct servers can't collapse onto one logger/dedup entry.
  4. rpc.mcp.enable rejection leaves the server stuck at Starting — The reconcile loop now sets its refresh flag before the enable, so the trailing inventory refresh always runs and settles/clears the optimistic Starting even on rejection.
  5. Explicit Start/Restart bypasses markStartingstartMcpServer now marks Starting before its (blocking) enable and reconciles via a try/finally inventory refresh. Added a unit test for this.
  6. Registry entries never removed (leak) — Added per-session teardown that disposes the loggers and deregisters their Output channels on session dispose / removal / provisional backend swap.

typecheck-client, valid-layers-check, hygiene, and the MCP unit suites are all green.

benvillalobos
benvillalobos previously approved these changes Jul 17, 2026
The MCP diagnostics registry now resolves `ILoggerService`/`IOutputService`
lazily when `record` runs (via the new eager state recording). The
enablement unit test instantiated the abstract service against a bare
`TestInstantiationService`, so `getMcpServers` threw
`this._loggerService.createLogger is not a function`. Stub both services
(NullLoggerService + a minimal IOutputService) in the test harness.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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