This tutorial explains how to integrate a Model Context Protocol (MCP) server into your VSCode extension, based on the implementation in Nx Console. The tutorial covers both VSCode and Cursor editor support.
- Overview
- Prerequisites
- Architecture
- Step-by-Step Implementation
- User Prompting and Configuration
- Testing
- Best Practices
The MCP (Model Context Protocol) allows VSCode extensions to provide context and tools to AI assistants. This tutorial shows you how to:
- Set up an HTTP-based MCP server within your extension
- Register the server with VSCode's MCP API
- Support both VSCode and Cursor editors
- Prompt users to enable the MCP server
- Handle dynamic configuration and lifecycle management
Before starting, ensure you have:
- VSCode Extension: A working VSCode extension project
- VSCode Version: VSCode 1.101.0 or later (for native MCP support)
- Dependencies:
@modelcontextprotocol/sdk(^1.20.2 or later)express(for HTTP server)- TypeScript configured
Install the required dependencies:
npm install @modelcontextprotocol/sdk express
npm install --save-dev @types/expressThe Nx Console MCP implementation uses a layered architecture:
┌─────────────────────────────────────────┐
│ Extension Activation (main.ts) │
│ - Calls initMcp(context) │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ MCP Initialization (init-mcp.ts) │
│ - Detects editor (VSCode/Cursor) │
│ - Finds available port │
│ - Creates appropriate server wrapper │
└──────────────┬──────────────────────────┘
│
┌──────┴──────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ VSCode │ │ Cursor │
│ Wrapper │ │ Wrapper │
└──────┬───────┘ └──────┬───────┘
│ │
└────────┬────────┘
▼
┌────────────────────┐
│ Core HTTP Server │
│ (Platform Agnostic)│
└────────────────────┘
- Core HTTP Server (
mcp-http-server-core.ts): Platform-agnostic Express server that handles MCP requests - VSCode Wrapper (
mcp-vscode-server.ts): VSCode-specific implementation using VSCode types - Cursor Wrapper (
mcp-cursor-server.ts): Cursor-specific implementation without VSCode type dependencies - Initialization (
init-mcp.ts): Detects environment and sets up appropriate server - Data Providers (
data-providers.ts): Provides workspace information and IDE integration
Create a platform-agnostic HTTP server that handles MCP protocol requests:
// lib/mcp-http-server-core.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express, { Request, Response } from 'express';
export class McpHttpServerCore {
private app: express.Application = express();
private appInstance?: ReturnType<express.Application['listen']>;
private mcpServers = new Set<McpServer>();
constructor(private mcpPort: number) {
this.startStreamableWebServer(mcpPort);
}
private startStreamableWebServer(port: number) {
// Handle POST requests for MCP protocol
this.app.post('/mcp', async (req: Request, res: Response) => {
console.log('Connecting to MCP via streamable http');
try {
// Create MCP server instance
const mcpServer = new McpServer(
{
name: 'Your MCP Server',
version: '1.0.0',
},
{
capabilities: {
tools: {
listChanged: true,
},
},
}
);
// Register your tools and resources here
// mcpServer.setRequestHandler(...)
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
this.mcpServers.add(mcpServer);
// Clean up on connection close
res.on('close', () => {
console.log('Request closed');
transport.close();
mcpServer.close();
this.mcpServers.delete(mcpServer);
});
await mcpServer.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
// Start the Express server
try {
this.appInstance = this.app.listen(port, () => {
console.log(`MCP server started on port ${port}`);
});
this.appInstance.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EADDRINUSE') {
console.error(`Port ${port} is already in use`);
} else {
console.error(`Failed to start MCP server: ${error.message}`);
}
});
} catch (error) {
console.error(`Failed to start MCP server: ${error}`);
}
}
public getUrl(): string {
return `http://localhost:${this.mcpPort}/mcp`;
}
public getPort(): number {
return this.mcpPort;
}
public stopMcpServer() {
console.log('Stopping MCP server');
this.mcpServers.forEach((server) => server.close());
this.mcpServers.clear();
if (this.appInstance) {
this.appInstance.close();
}
}
}Create a wrapper that uses VSCode types for the MCP server definition:
// lib/mcp-vscode-server.ts
import {
CancellationToken,
McpHttpServerDefinition,
McpServerDefinitionProvider,
Uri,
} from 'vscode';
import { McpHttpServerCore } from './mcp-http-server-core';
/**
* VSCode-specific MCP server definition provider
*/
export class YourMcpServerDefinitionProvider
implements McpServerDefinitionProvider<YourMcpHttpServerDefinition>
{
constructor(private server: McpStreamableWebServer | undefined) {}
async provideMcpServerDefinitions(
token: CancellationToken,
): Promise<YourMcpHttpServerDefinition[] | undefined> {
if (this.server === undefined) {
return undefined;
}
return [
new YourMcpHttpServerDefinition('Your MCP Server', this.server.getUri()),
];
}
}
/**
* VSCode-specific MCP HTTP server definition
*/
export class YourMcpHttpServerDefinition extends McpHttpServerDefinition {
constructor(
label: string,
uri: Uri,
headers?: Record<string, string>,
version?: string,
) {
super(label, uri, headers, version);
}
}
/**
* VSCode-specific wrapper around the core HTTP server
*/
export class McpStreamableWebServer {
private core: McpHttpServerCore;
constructor(mcpPort: number) {
this.core = new McpHttpServerCore(mcpPort);
}
public getUri(): Uri {
return Uri.parse(this.core.getUrl());
}
public stopMcpServer() {
this.core.stopMcpServer();
}
}Create a wrapper for Cursor that doesn't use VSCode types:
// lib/mcp-cursor-server.ts
import { McpHttpServerCore } from './mcp-http-server-core';
/**
* Cursor-specific wrapper around the core HTTP server
* No VSCode type dependencies - uses plain strings for URLs
*/
export class McpCursorServer {
private core: McpHttpServerCore;
constructor(mcpPort: number) {
this.core = new McpHttpServerCore(mcpPort);
}
public getUrl(): string {
return this.core.getUrl();
}
public getPort(): number {
return this.core.getPort();
}
public stopMcpServer() {
this.core.stopMcpServer();
}
}Create utilities to find available ports:
// lib/ports.ts
import { createServer } from 'net';
export async function isPortAvailable(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = createServer();
server.once('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EADDRINUSE') {
resolve(false);
} else {
resolve(false);
}
});
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port);
});
}
export async function findAvailablePort(
startPort: number = 3000,
maxAttempts: number = 100
): Promise<number | undefined> {
for (let i = 0; i < maxAttempts; i++) {
const port = startPort + i;
if (await isPortAvailable(port)) {
return port;
}
}
return undefined;
}Create utilities to detect which editor is running:
// lib/editor-helpers.ts
import { env } from 'vscode';
export function isInCursor(): boolean {
return env.appName.toLowerCase().includes('cursor');
}
export function isInWindsurf(): boolean {
return env.appName.toLowerCase().includes('windsurf');
}
export function isInVSCode(): boolean {
return env.appName.toLowerCase().includes('visual studio code');
}Create the main initialization function that ties everything together:
// lib/init-mcp.ts
import { commands, ExtensionContext, lm, version, window } from 'vscode';
import { isInCursor, isInVSCode } from './editor-helpers';
import { findAvailablePort, isPortAvailable } from './ports';
let mcpStreamableWebServer: any | undefined;
let mcpCursorServer: any | undefined;
let initialized = false;
export function stopMcpServer() {
mcpStreamableWebServer?.stopMcpServer();
mcpCursorServer?.stopMcpServer();
}
export async function initMcp(context: ExtensionContext) {
// Set context for conditional UI elements
commands.executeCommand('setContext', 'isInCursor', isInCursor());
commands.executeCommand('setContext', 'isInVSCode', isInVSCode());
if (initialized) {
return;
}
initialized = true;
const inVSCode = isInVSCode();
const inCursor = isInCursor();
// Only support VSCode and Cursor
if (!inVSCode && !inCursor) {
console.log('MCP setup is only available for VSCode or Cursor.');
return;
}
// Check VSCode version
if (inVSCode && !versionGreaterThanOrEqual(version, '1.101.0')) {
window.showErrorMessage(
'Your extension requires VSCode 1.101.0 or later for MCP support. Please update VSCode.'
);
return;
}
// Find available port
const availablePort = await findAvailablePort();
if (!availablePort) {
console.log('Could not find an available port for MCP server');
return;
}
// Initialize appropriate server
if (inCursor) {
await initCursorMcp(context, availablePort);
} else if (inVSCode) {
await initModernVSCodeMcp(context, availablePort);
}
}
async function initModernVSCodeMcp(context: ExtensionContext, mcpPort: number) {
// Dynamic import for VSCode-specific classes
const { McpStreamableWebServer, YourMcpServerDefinitionProvider } =
await import('./mcp-vscode-server.js');
mcpStreamableWebServer = new McpStreamableWebServer(mcpPort);
context.subscriptions.push({
dispose: () => {
mcpStreamableWebServer?.stopMcpServer();
},
});
console.log(`Configured MCP server on port ${mcpPort}`);
// Register with VSCode's MCP API
context.subscriptions.push(
lm.registerMcpServerDefinitionProvider(
'your-mcp-id',
new YourMcpServerDefinitionProvider(mcpStreamableWebServer),
),
);
}
async function initCursorMcp(context: ExtensionContext, mcpPort: number) {
// Dynamic import for Cursor-specific server
const { McpCursorServer } = await import('./mcp-cursor-server.js');
// Check if Cursor's MCP API is available
if ('cursor' in vscode && 'mcp' in (vscode as any).cursor) {
mcpCursorServer = new McpCursorServer(mcpPort);
context.subscriptions.push({
dispose: () => {
mcpCursorServer?.stopMcpServer();
},
});
const cursorApi = (vscode as any).cursor;
// Register with Cursor's MCP API
cursorApi.mcp.registerServer({
name: 'your-mcp-id',
server: {
url: `http://localhost:${mcpPort}/mcp`,
headers: {
'Content-Type': 'application/json',
},
},
});
context.subscriptions.push({
dispose: () => {
cursorApi.mcp.unregisterServer('your-mcp-id');
},
});
console.log(`Registered MCP HTTP server with Cursor on port ${mcpPort}`);
} else {
console.log('Cursor MCP API not available');
return;
}
}
function versionGreaterThanOrEqual(current: string, required: string): boolean {
const currentParts = current.split('.').map(Number);
const requiredParts = required.split('.').map(Number);
for (let i = 0; i < Math.max(currentParts.length, requiredParts.length); i++) {
const currentPart = currentParts[i] || 0;
const requiredPart = requiredParts[i] || 0;
if (currentPart > requiredPart) return true;
if (currentPart < requiredPart) return false;
}
return true;
}Update your extension's main activation function:
// main.ts or extension.ts
import { ExtensionContext } from 'vscode';
import { initMcp, stopMcpServer } from './lib/init-mcp';
export async function activate(context: ExtensionContext) {
console.log('Extension activating...');
// Initialize MCP server
await initMcp(context);
// ... rest of your extension activation code
}
export async function deactivate() {
// Stop MCP server on deactivation
stopMcpServer();
// ... rest of your deactivation code
}Add the MCP server definition provider to your package.json:
{
"name": "your-extension",
"displayName": "Your Extension",
"version": "1.0.0",
"engines": {
"vscode": "^1.101.0"
},
"activationEvents": [
"onStartupFinished"
],
"contributes": {
"mcpServerDefinitionProviders": [
{
"id": "your-mcp-id",
"label": "Your MCP Server"
}
]
}
}You can prompt users to enable your MCP server when certain conditions are met:
// lib/mcp-prompts.ts
import { window, workspace } from 'vscode';
export async function promptUserToEnableMcp() {
// Check if user has already been prompted
const config = workspace.getConfiguration('yourExtension');
const dontAskAgain = config.get<boolean>('mcpDontAskAgain', false);
if (dontAskAgain) {
return;
}
const selection = await window.showInformationMessage(
'Would you like to enable the MCP server for enhanced AI assistance?',
'Enable',
'Learn More',
"Don't ask again"
);
if (selection === 'Enable') {
// Enable MCP server
await config.update('mcpEnabled', true, true);
window.showInformationMessage('MCP server enabled! Please reload the window.');
} else if (selection === 'Learn More') {
// Open documentation
vscode.env.openExternal(vscode.Uri.parse('https://your-docs-url.com'));
} else if (selection === "Don't ask again") {
await config.update('mcpDontAskAgain', true, true);
}
}Add configuration options to your package.json:
{
"contributes": {
"configuration": {
"title": "Your Extension",
"properties": {
"yourExtension.mcpEnabled": {
"type": "boolean",
"default": true,
"description": "Enable MCP server for AI assistance"
},
"yourExtension.mcpPort": {
"type": "number",
"default": null,
"description": "Fixed port for MCP server (leave empty for automatic)"
},
"yourExtension.mcpDontAskAgain": {
"type": "boolean",
"default": false,
"description": "Don't prompt to enable MCP server"
}
}
}
}
}Implement periodic checks to notify users about updates:
// lib/periodic-check.ts
import { window, workspace } from 'vscode';
let checkTimer: NodeJS.Timeout | undefined;
export function setupPeriodicCheck(context: ExtensionContext) {
// Run first check after 1 minute
checkTimer = setTimeout(() => {
runCheck();
// Then check every hour
setInterval(() => {
runCheck();
}, 60 * 60 * 1000);
}, 60 * 1000);
context.subscriptions.push({
dispose: () => {
if (checkTimer) {
clearTimeout(checkTimer);
}
},
});
}
async function runCheck() {
const config = workspace.getConfiguration('yourExtension');
const dontAskAgain = config.get<boolean>('updateCheckDontAskAgain', false);
if (dontAskAgain) {
return;
}
// Check for updates or configuration issues
const needsUpdate = await checkForUpdates();
if (needsUpdate) {
const selection = await window.showInformationMessage(
'Your MCP configuration needs updating. Would you like to update now?',
'Update',
"Don't ask again"
);
if (selection === 'Update') {
// Perform update
await performUpdate();
} else if (selection === "Don't ask again") {
await config.update('updateCheckDontAskAgain', true, true);
}
}
}
async function checkForUpdates(): Promise<boolean> {
// Implement your update check logic
return false;
}
async function performUpdate(): Promise<void> {
// Implement your update logic
}-
Test in VSCode:
- Open your extension in VSCode 1.101.0+
- Verify the MCP server starts on activation
- Check the Output panel for MCP server logs
- Test AI assistant integration
-
Test in Cursor:
- Open your extension in Cursor
- Verify the MCP server registers with Cursor's API
- Test AI assistant integration
-
Test Port Management:
- Start multiple instances of VSCode/Cursor
- Verify each instance gets a unique port
- Test fixed port configuration
Create unit tests for your MCP components:
// test/mcp.test.ts
import * as assert from 'assert';
import { isPortAvailable, findAvailablePort } from '../lib/ports';
suite('MCP Port Management', () => {
test('isPortAvailable returns true for available port', async () => {
const available = await isPortAvailable(9999);
assert.strictEqual(typeof available, 'boolean');
});
test('findAvailablePort returns a port number', async () => {
const port = await findAvailablePort(3000, 10);
assert.ok(port === undefined || (port >= 3000 && port < 3010));
});
});Always check the VSCode version before enabling MCP features:
if (inVSCode && !versionGreaterThanOrEqual(version, '1.101.0')) {
// Show error or disable MCP features
}Use dynamic imports to avoid loading VSCode-specific types in Cursor:
const { McpStreamableWebServer } = await import('./mcp-vscode-server.js');- Allow users to configure a fixed port
- Automatically find available ports as fallback
- Handle port conflicts gracefully
Always handle errors gracefully:
try {
await mcpServer.connect(transport);
} catch (error) {
console.error('Error connecting MCP server:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Internal server error' });
}
}Always clean up resources on deactivation:
export async function deactivate() {
stopMcpServer();
// Clean up other resources
}- Don't spam users with prompts
- Respect "Don't ask again" preferences
- Provide clear documentation
- Log important events for debugging
- Never expose sensitive data through MCP
- Validate all inputs from MCP requests
- Use HTTPS in production if possible
- Implement rate limiting if needed
- Use connection pooling for MCP servers
- Clean up closed connections promptly
- Monitor memory usage
- Implement timeouts for long-running operations
-
Port Already in Use:
- Check if another instance is running
- Use automatic port finding
- Allow users to configure a different port
-
VSCode Version Too Old:
- Check version before enabling MCP
- Show clear error message
- Provide upgrade instructions
-
MCP Server Not Responding:
- Check server logs
- Verify port is accessible
- Check firewall settings
-
Cursor API Not Available:
- Verify Cursor version
- Check if MCP API is enabled
- Provide fallback behavior
This tutorial covered the complete implementation of an MCP server in a VSCode extension, including:
- Core HTTP server implementation
- VSCode and Cursor-specific wrappers
- Port management and editor detection
- User prompting and configuration
- Testing and best practices
The architecture is designed to be:
- Platform-agnostic: Core logic works in both VSCode and Cursor
- Maintainable: Clear separation of concerns
- User-friendly: Graceful error handling and clear prompts
- Extensible: Easy to add new features and tools
For more information, refer to: