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
2 changes: 2 additions & 0 deletions WHATS_NEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ We’ve restored full compatibility with the Official MCP Registry and just adde

What’s new

- Unified install controls let you choose between VS Code and Claude Code before running a single install button.
- Claude CLI installs now run through a background VS Code task with inline prompts, success toasts, and guidance when the CLI isn’t installed (including a one-click copy of the generated command).
- Remote endpoints now install cleanly with automatic `${input:...}` prompts for tokens and headers.
- Registry searches are working again, so results appear immediately with updated metadata.
- Remote MCP server results will show an "Install Remote" button that will set up VSCode with the server.
Expand Down
308 changes: 285 additions & 23 deletions src/panels/ExtensionPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,25 @@ import {
import { TelemetryEvents } from "../telemetry/types";
import { Messenger } from "vscode-messenger";
import {
aiAssistedSetupType,
deleteServerType,
getMcpConfigType,
searchServersType,
sendFeedbackType,
updateMcpConfigType,
updateServerEnvVarType,
cloudMCPInterestType,
previewReadmeType,
installFromConfigType,
registrySearchType,
aiAssistedSetupType,
deleteServerType,
getMcpConfigType,
searchServersType,
sendFeedbackType,
updateMcpConfigType,
updateServerEnvVarType,
cloudMCPInterestType,
previewReadmeType,
installFromConfigType,
installClaudeFromConfigType,
registrySearchType,
} from "../shared/types/rpcTypes";
import type {
InstallCommandPayload,
InstallInput,
InstallTransport,
ClaudeInstallRequest,
ClaudeInstallResponse,
} from "../shared/types/rpcTypes";
import axios from "axios";

Expand All @@ -51,7 +59,7 @@ async function getServersFromMcpJsonFile(

// Consolidates servers from global settings, workspace settings, and .vscode/mcp.json files
async function getAllServers(): Promise<Record<string, any>> {
const config = vscode.workspace.getConfiguration("mcp");
const config = vscode.workspace.getConfiguration("mcp");

// 1. Get servers from global settings
const globalServers = config.inspect<Record<string, any>>("servers")?.globalValue || {};
Expand All @@ -74,7 +82,222 @@ async function getAllServers(): Promise<Record<string, any>> {
mergedServers = { ...mergedServers, ...workspaceSettingsServers };
mergedServers = { ...mergedServers, ...mcpJsonFileServers };

return mergedServers;
return mergedServers;
}

const INPUT_PLACEHOLDER_REGEX = /\\?\${input:([^}]+)}/g;

function replaceInputPlaceholders(value: string, replacements: Map<string, string>): string {
return value.replace(INPUT_PLACEHOLDER_REGEX, (_, rawId) => {
const key = String(rawId ?? "").trim();
if (!key) {
return "";
}
return replacements.get(key) ?? "";
});
}

function applyInputsToPayload(
payload: InstallCommandPayload,
replacements: Map<string, string>
): InstallCommandPayload {
const args = payload.args?.map((arg) => replaceInputPlaceholders(arg, replacements));
const envEntries = payload.env ? Object.entries(payload.env) : [];
const env = envEntries.length
? envEntries.reduce<Record<string, string>>((acc, [key, value]) => {
acc[key] = replaceInputPlaceholders(value, replacements);
return acc;
}, {})
: undefined;
const headers = payload.headers?.map((header) => ({
name: header.name,
value: header.value !== undefined ? replaceInputPlaceholders(header.value, replacements) : header.value,
}));

return {
...payload,
args,
env,
headers,
inputs: undefined,
};
}

async function collectInstallInputs(inputs?: InstallInput[]): Promise<{
values: Map<string, string>;
canceled: boolean;
}> {
const values = new Map<string, string>();
if (!inputs || inputs.length === 0) {
return { values, canceled: false };
}

for (const input of inputs) {
if (values.has(input.id)) {
continue;
}
const response = await vscode.window.showInputBox({
prompt: input.description,
password: input.password ?? false,
ignoreFocusOut: true,
});
if (response === undefined) {
return { values, canceled: true };
}
values.set(input.id, response);
}

return { values, canceled: false };
}

function headersArrayToRecord(headers?: Array<{ name: string; value: string }>) {
if (!headers) {
return undefined;
}
const record: Record<string, string> = {};
for (const header of headers) {
if (!header?.name) {
continue;
}
record[header.name] = header.value ?? "";
}
return Object.keys(record).length > 0 ? record : undefined;
}

function buildClaudeConfigObject(payload: InstallCommandPayload, transport: InstallTransport) {
const config: Record<string, unknown> = { type: transport };
if (transport === "stdio") {
if (payload.command) {
config.command = payload.command;
}
if (payload.args && payload.args.length > 0) {
config.args = payload.args;
}
if (payload.env && Object.keys(payload.env).length > 0) {
config.env = payload.env;
}
} else {
if (payload.url) {
config.url = payload.url;
}
const headerRecord = headersArrayToRecord(payload.headers);
if (headerRecord) {
config.headers = headerRecord;
}
}
return config;
}

async function performVscodeInstall(payload: InstallCommandPayload) {
const response = await openMcpInstallUri(payload);
return !!(response && (response as any).success);
}

async function runClaudeCliTask(
name: string,
transport: InstallTransport,
config: Record<string, unknown>,
): Promise<ClaudeInstallResponse> {
const claudeBinary = process.platform === "win32" ? "claude.exe" : "claude";
const configJson = JSON.stringify(config);
const shellExecution = new vscode.ShellExecution(claudeBinary, [
"mcp",
"add-json",
name,
configJson,
]);

const definition: vscode.TaskDefinition = { type: "claude-cli", command: "add-json" };
const scope = vscode.workspace.workspaceFolders?.[0] ?? vscode.TaskScope.Workspace;
const task = new vscode.Task(definition, scope, `Claude MCP Install (${name})`, "Claude CLI", shellExecution);
task.presentationOptions = {
reveal: vscode.TaskRevealKind.Never,
focus: false,
showReuseMessage: false,
clear: true,
};

return await new Promise<ClaudeInstallResponse>((resolve) => {
let resolved = false;
const disposables: vscode.Disposable[] = [];
let execution: vscode.TaskExecution | undefined;

const cleanup = () => {
for (const disposable of disposables) {
disposable.dispose();
}
};

disposables.push(
vscode.tasks.onDidEndTaskProcess((event) => {
if (!execution || event.execution !== execution || resolved) {
return;
}
resolved = true;
cleanup();

const exitCode = event.exitCode;
if (exitCode === undefined || exitCode === null) {
resolve({
success: false,
cliAvailable: false,
errorMessage: "Claude CLI command could not be started.",
});
return;
}
if (exitCode === 127) {
resolve({
success: false,
cliAvailable: false,
errorMessage: "Claude CLI was not found (exit code 127).",
});
return;
}
if (exitCode === 0) {
resolve({ success: true, cliAvailable: true });
} else {
resolve({
success: false,
cliAvailable: true,
errorMessage: `Claude CLI exited with code ${exitCode}.`,
});
}
}),
);

disposables.push(
vscode.tasks.onDidEndTask((event) => {
if (!execution || event.execution !== execution || resolved) {
return;
}
resolved = true;
cleanup();
resolve({
success: false,
cliAvailable: false,
errorMessage: "Claude CLI task ended unexpectedly.",
});
}),
);

vscode.tasks.executeTask(task).then(
(taskExecution) => {
execution = taskExecution;
},
(error) => {
if (resolved) {
return;
}
resolved = true;
cleanup();
const message =
error instanceof Error
? error.message
: "Failed to launch Claude CLI.";
resolve({ success: false, cliAvailable: false, errorMessage: message });
},
);
});
}


Expand Down Expand Up @@ -184,15 +407,52 @@ export class CopilotMcpViewProvider implements vscode.WebviewViewProvider {
});

// Direct install path from structured config (Official Registry results)
messenger.onRequest(installFromConfigType, async (payload) => {
try {
const cmdResponse = await openMcpInstallUri(payload as any);
return !!(cmdResponse && (cmdResponse as any).success);
} catch (error) {
console.error("Error during direct install: ", error);
return false;
}
});
messenger.onRequest(installFromConfigType, async (payload) => {
try {
logWebviewInstallAttempt(payload.name);
return await performVscodeInstall(payload);
} catch (error) {
console.error("Error during direct install: ", error);
logError(error as Error, "registry-install", { target: "vscode" });
return false;
}
});

messenger.onRequest(installClaudeFromConfigType, async (payload: ClaudeInstallRequest) => {
logWebviewInstallAttempt(payload.name);
const { values, canceled } = await collectInstallInputs(payload.inputs);
if (canceled) {
return { success: false, cliAvailable: true, canceled: true } satisfies ClaudeInstallResponse;
}

const substitutedPayload = applyInputsToPayload(payload, values);

try {
const config = buildClaudeConfigObject(substitutedPayload, payload.transport);
const result = await runClaudeCliTask(payload.name, payload.transport, config);
if (result.success) {
void vscode.window.showInformationMessage(
`Claude CLI added ${payload.name}.`,
);
} else if (result.cliAvailable && result.errorMessage) {
logError(new Error(result.errorMessage), "claude-cli-install", {
transport: payload.transport,
mode: payload.mode,
});
}
return result;
} catch (error) {
logError(error as Error, "claude-cli-install", {
transport: payload.transport,
mode: payload.mode,
});
return {
success: false,
cliAvailable: true,
errorMessage: error instanceof Error ? error.message : String(error),
} satisfies ClaudeInstallResponse;
}
});

// Official Registry search proxied via extension (avoids webview CORS)
messenger.onRequest(registrySearchType, async (payload) => {
Expand All @@ -202,7 +462,9 @@ export class CopilotMcpViewProvider implements vscode.WebviewViewProvider {
limit: payload.limit ?? 10,
search: payload.search,
};
if (payload.cursor) params.cursor = payload.cursor;
if (payload.cursor) {
params.cursor = payload.cursor;
}
const res = await axios.get('https://registry.modelcontextprotocol.io/v0/servers', { params });
const data = res?.data ?? {};
return {
Expand Down
Loading