Skip to content
Merged
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ The right sidebar can show:

- MCP server status
- MCP tool lists
- Add and manage workspace-scoped MCP server profiles
- Register local stdio servers with commands, arguments, working directories, environment variables, and timeouts
- Register remote Streamable HTTP servers with URLs, headers, and timeouts
- Connect, disconnect, and remove extension-managed MCP profiles
- LSP server status
- Slash-command skills
- Available OpenCode agents
Expand All @@ -160,6 +164,19 @@ OpenCode UI works with OpenCode plugin-driven workflows, including community plu

Plugin-provided agents, skills, and capabilities can be surfaced directly inside the extension UI.

### Managing MCP Servers

Open the **MCP Servers** section in the Integrations panel and select **Add server**. Profiles are scoped to the current file-based workspace and are re-registered with OpenCode when the managed OpenCode server reconnects.

Supported connection types:

- **STDIO**: OpenCode starts a local command using a structured executable and argument list.
- **Streamable HTTP**: OpenCode connects to an HTTPS MCP endpoint with optional headers.

Managed profiles can be connected, disconnected, and removed from the panel. Removing a profile deletes this extension's saved profile and secrets and disconnects the current runtime entry. The pinned SDK does not provide dynamic MCP deletion, so the current OpenCode process may continue to list the entry until it restarts.

The extension stores non-sensitive profile metadata in workspace state. Environment values, HTTP header values, and OAuth client secrets are stored in VS Code `SecretStorage`; they are not included in webview state, status messages, logs, or project files. OAuth authentication controls remain gated until the pinned SDK's authentication callback contract is verified.

---

