Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
},
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.128",
"@modelcontextprotocol/sdk": "^1.29.0",
"@playwright/cli": "^0.1.9",
"@playwright/test": "^1.56.1",
"@stylistic/eslint-plugin-ts": "^2.8.0",
Expand Down
10 changes: 9 additions & 1 deletion src/vs/base/common/jsonRpcProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,15 @@ export function isJsonRpcRequest(message: JsonRpcMessage): message is IJsonRpcRe
}

export function isJsonRpcResponse(message: JsonRpcMessage): message is IJsonRpcSuccessResponse | IJsonRpcErrorResponse {
return hasKey(message, { id: true, result: true }) || hasKey(message, { id: true, error: true });
return isJsonRpcSuccessResponse(message) || isJsonRpcErrorResponse(message);
}

export function isJsonRpcSuccessResponse(message: JsonRpcMessage): message is IJsonRpcSuccessResponse {
return hasKey(message, { id: true, result: true });
}

export function isJsonRpcErrorResponse(message: JsonRpcMessage): message is IJsonRpcErrorResponse {
return hasKey(message, { id: true, error: true });
}

export function isJsonRpcNotification(message: JsonRpcMessage): message is IJsonRpcNotification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,11 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
* Authenticate with the remote agent host using a specific scheme.
*/
async authenticate(params: AuthenticateParams): Promise<AuthenticateResult> {
await this._sendRequest('authenticate', params);
await this._sendRequest('authenticate', {
resource: params.resource,
token: params.token,
...(params.server ? { server: params.server.toString() } : {}),
});
return { authenticated: true };
}

Expand Down
7 changes: 7 additions & 0 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ export interface AuthenticateParams {

/** The bearer token value (RFC 6750). */
readonly token: string;

/**
* Optional: if set, scopes the token to a specific MCP server's
* proxy. The host routes the call to {@link IMcpHostService.getServer}
* instead of fanning out to providers.
*/
readonly server?: URI;
}

/**
Expand Down
189 changes: 189 additions & 0 deletions src/vs/platform/agentHost/common/mcpHost/mcpHostService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IDisposable } from '../../../../base/common/lifecycle.js';
import { IObservable } from '../../../../base/common/observable.js';
import { URI } from '../../../../base/common/uri.js';
import { IMcpServerDefinition } from '../../../agentPlugins/common/pluginParsers.js';
import { createDecorator } from '../../../instantiation/common/instantiation.js';
import { McpMethodCallParams, McpMethodCallResult, McpNotificationParams } from '../state/protocol/commands.js';
import { AhpMcpUiHostCapabilities, McpServerSummary } from '../state/protocol/state.js';

/**
* The MCP Apps `_meta.ui` payload carried by a `Tool` definition.
*
* Mirrors the
* [MCP Apps spec](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx).
* The host captures these payloads opportunistically from `tools/list`
* responses; consumers (typically a Copilot/Claude agent's tool-call
* mapper) look them up by tool name when surfacing a tool call to AHP
* so the resulting `ToolCallBase._meta` carries `ui` /
* `uiHostCapabilities` for MCP-App-aware clients.
*/
export interface IMcpUiToolMeta {
/** `ui://…` URI of the UI resource for rendering the tool result. */
readonly resourceUri?: string;
/**
* Who can access this tool. Default `["model", "app"]`. `"model"` makes
* the tool visible to the agent; `"app"` makes it callable by the UI
* view from the same MCP server.
*/
readonly visibility?: readonly ('model' | 'app')[];
}

/**
* An upstream-originated `mcpMethodCall` request. The host invokes the
* configured {@link IMcpHostUpstreamDelegate.handleUpstreamRequest} once
* per inbound JSON-RPC request from an MCP server and writes the
* returned outcome back as a JSON-RPC response.
*/
export interface IUpstreamMcpRequest {
/** MCP server URI; matches {@link McpServerSummary.resource}. */
readonly server: URI;
/** JSON-RPC method (e.g. `sampling/createMessage`, `ui/open-link`). */
readonly method: string;
/** Method params; opaque to AHP. */
readonly params?: unknown;
}

/**
* Outcome the delegate produces for an {@link IUpstreamMcpRequest}.
*
* Either {@link result} or {@link error} is set. The host forwards either
* one to the upstream MCP server as a JSON-RPC success or error response.
*/
export interface IUpstreamMcpResponse {
readonly result?: unknown;
readonly error?: { code: number; message: string; data?: unknown };
}

