-
Notifications
You must be signed in to change notification settings - Fork 18
feat: add MCP providers for mcp-confluent bundling
#3359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8c9acf5
c47b1eb
9ce5f2d
889bdfb
4b27efe
3bac6cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -20,7 +20,7 @@ | |||||
| }, | ||||||
| "icon": "resources/confluent_logo-mark-meadow.png", | ||||||
| "engines": { | ||||||
| "vscode": "^1.96.2" | ||||||
| "vscode": "^1.101.0" | ||||||
|
||||||
| "vscode": "^1.101.0" | |
| "vscode": "^1.102.0" |
| 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, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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-confluentbeing present in the packaged extension at runtime (sorequire.resolve()can find it). The current build/pack flow packages fromout/and does not include anode_modulestree 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 intoout/node_modulesbeforevsce package, or adjust the packaging step so this dependency is included in the .vsix.