## Screenshots
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencode-vscode-chryzxc",
"displayName": "OpenCode — VS Code Client",
"description": "OpenCode client for VS Code with streaming chat, implementation plans, subagents, diff review, sessions, and quota monitoring.",
"description": "OpenCode client for VS Code with streaming chat, implementation plans, subagents, diff review, sessions, quota monitoring, and managed MCP servers.",
"license": "MIT",
"repository": {
"type": "git",
Expand All @@ -11,9 +11,9 @@
"bugs": {
"url": "https://github.com/chryzxc/vscode-opencode/issues"
},
"version": "0.3.6",
"version": "0.3.7",
"opencodeCompatibility": {
"extensionVersion": "0.3.6",
"extensionVersion": "0.3.7",
"sdk": {
"package": "@opencode-ai/sdk",
"supportedRange": ">=1.18.0 <1.19.0",
Expand Down Expand Up @@ -66,6 +66,9 @@
"subagents",
"multi agent",
"mcp",
"mcp server management",
"streamable http",
"stdio",
"model context protocol",
"session management",
"quota monitoring",
Expand Down
82 changes: 77 additions & 5 deletions src/providers/ChatViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
import * as path from "path";
import * as vscode from "vscode";
import { ErrorBuilder } from "./chat/ErrorBuilder";
import type { DisplayError } from "./chat/types";

Check warning on line 103 in src/providers/ChatViewProvider.ts

View workflow job for this annotation

GitHub Actions / verify

'DisplayError' is defined but never used
import {
CssGenerator,
FileThemeProcessor,
Expand All @@ -110,6 +110,7 @@
import type { TokenUsage } from "../services/GeminiTokenUsageTracker";
import { GeminiTokenUsageTracker } from "../services/GeminiTokenUsageTracker";
import { MessageStreamService } from "../services/MessageStreamService";
import { McpServerService, type ManagedMcpDraft, type McpClient } from "../services/McpServerService";
import { ModelCapabilitiesService } from "../services/ModelCapabilitiesService";
import { OpencodeServerManager } from "../services/OpencodeServerManager";
import { QuotaService } from "../services/QuotaService";
Expand Down Expand Up @@ -712,7 +713,7 @@
let permissions: unknown[] = [];

try {
const questionResponse = await (client as any).question.list();

Check warning on line 716 in src/providers/ChatViewProvider.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
questions = extractList(questionResponse, "questions").filter(belongsToSession);
} catch (questionError) {
this.logger.warn("Failed to list pending SDK questions", {
Expand All @@ -722,7 +723,7 @@
}

try {
const permissionResponse = await (client as any).permission.list();

Check warning on line 726 in src/providers/ChatViewProvider.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
permissions = extractList(permissionResponse, "permissions").filter(belongsToSession);
} catch (permissionError) {
this.logger.warn("Failed to list pending SDK permissions", {
Expand Down Expand Up @@ -856,8 +857,8 @@
private lastSendMessageArgs?: {
text: string;
files?: string[];
contexts?: any[];

Check warning on line 860 in src/providers/ChatViewProvider.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
images?: any[];

Check warning on line 861 in src/providers/ChatViewProvider.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
agent?: string;
};
/** OpenCode v2 accepts structured output only through the typed `format` field. */
Expand Down Expand Up @@ -885,6 +886,7 @@
private readonly recentUiErrorToastTimestamps = new Map<string, number>();
private readonly UI_ERROR_TOAST_DEDUPE_WINDOW_MS = 15_000;
private readonly installedSdkVersion = detectInstalledOpencodeSdkVersion();
private readonly mcpServerService: McpServerService;

/** ===== NEW: Module instances ===== */
private diagnosticsLogger!: DiagnosticsLogger;
Expand Down Expand Up @@ -926,6 +928,12 @@
this.streamService = new MessageStreamService(serverManager);
this.quotaService = new QuotaService();
this.sessionSnapshotLoader = new SessionSnapshotLoader(serverManager);
this.mcpServerService = new McpServerService(
context,
() => this.getWorkspaceDirectory(),
() => this.serverManager.ensureRunning() as Promise<McpClient>,
{ info: (message, data) => this.logger.info(message, data), warn: (message, data) => this.logger.warn(message, data) },
);
this.subagentTracker = new SubagentTracker(() => this.selectedModel);
this.configFilesProvider = new ConfigFilesProvider();
this.skillManager = new SkillManagerService(context);
Expand All @@ -943,7 +951,7 @@
this.fileThemeProcessor.subscribe(this);

// Load persisted model selection
const savedModel = this.context.globalState.get<any>("selectedModel");

Check warning on line 954 in src/providers/ChatViewProvider.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
if (
savedModel &&
typeof savedModel.providerID === "string" &&
Expand Down Expand Up @@ -1081,7 +1089,7 @@
* Wire callbacks between modules and the shell
*/
private wireModuleCallbacks(): void {
const postMessage = (msg: any) => {

Check warning on line 1092 in src/providers/ChatViewProvider.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
this.view?.webview.postMessage(msg);
};

Expand Down Expand Up @@ -1130,7 +1138,7 @@
clientRequestId?: string;
text?: string;
files?: string[];
contexts?: any[];

Check warning on line 1141 in src/providers/ChatViewProvider.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
images?: any[];
agent?: string;
userFacingText?: string;
Expand Down Expand Up @@ -4387,6 +4395,54 @@
);
break;
}
case "addMcpServer":
case "connectMcpServer":
case "disconnectMcpServer":
case "removeMcpServer": {
const requestID = typeof message.requestID === "string" ? message.requestID : undefined;
const operation = message.type === "addMcpServer"
? "adding"
: message.type === "connectMcpServer"
? "connecting"
: message.type === "disconnectMcpServer"
? "disconnecting"
: "removing";
const serverName = typeof message.name === "string" ? message.name.trim() : "";
const profileID = typeof message.profileId === "string" ? message.profileId : undefined;
if (message.type === "removeMcpServer") {
const confirmation = await vscode.window.showWarningMessage(
"This disconnects the current server and removes this extension's saved profile. The current OpenCode process may still list it until it restarts.",
{ modal: true },
"Remove",
);
if (confirmation !== "Remove") break;
}
this.view?.webview.postMessage({ type: "mcpOperationStarted", requestID, operation, serverName, profileID });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm whether any webview code consumes mcpOperationStarted or McpServerInfo.operation.
set -euo pipefail

rg -n -C3 'mcpOperationStarted' --glob '!**/dist/**'
rg -n -C3 '\boperation\b' webview/shared/src/chat/lib/types.ts webview/shared/src/chat/PanelComponents.tsx --glob '!**/dist/**'

Repository: chryzxc/vscode-opencode

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files matching ChatViewProvider/messageHandler/types =="
git ls-files | rg '(^|/)ChatViewProvider\.ts$|messageHandler\.ts$|types\.ts$' | head -200

echo "== locate provider/webview files =="
fd 'ChatViewProvider\.ts|messageHandler\.ts|types\.ts|PanelComponents\.tsx' . | sed -n '1,200p'

echo "== broad mcpOperationStarted search excluding dist if possible =="
rg -n -C3 'mcpOperationStarted|mcpOperationResult|McpServerInfo' . || true

Repository: chryzxc/vscode-opencode

Length of output: 8910


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ChatViewProvider MCP postMessage block =="
sed -n '4380,4445p' src/providers/ChatViewProvider.ts | nl -ba -v 4380

echo "== messageHandler mcpOperationResult block =="
sed -n '15690,15800p' webview/shared/src/chat/lib/messageHandler.ts | nl -ba -v 15690

echo "== McpServerInfo type =="
sed -n '880,905p' webview/shared/src/chat/lib/types.ts | nl -ba -v 880

echo "== all non-backup references to mcpOperationStarted/operation in focused files =="
rg -n -C2 'mcpOperationStarted|case "mcpOperationStarted"|operation:' src/providers/ChatViewProvider.ts webview/shared/src/chat/lib/messageHandler.ts webview/shared/src/chat/lib/types.ts || true

echo "== SET_MCP_SERVERS implementation =="
rg -n -C3 'SET_MCP_SERVERS' webview/shared/src/chat/lib/store.ts webview/shared/src/chat/lib/messageHandler.ts src/providers/ChatViewProvider.ts || true

Repository: chryzxc/vscode-opencode

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' "== ChatViewProvider MCP postMessage block =="
sed -n '4380,4445p' src/providers/ChatViewProvider.ts

printf '\n%s\n' "== messageHandler mcpOperationResult block =="
sed -n '15690,15800p' webview/shared/src/chat/lib/messageHandler.ts

printf '\n%s\n' "== McpServerInfo type =="
sed -n '880,905p' webview/shared/src/chat/lib/types.ts

printf '\n%s\n' "== all non-backup references to mcpOperationStarted/operation in focused files =="
rg -n -C2 'mcpOperationStarted|case "mcpOperationStarted"|operation:' src/providers/ChatViewProvider.ts webview/shared/src/chat/lib/messageHandler.ts webview/shared/src/chat/lib/types.ts || true

printf '\n%s\n' "== SET_MCP_SERVERS implementation =="
rg -n -C3 'SET_MCP_SERVERS' webview/shared/src/chat/lib/store.ts webview/shared/src/chat/lib/messageHandler.ts src/providers/ChatViewProvider.ts || true

Repository: chryzxc/vscode-opencode

Length of output: 11741


Add a consumer for mcpOperationStarted or remove it.

this.view?.webview.postMessage({ type: "mcpOperationStarted", ... }) posts start state, but the webview only handles mcpOperationResult and McpServerInfo.operation is never set, so MCP add/connect/disconnect/remove operations never show an in-progress state. Add the missing handler/reducer payload path, or drop this message and operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/ChatViewProvider.ts` at line 4420, Update the MCP operation
flow around the mcpOperationStarted postMessage in ChatViewProvider so the
message has a corresponding webview consumer that updates
McpServerInfo.operation and displays add/connect/disconnect/remove operations as
in progress; otherwise remove the unused mcpOperationStarted message and
operation payload. Keep the existing mcpOperationResult handling consistent with
whichever approach is chosen.

try {
if (message.type === "addMcpServer") {
await this.mcpServerService.add(message.draft as ManagedMcpDraft);
} else if (message.type === "connectMcpServer") {
await this.mcpServerService.connect(serverName);
} else if (message.type === "disconnectMcpServer") {
await this.mcpServerService.disconnect(serverName);
} else {
await this.mcpServerService.remove(serverName, profileID);
void vscode.window.showInformationMessage("MCP profile removed. The current OpenCode process may list it until it restarts.");
}
this.view?.webview.postMessage({ type: "mcpOperationResult", requestID, operation, success: true });
} catch (error) {
void vscode.window.showErrorMessage("OpenCode could not complete the MCP operation. Check the MCP status for details.");
this.view?.webview.postMessage({
type: "mcpOperationResult",
requestID,
operation,
success: false,
error: "MCP operation failed. Refresh the MCP status and check the server details.",
});
}
Comment on lines +4421 to +4442

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not discard the operation error.

The catch block replaces every failure with one generic string and logs nothing. McpServerService throws precise, user-safe validation messages, for example An MCP server named "x" is already managed. and Remote MCP URL must use HTTPS. The user sees none of them, and no host log entry records the cause. Log the error and forward the service message.

🐛 Proposed fix
           } catch (error) {
-            void vscode.window.showErrorMessage("OpenCode could not complete the MCP operation. Check the MCP status for details.");
+            const reason = error instanceof Error ? error.message : "MCP operation failed.";
+            this.logger.error("MCP management operation failed", { operation, serverName, profileID }, error instanceof Error ? error : undefined);
+            void vscode.window.showErrorMessage(`OpenCode could not complete the MCP operation: ${reason}`);
             this.view?.webview.postMessage({
               type: "mcpOperationResult",
               requestID,
               operation,
               success: false,
-              error: "MCP operation failed. Refresh the MCP status and check the server details.",
+              error: reason,
             });
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
if (message.type === "addMcpServer") {
await this.mcpServerService.add(message.draft as ManagedMcpDraft);
} else if (message.type === "connectMcpServer") {
await this.mcpServerService.connect(serverName);
} else if (message.type === "disconnectMcpServer") {
await this.mcpServerService.disconnect(serverName);
} else {
await this.mcpServerService.remove(serverName, profileID);
void vscode.window.showInformationMessage("MCP profile removed. The current OpenCode process may list it until it restarts.");
}
this.view?.webview.postMessage({ type: "mcpOperationResult", requestID, operation, success: true });
} catch (error) {
void vscode.window.showErrorMessage("OpenCode could not complete the MCP operation. Check the MCP status for details.");
this.view?.webview.postMessage({
type: "mcpOperationResult",
requestID,
operation,
success: false,
error: "MCP operation failed. Refresh the MCP status and check the server details.",
});
}
try {
if (message.type === "addMcpServer") {
await this.mcpServerService.add(message.draft as ManagedMcpDraft);
} else if (message.type === "connectMcpServer") {
await this.mcpServerService.connect(serverName);
} else if (message.type === "disconnectMcpServer") {
await this.mcpServerService.disconnect(serverName);
} else {
await this.mcpServerService.remove(serverName, profileID);
void vscode.window.showInformationMessage("MCP profile removed. The current OpenCode process may list it until it restarts.");
}
this.view?.webview.postMessage({ type: "mcpOperationResult", requestID, operation, success: true });
} catch (error) {
const reason = error instanceof Error ? error.message : "MCP operation failed.";
this.logger.error("MCP management operation failed", { operation, serverName, profileID }, error instanceof Error ? error : undefined);
void vscode.window.showErrorMessage(`OpenCode could not complete the MCP operation: ${reason}`);
this.view?.webview.postMessage({
type: "mcpOperationResult",
requestID,
operation,
success: false,
error: reason,
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/ChatViewProvider.ts` around lines 4421 - 4442, Update the catch
block in the MCP operation handler to log the caught error through the existing
host logging mechanism and forward its user-safe message in the failed
mcpOperationResult response. Preserve the generic fallback only when the caught
value does not provide a usable message, while retaining the existing error
notification.

await this.handleGetMcpStatus();
break;
}
case "getLspStatus": {
this.handleGetLspStatus().catch((err) =>
log.error("Failed to handle LSP status request", {
Expand Down Expand Up @@ -5060,6 +5116,7 @@
this.postErrorToast(serverError);
}
this.broadcastCompatibilityWarnings();
if (status === "running") void this.handleGetMcpStatus();
});
const serverErrorOutputSubscription = this.serverManager.onServerErrorOutput(
(snippet) => {
Expand Down Expand Up @@ -10026,12 +10083,10 @@
const client = await this.serverManager.ensureRunning();

log.featureStep(flow, 'fetching_mcp_and_tool_data');
const [mcpRes, toolIdsRes] = await Promise.all([
client.mcp.status(),
const [servers, toolIdsRes] = await Promise.all([
this.mcpServerService.status(client as McpClient),
client.tool.ids().catch(() => ({ data: [] })),
]);

const servers = mcpRes.data ?? {};
const toolIds: string[] = Array.isArray(toolIdsRes?.data)
? toolIdsRes.data
: [];
Expand All @@ -10041,9 +10096,26 @@
toolCount: toolIds.length,
});

const managedProfiles = new Map(this.mcpServerService.profiles().map((profile) => [profile.name, profile]));
const enrichedServers = Object.fromEntries(Object.entries(servers).filter(([name]) => !this.mcpServerService.wasRemoved(name)).map(([name, value]) => {
const profile = managedProfiles.get(name);
return [name, profile ? { ...(value as Record<string, unknown>), managed: true, profileId: profile.id, kind: profile.kind } : value];
}));
for (const profile of managedProfiles.values()) {
if (!Object.prototype.hasOwnProperty.call(enrichedServers, profile.name)) {
enrichedServers[profile.name] = {
status: "disconnected",
error: "The managed profile is saved but is not currently registered with OpenCode.",
managed: true,
profileId: profile.id,
kind: profile.kind,
};
}
}

this.view?.webview.postMessage({
type: "mcpStatus",
servers,
servers: enrichedServers,
toolIds,
});

Expand Down
Loading
Loading