/**
* Upstream-originated `mcpNotification` payload. Fire-and-forget; the
* host calls {@link IMcpHostUpstreamDelegate.handleUpstreamNotification}
* whenever the upstream MCP server publishes a JSON-RPC notification
* (e.g. `notifications/tools/list_changed`).
*/
export interface IUpstreamMcpNotification {
readonly server: URI;
readonly method: string;
readonly params?: unknown;
}

/**
* Bridge for forwarding upstream-originated MCP traffic out to an AHP
* client (typically wired up by `ProtocolServerHandler`). At most one
* delegate is installed at a time; calls when none is installed yield
* `MethodNotFound`.
*/
export interface IMcpHostUpstreamDelegate {
handleUpstreamRequest(request: IUpstreamMcpRequest): Promise<IUpstreamMcpResponse>;
handleUpstreamNotification(notification: IUpstreamMcpNotification): void;
}

/**
* Per-MCP-server handle owned by {@link IMcpHostService}. Lifetime is bounded
* by {@link IMcpHostService.setSessionServers} — once the parent session no
* longer lists the server, the handle is disposed.
*/
export interface IMcpServerHandle extends IDisposable {
/** The `mcp:/<sessionId>/<serverId>` URI; matches {@link McpServerSummary.resource}. */
readonly resource: URI;

/** Latest summary observed for this server. */
readonly summary: IObservable<McpServerSummary>;

/**
* Endpoint the upstream SDK should connect to. `undefined` until the
* proxy is up.
*/
readonly endpoint: IObservable<URI | undefined>;

/**
* Push a bearer token. Returns `true` when accepted by the upstream;
* `false` when the resource doesn't match this server.
*/
authenticate(resource: string, token: string): Promise<boolean>;

/**
* Forward an AHP-client → MCP server JSON-RPC **request** and resolve
* with the upstream's result. Rejects with a `ProtocolError` if the
* upstream returns a JSON-RPC error.
*/
callMethod(params: McpMethodCallParams): Promise<McpMethodCallResult>;

/**
* Forward an AHP-client → MCP server JSON-RPC **notification**.
* Fire-and-forget.
*/
notify(params: McpNotificationParams): void;

/**
* Most recent `_meta.ui` payload the upstream MCP server advertised
* for `toolName` via `tools/list`. Returns `undefined` when no
* MCP-Apps-enabled tool with that name has been observed (for
* example, before the SDK has listed tools, or for non-app tools).
*
* Captured opportunistically as `tools/list` responses flow through
* the proxy from either the upstream SDK or AHP-client
* `mcpMethodCall` traffic.
*/
getToolUiMeta(toolName: string): IMcpUiToolMeta | undefined;

/**
* The MCP-Apps host-capability set the AHP host satisfies for a View
* backed by this server, derived from the upstream's `initialize`
* response. Producers attach this to `_meta.uiHostCapabilities` on
* every tool-call state surfacing an MCP App so consumers can
* forward the same set into the view's `ui/initialize` response.
*
* Empty until the SDK's `initialize` handshake has completed through
* the proxy.
*/
getUiHostCapabilities(): AhpMcpUiHostCapabilities;
}

/**
* Owns per-(session, MCP server) state and bridges AHP traffic to the
* underlying MCP transports. Platform-agnostic; the node-only proxy
* mechanics live in `IMcpProxyFactory`.
*/
export interface IMcpHostService {
readonly _serviceBrand: undefined;

/**
* Replace the set of MCP servers registered for `session`. Diffing against
* the previous set, the service dispatches `session/mcpServerAdded` and
* `session/mcpServerRemoved` actions through the state manager.
*/
setSessionServers(session: URI, servers: readonly IMcpServerDefinition[]): readonly IMcpServerHandle[];

/** Return the current lightweight summaries for all MCP servers in `session`. */
getServerSummaries(session: URI): readonly McpServerSummary[];

/** Look up a handle by its resource URI. */
getServer(resource: URI): IMcpServerHandle | undefined;

/**
* Pure router for client `mcpMethodCall` requests. Resolves the handle
* from `params.server` and forwards. Used by `ProtocolServerHandler`.
*/
callMethod(params: McpMethodCallParams): Promise<McpMethodCallResult>;

/**
* Pure router for client `mcpNotification` notifications.
*/
notify(params: McpNotificationParams): void;

/**
* Install a delegate for upstream-originated traffic (the only direction
* the host service cannot satisfy locally). The disposable removes the
* delegate, after which upstream requests fail with `MethodNotFound`.
*/
setUpstreamDelegate(delegate: IMcpHostUpstreamDelegate): IDisposable;
}

