Skip to content

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

Merged
Connor Peet (connor4312) merged 3 commits into
mainfrom
connor/mcp-server-state-diagnostics
Jul 17, 2026
Merged

Fix agent host MCP server state and add per-server diagnostics#326384
Connor Peet (connor4312) merged 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
Connor Peet (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:

Tyler James Leonhardt (@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
Connor Peet (connor4312) force-pushed the connor/mcp-server-state-diagnostics branch from 6d8af8d to c4610dd Compare July 17, 2026 18:11
@connor4312
Connor Peet (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
Connor Peet (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.

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>
@connor4312
Connor Peet (connor4312) merged commit 2a8e7fc into main Jul 17, 2026
29 checks passed
@connor4312
Connor Peet (connor4312) deleted the connor/mcp-server-state-diagnostics branch July 17, 2026 23:02
@vs-code-engineering vs-code-engineering Bot added this to the 1.130.0 milestone Jul 17, 2026
Megan Rogge (meganrogge) added a commit that referenced this pull request Jul 18, 2026
Restores the optional readonly logOutputChannelId property on
IAgentHostMcpServer. #326384 removed it from the interface but left
consumers (mcpListWidget, aiCustomizationManagementEditor fixture,
mcpListWidget.test) still referencing it, breaking tsc in the PR merge.
This non-behavioral change re-declares the optional property those
consumers already expect.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d527ed3c-29a9-435f-8772-c8117077d394
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.

4 participants