Created
April 4, 2025 22:19
-
-
Save ruvnet/2a8d3c38e8469287fb2c53f512cf5c62 to your computer and use it in GitHub Desktop.
Revisions
-
ruvnet created this gist
Apr 4, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,776 @@ # Building a VSCode Remote Access MCP Server for Collaborative Agentic Development Before diving into the implementation, let's understand what makes this solution valuable: it creates a bridge between isolated development environments, enabling real-time collaboration without the limitations of traditional remote development approaches. ## MCP Server Architecture The MCP (Model Context Protocol) server architecture consists of several key components that work together to facilitate communication between multiple VSCode instances: 1. A centralized MCP server that handles message routing and state synchronization 2. Client connections from multiple workspaces or codespaces 3. STDIO transport layer for reliable communication 4. Extension modules for specific collaborative features ### Setting Up the MCP Server Configuration First, we'll create the server configuration file that defines our MCP server: ```json { // Input variables for secure credential management "inputs": [ { "type": "promptString", "id": "server-token", "description": "Authentication Token for MCP Server", "password": true } ], "servers": { "CollabMCPServer": { "type": "stdio", "command": "node", "args": ["./dist/server.js"], "env": { "SERVER_TOKEN": "${input:server-token}", "SERVER_PORT": "3000", "LOG_LEVEL": "info" } } } } ``` This configuration will be placed in the `.vscode/mcp.json` file of your project[1]. It sets up a server named "CollabMCPServer" that uses STDIO for communication and runs via Node.js. ## Server Implementation Now, let's implement the MCP server in JavaScript/TypeScript: ```typescript // server.ts import * as net from 'net'; import { EventEmitter } from 'events'; import { MCPMessage, ClientConnection, CollaborationSession } from './types'; class MCPServer extends EventEmitter { private clients: Map<string, ClientConnection> = new Map(); private sessions: Map<string, CollaborationSession> = new Map(); constructor() { super(); this.setupStdioHandling(); console.log('MCP Server initialized with STDIO transport'); } private setupStdioHandling() { // Set up raw mode for STDIO to handle binary data if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.on('data', this.handleIncomingData.bind(this)); process.stdin.resume(); // Handle process termination process.on('SIGTERM', this.shutdown.bind(this)); process.on('SIGINT', this.shutdown.bind(this)); } private handleIncomingData(data: Buffer) { try { const message = JSON.parse(data.toString('utf8')) as MCPMessage; this.routeMessage(message); } catch (error) { console.error('Error parsing incoming message:', error); } } private routeMessage(message: MCPMessage) { switch (message.type) { case 'connection': this.handleNewConnection(message); break; case 'terminal': this.handleTerminalData(message); break; case 'extension': this.handleExtensionSync(message); break; case 'editor': this.handleEditorCollaboration(message); break; default: console.warn('Unknown message type:', message.type); } } private handleNewConnection(message: MCPMessage) { const { clientId, workspaceId } = message.payload; this.clients.set(clientId, { id: clientId, workspaceId, connectedAt: new Date(), isActive: true }); // Send acknowledgment back this.sendToClient(clientId, { type: 'connection_ack', payload: { status: 'connected', serverTime: new Date().toISOString(), connectedClients: this.getActiveClientCount() } }); console.log(`Client ${clientId} from workspace ${workspaceId} connected`); } private handleTerminalData(message: MCPMessage) { const { sessionId, data, sourceClientId } = message.payload; // Broadcast terminal data to all clients in the session except the source this.broadcastToSession(sessionId, message, sourceClientId); } private handleExtensionSync(message: MCPMessage) { const { extensionId, state, sourceClientId } = message.payload; // Broadcast extension state to all connected clients except the source this.broadcastToAll(message, sourceClientId); } private handleEditorCollaboration(message: MCPMessage) { const { documentUri, edit, cursorPosition, sourceClientId } = message.payload; // Broadcast editor changes to all clients in the session except the source this.broadcastToSession(message.payload.sessionId, message, sourceClientId); } private broadcastToSession(sessionId: string, message: MCPMessage, excludeClientId?: string) { const session = this.sessions.get(sessionId); if (!session) return; for (const clientId of session.participants) { if (clientId !== excludeClientId) { this.sendToClient(clientId, message); } } } private broadcastToAll(message: MCPMessage, excludeClientId?: string) { for (const [clientId, client] of this.clients.entries()) { if (client.isActive && clientId !== excludeClientId) { this.sendToClient(clientId, message); } } } private sendToClient(clientId: string, message: MCPMessage) { const client = this.clients.get(clientId); if (!client || !client.isActive) return; const messageStr = JSON.stringify(message); process.stdout.write(messageStr + '\n'); } private getActiveClientCount(): number { let count = 0; for (const client of this.clients.values()) { if (client.isActive) count++; } return count; } private shutdown() { console.log('MCP Server shutting down...'); // Notify all clients about server shutdown this.broadcastToAll({ type: 'server_shutdown', payload: { reason: 'Server process terminated', time: new Date().toISOString() } }); process.exit(0); } } // Start the server const server = new MCPServer(); ``` ## Client Implementation Now, let's implement the client component that will run in each VSCode workspace: ```typescript // client.ts import * as vscode from 'vscode'; import { v4 as uuidv4 } from 'uuid'; import { MCPMessage, CollaborationSession } from './types'; export class MCPClient { private clientId: string; private workspaceId: string; private serverProcess: any; private isConnected: boolean = false; private messageQueue: MCPMessage[] = []; private activeSessions: Map<string, CollaborationSession> = new Map(); constructor(context: vscode.ExtensionContext) { this.clientId = uuidv4(); this.workspaceId = vscode.workspace.name || 'default-workspace'; // Register event handlers context.subscriptions.push( vscode.window.onDidChangeActiveTerminal(this.handleTerminalChange.bind(this)), vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange.bind(this)), vscode.workspace.onDidChangeTextDocument(this.handleDocumentChange.bind(this)) ); } public async connect(): Promise<boolean> { try { // Use VSCode's MCP server configuration await vscode.commands.executeCommand('mcp.startServer', 'CollabMCPServer'); // Create connection message const connectionMessage: MCPMessage = { type: 'connection', payload: { clientId: this.clientId, workspaceId: this.workspaceId, capabilities: ['terminal', 'editor', 'extensions'] } }; // Send connection request this.sendMessage(connectionMessage); this.isConnected = true; vscode.window.showInformationMessage('Connected to MCP Collaboration Server'); return true; } catch (error) { vscode.window.showErrorMessage(`Failed to connect to MCP Server: ${error}`); return false; } } public disconnect(): void { if (!this.isConnected) return; const disconnectMessage: MCPMessage = { type: 'disconnection', payload: { clientId: this.clientId, reason: 'Client disconnected' } }; this.sendMessage(disconnectMessage); this.isConnected = false; vscode.window.showInformationMessage('Disconnected from MCP Collaboration Server'); } public createCollaborationSession(): string { const sessionId = uuidv4(); const session: CollaborationSession = { id: sessionId, createdBy: this.clientId, createdAt: new Date(), participants: [this.clientId], activeDocument: null, sharedTerminal: null }; this.activeSessions.set(sessionId, session); const sessionMessage: MCPMessage = { type: 'session_create', payload: { sessionId, createdBy: this.clientId, workspaceId: this.workspaceId } }; this.sendMessage(sessionMessage); return sessionId; } public joinSession(sessionId: string): boolean { const joinMessage: MCPMessage = { type: 'session_join', payload: { sessionId, clientId: this.clientId, workspaceId: this.workspaceId } }; this.sendMessage(joinMessage); return true; } private handleTerminalChange(terminal: vscode.Terminal | undefined): void { if (!terminal || !this.isConnected) return; // Implementation for sharing terminal state } private handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent): void { if (!this.isConnected) return; // Implementation for sharing cursor position } private handleDocumentChange(event: vscode.TextDocumentChangeEvent): void { if (!this.isConnected) return; // Implementation for sharing document changes } private sendMessage(message: MCPMessage): void { if (!this.isConnected) { this.messageQueue.push(message); return; } // Use VSCode's commands to send message to the MCP server vscode.commands.executeCommand('mcp.sendMessage', 'CollabMCPServer', JSON.stringify(message)); } } ``` ## Extension Entry Point Let's create the extension entry point that initializes the MCP server and client: ```typescript // extension.ts import * as vscode from 'vscode'; import { MCPClient } from './client'; export function activate(context: vscode.ExtensionContext) { console.log('Activating Collaborative MCP Server Extension'); const client = new MCPClient(context); // Register commands context.subscriptions.push( vscode.commands.registerCommand('collaborativeMcp.connect', async () => { await client.connect(); }), vscode.commands.registerCommand('collaborativeMcp.disconnect', () => { client.disconnect(); }), vscode.commands.registerCommand('collaborativeMcp.createSession', () => { const sessionId = client.createCollaborationSession(); vscode.env.clipboard.writeText(sessionId); vscode.window.showInformationMessage(`Collaboration session created. ID copied to clipboard: ${sessionId}`); }), vscode.commands.registerCommand('collaborativeMcp.joinSession', async () => { const sessionId = await vscode.window.showInputBox({ prompt: 'Enter session ID to join', placeHolder: 'Session ID' }); if (sessionId) { client.joinSession(sessionId); } }) ); } export function deactivate() { console.log('Deactivating Collaborative MCP Server Extension'); } ``` ## Type Definitions Let's define the types used throughout the implementation: ```typescript // types.ts export interface MCPMessage { type: string; payload: any; } export interface ClientConnection { id: string; workspaceId: string; connectedAt: Date; isActive: boolean; } export interface CollaborationSession { id: string; createdBy: string; createdAt: Date; participants: string[]; activeDocument: string | null; sharedTerminal: string | null; } ``` ## Unit Tests Now, let's implement comprehensive unit tests for our MCP server: ```typescript // server.test.ts import * as assert from 'assert'; import { MCPServer } from '../src/server'; import { MCPClient } from '../src/client'; import { MCPMessage } from '../src/types'; import * as sinon from 'sinon'; // Mock VSCode API const vscode = { window: { showInformationMessage: sinon.stub(), showErrorMessage: sinon.stub() }, commands: { executeCommand: sinon.stub() }, workspace: { name: 'test-workspace' } }; describe('MCP Server Tests', () => { let server: any; let client: any; let stdoutSpy: sinon.SinonSpy; beforeEach(() => { // Redirect stdout to capture server output stdoutSpy = sinon.spy(process.stdout, 'write'); // Create server instance server = new MCPServer(); // Create a mock client context const mockContext = { subscriptions: [] }; // Create client instance client = new MCPClient(mockContext as any); // Reset stubs vscode.commands.executeCommand.reset(); vscode.window.showInformationMessage.reset(); }); afterEach(() => { // Restore stdout stdoutSpy.restore(); }); it('should initialize the server correctly', () => { assert.ok(server); assert.strictEqual(server.clients.size, 0); assert.strictEqual(server.sessions.size, 0); }); it('should handle client connections', () => { // Simulate a client connection message const connectionMessage: MCPMessage = { type: 'connection', payload: { clientId: 'test-client-1', workspaceId: 'test-workspace' } }; // Send message to server server.routeMessage(connectionMessage); // Verify client was added assert.strictEqual(server.clients.size, 1); assert.ok(server.clients.has('test-client-1')); assert.strictEqual(server.clients.get('test-client-1').workspaceId, 'test-workspace'); // Verify acknowledgment was sent assert.ok(stdoutSpy.calledOnce); const response = JSON.parse(stdoutSpy.firstCall.args[0]); assert.strictEqual(response.type, 'connection_ack'); assert.strictEqual(response.payload.status, 'connected'); }); it('should create and join collaboration sessions', () => { // Set up two test clients const client1Id = 'test-client-1'; const client2Id = 'test-client-2'; const sessionId = 'test-session-1'; // Connect clients server.routeMessage({ type: 'connection', payload: { clientId: client1Id, workspaceId: 'workspace-1' } }); server.routeMessage({ type: 'connection', payload: { clientId: client2Id, workspaceId: 'workspace-2' } }); // Create a session server.routeMessage({ type: 'session_create', payload: { sessionId, createdBy: client1Id, workspaceId: 'workspace-1' } }); // Join the session server.routeMessage({ type: 'session_join', payload: { sessionId, clientId: client2Id, workspaceId: 'workspace-2' } }); // Verify session was created assert.ok(server.sessions.has(sessionId)); const session = server.sessions.get(sessionId); assert.strictEqual(session.participants.length, 2); assert.ok(session.participants.includes(client1Id)); assert.ok(session.participants.includes(client2Id)); }); it('should broadcast terminal data to session participants', () => { // Set up test clients and session const client1Id = 'test-client-1'; const client2Id = 'test-client-2'; const sessionId = 'test-session-1'; // Connect clients server.routeMessage({ type: 'connection', payload: { clientId: client1Id, workspaceId: 'workspace-1' } }); server.routeMessage({ type: 'connection', payload: { clientId: client2Id, workspaceId: 'workspace-2' } }); // Create and join session server.routeMessage({ type: 'session_create', payload: { sessionId, createdBy: client1Id, workspaceId: 'workspace-1' } }); server.routeMessage({ type: 'session_join', payload: { sessionId, clientId: client2Id, workspaceId: 'workspace-2' } }); // Reset spy to clear previous calls stdoutSpy.resetHistory(); // Send terminal data const terminalData = "echo 'Hello, World!'"; server.routeMessage({ type: 'terminal', payload: { sessionId, data: terminalData, sourceClientId: client1Id } }); // Verify data was broadcasted to other client assert.ok(stdoutSpy.calledOnce); const response = JSON.parse(stdoutSpy.firstCall.args[0]); assert.strictEqual(response.type, 'terminal'); assert.strictEqual(response.payload.data, terminalData); }); it('should handle graceful server shutdown', () => { // Connect test clients server.routeMessage({ type: 'connection', payload: { clientId: 'test-client-1', workspaceId: 'workspace-1' } }); server.routeMessage({ type: 'connection', payload: { clientId: 'test-client-2', workspaceId: 'workspace-2' } }); // Mock process.exit to prevent actual exit during test const exitStub = sinon.stub(process, 'exit'); // Reset spy to clear previous calls stdoutSpy.resetHistory(); // Trigger shutdown server.shutdown(); // Verify shutdown messages were sent to clients assert.strictEqual(stdoutSpy.callCount, 2); const response1 = JSON.parse(stdoutSpy.firstCall.args[0]); assert.strictEqual(response1.type, 'server_shutdown'); // Verify exit was called assert.ok(exitStub.calledOnce); assert.strictEqual(exitStub.firstCall.args[0], 0); // Restore stub exitStub.restore(); }); }); ``` ## Package.json Configuration Finally, let's configure the extension's package.json: ```json { "name": "vscode-collaborative-mcp", "displayName": "Collaborative MCP Server", "version": "1.0.0", "description": "An MCP server for VSCode that enables collaborative development across workspaces", "main": "./dist/extension.js", "engines": { "vscode": "^1.70.0" }, "categories": [ "Other" ], "activationEvents": [ "onCommand:collaborativeMcp.connect", "onCommand:collaborativeMcp.createSession", "onCommand:collaborativeMcp.joinSession" ], "contributes": { "commands": [ { "command": "collaborativeMcp.connect", "title": "Connect to Collaborative MCP Server" }, { "command": "collaborativeMcp.disconnect", "title": "Disconnect from Collaborative MCP Server" }, { "command": "collaborativeMcp.createSession", "title": "Create a new collaboration session" }, { "command": "collaborativeMcp.joinSession", "title": "Join an existing collaboration session" } ] }, "scripts": { "vscode:prepublish": "npm run package", "compile": "webpack", "watch": "webpack --watch", "package": "webpack --mode production --devtool hidden-source-map", "compile-tests": "tsc -p . --outDir out", "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile && npm run lint", "lint": "eslint src --ext ts", "test": "node ./out/test/runTest.js" }, "dependencies": { "uuid": "^8.3.2" }, "devDependencies": { "@types/mocha": "^9.1.1", "@types/node": "16.x", "@types/sinon": "^10.0.13", "@types/uuid": "^8.3.4", "@types/vscode": "^1.70.0", "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", "@vscode/test-electron": "^2.1.5", "eslint": "^8.20.0", "mocha": "^10.0.0", "sinon": "^14.0.0", "ts-loader": "^9.3.1", "typescript": "^4.7.4", "webpack": "^5.74.0", "webpack-cli": "^4.10.0" } } ``` ## Setting Up a Multi-Workspace Collaboration Environment To utilize this MCP server across multiple workspaces, follow these steps: 1. Install the extension in all participating VSCode instances 2. In one workspace, run the "Connect to Collaborative MCP Server" command 3. Create a new collaboration session using "Create a new collaboration session" 4. Copy the session ID that appears 5. In other workspaces, connect to the MCP server and join the session using "Join an existing collaboration session" 6. Enter the session ID when prompted Once connected, all workspaces will be able to share: - Terminal sessions - Editor changes - Cursor positions - Extension states ## Conclusion This implementation provides a comprehensive VSCode MCP server that enables collaborative development across multiple workspaces or codespaces using STDIO communication. The server supports sharing terminals, synchronizing extension states, and collaborative editing. The solution includes a robust architecture with: - A centralized MCP server for message routing - Client connections from multiple workspaces - STDIO-based transport for reliable communication - Comprehensive unit tests to ensure functionality By following this guide, you can create a powerful collaborative development environment that enhances team productivity and enables seamless remote work[2][4]. Sources [1] Use MCP servers in VS Code (Preview) https://code.visualstudio.com/docs/copilot/chat/mcp-servers [2] Working collaboratively in a codespace - GitHub Docs https://docs.github.com/en/codespaces/developing-in-a-codespace/working-collaboratively-in-a-codespace [3] A Detailed Guide To Playwright MCP Server - QA Touch https://www.qatouch.com/blog/playwright-mcp-server/ [4] Visual Studio Code Server https://code.visualstudio.com/docs/remote/vscode-server [5] How to set up a remote connexion using codespace - VS Code https://discourse.julialang.org/t/how-to-set-up-a-remote-connexion-using-codespace/44138 [6] Tutorial: Test a .NET class library using Visual Studio Code https://learn.microsoft.com/en-us/dotnet/core/tutorials/testing-library-with-visual-studio-code [7] Setup MCP Servers in VS Code in Seconds #vscode #githubcopilot ... https://www.youtube.com/watch?v=vhdRUJlgwD4 [8] VSCode MCP Server - Visual Studio Marketplace https://marketplace.visualstudio.com/items?itemName=SemanticWorkbenchTeam.mcp-server-vscode [9] Try out MCP servers in VS Code : r/ChatGPTCoding - Reddit https://www.reddit.com/r/ChatGPTCoding/comments/1jfr05y/try_out_mcp_servers_in_vs_code/ [10] What is a VS Code workspace? https://code.visualstudio.com/docs/editing/workspaces/workspaces [11] Cursor terminal access for auto-fixing unit tests https://forum.cursor.com/t/cursor-terminal-access-for-auto-fixing-unit-tests/69922 [12] VSCode Remote Development via SSH and Tunnel - FASRC DOCS https://docs.rc.fas.harvard.edu/kb/vscode-remote-development-via-ssh-or-tunnel/ [13] Get started with unit testing - Visual Studio (Windows) | Microsoft Learn https://learn.microsoft.com/en-us/visualstudio/test/getting-started-with-unit-testing?view=vs-2022 [14] Configuring VS Code Workspaces for Multiple ObjectScript ... https://www.youtube.com/watch?v=CGEqFea0hh8 [15] Remote Development FAQ - Visual Studio Code https://code.visualstudio.com/docs/remote/faq [16] Supporting Remote Development and GitHub Codespaces https://code.visualstudio.com/api/advanced-topics/remote-extensions [17] VS Code Remote Development https://code.visualstudio.com/docs/remote/remote-overview [18] Multi-root Workspaces - Visual Studio Code https://code.visualstudio.com/docs/editor/workspaces/multi-root-workspaces [19] punkpeye/awesome-mcp-servers - GitHub https://github.com/punkpeye/awesome-mcp-servers [20] Getting Started with Remote Development - YouTube https://www.youtube.com/watch?v=QA9jlp-o5vQ [21] Effortless Pair Programming with GitHub Codespaces and VSCode https://microsoft.github.io/code-with-engineering-playbook/agile-development/advanced-topics/collaboration/pair-programming-tools/ [22] Remote Development using SSH - Visual Studio Code https://code.visualstudio.com/docs/remote/ssh [23] What options are there for collaborating with Visual Studio Code? https://stackoverflow.com/questions/62441404/what-options-are-there-for-collaborating-with-visual-studio-code [24] MCP Testing - MCP Servers https://mcp.so/server/mcp-testing [25] Introducing Remote Development for Visual Studio Code : r/vscode https://www.reddit.com/r/vscode/comments/bjycxh/introducing_remote_development_for_visual_studio/ [26] How to setup Visual Studio Codespaces for collaborative use https://techcommunity.microsoft.com/blog/itopstalkblog/how-to-setup-visual-studio-codespaces-for-collaborative-use/1436412 [27] VikashLoomba/copilot-mcp: A powerful VSCode extension ... - GitHub https://github.com/VikashLoomba/copilot-mcp [28] MCP: in a codespace, filepaths are rewritten for windows. #244178 https://github.com/microsoft/vscode/issues/244178 [29] Share a server or terminal in Visual Studio Code - Learn Microsoft https://learn.microsoft.com/en-us/visualstudio/liveshare/use/share-server-visual-studio-code [30] Visual studio unit tests and client server programs - Stack Overflow https://stackoverflow.com/questions/6190585/visual-studio-unit-tests-and-client-server-programs [31] VSCode MCP - MCP Servers https://mcp.so/server/vscode-mcp/block [32] GitHub Codespaces - Visual Studio Code https://code.visualstudio.com/docs/remote/codespaces [33] Creating Unit Tests using ChatGPT's Visual Studio Code Extension ... https://www.youtube.com/watch?v=LlUndDIBRlY [34] Visual Studio Code - Multiple Remote SSH connections in one ... https://stackoverflow.com/questions/58350537/visual-studio-code-multiple-remote-ssh-connections-in-one-workspace [35] Extending Copilot Chat with the Model Context Protocol (MCP) https://docs.github.com/en/copilot/customizing-copilot/extending-copilot-chat-with-mcp [36] Using fnm to launch MCP server in Visual Studio Code ... - GitHub https://github.com/Schniz/fnm/issues/1406