export const IMcpHostService = createDecorator<IMcpHostService>('mcpHostService');


47 changes: 47 additions & 0 deletions src/vs/platform/agentHost/common/mcpHost/nullMcpHostService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IDisposable } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { IMcpServerDefinition } from '../../../agentPlugins/common/pluginParsers.js';
import { McpMethodCallParams, McpMethodCallResult, McpNotificationParams } from '../state/protocol/commands.js';
import type { McpServerSummary } from '../state/protocol/state.js';
import { JsonRpcErrorCodes, ProtocolError } from '../state/sessionProtocol.js';
import { IMcpHostService, IMcpHostUpstreamDelegate, IMcpServerHandle } from './mcpHostService.js';

/**
* No-op {@link IMcpHostService}. Returns no servers, refuses `mcpMethodCall`
* requests with `MethodNotFound`, and silently drops `mcpNotification`s.
* Used by surfaces that don't host MCP traffic (browser-side stubs and tests).
*/
export class NullMcpHostService implements IMcpHostService {
declare readonly _serviceBrand: undefined;

setSessionServers(_session: URI, _servers: readonly IMcpServerDefinition[]): readonly IMcpServerHandle[] {
return [];
}

getServerSummaries(_session: URI): readonly McpServerSummary[] {
return [];
}

getServer(_resource: URI): IMcpServerHandle | undefined {
return undefined;
}

async callMethod(_params: McpMethodCallParams): Promise<McpMethodCallResult> {
throw new ProtocolError(JsonRpcErrorCodes.MethodNotFound, 'mcpMethodCall is not supported by this host');
}

notify(_params: McpNotificationParams): void {
// no-op
}

setUpstreamDelegate(_delegate: IMcpHostUpstreamDelegate): IDisposable {
return { dispose: () => { /* no-op */ } };
}
}


44 changes: 44 additions & 0 deletions src/vs/platform/agentHost/common/state/mcpServerUri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { URI } from '../../../../base/common/uri.js';

/** URI scheme for MCP server resources surfaced as `McpServerSummary.resource`. */
export const MCP_SERVER_SCHEME = 'mcp';

/**
* Build an `mcp:/<sessionPath>/<serverId>` URI.
*
* The path encodes the session URI's path component (typically a UUID) so
* the server URI is **self-identifying to a session** per
* `McpMessageParams` — the host service can derive the owning session by
* parsing this URI in O(1) without a side map.
*/
export function buildMcpServerUri(session: URI, serverId: string): URI {
const sessionPath = session.path.replace(/^\/+/, '');
return URI.from({
scheme: MCP_SERVER_SCHEME,
path: `/${sessionPath}/${encodeURIComponent(serverId)}`,
});
}

