Skip to content

Instantly share code, notes, and snippets.

@BLamy
Created October 29, 2025 19:52
Show Gist options
  • Select an option

  • Save BLamy/6e3614f1270f8a9209b2bf3712387a6a to your computer and use it in GitHub Desktop.

Select an option

Save BLamy/6e3614f1270f8a9209b2bf3712387a6a to your computer and use it in GitHub Desktop.

How to Add an MCP Server to Your VSCode Extension

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.

Table of Contents

  1. Overview
  2. Prerequisites
  3. Architecture
  4. Step-by-Step Implementation
  5. User Prompting and Configuration
  6. Testing
  7. Best Practices

Overview

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

Prerequisites

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/express

Architecture

The 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)│
    └────────────────────┘

Key Components

  1. Core HTTP Server (mcp-http-server-core.ts): Platform-agnostic Express server that handles MCP requests
  2. VSCode Wrapper (mcp-vscode-server.ts): VSCode-specific implementation using VSCode types
  3. Cursor Wrapper (mcp-cursor-server.ts): Cursor-specific implementation without VSCode type dependencies
  4. Initialization (init-mcp.ts): Detects environment and sets up appropriate server
  5. Data Providers (data-providers.ts): Provides workspace information and IDE integration

Step-by-Step Implementation

Step 1: Create the Core HTTP Server

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();
    }
  }
}

Step 2: Create VSCode-Specific Wrapper

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();
  }
}

Step 3: Create Cursor-Specific Wrapper

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();
  }
}

Step 4: Create Port Management Utilities

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;
}

Step 5: Create Editor Detection Utilities

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');
}

Step 6: Create Main Initialization Logic

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;
}

Step 7: Update Extension Activation

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
}

Step 8: Update package.json

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"
      }
    ]
  }
}

User Prompting and Configuration

Prompting Users to Enable MCP

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);
  }
}

Configuration Settings

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"
        }
      }
    }
  }
}

Periodic Checks and Updates

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
}

Testing

Manual Testing

  1. 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
  2. Test in Cursor:

    • Open your extension in Cursor
    • Verify the MCP server registers with Cursor's API
    • Test AI assistant integration
  3. Test Port Management:

    • Start multiple instances of VSCode/Cursor
    • Verify each instance gets a unique port
    • Test fixed port configuration

Automated Testing

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));
  });
});

Best Practices

1. Version Checking

Always check the VSCode version before enabling MCP features:

if (inVSCode && !versionGreaterThanOrEqual(version, '1.101.0')) {
  // Show error or disable MCP features
}

2. Dynamic Imports

Use dynamic imports to avoid loading VSCode-specific types in Cursor:

const { McpStreamableWebServer } = await import('./mcp-vscode-server.js');

3. Port Management

  • Allow users to configure a fixed port
  • Automatically find available ports as fallback
  • Handle port conflicts gracefully

4. Error Handling

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' });
  }
}

5. Cleanup

Always clean up resources on deactivation:

export async function deactivate() {
  stopMcpServer();
  // Clean up other resources
}

6. User Experience

  • Don't spam users with prompts
  • Respect "Don't ask again" preferences
  • Provide clear documentation
  • Log important events for debugging

7. Security

  • Never expose sensitive data through MCP
  • Validate all inputs from MCP requests
  • Use HTTPS in production if possible
  • Implement rate limiting if needed

8. Performance

  • Use connection pooling for MCP servers
  • Clean up closed connections promptly
  • Monitor memory usage
  • Implement timeouts for long-running operations

Troubleshooting

Common Issues

  1. Port Already in Use:

    • Check if another instance is running
    • Use automatic port finding
    • Allow users to configure a different port
  2. VSCode Version Too Old:

    • Check version before enabling MCP
    • Show clear error message
    • Provide upgrade instructions
  3. MCP Server Not Responding:

    • Check server logs
    • Verify port is accessible
    • Check firewall settings
  4. Cursor API Not Available:

    • Verify Cursor version
    • Check if MCP API is enabled
    • Provide fallback behavior

Conclusion

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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment