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
26 changes: 23 additions & 3 deletions Gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ export function clean(done) {

pack.description = "Create .vsix file for the extension. Make sure to pre-build assets.";
export function pack(done) {
// install runtime dependencies (e.g., the bundled MCP server) in the output directory so they're
// included in the .vsix; the Rollup bundle itself doesn't need these (they're marked external)
const npmResult = spawnSync("npm", ["install", "--production"], {
stdio: "inherit",
cwd: DESTINATION,
shell: IS_WINDOWS,
});
if (npmResult.error) throw npmResult.error;

var vsceCommandArgs = ["vsce", "package"];
// Check if TARGET is set, if so, add it to the command
if (process.env.TARGET) {
Expand Down Expand Up @@ -141,7 +150,13 @@ export function build(done) {
}),
],
onLog: handleBuildLog,
external: ["vscode", "electron"],
external: [
"vscode",
"electron",
// the MCP server is spawned as a separate stdio child process and must not be bundled into
// the extension; require.resolve finds it in node_modules at runtime
/^@confluentinc\/mcp-confluent/,
],
Comment on lines +153 to +159
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

The new MCP externals rely on @confluentinc/mcp-confluent being present in the packaged extension at runtime (so require.resolve() can find it). The current build/pack flow packages from out/ and does not include a node_modules tree there, so the MCP server module is likely missing in the resulting .vsix and MCP provider calls will fail at runtime. Please ensure the server package (and its transitive deps/native addons) are copied/installed into out/node_modules before vsce package, or adjust the packaging step so this dependency is included in the .vsix.

Copilot uses AI. Check for mistakes.
context: "globalThis",
};
/** @type {import("rollup").OutputOptions} */
Expand Down Expand Up @@ -446,10 +461,15 @@ function pkgjson() {
pkg.version += process.env.CI ? "" : `+${Math.random().toString(16).slice(2, 8)}`;
// no package.type: the bundle is CommonJS module
delete pkg.type;
// no dev only manifests: scripts, dependencies
// no dev-only manifests
delete pkg.scripts;
delete pkg.dependencies;
delete pkg.devDependencies;
// preserve only runtime dependencies that must ship outside the Rollup bundle
// (spawned as separate child processes, not imported into the extension host)
const runtimeDeps = ["@confluentinc/mcp-confluent"];
pkg.dependencies = Object.fromEntries(
Object.entries(pkg.dependencies || {}).filter(([k]) => runtimeDeps.includes(k)),
);
// the target folder is flat so the entry point is known to be in the root
pkg.main = "extension.js";
return JSON.stringify(pkg, null, 2);
Expand Down
2,483 changes: 1,977 additions & 506 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"icon": "resources/confluent_logo-mark-meadow.png",
"engines": {
"vscode": "^1.96.2"
"vscode": "^1.101.0"
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

engines.vscode was bumped to ^1.101.0 to support MCP APIs, but the PR also updates @types/vscode to a newer major minor. Please confirm the MCP API surface you’re using is actually available starting in 1.101; otherwise users on 1.101 could hit runtime undefined even though the extension declares compatibility.

Suggested change
"vscode": "^1.101.0"
"vscode": "^1.102.0"

Copilot uses AI. Check for mistakes.
},
"categories": [
"Programming Languages",
Expand Down Expand Up @@ -51,6 +51,16 @@
],
"main": "./out/extension.js",
"contributes": {
"mcpServerDefinitionProviders": [
{
"id": "confluent-mcp-direct",
"label": "Confluent MCP Server (Direct)"
},
{
"id": "confluent-mcp-local",
"label": "Confluent MCP Server (Local)"
}
],
"authentication": [
{
"id": "confluent-cloud-auth-provider",
Expand Down Expand Up @@ -2671,7 +2681,7 @@
"@types/sanitize-html": "^2.13.0",
"@types/sinon": "^17.0.3",
"@types/tail": "^2.2.3",
"@types/vscode": "^1.96.0",
"@types/vscode": "^1.101.0",
"@types/ws": "^8.5.13",
"@types/yauzl": "^2.10.3",
"@types/yazl": "^3.3.0",
Expand Down Expand Up @@ -2704,6 +2714,7 @@
"yazl": "^2.5.1"
},
"dependencies": {
"@confluentinc/mcp-confluent": "^1.2.1",
"@segment/analytics-node": "^2.1.2",
"@sentry/node": "^9.3.0",
"@sentry/profiling-node": "^9.3.0",
Expand Down
3 changes: 3 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { setFlinkWorkspaceListener } from "./flinkSql/flinkWorkspace";
import { IconNames } from "./icons";
import { constructResourceLoaderSingletons } from "./loaders";
import { cleanupOldLogFiles, EXTENSION_OUTPUT_CHANNEL, Logger } from "./logging";
import { registerMcpServerProviders } from "./mcp/serverProvider";
import { FlinkStatementResultsPanelProvider } from "./panelProviders/flinkStatementResults";
import { getSidecar, getSidecarManager } from "./sidecar";
import { createLocalConnection, getLocalConnection } from "./sidecar/connections/local";
Expand Down Expand Up @@ -346,6 +347,8 @@ async function _activateExtension(
chatParticipant.iconPath = new vscode.ThemeIcon(IconNames.CONFLUENT_LOGO);
context.subscriptions.push(chatParticipant, feedbackListener, ...registerChatTools());

context.subscriptions.push(registerMcpServerProviders());

// track the status bar for CCloud notices (fetched from the Statuspage Status API)
enableCCloudStatusPolling();
context.subscriptions.push(getCCloudStatusBarItem());
Expand Down
157 changes: 157 additions & 0 deletions src/mcp/baseMcpServerProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import type {
CancellationToken,
McpServerDefinitionProvider,
McpStdioServerDefinition as McpStdioServerDefinitionType,
} from "vscode";
import { EventEmitter, McpStdioServerDefinition } from "vscode";
import { Logger } from "../logging";
import { DisposableCollection } from "../utils/disposables";

const logger = new Logger("mcp.baseMcpServerProvider");

/**
* Env var key used to stash the connection ID inside the {@link McpStdioServerDefinition} so that
* {@linkcode BaseMcpServerProvider.resolveMcpServerDefinition} can match by ID rather than label.
* The MCP server ignores unrecognized env vars, so this is safe to include.
*/
const CONNECTION_ID_ENV_KEY = "__CONFLUENT_CONNECTION_ID";

/** A label + env var map pair that the base class converts into an {@link McpStdioServerDefinition}. */
export interface McpConnectionEnvMap {
/** Stable identifier used to match definitions during resolve (e.g., a connection ID). */
id: string;
/** Human-readable label shown in the VS Code tool picker. */
label: string;
env: Record<string, string>;
}

/**
* Abstract base for connection-type-specific MCP server providers. Subclasses supply connection
* data via {@linkcode loadConnectionEnvMaps}; this class handles building
* {@link McpStdioServerDefinition} instances, version tracking, and the change-event plumbing.
*/
export abstract class BaseMcpServerProvider
extends DisposableCollection
implements McpServerDefinitionProvider<McpStdioServerDefinitionType>
{
protected readonly changeEmitter = new EventEmitter<void>();
readonly onDidChangeMcpServerDefinitions = this.changeEmitter.event;

constructor() {
super();
this.disposables.push(this.changeEmitter);
}

/**
* Return one {@link McpStdioServerDefinition} per connection that has usable credentials.
* Called eagerly by VS Code - must not prompt for user interaction.
*/
async provideMcpServerDefinitions(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: CancellationToken,
): Promise<McpStdioServerDefinitionType[]> {
const envMaps = await this.loadConnectionEnvMaps();
const version = getMcpServerVersion();
const definitions: McpStdioServerDefinitionType[] = [];

for (const { id, label, env } of envMaps) {
if (Object.keys(env).length === 0) continue;
const def = buildServerDefinition(id, label, env, version);
if (!def) continue;
definitions.push(def);
logger.info(`MCP server definition created: "${label}" (${id})`);
}

logger.info(`Providing ${definitions.length} MCP server definition(s)`);
return definitions;
}

/**
* Called just before VS Code starts a server. Re-reads connection data in case it changed since
* {@linkcode provideMcpServerDefinitions} was last called.
*/
async resolveMcpServerDefinition(
server: McpStdioServerDefinitionType,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: CancellationToken,
): Promise<McpStdioServerDefinitionType | undefined> {
const envMaps = await this.loadConnectionEnvMaps();
const version = getMcpServerVersion();

const serverId = server.env[CONNECTION_ID_ENV_KEY];
for (const { id, label, env } of envMaps) {
if (id === serverId && Object.keys(env).length > 0) {
logger.info(`Resolved MCP server definition for "${label}" (${id})`);
return buildServerDefinition(id, label, env, version);
}
}

logger.warn(`Could not resolve MCP server definition for "${server.label}"`);
return undefined;
}

/**
* Load the connection-specific label + env var mappings. Each entry produces one
* {@link McpStdioServerDefinition}. Empty env maps are filtered out by the base class.
*/
protected abstract loadConnectionEnvMaps(): Promise<McpConnectionEnvMap[]>;
}

/**
* Resolve the absolute filesystem path to the bundled MCP server entry point.
*
* We use `require.resolve` instead of building a path from `extensionUri` or `extensionPath`
* because this project's launch.json sets `extensionDevelopmentPath` to `${workspaceFolder}/out`,
* which means `extensionPath` points to the compiled output directory rather than the project root
* where `node_modules` lives. `require.resolve` walks up the directory tree to find `node_modules`
* using Node's standard module resolution, so it works from both the `out/` bundle (dev) and a
* packaged extension (production).
*
* Note: `import.meta.resolve` would be the preferred ESM approach, but the esbuild bundler strips
* `import.meta.resolve` in the bundled output, making it `undefined` at runtime.
*/
function getMcpServerEntryPath(): string | undefined {
try {
return require.resolve("@confluentinc/mcp-confluent/dist/index.js");
} catch {
logger.warn("@confluentinc/mcp-confluent not found; MCP tools will be unavailable");
return undefined;
}
}

/** Read the bundled MCP server version from its package.json. */
function getMcpServerVersion(): string {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require("@confluentinc/mcp-confluent/package.json").version;
} catch {
return "unknown";
}
}

/**
* Build a single {@link McpStdioServerDefinition} from a label, env map, and version.
*
* Uses `"node"` (system Node.js) instead of `process.execPath` (Electron's Node) because the MCP
* server depends on `@confluentinc/kafka-javascript`, which has native addons compiled against the
* system Node ABI. Electron ships a different Node version with an incompatible
* `NODE_MODULE_VERSION`, so loading those native modules from `process.execPath` fails with
* `ERR_DLOPEN_FAILED`.
*/
function buildServerDefinition(
id: string,
label: string,
env: Record<string, string>,
version: string,
): McpStdioServerDefinitionType | undefined {
const serverScript = getMcpServerEntryPath();
if (!serverScript) return undefined;

return new McpStdioServerDefinition(
label,
"node",
[serverScript],
{ ...env, [CONNECTION_ID_ENV_KEY]: id },
version,
);
}
Loading