/**
* Parse an `mcp:/<sessionPath>/<serverId>` URI back into its parts. Returns
* `undefined` if the URI is not in the expected scheme/shape.
*/
export function parseMcpServerUri(uri: URI): { sessionPath: string; serverId: string } | undefined {
if (uri.scheme !== MCP_SERVER_SCHEME) {
return undefined;
}
const path = uri.path.replace(/^\/+/, '');
const lastSlash = path.lastIndexOf('/');
if (lastSlash < 1) {
return undefined;
}
return {
sessionPath: path.slice(0, lastSlash),
serverId: decodeURIComponent(path.slice(lastSlash + 1)),
};
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5f79fe4
a3ea9b4
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
// Generated from types/actions.ts — do not edit
// Run `npm run generate` to regenerate.

import { ActionType, type StateAction, type RootAgentsChangedAction, type RootActiveSessionsChangedAction, type RootTerminalsChangedAction, type RootConfigChangedAction, type SessionReadyAction, type SessionCreationFailedAction, type SessionTurnStartedAction, type SessionDeltaAction, type SessionResponsePartAction, type SessionToolCallStartAction, type SessionToolCallDeltaAction, type SessionToolCallReadyAction, type SessionToolCallConfirmedAction, type SessionToolCallCompleteAction, type SessionToolCallResultConfirmedAction, type SessionToolCallContentChangedAction, type SessionTurnCompleteAction, type SessionTurnCancelledAction, type SessionErrorAction, type SessionTitleChangedAction, type SessionUsageAction, type SessionReasoningAction, type SessionModelChangedAction, type SessionServerToolsChangedAction, type SessionActiveClientChangedAction, type SessionActiveClientToolsChangedAction, type SessionPendingMessageSetAction, type SessionPendingMessageRemovedAction, type SessionQueuedMessagesReorderedAction, type SessionInputRequestedAction, type SessionInputAnswerChangedAction, type SessionInputCompletedAction, type SessionCustomizationsChangedAction, type SessionCustomizationToggledAction, type SessionTruncatedAction, type SessionIsReadChangedAction, type SessionIsArchivedChangedAction, type SessionActivityChangedAction, type SessionDiffsChangedAction, type SessionConfigChangedAction, type SessionMetaChangedAction, type TerminalDataAction, type TerminalInputAction, type TerminalResizedAction, type TerminalClaimedAction, type TerminalTitleChangedAction, type TerminalCwdChangedAction, type TerminalExitedAction, type TerminalClearedAction, type TerminalCommandDetectionAvailableAction, type TerminalCommandExecutedAction, type TerminalCommandFinishedAction } from './actions.js';
import { ActionType, type StateAction, type RootAgentsChangedAction, type RootActiveSessionsChangedAction, type RootTerminalsChangedAction, type RootConfigChangedAction, type SessionReadyAction, type SessionCreationFailedAction, type SessionTurnStartedAction, type SessionDeltaAction, type SessionResponsePartAction, type SessionToolCallStartAction, type SessionToolCallDeltaAction, type SessionToolCallReadyAction, type SessionToolCallConfirmedAction, type SessionToolCallCompleteAction, type SessionToolCallResultConfirmedAction, type SessionToolCallContentChangedAction, type SessionTurnCompleteAction, type SessionTurnCancelledAction, type SessionErrorAction, type SessionTitleChangedAction, type SessionUsageAction, type SessionReasoningAction, type SessionModelChangedAction, type SessionServerToolsChangedAction, type SessionActiveClientChangedAction, type SessionActiveClientToolsChangedAction, type SessionPendingMessageSetAction, type SessionPendingMessageRemovedAction, type SessionQueuedMessagesReorderedAction, type SessionInputRequestedAction, type SessionInputAnswerChangedAction, type SessionInputCompletedAction, type SessionCustomizationsChangedAction, type SessionCustomizationToggledAction, type SessionTruncatedAction, type SessionIsReadChangedAction, type SessionIsArchivedChangedAction, type SessionActivityChangedAction, type SessionDiffsChangedAction, type SessionConfigChangedAction, type SessionMetaChangedAction, type TerminalDataAction, type TerminalInputAction, type TerminalResizedAction, type TerminalClaimedAction, type TerminalTitleChangedAction, type TerminalCwdChangedAction, type TerminalExitedAction, type TerminalClearedAction, type TerminalCommandDetectionAvailableAction, type TerminalCommandExecutedAction, type TerminalCommandFinishedAction, type McpServerAddedAction, type McpServerRemovedAction, type McpServerStatusChangedAction } from './actions.js';


// ─── Root vs Session vs Terminal Action Unions ───────────────────────────────
// ─── Root vs Session vs Terminal Action Unions ───────────────────────────

/** Union of all root-scoped actions. */
export type RootAction =
Expand Down Expand Up @@ -73,6 +73,9 @@ export type SessionAction =
| SessionDiffsChangedAction
| SessionConfigChangedAction
| SessionMetaChangedAction
| McpServerAddedAction
| McpServerRemovedAction
| McpServerStatusChangedAction
;

/** Union of session actions that clients may dispatch. */
Expand Down Expand Up @@ -118,6 +121,9 @@ export type ServerSessionAction =
| SessionActivityChangedAction
| SessionDiffsChangedAction
| SessionMetaChangedAction
| McpServerAddedAction
| McpServerRemovedAction
| McpServerStatusChangedAction
;

/** Union of all terminal-scoped actions. */
Expand Down Expand Up @@ -213,4 +219,7 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool
[ActionType.TerminalCommandDetectionAvailable]: false,
[ActionType.TerminalCommandExecuted]: false,
[ActionType.TerminalCommandFinished]: false,
[ActionType.McpServerAdded]: false,
[ActionType.McpServerRemoved]: false,
[ActionType.McpServerStatusChanged]: false,
};
Loading