├── .nvmrc ├── .prettierignore ├── .npmrc ├── pnpm-workspace.yaml ├── packages ├── vscode-mcp-bridge │ ├── .gitattributes │ ├── test-workspace │ │ ├── package.json │ │ ├── index.ts │ │ └── add.ts │ ├── CHANGELOG.md │ ├── assets │ │ └── logo.png │ ├── scripts │ │ ├── tsconfig.json │ │ └── esbuild.ts │ ├── .gitignore │ ├── src │ │ ├── tsconfig.json │ │ ├── commands │ │ │ ├── sleep.ts │ │ │ ├── copy-opened-files-path.ts │ │ │ └── copy-current-selection-reference.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── health.ts │ │ │ ├── open-files.ts │ │ │ ├── get-references.ts │ │ │ ├── rename-symbol.ts │ │ │ ├── execute-command.ts │ │ │ ├── list-workspaces.ts │ │ │ └── get-diagnostics.ts │ │ ├── utils │ │ │ ├── types.ts │ │ │ ├── get-usage-code.ts │ │ │ ├── detect-ide.ts │ │ │ ├── workspace.ts │ │ │ ├── file-safety-check.ts │ │ │ └── resolve-symbol-position.ts │ │ ├── logger.ts │ │ ├── extension.ts │ │ └── socket-server.ts │ ├── .vscode │ │ ├── extensions.json │ │ ├── tasks.json │ │ └── launch.json │ ├── test │ │ ├── tsconfig.json │ │ ├── sample.test.ts │ │ ├── runTests.ts │ │ └── index.ts │ ├── .vscodeignore │ ├── tsconfig.base.json │ ├── LICENSE.md │ ├── README.md │ └── package.json ├── vscode-mcp-server │ ├── README.md │ ├── src │ │ ├── utils │ │ │ ├── format-tool-call-error.ts │ │ │ ├── workspace-schema.ts │ │ │ └── workspace-discovery.ts │ │ ├── tools │ │ │ ├── index.ts │ │ │ ├── execute-command.ts │ │ │ ├── open-files.ts │ │ │ ├── get-references.ts │ │ │ ├── rename-symbol.ts │ │ │ ├── health-check.ts │ │ │ ├── get-diagnostics.ts │ │ │ ├── get-symbol-lsp-info.ts │ │ │ └── list-workspaces.ts │ │ ├── constants.ts │ │ ├── server.ts │ │ └── index.ts │ ├── tsconfig.json │ └── package.json └── vscode-mcp-ipc │ ├── src │ ├── index.ts │ ├── events │ │ ├── rename-symbol.ts │ │ ├── get-references.ts │ │ ├── execute-command.ts │ │ ├── health-check.ts │ │ ├── open-file.ts │ │ ├── get-diagnostics.ts │ │ ├── list-workspaces.ts │ │ ├── index.ts │ │ └── get-symbol-lsp-info.ts │ ├── common.ts │ └── dispatch.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── .vscode └── settings.json ├── CLAUDE.md ├── eslint.config.mjs ├── .claude └── settings.json ├── .prettierrc.mjs ├── package.json ├── .cursor └── rules │ ├── typescript.mdc │ ├── vscode-mcp-project-architecture.mdc │ └── vscode-mcp-development-guide.mdc ├── .github └── workflows │ ├── claude.yml │ ├── claude-code-review.yml │ └── vscode-extension-ci.yml ├── LICENSE ├── .gitignore └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v24.11.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test-workspace -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry="https://registry.npmmirror.com/" 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/test-workspace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-workspace" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["typescript", "json", "jsonc", "markdown"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/test-workspace/index.ts: -------------------------------------------------------------------------------- 1 | import { add } from './add'; 2 | 3 | console.log(add(1, 2)); 4 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/test-workspace/add.ts: -------------------------------------------------------------------------------- 1 | export const sum = (a: number, b: number) => { 2 | return a + b; 3 | }; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjx666/vscode-mcp/HEAD/packages/vscode-mcp-bridge/assets/logo.png -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "include": ["**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | 6 | # esbuild-visualizer 7 | meta.json 8 | stats.html 9 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out/src" 5 | }, 6 | "include": ["**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/README.md: -------------------------------------------------------------------------------- 1 | # @vscode-mcp/vscode-mcp-server 2 | 3 | A Model Context Protocol (MCP) server that provides access to VSCode workspace information through Unix Socket communication. 4 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["connor4312.esbuild-problem-matchers"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/commands/sleep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sleep command implementation 3 | */ 4 | export async function sleepCommand(duration: number): Promise { 5 | await new Promise(resolve => { 6 | setTimeout(resolve, duration * 1000); 7 | }); 8 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/index.ts: -------------------------------------------------------------------------------- 1 | // Export all event types and interfaces 2 | export * from './events/index.js'; 3 | 4 | // Export dispatcher functionality 5 | export * from './dispatch.js'; 6 | 7 | // Re-export useful types from type-fest 8 | export type { Jsonifiable } from 'type-fest'; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out/test" 5 | }, 6 | "include": ["**/*.ts"], 7 | "references": [ 8 | { 9 | "path": "../src/tsconfig.json" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | - Read project architecture: @.cursor/rules/vscode-mcp-project-architecture.mdc 6 | - Read development guide: @.cursor/rules/vscode-mcp-development-guide.mdc 7 | - Read typescript code style: @.cursor/rules/typescript.mdc 8 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import yutengjingEslintConfigTypescript from '@yutengjing/eslint-config-typescript'; 2 | import { defineConfig } from 'eslint/config'; 3 | 4 | export default defineConfig([ 5 | yutengjingEslintConfigTypescript, 6 | { 7 | rules: { 8 | 'n/prefer-global/process': 'off', 9 | }, 10 | }, 11 | ]); 12 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/.vscodeignore: -------------------------------------------------------------------------------- 1 | # source 2 | src 3 | scripts 4 | assets 5 | !assets/logo.png 6 | 7 | # output 8 | out 9 | !out/src/extension.js 10 | *.map 11 | *.vsix 12 | 13 | # test 14 | test 15 | test-workspace 16 | .vscode-test 17 | 18 | # configs 19 | .github 20 | .vscode 21 | tsconfig.* 22 | .eslintrc* 23 | .prettier* 24 | .gitignore 25 | pnpm-lock.yaml 26 | 27 | # esbuild-visualizer 28 | meta.json 29 | stats.html 30 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/test/sample.test.ts: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert'; 2 | 3 | import vscode from 'vscode'; 4 | 5 | describe('#test sample', () => { 6 | before(() => { 7 | vscode.window.showInformationMessage('Test begin!'); 8 | }); 9 | 10 | it('one plus one equals two', () => { 11 | strictEqual(2, 1 + 1); 12 | }); 13 | 14 | after(() => { 15 | vscode.window.showInformationMessage('Test end!'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/utils/format-tool-call-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 格式化工具调用错误 3 | * 按照 MCP 官方标准,使用 isError: true 来标识错误 4 | * 5 | * @param toolName 工具名称 6 | * @param error 错误对象 7 | * @returns 格式化的错误响应 8 | */ 9 | export function formatToolCallError(toolName: string, error: unknown, additionalTips?: string) { 10 | return { 11 | isError: true, 12 | content: [ 13 | { 14 | type: "text" as const, 15 | text: `❌ ${toolName} failed: ${String(error)} ${additionalTips ? `\n\n${additionalTips}` : ''}` 16 | } 17 | ] 18 | }; 19 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/services/index.ts: -------------------------------------------------------------------------------- 1 | // Export service functions 2 | export { executeCommand } from './execute-command'; 3 | export { getDiagnostics } from './get-diagnostics'; 4 | export { getReferences } from './get-references'; 5 | export { getSymbolLSPInfo } from './get-symbol-lsp-info'; 6 | export { health } from './health'; 7 | export { listWorkspaces } from './list-workspaces'; 8 | export { openFiles } from './open-files'; 9 | export { renameSymbol } from './rename-symbol'; 10 | 11 | // Export utilities 12 | export { getCurrentWorkspacePath } from '../utils/workspace.js'; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BaseResponse, 3 | EventName, 4 | EventParams} from '@vscode-mcp/vscode-mcp-ipc'; 5 | 6 | 7 | 8 | // Re-export IPC types for convenience 9 | export { 10 | BaseRequest, 11 | BaseResponse, 12 | EventName, 13 | EventParams, 14 | EventResult 15 | } from '@vscode-mcp/vscode-mcp-ipc'; 16 | 17 | /** 18 | * Type-safe service handler function 19 | */ 20 | export type ServiceHandler = ( 21 | id: string, 22 | params: EventParams 23 | ) => Promise; -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(bun run:*)", 5 | "Bash(find:*)", 6 | "Bash(git add:*)", 7 | "Bash(git commit:*)", 8 | "Bash(npm run:*)", 9 | "Bash(npx tsc:*)", 10 | "Bash(pnpm compile:test:*)", 11 | "mcp__grep", 12 | "mcp__ide", 13 | "mcp__vscode-mcp", 14 | "mcp__zen", 15 | "WebFetch(domain:code.visualstudio.com)", 16 | "WebFetch(domain:github.com)" 17 | ], 18 | "deny": [] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/test/runTests.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | (async function go() { 6 | const projectPath = resolve(__dirname, '../../'); 7 | const extensionDevelopmentPath = projectPath; 8 | const extensionTestsPath = resolve(projectPath, './out/test'); 9 | const testWorkspace = resolve(projectPath, './test-workspace'); 10 | 11 | await runTests({ 12 | version: 'insiders', 13 | extensionDevelopmentPath, 14 | extensionTestsPath, 15 | launchArgs: ['--disable-extensions', testWorkspace], 16 | }); 17 | })(); 18 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "esbuild:watch", 9 | "problemMatcher": "$esbuild-watch", 10 | "isBackground": true, 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | }, 16 | { 17 | "label": "compile:test", 18 | "type": "npm", 19 | "script": "compile:test", 20 | "problemMatcher": "$tsc" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | // const config = require('@yutengjing/prettier-config'); 2 | import config from '@yutengjing/prettier-config'; 3 | 4 | /** @type {import('prettier').Config} */ 5 | export default { 6 | ...config, 7 | quoteProps: 'as-needed', 8 | overrides: [ 9 | ...config.overrides, 10 | { 11 | files: ['.cursorrules', 'mdc-editor-*', '.cursor/rules/**/*'], 12 | options: { 13 | parser: 'markdown', 14 | tabWidth: 2, 15 | }, 16 | }, 17 | { 18 | // for .markdownlint.jsonc 19 | files: '*.jsonc', 20 | options: { 21 | trailingComma: 'none', 22 | }, 23 | }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-mcp", 3 | "version": "4.5.0", 4 | "description": "", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "release": "bumpp -c \"release: v%s\" -r && pnpm -r publish" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "packageManager": "pnpm@10.22.0", 14 | "devDependencies": { 15 | "@types/node": "~24.10.1", 16 | "@typescript/native-preview": "latest", 17 | "@yutengjing/eslint-config-typescript": "^2.6.1", 18 | "@yutengjing/prettier-config": "^2.0.0", 19 | "bumpp": "^10.3.1", 20 | "eslint": "^9.39.1", 21 | "prettier": "^3.6.2", 22 | "tsx": "^4.20.6", 23 | "typescript": "~5.9.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * VSCode MCP Tools Index 3 | * 4 | * This module exports all tool registration functions. 5 | */ 6 | 7 | // 工具错误处理函数 8 | export { formatToolCallError } from "../utils/format-tool-call-error.js"; 9 | 10 | // 工具注册函数 11 | export { registerExecuteCommand } from "./execute-command.js"; 12 | export { registerGetDiagnostics } from "./get-diagnostics.js"; 13 | export { registerGetReferences } from "./get-references.js"; 14 | export { registerGetSymbolLSPInfo } from "./get-symbol-lsp-info.js"; 15 | export { registerHealthCheck } from "./health-check.js"; 16 | export { registerListWorkspaces } from "./list-workspaces.js"; 17 | export { registerOpenFiles } from "./open-files.js"; 18 | export { registerRenameSymbol } from "./rename-symbol.js"; 19 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/utils/workspace-schema.ts: -------------------------------------------------------------------------------- 1 | 2 | import { z } from 'zod'; 3 | 4 | /** 5 | * Shared schema for validating VSCode workspace paths 6 | * Ensures the path is absolute, relative paths like "." are not supported 7 | */ 8 | export const workspacePathSchema = z 9 | .string() 10 | .describe('Absolute path to the VSCode workspace folder.') 11 | 12 | /** 13 | * Helper object for creating tool input schemas that include workspace_path 14 | * Can be merged with other schema shapes 15 | * 16 | * @example 17 | * ```typescript 18 | * const inputSchema = { 19 | * ...workspacePathInputSchema, 20 | * ...SomeToolInputSchema.shape, 21 | * }; 22 | * ``` 23 | */ 24 | export const workspacePathInputSchema = { 25 | workspace_path: workspacePathSchema, 26 | } as const; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/test/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import { glob } from 'glob'; 4 | import Mocha from 'mocha'; 5 | 6 | /** 7 | * !: must be synchronized 8 | */ 9 | export function run(testsRoot: string, cb: (error: any, failures?: number) => void): void { 10 | const mocha = new Mocha({ color: true }); 11 | 12 | glob('**/**.test.js', { cwd: testsRoot }) 13 | .then((files) => { 14 | for (const f of files) mocha.addFile(path.resolve(testsRoot, f)); 15 | 16 | try { 17 | mocha.run((failures) => { 18 | cb(null, failures); 19 | }); 20 | } catch (error) { 21 | cb(error); 22 | } 23 | }) 24 | .catch((error) => cb(error)); 25 | } 26 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * VSCode MCP Tool Names 3 | * 4 | * Centralized definition of all available tool names used throughout the MCP server. 5 | * This enum ensures type safety and consistent naming across the codebase. 6 | */ 7 | export enum VscodeMcpToolName { 8 | EXECUTE_COMMAND = 'execute_command', 9 | GET_DIAGNOSTICS = 'get_diagnostics', 10 | GET_REFERENCES = 'get_references', 11 | GET_SYMBOL_LSP_INFO = 'get_symbol_lsp_info', 12 | HEALTH_CHECK = 'health_check', 13 | LIST_WORKSPACES = 'list_workspaces', 14 | OPEN_FILES = 'open_files', 15 | RENAME_SYMBOL = 'rename_symbol', 16 | } 17 | 18 | /** 19 | * Get all available tool names as an array 20 | */ 21 | export function getAllToolNames(): string[] { 22 | return Object.values(VscodeMcpToolName); 23 | } 24 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "lib": ["ES2022"], 6 | "moduleResolution": "node", 7 | "allowJs": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true, 17 | "resolveJsonModule": true, 18 | "allowSyntheticDefaultImports": true, 19 | "noEmitOnError": true, 20 | "types": ["node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules", "dist"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "lib": ["ES2022"], 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "removeComments": false, 17 | "noEmitOnError": true, 18 | "isolatedModules": true, 19 | "allowSyntheticDefaultImports": true, 20 | "resolveJsonModule": true, 21 | "verbatimModuleSyntax": false 22 | }, 23 | "include": ["src/**/*"], 24 | "exclude": ["node_modules", "dist"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "sourceMap": true, 5 | 6 | /* Basic Options */ 7 | "target": "ES2023", 8 | "module": "CommonJS", 9 | 10 | /* Strict Type-Checking Options */ 11 | "strict": true, 12 | 13 | /* Additional Checks */ 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | 17 | /* Module Resolution Options */ 18 | "moduleResolution": "Node", 19 | "esModuleInterop": true, 20 | "resolveJsonModule": true, 21 | 22 | /* Experimental Options */ 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true, 25 | 26 | /* Advanced Options */ 27 | "forceConsistentCasingInFileNames": true, 28 | "skipLibCheck": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode-mcp/vscode-mcp-ipc", 3 | "version": "4.5.0", 4 | "type": "module", 5 | "description": "IPC communication layer between MCP Server and VSCode extension", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "publishConfig": { 9 | "registry": "https://registry.npmjs.org/", 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "build:watch": "tsc --watch", 15 | "clean": "rm -rf dist", 16 | "typecheck": "tsgo --noEmit", 17 | "prepublishOnly": "pnpm run clean && pnpm run build" 18 | }, 19 | "keywords": [ 20 | "mcp", 21 | "vscode", 22 | "ipc", 23 | "unix-socket" 24 | ], 25 | "author": { 26 | "name": "YuTengjing", 27 | "email": "ytj2713151713@gmail.com" 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "peerDependencies": { 33 | "type-fest": "^5.0.0" 34 | }, 35 | "dependencies": { 36 | "zod": "^3.25.76" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/rename-symbol.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { SymbolLocatorSchema } from '../common.js'; 4 | 5 | export const RenameSymbolInputSchema = SymbolLocatorSchema.extend({ 6 | newName: z.string().describe('New symbol name'), 7 | }); 8 | 9 | export const RenameSymbolOutputSchema = z 10 | .object({ 11 | success: z.boolean().describe('Whether rename was successful'), 12 | symbolName: z.string().optional().describe('Original symbol name'), 13 | modifiedFiles: z 14 | .array( 15 | z.object({ 16 | uri: z.string().describe('File URI'), 17 | changeCount: z.number().describe('Number of changes in this file'), 18 | }), 19 | ) 20 | .describe('List of modified files'), 21 | totalChanges: z.number().describe('Total number of changes made'), 22 | error: z.string().optional().describe('Error message if rename failed'), 23 | }) 24 | .strict(); 25 | 26 | export type RenameSymbolPayload = z.infer; 27 | export type RenameSymbolResult = z.infer; -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/get-references.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get references event types and schemas 3 | */ 4 | 5 | import { z } from 'zod'; 6 | 7 | import { LocationSchema, SymbolLocatorSchema } from '../common.js'; 8 | 9 | /** 10 | * Get references input schema 11 | */ 12 | export const GetReferencesInputSchema = SymbolLocatorSchema.extend({ 13 | includeDeclaration: z.boolean().optional().describe('Whether to include the declaration in the results'), 14 | usageCodeLineRange: z.number().optional().default(5).describe('Number of lines to include around each reference for usage context. 5 = ±5 lines (11 total), 0 = only reference line, -1 = no usage code'), 15 | }); 16 | 17 | /** 18 | * Get references output schema 19 | */ 20 | export const GetReferencesOutputSchema = z.object({ 21 | locations: z.array(LocationSchema), 22 | }).strict(); 23 | 24 | /** 25 | * Get references payload (input parameters) 26 | */ 27 | export type GetReferencesPayload = z.infer; 28 | 29 | /** 30 | * Get references result (output data) 31 | */ 32 | export type GetReferencesResult = z.infer; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2025] [YuTengjing] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode-mcp/vscode-mcp-server", 3 | "version": "4.5.0", 4 | "type": "module", 5 | "description": "MCP Server that connects to VSCode Extension via Unix Socket", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "bin": { 9 | "vscode-mcp-server": "dist/index.js" 10 | }, 11 | "publishConfig": { 12 | "registry": "https://registry.npmjs.org/", 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "build": "tsc", 17 | "build:watch": "tsc --watch", 18 | "clean": "rm -rf dist", 19 | "dev": "tsx src/index.ts", 20 | "start": "node dist/index.js", 21 | "typecheck": "tsgo --noEmit", 22 | "prepublishOnly": "pnpm run clean && pnpm run build" 23 | }, 24 | "keywords": [ 25 | "mcp", 26 | "vscode", 27 | "server", 28 | "model-context-protocol" 29 | ], 30 | "author": { 31 | "name": "YuTengjing", 32 | "email": "ytj2713151713@gmail.com" 33 | }, 34 | "files": [ 35 | "dist" 36 | ], 37 | "dependencies": { 38 | "@modelcontextprotocol/sdk": "^1.18.1", 39 | "@vscode-mcp/vscode-mcp-ipc": "workspace:*", 40 | "zod": "^3.25.76" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/execute-command.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Execute command event types and schemas 3 | */ 4 | 5 | import { z } from 'zod'; 6 | 7 | 8 | /** 9 | * Execute command input schema 10 | */ 11 | export const ExecuteCommandInputSchema = z.object({ 12 | command: z.string().describe('VSCode command to execute (e.g., \'vscode.open\', \'editor.action.formatDocument\')'), 13 | args: z.string().optional().describe(`Optional JSON string of arguments array to pass to the command 14 | - args parameter must be a JSON string representing an array of arguments 15 | - For file paths: Use absolute paths like '["file:///absolute/path/to/file.ts"]' (VSCode commands still expect file:// URIs)`), 16 | saveAllEditors: z.boolean().optional().default(true).describe('Save all dirty editors after executing the command (default: true)'), 17 | }).strict(); 18 | 19 | /** 20 | * Execute command output schema 21 | */ 22 | export const ExecuteCommandOutputSchema = z.object({ 23 | result: z.unknown().describe('Result from the command execution'), 24 | }).strict(); 25 | 26 | /** 27 | * Execute command payload (input parameters) 28 | */ 29 | export type ExecuteCommandPayload = z.infer; 30 | 31 | /** 32 | * Execute command result (output data) 33 | */ 34 | export type ExecuteCommandResult = z.infer; -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/health-check.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Health check event types and schemas 3 | */ 4 | 5 | import { z } from 'zod'; 6 | 7 | /** 8 | * Health check input schema 9 | */ 10 | export const HealthCheckInputSchema = z.object({}).strict(); 11 | 12 | /** 13 | * Health check output schema 14 | */ 15 | export const HealthCheckOutputSchema = z.object({ 16 | status: z.enum(['ok', 'error']), 17 | extension_version: z.string().describe('VSCode MCP Bridge extension version'), 18 | workspace: z.string().optional().describe('Current workspace path'), 19 | timestamp: z.string().describe('Health check timestamp'), 20 | error: z.string().optional().describe('Error message if status is error'), 21 | system_info: z.object({ 22 | platform: z.string().describe('Operating system platform'), 23 | node_version: z.string().describe('Node.js version'), 24 | vscode_version: z.string().optional().describe('VSCode version if available'), 25 | ide_type: z.string().optional().describe('IDE type (vscode, cursor, windsurf, trae, unknown)'), 26 | }).optional().describe('System information'), 27 | }).strict(); 28 | 29 | /** 30 | * Health check payload (input parameters) 31 | */ 32 | export type HealthCheckPayload = z.infer; 33 | 34 | /** 35 | * Health check result (output data) 36 | */ 37 | export type HealthCheckResult = z.infer; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/utils/get-usage-code.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * Get usage code around a specific range in a document 5 | * @param uri - Document URI 6 | * @param range - Target range 7 | * @param lineRange - Number of lines to include before and after (0 = only target line, -1 = no code) 8 | * @returns Usage code string or undefined if unable to read 9 | */ 10 | export async function getUsageCode(uri: vscode.Uri, range: vscode.Range, lineRange: number = 5): Promise { 11 | try { 12 | const document = await vscode.workspace.openTextDocument(uri); 13 | 14 | if (lineRange === -1) { 15 | // No usage code requested 16 | return undefined; 17 | } 18 | 19 | if (lineRange === 0) { 20 | // Only return the reference line 21 | return document.lineAt(range.start.line).text; 22 | } 23 | 24 | // Calculate start and end lines with boundaries 25 | const startLine = Math.max(0, range.start.line - lineRange); 26 | const endLine = Math.min(document.lineCount - 1, range.start.line + lineRange); 27 | 28 | // Get the range of lines 29 | const lines: string[] = []; 30 | for (let i = startLine; i <= endLine; i++) { 31 | lines.push(document.lineAt(i).text); 32 | } 33 | 34 | return lines.join('\n'); 35 | } catch { 36 | // If we can't read the file, return undefined (no usage code) 37 | return undefined; 38 | } 39 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/services/health.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | 3 | import type { EventParams, EventResult } from '@vscode-mcp/vscode-mcp-ipc'; 4 | import * as vscode from 'vscode'; 5 | 6 | import packageJson from '../../package.json'; 7 | import { detectIdeType } from '../utils/detect-ide'; 8 | import { getCurrentWorkspacePath } from '../utils/workspace'; 9 | 10 | /** 11 | * Handle health check 12 | */ 13 | export const health = async ( 14 | _payload: EventParams<'health'> 15 | ): Promise> => { 16 | try { 17 | const workspacePath = getCurrentWorkspacePath(); 18 | const ideType = await detectIdeType(); 19 | 20 | return { 21 | status: 'ok', 22 | extension_version: packageJson.version, 23 | workspace: workspacePath || undefined, 24 | timestamp: new Date().toISOString(), 25 | system_info: { 26 | platform: os.platform(), 27 | node_version: process.version, 28 | vscode_version: vscode.version, 29 | ide_type: ideType 30 | } 31 | }; 32 | } catch (error) { 33 | return { 34 | status: 'error', 35 | extension_version: packageJson.version, 36 | timestamp: new Date().toISOString(), 37 | error: `Health check failed: ${error instanceof Error ? error.message : String(error)}`, 38 | system_info: { 39 | platform: os.platform(), 40 | node_version: process.version, 41 | vscode_version: vscode.version, 42 | ide_type: 'unknown' 43 | } 44 | }; 45 | } 46 | }; -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/open-file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Open files event types and schemas 3 | */ 4 | 5 | import { z } from 'zod'; 6 | 7 | import { FilePathSchema } from '../common.js'; 8 | 9 | /** 10 | * Single file open request 11 | */ 12 | const FileOpenRequestSchema = z.object({ 13 | filePath: FilePathSchema.describe('File path to open'), 14 | showEditor: z.boolean().optional().default(true).describe('Whether to show the file in editor (default: true)'), 15 | }).strict(); 16 | 17 | /** 18 | * Single file open result 19 | */ 20 | const FileOpenResultSchema = z.object({ 21 | filePath: z.string().describe('File path that was processed'), 22 | success: z.boolean().describe('Whether the file was opened successfully'), 23 | message: z.string().optional().describe('Optional message about the operation'), 24 | }).strict(); 25 | 26 | /** 27 | * Open files input schema 28 | */ 29 | export const OpenFilesInputSchema = z.object({ 30 | files: z.array(FileOpenRequestSchema).describe('Array of files to open'), 31 | }).strict(); 32 | 33 | /** 34 | * Open files output schema 35 | */ 36 | export const OpenFilesOutputSchema = z.object({ 37 | results: z.array(FileOpenResultSchema).describe('Results for each file open operation'), 38 | }).strict(); 39 | 40 | /** 41 | * Open files payload (input parameters) 42 | */ 43 | export type OpenFilesPayload = z.infer; 44 | 45 | /** 46 | * Open files result (output data) 47 | */ 48 | export type OpenFilesResult = z.infer; 49 | 50 | /** 51 | * Individual file open request type 52 | */ 53 | export type FileOpenRequest = z.infer; 54 | 55 | /** 56 | * Individual file open result type 57 | */ 58 | export type FileOpenResult = z.infer; -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common types and schemas used across events 3 | */ 4 | 5 | import { z } from 'zod'; 6 | 7 | /** 8 | * Position schema 9 | */ 10 | export const PositionSchema = z.object({ 11 | line: z.number(), 12 | character: z.number(), 13 | }).strict(); 14 | 15 | /** 16 | * Range schema 17 | */ 18 | export const RangeSchema = z.object({ 19 | start: PositionSchema, 20 | end: PositionSchema, 21 | }).strict(); 22 | 23 | /** 24 | * Location schema 25 | */ 26 | export const LocationSchema = z.object({ 27 | uri: z.string(), 28 | range: RangeSchema, 29 | usageCode: z.string().optional().describe('Usage context code around the reference location (included based on usageCodeLineRange parameter)'), 30 | }).strict(); 31 | 32 | /** 33 | * Position type 34 | */ 35 | export type Position = z.infer; 36 | 37 | /** 38 | * Range type 39 | */ 40 | export type Range = z.infer; 41 | 42 | /** 43 | * Location type 44 | */ 45 | export type Location = z.infer; 46 | 47 | 48 | /** 49 | * File path schema - supports both absolute and relative paths 50 | */ 51 | export const FilePathSchema = z.string().describe('File path (absolute or relative to workspace root)'); 52 | 53 | /** 54 | * Symbol locator schema - common fields for locating symbols in code 55 | */ 56 | export const SymbolLocatorSchema = z.object({ 57 | filePath: FilePathSchema, 58 | symbol: z.string().describe('Symbol name'), 59 | codeSnippet: z.string().optional().describe(`Optional code snippet to help precisely locate the symbol when there are multiple symbols with the same name. Eg: "function getUserName()" when locating the symbol "getUserName"`), 60 | }).strict(); 61 | 62 | /** 63 | * Symbol locator type 64 | */ 65 | export type SymbolLocator = z.infer; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/utils/detect-ide.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export type IDE = 'vscode' | 'cursor' | 'windsurf' | 'trae' | 'unknown'; 4 | 5 | 6 | /** 7 | * Get the current IDE type based on environment detection 8 | * Based on stagewise project implementation 9 | */ 10 | export function getCurrentIDE(): IDE { 11 | const appName = vscode.env.appName.toLowerCase(); 12 | 13 | if (appName.includes('windsurf')) { 14 | return 'windsurf'; 15 | } else if (appName.includes('cursor')) { 16 | return 'cursor'; 17 | } else if (appName.includes('trae')) { 18 | return 'trae'; 19 | } else if (appName.includes('visual studio code')) { 20 | return 'vscode'; 21 | } 22 | 23 | return 'unknown'; 24 | } 25 | 26 | /** 27 | * Detect IDE type asynchronously with additional command checking 28 | */ 29 | export async function detectIdeType(): Promise { 30 | try { 31 | // First try direct environment detection 32 | const directDetection = getCurrentIDE(); 33 | if (directDetection !== 'unknown') { 34 | return directDetection; 35 | } 36 | 37 | // Fallback to command-based detection 38 | const commands = await vscode.commands.getCommands(); 39 | 40 | // Check for Cursor-specific commands 41 | if (commands.some(cmd => cmd.includes('composer'))) { 42 | return 'cursor'; 43 | } 44 | 45 | // Check for Windsurf-specific commands 46 | if (commands.some(cmd => cmd.includes('windsurf'))) { 47 | return 'windsurf'; 48 | } 49 | 50 | // Check for Trae-specific commands 51 | if (commands.some(cmd => cmd.includes('icube'))) { 52 | return 'trae'; 53 | } 54 | 55 | // Default to vscode if we can't detect anything specific 56 | return 'vscode'; 57 | } catch (error) { 58 | console.warn('Failed to detect IDE type:', error); 59 | return 'vscode'; 60 | } 61 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/services/open-files.ts: -------------------------------------------------------------------------------- 1 | import type { EventParams, EventResult } from '@vscode-mcp/vscode-mcp-ipc'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { resolveFilePath } from '../utils/workspace.js'; 5 | 6 | /** 7 | * Handle opening multiple files 8 | */ 9 | export const openFiles = async ( 10 | payload: EventParams<'openFiles'> 11 | ): Promise> => { 12 | const results = await Promise.all( 13 | payload.files.map(async (fileRequest) => { 14 | try { 15 | const uri = resolveFilePath(fileRequest.filePath); 16 | 17 | // Always open the document to load it 18 | const document = await vscode.workspace.openTextDocument(uri); 19 | 20 | // Conditionally show in editor based on showEditor parameter 21 | const showEditor = fileRequest.showEditor ?? true; // Default to true 22 | if (showEditor) { 23 | await vscode.window.showTextDocument(document, { 24 | preview: false, // Don't use preview mode 25 | preserveFocus: false // Give focus to the opened file 26 | }); 27 | } 28 | 29 | return { 30 | filePath: fileRequest.filePath, 31 | success: true, 32 | message: showEditor 33 | ? 'File opened and displayed in editor' 34 | : 'File opened in background' 35 | }; 36 | } catch (error) { 37 | return { 38 | filePath: fileRequest.filePath, 39 | success: false, 40 | message: `Failed to open file: ${String(error)}` 41 | }; 42 | } 43 | }) 44 | ); 45 | 46 | return { results }; 47 | }; -------------------------------------------------------------------------------- /.cursor/rules/typescript.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: TypeScript code style and optimization guidelines 3 | globs: *.ts 4 | alwaysApply: false 5 | --- 6 | 7 | # TypeScript Code Style Guide 8 | 9 | ## Types and Type Safety 10 | 11 | - Avoid explicit type annotations when TypeScript can infer types. 12 | - Avoid implicitly `any` variables; explicitly type when necessary (e.g., `let a: number` instead of `let a`). 13 | - Use the most accurate type possible (e.g., prefer `Record` over `object`). 14 | - Prefer `interface` over `type` for object shapes (e.g., React component props). Keep `type` for unions, intersections, and utility types. 15 | - Prefer `as const satisfies XyzInterface` over plain `as const` when suitable. 16 | 17 | ## Asynchronous Patterns and Concurrency 18 | 19 | - Prefer `async`/`await` over callbacks or chained `.then` promises. 20 | - Prefer async APIs over sync ones (avoid `*Sync`). 21 | - Prefer promise-based variants (e.g., `import { readFile } from 'fs/promises'`) over callback-based APIs from `fs`. 22 | - Where safe, convert sequential async flows to concurrent ones with `Promise.all`, `Promise.race`, etc. 23 | 24 | ## Code Structure and Readability 25 | 26 | - Refactor repeated logic into reusable functions. 27 | - Prefer object destructuring when accessing and using properties. 28 | - Use consistent, descriptive naming; avoid obscure abbreviations. 29 | - Use semantically meaningful variable, function, and class names. 30 | - Replace magic numbers or strings with well-named constants. 31 | - Keep meaningful code comments; do not remove them when applying edits. Update comments when behavior changes. 32 | - Ensure JSDoc comments accurately reflect the implementation. 33 | - Look for opportunities to simplify or modernize code with the latest JavaScript/TypeScript features where it improves clarity. 34 | - Defer formatting to tooling; ignore purely formatting-only issues and autofixable lint problems. 35 | - Respect project Prettier settings. 36 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Dev", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--disable-extensions", 14 | "--extensionDevelopmentPath=${workspaceFolder}", 15 | "${workspaceFolder}/test-workspace" 16 | ], 17 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 18 | "skipFiles": [ 19 | "/**", 20 | "**/node_modules/**", 21 | "**/resources/app/out/vs/**", 22 | "**/.vscode-insiders/extensions/", 23 | "**/.vscode/extensions/" 24 | ], 25 | "sourceMaps": true, 26 | "env": { 27 | "VSCODE_DEBUG_MODE": "true" 28 | }, 29 | "preLaunchTask": "${defaultBuildTask}" 30 | }, 31 | { 32 | "name": "Test", 33 | "type": "extensionHost", 34 | "request": "launch", 35 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 36 | "skipFiles": [ 37 | "/**", 38 | "**/node_modules/**", 39 | "**/resources/app/out/vs/**", 40 | "**/.vscode-insiders/extensions/", 41 | "**/.vscode/extensions/" 42 | ], 43 | "sourceMaps": true, 44 | "args": [ 45 | "--extensionDevelopmentPath=${workspaceFolder}", 46 | "--extensionTestsPath=${workspaceFolder}/out/test/" 47 | ], 48 | "env": { 49 | "VSCODE_DEBUG_MODE": "true" 50 | }, 51 | "preLaunchTask": "compile:test" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/get-diagnostics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get diagnostics event types and schemas 3 | */ 4 | 5 | import { z } from 'zod'; 6 | 7 | import { FilePathSchema, RangeSchema } from '../common.js'; 8 | 9 | /** 10 | * Diagnostic schema 11 | */ 12 | const DiagnosticSchema = z.object({ 13 | range: RangeSchema, 14 | message: z.string(), 15 | severity: z.enum(['error', 'warning', 'info', 'hint']), 16 | source: z.string().optional(), 17 | code: z.union([z.string(), z.number()]).optional(), 18 | }).strict(); 19 | 20 | /** 21 | * Single file diagnostic result 22 | */ 23 | const FileDiagnosticSchema = z.object({ 24 | uri: z.string(), 25 | diagnostics: z.array(DiagnosticSchema), 26 | }).strict(); 27 | 28 | /** 29 | * Get diagnostics input schema 30 | */ 31 | export const GetDiagnosticsInputSchema = z.object({ 32 | __NOT_RECOMMEND__filePaths: z.array(FilePathSchema).describe('Array of file paths to get diagnostics for. If empty array is provided, will get diagnostics for all git modified files (staged and unstaged) in the workspace.'), 33 | sources: z.array(z.string()).optional().default([]).describe('Array of diagnostic sources to include (e.g., "eslint", "ts", "typescript"). If empty array provided, includes all sources.'), 34 | severities: z.array(z.enum(['error', 'warning', 'info', 'hint'])).optional().default(['error', 'warning', 'info', 'hint']).describe('Array of severity levels to include. Supported values: "error", "warning", "info", "hint".'), 35 | }).strict(); 36 | 37 | /** 38 | * Get diagnostics output schema 39 | */ 40 | export const GetDiagnosticsOutputSchema = z.object({ 41 | files: z.array(FileDiagnosticSchema), 42 | }).strict(); 43 | 44 | /** 45 | * Get diagnostics payload (input parameters) 46 | */ 47 | export type GetDiagnosticsPayload = z.infer; 48 | 49 | /** 50 | * Get diagnostics result (output data) 51 | */ 52 | export type GetDiagnosticsResult = z.infer; 53 | 54 | /** 55 | * Diagnostic types (for backward compatibility) 56 | */ 57 | export type Diagnostic = z.infer; 58 | 59 | /** 60 | * File diagnostic type 61 | */ 62 | export type FileDiagnostic = z.infer; -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/README.md: -------------------------------------------------------------------------------- 1 | # vscode-mcp-ipc 2 | 3 | IPC communication layer between MCP Server and VSCode extension. 4 | 5 | ## 功能 6 | 7 | - 定义 MCP Server 和 VSCode 扩展之间的事件和数据结构 8 | - 提供 Unix Socket 通信的封装 9 | - 支持类型安全的事件调度 10 | 11 | ## 使用方法 12 | 13 | ### 创建 Dispatcher 14 | 15 | ```typescript 16 | import { createDispatcher } from '@vscode-mcp/vscode-mcp-ipc'; 17 | 18 | const dispatcher = createDispatcher('/path/to/workspace'); 19 | ``` 20 | 21 | ### 发送事件 22 | 23 | ```typescript 24 | // 获取诊断信息 25 | const diagnostics = await dispatcher.dispatch('getDiagnostics', { 26 | uri: 'file:///path/to/file.ts', 27 | }); 28 | 29 | // 获取定义 30 | const definitions = await dispatcher.dispatch('getDefinition', { 31 | uri: 'file:///path/to/file.ts', 32 | line: 10, 33 | character: 5, 34 | }); 35 | 36 | // 健康检查 37 | const health = await dispatcher.dispatch('health', {}); 38 | 39 | // 请求用户输入 40 | const input = await dispatcher.dispatch('requestInput', { 41 | prompt: 'Please enter your API key:', 42 | placeholder: 'Enter API key here...', 43 | title: 'API Configuration', 44 | password: true, 45 | validateInput: true, 46 | }); 47 | 48 | // 执行 VSCode 命令 49 | const result = await dispatcher.dispatch('executeCommand', { 50 | command: 'editor.action.formatDocument', 51 | }); 52 | 53 | // 执行带参数的 VSCode 命令 54 | const openResult = await dispatcher.dispatch('executeCommand', { 55 | command: 'vscode.open', 56 | args: ['file:///path/to/file.ts'], 57 | }); 58 | ``` 59 | 60 | ### 测试连接 61 | 62 | ```typescript 63 | const isConnected = await dispatcher.testConnection(); 64 | if (isConnected) { 65 | console.log('Connected to VSCode extension'); 66 | } else { 67 | console.log('Failed to connect'); 68 | } 69 | ``` 70 | 71 | ## 支持的事件 72 | 73 | 查看 `src/events/index.ts` 中的 `EventMap` 接口来了解所有可用的事件类型和参数。 74 | 75 | 每个事件都有对应的输入和输出类型定义,确保类型安全的通信。 76 | 77 | ## Socket 路径生成 78 | 79 | Socket 路径基于工作区路径的 MD5 哈希生成: 80 | 81 | ```typescript 82 | function generateSocketPath(workspacePath: string): string { 83 | const hash = crypto.createHash('md5').update(workspacePath).digest('hex').slice(0, 8); 84 | return process.platform === 'win32' 85 | ? `\\\\.\\pipe\\vscode-mcp-${hash}` 86 | : path.join(os.tmpdir(), `vscode-mcp-${hash}.sock`); 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/services/get-references.ts: -------------------------------------------------------------------------------- 1 | import type { EventParams, EventResult } from '@vscode-mcp/vscode-mcp-ipc'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { getUsageCode } from '../utils/get-usage-code.js'; 5 | import { resolveSymbolPosition } from '../utils/resolve-symbol-position.js'; 6 | import { ensureFileIsOpen, resolveFilePath } from '../utils/workspace.js'; 7 | 8 | /** 9 | * Handle get references 10 | */ 11 | export const getReferences = async ( 12 | payload: EventParams<'getReferences'> 13 | ): Promise> => { 14 | // Resolve file path to URI 15 | const uri = resolveFilePath(payload.filePath); 16 | 17 | // Ensure file is open to get accurate references 18 | await ensureFileIsOpen(uri.toString()); 19 | 20 | // Resolve symbol to position 21 | const position = await resolveSymbolPosition(uri, payload.symbol, payload.codeSnippet); 22 | 23 | // Execute references provider 24 | const references = await vscode.commands.executeCommand( 25 | 'vscode.executeReferenceProvider', 26 | uri, 27 | position, 28 | { includeDeclaration: payload.includeDeclaration ?? false } 29 | ); 30 | 31 | if (!references || references.length === 0) { 32 | return { locations: [] }; 33 | } 34 | 35 | const locations = await Promise.all( 36 | references.map(async (ref) => { 37 | const location: any = { 38 | uri: ref.uri.toString(), 39 | range: { 40 | start: { line: ref.range.start.line, character: ref.range.start.character }, 41 | end: { line: ref.range.end.line, character: ref.range.end.character } 42 | } 43 | }; 44 | 45 | // Add usage code if requested 46 | if (payload.usageCodeLineRange !== -1) { 47 | const usageCode = await getUsageCode(ref.uri, ref.range, payload.usageCodeLineRange ?? 5); 48 | if (usageCode) { 49 | location.usageCode = usageCode; 50 | } 51 | } 52 | 53 | return location; 54 | }) 55 | ); 56 | 57 | return { locations }; 58 | }; 59 | 60 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/README.md: -------------------------------------------------------------------------------- 1 | # VSCode MCP Bridge 2 | 3 |
4 | 5 | [![Version](https://img.shields.io/visual-studio-marketplace/v/YuTengjing.vscode-mcp-bridge)](https://marketplace.visualstudio.com/items/YuTengjing.vscode-mcp-bridge/changelog) [![Installs](https://img.shields.io/visual-studio-marketplace/i/YuTengjing.vscode-mcp-bridge)](https://marketplace.visualstudio.com/items?itemName=YuTengjing.vscode-mcp-bridge) [![Downloads](https://img.shields.io/visual-studio-marketplace/d/YuTengjing.vscode-mcp-bridge)](https://marketplace.visualstudio.com/items?itemName=YuTengjing.vscode-mcp-bridge) [![Rating Star](https://img.shields.io/visual-studio-marketplace/stars/YuTengjing.vscode-mcp-bridge)](https://marketplace.visualstudio.com/items?itemName=YuTengjing.vscode-mcp-bridge&ssr=false#review-details) [![Last Updated](https://img.shields.io/visual-studio-marketplace/last-updated/YuTengjing.vscode-mcp-bridge)](https://github.com/tjx666/vscode-mcp) 6 | 7 |
8 | 9 | check [VSCode MCP Repository](https://github.com/tjx666/vscode-mcp) for usage and more details. 10 | 11 | ## Commands & Keybindings 12 | 13 | This extension provides several useful commands to enhance your development workflow: 14 | 15 | ### 📋 Copy Opened Files Path 16 | 17 | **Command**: `VSCode MCP Bridge: Copy Opened Files Path` 18 | 19 | use shortcut `alt+cmd+o` to send all opened files path to active terminal. 20 | 21 | ### ⏱️ Sleep 22 | 23 | **Command**: `VSCode MCP Bridge: Sleep` 24 | 25 | Utility command that pauses execution for a specified duration (in seconds). Designed for use in VSCode shortcuts.json `runCommands` sequences to add delays between multiple commands. 26 | 27 | ## My extensions 28 | 29 | - [Open in External App](https://github.com/tjx666/open-in-external-app) 30 | - [VSCode archive](https://github.com/tjx666/vscode-archive) 31 | - [Neo File Utils](https://github.com/tjx666/vscode-neo-file-utils) 32 | - [VSCode FE Helper](https://github.com/tjx666/vscode-fe-helper) 33 | - [Modify File Warning](https://github.com/tjx666/modify-file-warning) 34 | - [Power Edit](https://github.com/tjx666/power-edit) 35 | - [Adobe Extension Development Tools](https://github.com/tjx666/vscode-adobe-extension-devtools) 36 | - [Scripting Listener](https://github.com/tjx666/scripting-listener) 37 | 38 | Check all here: [publishers/YuTengjing](https://marketplace.visualstudio.com/publishers/YuTengjing) 39 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/utils/workspace.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import * as vscode from 'vscode'; 4 | 5 | /** 6 | * Get current workspace path 7 | */ 8 | export function getCurrentWorkspacePath(): string | null { 9 | const workspaceFolders = vscode.workspace.workspaceFolders; 10 | if (!workspaceFolders || workspaceFolders.length === 0) { 11 | return null; 12 | } 13 | 14 | // Use the first workspace folder 15 | return workspaceFolders[0].uri.fsPath; 16 | } 17 | 18 | /** 19 | * Open a file to ensure it's loaded and LSP has processed it 20 | */ 21 | export async function ensureFileIsOpen(uriString: string): Promise { 22 | try { 23 | const uri = vscode.Uri.parse(uriString); 24 | const document = await vscode.workspace.openTextDocument(uri); 25 | 26 | // We don't need to show the document in the editor, just ensure it's loaded 27 | // This will trigger LSP processing and populate accurate language features 28 | await document.save(); // This is just to ensure the document is fully processed 29 | } catch (error) { 30 | // If we can't open the file, we'll still try to get language features 31 | // This might happen with files that are not in the workspace 32 | console.warn(`Could not open file ${uriString}: ${error}`); 33 | } 34 | } 35 | 36 | /** 37 | * Resolve a file path to VSCode URI 38 | * Supports both absolute paths and relative paths (relative to workspace root) 39 | */ 40 | export function resolveFilePath(filePath: string): vscode.Uri { 41 | if (path.isAbsolute(filePath)) { 42 | // Absolute path - convert directly to URI 43 | return vscode.Uri.file(filePath); 44 | } else { 45 | // Relative path - resolve relative to workspace root 46 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 47 | if (!workspaceFolder) { 48 | throw new Error('No workspace folder found for relative path resolution'); 49 | } 50 | const absolutePath = path.join(workspaceFolder.uri.fsPath, filePath); 51 | return vscode.Uri.file(absolutePath); 52 | } 53 | } 54 | 55 | /** 56 | * Convert an array of file paths to VSCode URIs 57 | */ 58 | export function resolveFilePaths(filePaths: string[]): vscode.Uri[] { 59 | return filePaths.map(filePath => resolveFilePath(filePath)); 60 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/execute-command.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { createDispatcher, ExecuteCommandInputSchema } from "@vscode-mcp/vscode-mcp-ipc"; 3 | 4 | import { VscodeMcpToolName } from "../constants.js"; 5 | import { formatToolCallError } from "../utils/format-tool-call-error.js"; 6 | import { workspacePathInputSchema } from "../utils/workspace-schema.js"; 7 | 8 | const inputSchema = { 9 | ...workspacePathInputSchema, 10 | ...ExecuteCommandInputSchema.shape 11 | }; 12 | 13 | const DESCRIPTION = `Execute VSCode commands with arguments 14 | **Common Use Cases:** 15 | - Format code: 'editor.action.formatDocument' with args: '[]' 16 | - Open files: 'vscode.open' with args: '["file:///absolute/path/to/file.ts"]' 17 | - Save all files: 'workbench.action.files.saveAll' with args: '[]' 18 | - Auto-fix issues: 'editor.action.fixAll' with args: '[]' 19 | - Restart TypeScript: 'typescript.restartTsServer' with args: '[]' 20 | - Restart ESLint: 'eslint.restart' with args: '[]' 21 | 22 | **Important Notes:** 23 | - Commands and arguments may change with VSCode updates, it's recommended to search in the VSCode official repository to confirm the command and arguments are correct before use 24 | - Commands like 'reloadWindow', 'reloadExtensionHost' will interrupt conversation 25 | - ⚠️ WARNING: May cause irreversible changes, use with caution 26 | `; 27 | 28 | export function registerExecuteCommand(server: McpServer) { 29 | server.registerTool(VscodeMcpToolName.EXECUTE_COMMAND, { 30 | title: "⚠️ Execute VSCode Command", 31 | description: DESCRIPTION, 32 | inputSchema, 33 | annotations: { 34 | title: "⚠️ Execute VSCode Command", 35 | readOnlyHint: false, 36 | destructiveHint: true, 37 | idempotentHint: false, 38 | openWorldHint: false 39 | } 40 | }, async ({ workspace_path, command, args, saveAllEditors }) => { 41 | try { 42 | const dispatcher = await createDispatcher(workspace_path); 43 | const result = await dispatcher.dispatch("executeCommand", { command, args, saveAllEditors }); 44 | 45 | return { 46 | content: [{ 47 | type: "text" as const, 48 | text: JSON.stringify(result, null, 2) 49 | }] 50 | }; 51 | } catch (error) { 52 | return formatToolCallError("Execute Command", error); 53 | } 54 | }); 55 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/logger.ts: -------------------------------------------------------------------------------- 1 | import type { OutputChannel } from 'vscode'; 2 | import vscode from 'vscode'; 3 | 4 | class Logger { 5 | channel: OutputChannel | undefined; 6 | 7 | constructor(private name = '', private language = 'log') {} 8 | 9 | private _initChannel() { 10 | const prefix = 'VSCode MCP Bridge'; 11 | this.channel = vscode.window.createOutputChannel( 12 | `${prefix} ${this.name}`.trim(), 13 | this.language, 14 | ); 15 | } 16 | 17 | private _output(message: string, level: string): void { 18 | const enableLog = vscode.workspace.getConfiguration().get('vscode-mcp-bridge.enableLog'); 19 | if (!enableLog) return; 20 | 21 | if (this.channel === undefined) { 22 | this._initChannel(); 23 | } 24 | 25 | this.channel!.append(`[${level}] ${message}\n`); 26 | } 27 | 28 | private _formatJson(obj: any): string { 29 | try { 30 | return JSON.stringify(obj, null, 2); 31 | } catch { 32 | return String(obj); 33 | } 34 | } 35 | 36 | info(message: string) { 37 | this._output(message, 'INFO'); 38 | } 39 | 40 | error(message: string) { 41 | this._output(message, 'ERROR'); 42 | } 43 | 44 | /** 45 | * Log service call with structured information 46 | */ 47 | logServiceCall(method: string, payload: any, result: any) { 48 | const timestamp = new Date().toISOString(); 49 | const message = [ 50 | `[${method}] Service Call at ${timestamp}`, 51 | `Request: ${this._formatJson(payload)}`, 52 | `Response: ${this._formatJson(result)}`, 53 | '---' 54 | ].join('\n'); 55 | 56 | this._output(message, 'INFO'); 57 | } 58 | 59 | /** 60 | * Log service error with structured information 61 | */ 62 | logServiceError(method: string, payload: any, error: any) { 63 | const timestamp = new Date().toISOString(); 64 | const message = [ 65 | `[${method}] Service Error at ${timestamp}`, 66 | `Request: ${this._formatJson(payload)}`, 67 | `Error: ${String(error)}`, 68 | '---' 69 | ].join('\n'); 70 | 71 | this._output(message, 'ERROR'); 72 | } 73 | 74 | dispose(): void { 75 | this.channel?.dispose(); 76 | } 77 | } 78 | 79 | export const logger = new Logger(); -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/list-workspaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List workspaces event types and schemas 3 | */ 4 | 5 | import { z } from 'zod'; 6 | 7 | /** 8 | * List workspaces input schema 9 | */ 10 | export const ListWorkspacesInputSchema = z.object({}).strict(); 11 | 12 | /** 13 | * Workspace info schema 14 | */ 15 | export const WorkspaceInfoSchema = z.object({ 16 | workspace_path: z.string().describe('Workspace path or identifier'), 17 | workspace_name: z.string().optional().describe('Workspace friendly name'), 18 | workspace_type: z.enum(['single-folder', 'multi-folder', 'workspace-file']).optional() 19 | .describe('Type of VSCode workspace'), 20 | folders: z.array(z.string()).optional() 21 | .describe('List of folders in multi-folder workspace'), 22 | status: z.enum(['active', 'available', 'error']) 23 | .describe('Connection status of the workspace'), 24 | extension_version: z.string().optional() 25 | .describe('VSCode MCP Bridge extension version'), 26 | vscode_version: z.string().optional() 27 | .describe('VSCode version'), 28 | ide_type: z.string().optional() 29 | .describe('IDE type (vscode, cursor, windsurf, trae, unknown)'), 30 | socket_path: z.string().optional() 31 | .describe('Socket file path (for debugging)'), 32 | error: z.string().optional() 33 | .describe('Error message if status is error'), 34 | last_seen: z.string().optional() 35 | .describe('Last active timestamp') 36 | }).strict(); 37 | 38 | /** 39 | * List workspaces output schema 40 | */ 41 | export const ListWorkspacesOutputSchema = z.object({ 42 | workspaces: z.array(WorkspaceInfoSchema) 43 | .describe('List of discovered workspaces'), 44 | summary: z.object({ 45 | total: z.number().describe('Total number of workspaces found'), 46 | active: z.number().describe('Number of active workspaces'), 47 | available: z.number().describe('Number of available but untested workspaces'), 48 | cleaned: z.number().describe('Number of zombie sockets cleaned') 49 | }).strict().describe('Summary statistics') 50 | }).strict(); 51 | 52 | /** 53 | * List workspaces payload (input parameters) 54 | */ 55 | export type ListWorkspacesPayload = z.infer; 56 | 57 | /** 58 | * List workspaces result (output data) 59 | */ 60 | export type ListWorkspacesResult = z.infer; 61 | 62 | /** 63 | * Workspace info type 64 | */ 65 | export type WorkspaceInfo = z.infer; -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/open-files.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { createDispatcher, OpenFilesInputSchema } from "@vscode-mcp/vscode-mcp-ipc"; 3 | 4 | import { VscodeMcpToolName } from "../constants.js"; 5 | import { formatToolCallError } from "../utils/format-tool-call-error.js"; 6 | import { workspacePathInputSchema } from "../utils/workspace-schema.js"; 7 | 8 | const inputSchema = { 9 | ...workspacePathInputSchema, 10 | ...OpenFilesInputSchema.shape 11 | }; 12 | 13 | const DESCRIPTION = `Open files in vscode` 14 | export function registerOpenFiles(server: McpServer) { 15 | server.registerTool(VscodeMcpToolName.OPEN_FILES, { 16 | title: "Open Files", 17 | description: DESCRIPTION, 18 | inputSchema, 19 | annotations: { 20 | title: "Open Files", 21 | readOnlyHint: true, 22 | destructiveHint: false, 23 | idempotentHint: true, 24 | openWorldHint: false 25 | } 26 | }, async ({ workspace_path, files }) => { 27 | const dispatcher = await createDispatcher(workspace_path); 28 | 29 | try { 30 | const result = await dispatcher.dispatch("openFiles", { files }); 31 | 32 | // Handle case where no files were provided 33 | if (result.results.length === 0) { 34 | return { 35 | content: [{ 36 | type: "text", 37 | text: "📄 No files provided to open." 38 | }] 39 | }; 40 | } 41 | 42 | // Count successful and failed operations 43 | const successful = result.results.filter(r => r.success); 44 | const failed = result.results.filter(r => !r.success); 45 | 46 | // Format output for better readability 47 | const output = result.results.map(fileResult => { 48 | if (fileResult.success) { 49 | return `✅ ${fileResult.filePath}\n ${fileResult.message}`; 50 | } else { 51 | return `❌ ${fileResult.filePath}\n ${fileResult.message}`; 52 | } 53 | }).join('\n\n'); 54 | 55 | const summary = `📁 Opened ${successful.length}/${result.results.length} files successfully${ 56 | failed.length > 0 ? ` (${failed.length} failed)` : '' 57 | }:\n\n${ output}`; 58 | 59 | return { 60 | content: [{ 61 | type: "text", 62 | text: summary 63 | }] 64 | }; 65 | } catch (error) { 66 | return formatToolCallError("Open Files", error); 67 | } 68 | }); 69 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/utils/file-safety-check.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | 4 | import * as vscode from 'vscode'; 5 | 6 | import { logger } from '../logger.js'; 7 | 8 | const execAsync = promisify(exec); 9 | 10 | export interface FileSafetyResult { 11 | safe: boolean; 12 | error?: string; 13 | } 14 | 15 | /** 16 | * Check if a file is safe to operate on (rename/remove) 17 | * Safety criteria: 18 | * 1. File must be within workspace boundaries 19 | * 2. File must be tracked by git (committed or staged) 20 | */ 21 | export async function checkFileSafety(uri: vscode.Uri): Promise { 22 | // 1. Check workspace boundaries 23 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); 24 | if (!workspaceFolder) { 25 | return { 26 | safe: false, 27 | error: 'File is outside workspace boundaries - only workspace files can be modified for safety', 28 | }; 29 | } 30 | 31 | // 2. Check git tracking status 32 | const gitResult = await checkGitTracking(uri, workspaceFolder); 33 | if (!gitResult.safe) { 34 | return gitResult; 35 | } 36 | 37 | return { safe: true }; 38 | } 39 | 40 | /** 41 | * Check if file is tracked by git (committed or staged) 42 | */ 43 | async function checkGitTracking(uri: vscode.Uri, workspaceFolder: vscode.WorkspaceFolder): Promise { 44 | try { 45 | const relativePath = vscode.workspace.asRelativePath(uri, false); 46 | const workspacePath = workspaceFolder.uri.fsPath; 47 | 48 | // Check if file is tracked by git (either committed or staged) 49 | const { stdout: lsFilesOutput } = await execAsync( 50 | `git ls-files --cached "${relativePath}"`, 51 | { cwd: workspacePath } 52 | ); 53 | 54 | if (lsFilesOutput.trim()) { 55 | // File is tracked (committed or staged) 56 | return { safe: true }; 57 | } 58 | 59 | // File is not tracked by git 60 | return { 61 | safe: false, 62 | error: 'File is not tracked by git - only git-tracked files can be modified for safety. Please add the file to git first.', 63 | }; 64 | 65 | } catch (error) { 66 | logger.info(`Git check failed for ${uri.fsPath}: ${error}`); 67 | 68 | // If git is not available or not a git repository, be more permissive 69 | // but only for files within workspace 70 | return { 71 | safe: false, 72 | error: 'Unable to verify git tracking status - this might not be a git repository. For safety, only git-tracked files can be modified.', 73 | }; 74 | } 75 | } -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | actions: read # Required for Claude to read CI results on PRs 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 1 32 | 33 | - name: Run Claude Code 34 | id: claude 35 | uses: anthropics/claude-code-action@beta 36 | with: 37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 38 | 39 | # This is an optional setting that allows Claude to read CI results on PRs 40 | additional_permissions: | 41 | actions: read 42 | 43 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1) 44 | # model: "claude-opus-4-1-20250805" 45 | 46 | # Optional: Customize the trigger phrase (default: @claude) 47 | # trigger_phrase: "/claude" 48 | 49 | # Optional: Trigger when specific user is assigned to an issue 50 | # assignee_trigger: "claude-bot" 51 | 52 | # Optional: Allow Claude to run specific commands 53 | # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" 54 | 55 | # Optional: Add custom instructions for Claude to customize its behavior for your project 56 | # custom_instructions: | 57 | # Follow our coding standards 58 | # Ensure all new code has tests 59 | # Use TypeScript for new files 60 | 61 | # Optional: Custom environment variables for Claude 62 | # claude_env: | 63 | # NODE_ENV: test 64 | 65 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/server.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | 3 | import { VscodeMcpToolName } from "./constants.js"; 4 | import { 5 | registerExecuteCommand, 6 | registerGetDiagnostics, 7 | registerGetReferences, 8 | registerGetSymbolLSPInfo, 9 | registerHealthCheck, 10 | registerListWorkspaces, 11 | registerOpenFiles, 12 | registerRenameSymbol, 13 | } from "./tools/index.js"; 14 | 15 | // Tool registration mapping 16 | type ToolRegistrationFunction = (server: McpServer, ...args: any[]) => void; 17 | 18 | const TOOL_REGISTRY: Record = { 19 | [VscodeMcpToolName.HEALTH_CHECK]: (server: McpServer, version: string) => registerHealthCheck(server, version), 20 | [VscodeMcpToolName.GET_DIAGNOSTICS]: registerGetDiagnostics, 21 | [VscodeMcpToolName.GET_SYMBOL_LSP_INFO]: registerGetSymbolLSPInfo, 22 | [VscodeMcpToolName.GET_REFERENCES]: registerGetReferences, 23 | [VscodeMcpToolName.EXECUTE_COMMAND]: registerExecuteCommand, 24 | [VscodeMcpToolName.OPEN_FILES]: registerOpenFiles, 25 | [VscodeMcpToolName.RENAME_SYMBOL]: registerRenameSymbol, 26 | [VscodeMcpToolName.LIST_WORKSPACES]: registerListWorkspaces, 27 | }; 28 | 29 | /** 30 | * Create and configure the VSCode MCP Server 31 | */ 32 | export function createVSCodeMCPServer( 33 | name: string, 34 | version: string, 35 | enabledTools: string[] = [], 36 | disabledTools: string[] = [] 37 | ): McpServer { 38 | const server = new McpServer({ 39 | name, 40 | version 41 | }); 42 | 43 | const enabledSet = new Set(enabledTools); 44 | const disabledSet = new Set(disabledTools); 45 | const registeredTools: string[] = []; 46 | const skippedTools: string[] = []; 47 | 48 | // Register tools conditionally 49 | for (const [toolName, registerFunction] of Object.entries(TOOL_REGISTRY)) { 50 | // If enabledTools is specified, only register those tools 51 | // Then exclude disabledTools 52 | const shouldRegister = 53 | (enabledTools.length === 0 || enabledSet.has(toolName)) && 54 | !disabledSet.has(toolName); 55 | 56 | if (!shouldRegister) { 57 | skippedTools.push(toolName); 58 | } else { 59 | if (toolName === VscodeMcpToolName.HEALTH_CHECK) { 60 | registerFunction(server, version); 61 | } else { 62 | registerFunction(server); 63 | } 64 | registeredTools.push(toolName); 65 | } 66 | } 67 | 68 | // Log registration results 69 | console.error(`✅ Registered tools: ${registeredTools.join(', ')}`); 70 | if (skippedTools.length > 0) { 71 | console.error(`⏭️ Skipped tools: ${skippedTools.join(', ')}`); 72 | } 73 | 74 | return server; 75 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/get-references.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { createDispatcher, GetReferencesInputSchema } from "@vscode-mcp/vscode-mcp-ipc"; 3 | 4 | import { VscodeMcpToolName } from "../constants.js"; 5 | import { formatToolCallError } from "../utils/format-tool-call-error.js"; 6 | import { workspacePathInputSchema } from "../utils/workspace-schema.js"; 7 | 8 | const inputSchema = { 9 | ...workspacePathInputSchema, 10 | ...GetReferencesInputSchema.shape 11 | }; 12 | 13 | const DESCRIPTION = `Find all reference locations of a symbol (variable, function, class, etc.) across the codebase 14 | 15 | **Return Format:** 16 | Array of reference locations with file paths and exact positions 17 | `; 18 | 19 | export function registerGetReferences(server: McpServer) { 20 | server.registerTool(VscodeMcpToolName.GET_REFERENCES, { 21 | title: "Get References", 22 | description: DESCRIPTION, 23 | inputSchema, 24 | annotations: { 25 | title: "Get References", 26 | readOnlyHint: true, 27 | destructiveHint: false, 28 | idempotentHint: true, 29 | openWorldHint: false 30 | } 31 | }, async ({ workspace_path, filePath, symbol, codeSnippet, includeDeclaration, usageCodeLineRange }) => { 32 | try { 33 | const dispatcher = await createDispatcher(workspace_path); 34 | const result = await dispatcher.dispatch("getReferences", { 35 | filePath, 36 | symbol, 37 | codeSnippet, 38 | includeDeclaration, 39 | usageCodeLineRange 40 | }); 41 | 42 | // Check if result is empty and provide helpful feedback 43 | if (result.locations && result.locations.length === 0) { 44 | return { 45 | content: [{ 46 | type: "text" as const, 47 | text: `❌ No references found for symbol "${symbol}" in ${filePath} 48 | 49 | 💡 **Troubleshooting Tips:** 50 | - Make sure the symbol name is spelled correctly 51 | - Try providing a codeSnippet if there are multiple symbols with the same name 52 | - Try setting includeDeclaration: true to include the symbol definition itself 53 | - Verify the file URI is correct and the file exists 54 | - Ensure the language server extension is installed and running 55 | - Some symbols may not have references if they're unused or only declared 56 | 57 | 📄 **Raw Result:** 58 | ${JSON.stringify(result, null, 2)}` 59 | }] 60 | }; 61 | } 62 | 63 | return { 64 | content: [{ 65 | type: "text" as const, 66 | text: JSON.stringify(result, null, 2) 67 | }] 68 | }; 69 | } catch (error) { 70 | return formatToolCallError("Get References", error); 71 | } 72 | }); 73 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/scripts/esbuild.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | 3 | import fs from 'node:fs/promises'; 4 | import path from 'node:path'; 5 | 6 | import type { BuildContext, BuildOptions } from 'esbuild'; 7 | import esbuild from 'esbuild'; 8 | 9 | const isWatchMode = process.argv.includes('--watch'); 10 | const options: BuildOptions = { 11 | color: true, 12 | logLevel: 'info', 13 | entryPoints: ['src/extension.ts'], 14 | bundle: true, 15 | metafile: process.argv.includes('--metafile'), 16 | outdir: './out/src', 17 | external: [ 18 | 'vscode', 19 | 'typescript', // vue-component-meta 20 | ], 21 | format: 'cjs', 22 | platform: 'node', 23 | target: 'ESNext', 24 | tsconfig: './src/tsconfig.json', 25 | sourcemap: process.argv.includes('--sourcemap'), 26 | minify: process.argv.includes('--minify'), 27 | plugins: [ 28 | { 29 | name: 'umd2esm', 30 | setup(build) { 31 | build.onResolve({ filter: /^(vscode-.*|estree-walker|jsonc-parser)/ }, (args) => { 32 | const pathUmdMay = require.resolve(args.path, { 33 | paths: [args.resolveDir], 34 | }); 35 | // Call twice the replace is to solve the problem of the path in Windows 36 | const pathEsm = pathUmdMay 37 | .replace('/umd/', '/esm/') 38 | .replace('\\umd\\', '\\esm\\'); 39 | return { path: pathEsm }; 40 | }); 41 | }, 42 | }, 43 | { 44 | name: 'meta', 45 | setup(build) { 46 | build.onEnd(async (result) => { 47 | if (result.metafile && result.errors.length === 0) { 48 | return fs.writeFile( 49 | path.resolve(__dirname, '../meta.json'), 50 | JSON.stringify(result.metafile), 51 | ); 52 | } 53 | }); 54 | }, 55 | }, 56 | ], 57 | }; 58 | 59 | async function main() { 60 | let ctx: BuildContext | undefined; 61 | try { 62 | if (isWatchMode) { 63 | ctx = await esbuild.context(options); 64 | await ctx.watch(); 65 | } else { 66 | const result = await esbuild.build(options); 67 | if (process.argv.includes('--analyze')) { 68 | const chunksTree = await esbuild.analyzeMetafile(result.metafile!, { color: true }); 69 | console.log(chunksTree); 70 | } 71 | } 72 | } catch (error) { 73 | console.error(error); 74 | ctx?.dispose(); 75 | process.exit(1); 76 | } 77 | } 78 | 79 | main(); 80 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/rename-symbol.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { createDispatcher, RenameSymbolInputSchema } from "@vscode-mcp/vscode-mcp-ipc"; 3 | 4 | import { VscodeMcpToolName } from "../constants.js"; 5 | import { formatToolCallError } from "../utils/format-tool-call-error.js"; 6 | import { workspacePathInputSchema } from "../utils/workspace-schema.js"; 7 | 8 | const inputSchema = { 9 | ...workspacePathInputSchema, 10 | ...RenameSymbolInputSchema.shape 11 | }; 12 | const DESCRIPTION = `Rename a symbol with VSCode F2 ability 13 | 14 | Advantages over search and replace: 15 | 16 | - Faster 17 | - More accurate 18 | - Automatically updates imports and reference locations 19 | 20 | **Important Notes:** 21 | - Some symbols may not be renameable (e.g., built-in types, external libraries)`; 22 | 23 | export function registerRenameSymbol(server: McpServer) { 24 | server.registerTool(VscodeMcpToolName.RENAME_SYMBOL, { 25 | title: "⚠️ Rename Symbol", 26 | description: DESCRIPTION, 27 | inputSchema, 28 | annotations: { 29 | title: "⚠️ Rename Symbol", 30 | readOnlyHint: false, 31 | destructiveHint: true, 32 | idempotentHint: false, 33 | openWorldHint: false 34 | } 35 | }, async ({ workspace_path, filePath, symbol, codeSnippet, newName }) => { 36 | const dispatcher = await createDispatcher(workspace_path); 37 | 38 | try { 39 | const result = await dispatcher.dispatch("renameSymbol", { filePath, symbol, codeSnippet, newName }); 40 | 41 | if (result.success) { 42 | const filesList = result.modifiedFiles 43 | .map(f => ` 📄 ${f.uri} (${f.changeCount} changes)`) 44 | .join('\n'); 45 | 46 | return { 47 | content: [{ 48 | type: "text", 49 | text: `✅ Successfully renamed '${result.symbolName}' to '${newName}' 50 | 51 | 📊 Summary: 52 | - Total changes: ${result.totalChanges} 53 | - Modified files: ${result.modifiedFiles.length} 54 | 55 | 📁 Files modified: 56 | ${filesList}` 57 | }] 58 | }; 59 | } else { 60 | return { 61 | content: [{ 62 | type: "text", 63 | text: `❌ Rename failed: ${result.error} 64 | 65 | 💡 **Troubleshooting Tips:** 66 | - Make sure the symbol name is spelled correctly 67 | - Try providing a codeSnippet if there are multiple symbols with the same name 68 | - Some symbols cannot be renamed (e.g., built-in types, external libraries) 69 | - Verify the file URI is correct and the file exists 70 | - Ensure the language server extension is installed and running` 71 | }] 72 | }; 73 | } 74 | } catch (error) { 75 | return formatToolCallError("Rename Symbol", error); 76 | } 77 | }); 78 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | "Anti 996" License Version 1.0 (Draft) 4 | 5 | Permission is hereby granted to any individual or legal entity 6 | obtaining a copy of this licensed work (including the source code, 7 | documentation and/or related items, hereinafter collectively referred 8 | to as the "licensed work"), free of charge, to deal with the licensed 9 | work for any purpose, including without limitation, the rights to use, 10 | reproduce, modify, prepare derivative works of, distribute, publish 11 | and sublicense the licensed work, subject to the following conditions: 12 | 13 | 1. The individual or the legal entity must conspicuously display, 14 | without modification, this License and the notice on each redistributed 15 | or derivative copy of the Licensed Work. 16 | 17 | 2. The individual or the legal entity must strictly comply with all 18 | applicable laws, regulations, rules and standards of the jurisdiction 19 | relating to labor and employment where the individual is physically 20 | located or where the individual was born or naturalized; or where the 21 | legal entity is registered or is operating (whichever is stricter). In 22 | case that the jurisdiction has no such laws, regulations, rules and 23 | standards or its laws, regulations, rules and standards are 24 | unenforceable, the individual or the legal entity are required to 25 | comply with Core International Labor Standards. 26 | 27 | 3. The individual or the legal entity shall not induce, suggest or force 28 | its employee(s), whether full-time or part-time, or its independent 29 | contractor(s), in any methods, to agree in oral or written form, to 30 | directly or indirectly restrict, weaken or relinquish his or her 31 | rights or remedies under such laws, regulations, rules and standards 32 | relating to labor and employment as mentioned above, no matter whether 33 | such written or oral agreements are enforceable under the laws of the 34 | said jurisdiction, nor shall such individual or the legal entity 35 | limit, in any methods, the rights of its employee(s) or independent 36 | contractor(s) from reporting or complaining to the copyright holder or 37 | relevant authorities monitoring the compliance of the license about 38 | its violation(s) of the said license. 39 | 40 | THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 41 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 42 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 43 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, 44 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 45 | OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION WITH THE 46 | LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. 47 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/commands/copy-opened-files-path.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import * as vscode from 'vscode'; 4 | 5 | /** 6 | * Options for copyOpenedFilesPath command 7 | */ 8 | export interface CopyOpenedFilesPathOptions { 9 | isSendToActiveTerminal?: boolean; 10 | includeAtSymbol?: boolean; 11 | addQuotes?: boolean; 12 | focusTerminal?: boolean; 13 | } 14 | 15 | /** 16 | * Copy opened files path command implementation 17 | */ 18 | export async function copyOpenedFilesPathCommand(options: CopyOpenedFilesPathOptions = {}): Promise { 19 | const { isSendToActiveTerminal = false, includeAtSymbol = false, addQuotes = true, focusTerminal = false } = options; 20 | 21 | // Get workspace root path 22 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 23 | const workspaceRoot = workspaceFolder?.uri.fsPath; 24 | 25 | // Get all visible tabs from all tab groups 26 | const visibleFiles: string[] = []; 27 | 28 | for (const tabGroup of vscode.window.tabGroups.all) { 29 | for (const tab of tabGroup.tabs) { 30 | if (tab.input instanceof vscode.TabInputText) { 31 | const uri = tab.input.uri; 32 | if (uri.scheme === 'file') { 33 | const filePath = uri.fsPath; 34 | 35 | // If no workspace or file is outside workspace, use absolute path 36 | if (!workspaceRoot) { 37 | visibleFiles.push(filePath); 38 | } else { 39 | const relativePath = path.relative(workspaceRoot, filePath); 40 | 41 | // If relative path starts with '..', file is outside workspace, use absolute path 42 | visibleFiles.push(relativePath.startsWith('..') ? filePath : relativePath); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | const openedFiles = [...new Set(visibleFiles)].sort(); // Remove duplicates and sort 50 | 51 | if (openedFiles.length === 0) { 52 | return; 53 | } 54 | 55 | // Format paths as text (one per line) 56 | const pathsText = openedFiles.map(file => { 57 | const formattedPath = includeAtSymbol ? `@${file}` : file; 58 | return addQuotes ? `'${formattedPath}'` : formattedPath; 59 | }).join('\n'); 60 | 61 | if (isSendToActiveTerminal) { 62 | // Send to active terminal 63 | const activeTerminal = vscode.window.activeTerminal; 64 | if (activeTerminal) { 65 | activeTerminal.sendText(pathsText); 66 | if (focusTerminal) { 67 | activeTerminal.show(); 68 | } 69 | } 70 | } else { 71 | // Copy to clipboard 72 | await vscode.env.clipboard.writeText(pathsText); 73 | } 74 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional stylelint cache 57 | .stylelintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variable files 69 | .env 70 | .env.* 71 | !.env.example 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | .parcel-cache 76 | 77 | # Next.js build output 78 | .next 79 | out 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and not Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # vuepress v2.x temp and cache directory 95 | .temp 96 | .cache 97 | 98 | # Sveltekit cache directory 99 | .svelte-kit/ 100 | 101 | # vitepress build output 102 | **/.vitepress/dist 103 | 104 | # vitepress cache directory 105 | **/.vitepress/cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # Firebase cache directory 120 | .firebase/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v3 129 | .pnp.* 130 | .yarn/* 131 | !.yarn/patches 132 | !.yarn/plugins 133 | !.yarn/releases 134 | !.yarn/sdks 135 | !.yarn/versions 136 | 137 | # Vite logs files 138 | vite.config.js.timestamp-* 139 | vite.config.ts.timestamp-* 140 | 141 | .DS_Store 142 | 143 | # Added by Task Master AI 144 | dev-debug.log 145 | # Environment variables 146 | # Editor directories and files 147 | .idea 148 | .vscode 149 | *.suo 150 | *.ntvs* 151 | *.njsproj 152 | *.sln 153 | *.sw? 154 | # OS specific 155 | # Task files 156 | tasks.json 157 | tasks/ -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/commands/copy-current-selection-reference.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import * as vscode from 'vscode'; 4 | 5 | /** 6 | * Options for copyCurrentSelectionReference command 7 | */ 8 | export interface CopyCurrentSelectionReferenceOptions { 9 | isSendToActiveTerminal?: boolean; 10 | includeRange?: boolean; 11 | includeAtSymbol?: boolean; 12 | addSpaces?: boolean; 13 | focusTerminal?: boolean; 14 | } 15 | 16 | /** 17 | * Copy current selection reference command implementation 18 | */ 19 | export async function copyCurrentSelectionReferenceCommand(options: CopyCurrentSelectionReferenceOptions = {}): Promise { 20 | const { 21 | isSendToActiveTerminal = false, 22 | includeRange = true, 23 | includeAtSymbol = true, 24 | addSpaces = false, 25 | focusTerminal = false 26 | } = options; 27 | 28 | // Get active editor 29 | const activeEditor = vscode.window.activeTextEditor; 30 | if (!activeEditor) { 31 | return; 32 | } 33 | 34 | const uri = activeEditor.document.uri; 35 | if (uri.scheme !== 'file') { 36 | return; 37 | } 38 | 39 | // Get workspace root path 40 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 41 | const workspaceRoot = workspaceFolder?.uri.fsPath; 42 | 43 | // Get relative or absolute file path 44 | const filePath = uri.fsPath; 45 | let pathText: string; 46 | 47 | if (!workspaceRoot) { 48 | pathText = filePath; 49 | } else { 50 | const relativePath = path.relative(workspaceRoot, filePath); 51 | // If relative path starts with '..', file is outside workspace, use absolute path 52 | pathText = relativePath.startsWith('..') ? filePath : relativePath; 53 | } 54 | 55 | // Get selection range 56 | const selection = activeEditor.selection; 57 | let referenceText = pathText; 58 | 59 | // Add line range if includeRange is true and there's a selection 60 | if (includeRange && !selection.isEmpty) { 61 | // Multi-line or single-line selection 62 | const startLine = selection.start.line + 1; // VSCode lines are 0-based, but display as 1-based 63 | const endLine = selection.end.line + 1; 64 | 65 | if (startLine === endLine) { 66 | referenceText += `#L${startLine}`; 67 | } else { 68 | referenceText += `#L${startLine}-${endLine}`; 69 | } 70 | } 71 | 72 | // Add @ symbol if includeAtSymbol is true 73 | if (includeAtSymbol) { 74 | referenceText = `@${referenceText}`; 75 | } 76 | 77 | // Add spaces if addSpaces is true 78 | if (addSpaces) { 79 | referenceText = ` ${referenceText} `; 80 | } 81 | 82 | if (isSendToActiveTerminal) { 83 | // Send to active terminal 84 | const activeTerminal = vscode.window.activeTerminal; 85 | if (activeTerminal) { 86 | activeTerminal.sendText(referenceText, false); 87 | if (focusTerminal) { 88 | activeTerminal.show(); 89 | } 90 | } 91 | } else { 92 | // Copy to clipboard 93 | await vscode.env.clipboard.writeText(referenceText); 94 | } 95 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/index.ts: -------------------------------------------------------------------------------- 1 | // Import all event modules 2 | import type { ExecuteCommandPayload, ExecuteCommandResult } from './execute-command.js'; 3 | import type { GetDiagnosticsPayload, GetDiagnosticsResult } from './get-diagnostics.js'; 4 | import type { GetReferencesPayload, GetReferencesResult } from './get-references.js'; 5 | import type { GetSymbolLSPInfoPayload, GetSymbolLSPInfoResult } from './get-symbol-lsp-info.js'; 6 | import type { HealthCheckPayload, HealthCheckResult } from './health-check.js'; 7 | import type { ListWorkspacesPayload, ListWorkspacesResult } from './list-workspaces.js'; 8 | import type { OpenFilesPayload, OpenFilesResult } from './open-file.js'; 9 | import type { RenameSymbolPayload, RenameSymbolResult } from './rename-symbol.js'; 10 | 11 | // Re-export all event types and schemas 12 | export * from '../common.js'; 13 | export * from './execute-command.js'; 14 | export * from './get-diagnostics.js'; 15 | export * from './get-references.js'; 16 | export * from './get-symbol-lsp-info.js'; 17 | export * from './health-check.js'; 18 | export * from './list-workspaces.js'; 19 | export * from './open-file.js'; 20 | export * from './rename-symbol.js'; 21 | 22 | /** 23 | * Base request structure 24 | */ 25 | export interface BaseRequest { 26 | id: string; 27 | method: string; 28 | params?: Record; 29 | } 30 | 31 | /** 32 | * Base response structure 33 | */ 34 | export interface BaseResponse { 35 | id: string; 36 | result?: any; 37 | error?: { 38 | code: number; 39 | message: string; 40 | details?: string; 41 | }; 42 | } 43 | 44 | /** 45 | * Event definitions for MCP Server -> VSCode Extension communication 46 | */ 47 | export interface EventMap { 48 | /** Health check */ 49 | health: { 50 | params: HealthCheckPayload; 51 | result: HealthCheckResult; 52 | }; 53 | 54 | 55 | /** LSP Methods */ 56 | getDiagnostics: { 57 | params: GetDiagnosticsPayload; 58 | result: GetDiagnosticsResult; 59 | }; 60 | 61 | getSymbolLSPInfo: { 62 | params: GetSymbolLSPInfoPayload; 63 | result: GetSymbolLSPInfoResult; 64 | }; 65 | 66 | getReferences: { 67 | params: GetReferencesPayload; 68 | result: GetReferencesResult; 69 | }; 70 | 71 | 72 | /** Execute VSCode command */ 73 | executeCommand: { 74 | params: ExecuteCommandPayload; 75 | result: ExecuteCommandResult; 76 | }; 77 | 78 | 79 | /** Open files */ 80 | openFiles: { 81 | params: OpenFilesPayload; 82 | result: OpenFilesResult; 83 | }; 84 | 85 | 86 | 87 | /** Rename symbol */ 88 | renameSymbol: { 89 | params: RenameSymbolPayload; 90 | result: RenameSymbolResult; 91 | }; 92 | 93 | 94 | /** List available workspaces */ 95 | listWorkspaces: { 96 | params: ListWorkspacesPayload; 97 | result: ListWorkspacesResult; 98 | }; 99 | } 100 | 101 | /** 102 | * Event names type 103 | */ 104 | export type EventName = keyof EventMap; 105 | 106 | /** 107 | * Event parameters type 108 | */ 109 | export type EventParams = EventMap[T]['params']; 110 | 111 | /** 112 | * Event result type 113 | */ 114 | export type EventResult = EventMap[T]['result']; -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code Review 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | # Optional: Only run on specific file changes 7 | # paths: 8 | # - "src/**/*.ts" 9 | # - "src/**/*.tsx" 10 | # - "src/**/*.js" 11 | # - "src/**/*.jsx" 12 | 13 | jobs: 14 | claude-review: 15 | # Optional: Filter by PR author 16 | # if: | 17 | # github.event.pull_request.user.login == 'external-contributor' || 18 | # github.event.pull_request.user.login == 'new-developer' || 19 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | pull-requests: read 25 | issues: read 26 | id-token: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 1 33 | 34 | - name: Run Claude Code Review 35 | id: claude-review 36 | uses: anthropics/claude-code-action@beta 37 | with: 38 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 39 | 40 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1) 41 | # model: "claude-opus-4-1-20250805" 42 | 43 | # Direct prompt for automated review (no @claude mention needed) 44 | direct_prompt: | 45 | Please review this pull request and provide feedback on: 46 | - Code quality and best practices 47 | - Potential bugs or issues 48 | - Performance considerations 49 | - Security concerns 50 | - Test coverage 51 | 52 | Be constructive and helpful in your feedback. 53 | 54 | # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR 55 | # use_sticky_comment: true 56 | 57 | # Optional: Customize review based on file types 58 | # direct_prompt: | 59 | # Review this PR focusing on: 60 | # - For TypeScript files: Type safety and proper interface usage 61 | # - For API endpoints: Security, input validation, and error handling 62 | # - For React components: Performance, accessibility, and best practices 63 | # - For tests: Coverage, edge cases, and test quality 64 | 65 | # Optional: Different prompts for different authors 66 | # direct_prompt: | 67 | # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && 68 | # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || 69 | # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} 70 | 71 | # Optional: Add specific tools for running tests or linting 72 | # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" 73 | 74 | # Optional: Skip review for certain conditions 75 | # if: | 76 | # !contains(github.event.pull_request.title, '[skip-review]') && 77 | # !contains(github.event.pull_request.title, '[WIP]') 78 | 79 | -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/events/get-symbol-lsp-info.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get symbol LSP info event types and schemas 3 | * This combines all LSP-related symbol information queries into one unified tool 4 | */ 5 | 6 | import { z } from 'zod'; 7 | 8 | import { LocationSchema, RangeSchema, SymbolLocatorSchema } from '../common.js'; 9 | 10 | /** 11 | * LSP info types that can be requested 12 | */ 13 | export const LSPInfoTypeSchema = z.enum([ 14 | 'hover', 15 | 'signature_help', 16 | 'type_definition', 17 | 'definition', 18 | 'implementation', 19 | 'all' 20 | ]); 21 | 22 | /** 23 | * Hover schema 24 | */ 25 | const HoverSchema = z.object({ 26 | contents: z.union([z.string(), z.array(z.string())]), 27 | range: RangeSchema.optional(), 28 | }).strict(); 29 | 30 | /** 31 | * Parameter information schema for signature help 32 | */ 33 | const ParameterInformationSchema = z.object({ 34 | label: z.string(), 35 | documentation: z.string().optional(), 36 | }).strict(); 37 | 38 | /** 39 | * Signature information schema 40 | */ 41 | const SignatureInformationSchema = z.object({ 42 | label: z.string(), 43 | documentation: z.string().optional(), 44 | parameters: z.array(ParameterInformationSchema).optional(), 45 | }).strict(); 46 | 47 | /** 48 | * Signature help schema 49 | */ 50 | const SignatureHelpSchema = z.object({ 51 | signatures: z.array(SignatureInformationSchema), 52 | activeSignature: z.number().optional(), 53 | activeParameter: z.number().optional(), 54 | }).strict(); 55 | 56 | /** 57 | * Get symbol LSP info input schema 58 | */ 59 | export const GetSymbolLSPInfoInputSchema = SymbolLocatorSchema.extend({ 60 | infoType: LSPInfoTypeSchema.optional().default('all').describe(`Type of LSP information to retrieve. 61 | **Info Types Available:** 62 | - **all**(default): Returns all available information 63 | - **hover**: Rich type information and documentation 64 | - **signature_help**: Function parameters and overloads 65 | - **type_definition**: Where the symbol's type is defined 66 | - **definition**: Where the symbol is defined 67 | - **implementation**: All implementations of interfaces/abstract classes`), 68 | }).strict(); 69 | 70 | /** 71 | * Get symbol LSP info output schema 72 | */ 73 | export const GetSymbolLSPInfoOutputSchema = z.object({ 74 | hover: z.array(HoverSchema).optional().describe('Hover information for the symbol'), 75 | signature_help: SignatureHelpSchema.nullable().optional().describe('Function signature help information'), 76 | type_definition: z.array(LocationSchema).optional().describe('Symbol type definition locations'), 77 | definition: z.array(LocationSchema).optional().describe('Symbol definition locations'), 78 | implementation: z.array(LocationSchema).optional().describe('Symbol implementation locations'), 79 | }).strict(); 80 | 81 | /** 82 | * Get symbol LSP info payload (input parameters) 83 | */ 84 | export type GetSymbolLSPInfoPayload = z.infer; 85 | 86 | /** 87 | * Get symbol LSP info result (output data) 88 | */ 89 | export type GetSymbolLSPInfoResult = z.infer; 90 | 91 | /** 92 | * LSP info type 93 | */ 94 | export type LSPInfoType = z.infer; 95 | 96 | /** 97 | * Internal types for schema composition - not exported to avoid conflicts 98 | * These types are already exported by their respective modules 99 | */ -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/services/rename-symbol.ts: -------------------------------------------------------------------------------- 1 | import type { EventParams, EventResult } from '@vscode-mcp/vscode-mcp-ipc'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { logger } from '../logger.js'; 5 | import { resolveSymbolPosition } from '../utils/resolve-symbol-position.js'; 6 | import { resolveFilePath } from '../utils/workspace.js'; 7 | 8 | export const renameSymbol = async ( 9 | payload: EventParams<'renameSymbol'>, 10 | ): Promise> => { 11 | logger.info(`Renaming symbol "${payload.symbol}" in ${payload.filePath} to "${payload.newName}"`); 12 | 13 | try { 14 | // 1. Resolve file path to URI and resolve symbol to position 15 | const uri = resolveFilePath(payload.filePath); 16 | const position = await resolveSymbolPosition(uri, payload.symbol, payload.codeSnippet); 17 | 18 | // 2. Validate the document exists 19 | await vscode.workspace.openTextDocument(uri); 20 | 21 | // 3. Validate new name 22 | if (!payload.newName.trim()) { 23 | return { 24 | success: false, 25 | modifiedFiles: [], 26 | totalChanges: 0, 27 | error: 'New name cannot be empty', 28 | }; 29 | } 30 | 31 | // 4. Execute rename using VSCode command 32 | const workspaceEdit = await vscode.commands.executeCommand( 33 | 'vscode.executeDocumentRenameProvider', 34 | uri, 35 | position, 36 | payload.newName 37 | ); 38 | 39 | if (!workspaceEdit) { 40 | return { 41 | success: false, 42 | modifiedFiles: [], 43 | totalChanges: 0, 44 | error: `Symbol "${payload.symbol}" is not renameable (no symbol found at resolved position)`, 45 | }; 46 | } 47 | 48 | // 5. Apply the workspace edit 49 | const success = await vscode.workspace.applyEdit(workspaceEdit); 50 | 51 | if (!success) { 52 | return { 53 | success: false, 54 | modifiedFiles: [], 55 | totalChanges: 0, 56 | error: 'Failed to apply rename edits', 57 | }; 58 | } 59 | 60 | // 6. Collect statistics about the changes 61 | const modifiedFiles: Array<{ uri: string; changeCount: number }> = []; 62 | let totalChanges = 0; 63 | let symbolName = 'unknown'; 64 | 65 | // Extract original symbol name from the first edit if available 66 | const entries = workspaceEdit.entries(); 67 | if (entries.length > 0) { 68 | const [firstUri, firstEdits] = entries[0]; 69 | if (firstEdits.length > 0) { 70 | const firstEdit = firstEdits[0]; 71 | // Try to get the original text from the first edit 72 | try { 73 | const editDoc = await vscode.workspace.openTextDocument(firstUri); 74 | symbolName = editDoc.getText(firstEdit.range); 75 | } catch { 76 | // If we can't get the original text, keep 'unknown' 77 | } 78 | } 79 | } 80 | 81 | // Process all entries to collect statistics 82 | for (const [fileUri, textEdits] of entries) { 83 | modifiedFiles.push({ 84 | uri: fileUri.toString(), 85 | changeCount: textEdits.length, 86 | }); 87 | totalChanges += textEdits.length; 88 | } 89 | 90 | // Save all dirty editors after rename operation 91 | await vscode.workspace.saveAll(false); 92 | 93 | logger.info(`Rename completed successfully: "${symbolName}" -> "${payload.newName}", ${modifiedFiles.length} files, ${totalChanges} changes`); 94 | 95 | return { 96 | success: true, 97 | symbolName, 98 | modifiedFiles, 99 | totalChanges, 100 | }; 101 | } catch (error) { 102 | logger.error(`Rename symbol failed: ${error}`); 103 | return { 104 | success: false, 105 | modifiedFiles: [], 106 | totalChanges: 0, 107 | error: `Rename failed: ${error}`, 108 | }; 109 | } 110 | }; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/services/execute-command.ts: -------------------------------------------------------------------------------- 1 | // cSpell:ignore Jsonifiable 2 | import type { EventParams, EventResult, Jsonifiable } from '@vscode-mcp/vscode-mcp-ipc'; 3 | import * as vscode from 'vscode'; 4 | 5 | /** 6 | * Check if a value is JSON serializable 7 | */ 8 | function isJsonifiable(value: unknown): value is Jsonifiable { 9 | // undefined is not JSON serializable (it gets omitted in objects, converted to null in arrays) 10 | if (value === undefined) { 11 | return false; 12 | } 13 | 14 | try { 15 | JSON.stringify(value); 16 | return true; 17 | } catch { 18 | return false; 19 | } 20 | } 21 | 22 | /** 23 | * Convert unknown value to Jsonifiable, with fallback for non-serializable values 24 | */ 25 | function toJsonifiable(value: unknown): Jsonifiable { 26 | if (isJsonifiable(value)) { 27 | return value; 28 | } 29 | 30 | // Fallback for non-serializable values 31 | if (typeof value === 'function') { 32 | return '[Function]'; 33 | } 34 | if (typeof value === 'symbol') { 35 | return '[Symbol]'; 36 | } 37 | if (value === undefined) { 38 | return null; 39 | } 40 | 41 | // Try to convert objects to a serializable format 42 | try { 43 | // Use structuredClone to create a deep copy, then verify it's JSON serializable 44 | const cloned = structuredClone(value); 45 | return isJsonifiable(cloned) ? cloned : String(value); 46 | } catch { 47 | return String(value); 48 | } 49 | } 50 | 51 | /** 52 | * Process arguments to convert them to appropriate VSCode types 53 | */ 54 | function processArguments(args: unknown[]): unknown[] { 55 | return args.map(arg => { 56 | // Convert URI strings to vscode.Uri objects 57 | if (typeof arg === 'string' && (arg.startsWith('file://') || arg.startsWith('vscode://') || arg.startsWith('http://') || arg.startsWith('https://'))) { 58 | try { 59 | return vscode.Uri.parse(arg); 60 | } catch { 61 | // If parsing fails, return the original string 62 | return arg; 63 | } 64 | } 65 | 66 | // Keep other arguments as-is 67 | return arg; 68 | }); 69 | } 70 | 71 | /** 72 | * Handle execute command 73 | */ 74 | export const executeCommand = async ( 75 | payload: EventParams<'executeCommand'> 76 | ): Promise> => { 77 | const { command, args, saveAllEditors } = payload; 78 | 79 | try { 80 | // Parse JSON string arguments if provided 81 | let parsedArgs: unknown[] = []; 82 | if (args) { 83 | try { 84 | parsedArgs = JSON.parse(args); 85 | if (!Array.isArray(parsedArgs)) { 86 | throw new TypeError('Args must be an array'); 87 | } 88 | } catch (parseError) { 89 | throw new Error(`Invalid JSON in args parameter: ${parseError instanceof Error ? parseError.message : String(parseError)}`); 90 | } 91 | } 92 | 93 | // Process arguments to convert URI strings to vscode.Uri objects 94 | const processedArgs = processArguments(parsedArgs); 95 | 96 | // Execute the VSCode command 97 | const result = await vscode.commands.executeCommand(command, ...processedArgs); 98 | 99 | // Save all dirty editors after command execution if requested 100 | if (saveAllEditors) { 101 | await vscode.workspace.saveAll(false); 102 | } 103 | 104 | return { 105 | result: toJsonifiable(result) 106 | }; 107 | } catch (error) { 108 | // If the command fails, we still return a result but with error information 109 | return { 110 | result: { 111 | error: error instanceof Error ? error.message : String(error), 112 | command, 113 | args: args || '' 114 | } 115 | }; 116 | } 117 | }; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/services/list-workspaces.ts: -------------------------------------------------------------------------------- 1 | import type { EventParams, EventResult, WorkspaceInfo } from '@vscode-mcp/vscode-mcp-ipc'; 2 | import * as vscode from 'vscode'; 3 | 4 | import packageJson from '../../package.json'; 5 | import { detectIdeType } from '../utils/detect-ide'; 6 | 7 | /** 8 | * Get workspace type 9 | */ 10 | function getWorkspaceType(): 'single-folder' | 'multi-folder' | 'workspace-file' | undefined { 11 | const workspaceFolders = vscode.workspace.workspaceFolders; 12 | 13 | if (!workspaceFolders || workspaceFolders.length === 0) { 14 | return undefined; 15 | } 16 | 17 | if (workspaceFolders.length === 1) { 18 | // Check if it's a .code-workspace file 19 | if (vscode.workspace.workspaceFile) { 20 | return 'workspace-file'; 21 | } 22 | return 'single-folder'; 23 | } 24 | 25 | return 'multi-folder'; 26 | } 27 | 28 | /** 29 | * Get workspace name 30 | */ 31 | function getWorkspaceName(): string | undefined { 32 | const workspaceFolders = vscode.workspace.workspaceFolders; 33 | 34 | if (!workspaceFolders || workspaceFolders.length === 0) { 35 | return undefined; 36 | } 37 | 38 | // For workspace file, use the workspace file name 39 | if (vscode.workspace.workspaceFile) { 40 | const path = vscode.workspace.workspaceFile.path; 41 | const segments = path.split('/'); 42 | const fileName = segments.at(-1); 43 | return fileName?.replace('.code-workspace', ''); 44 | } 45 | 46 | // For single folder, use folder name 47 | if (workspaceFolders.length === 1) { 48 | return workspaceFolders[0].name; 49 | } 50 | 51 | // For multi-folder, join folder names 52 | return workspaceFolders.map(f => f.name).join(', '); 53 | } 54 | 55 | /** 56 | * Get all workspace folders 57 | */ 58 | function getWorkspaceFolders(): string[] | undefined { 59 | const workspaceFolders = vscode.workspace.workspaceFolders; 60 | 61 | if (!workspaceFolders || workspaceFolders.length === 0) { 62 | return undefined; 63 | } 64 | 65 | return workspaceFolders.map(f => f.uri.fsPath); 66 | } 67 | 68 | /** 69 | * Handle list workspaces request 70 | */ 71 | export const listWorkspaces = async ( 72 | _payload: EventParams<'listWorkspaces'> 73 | ): Promise> => { 74 | try { 75 | // For the VSCode extension, we can only return the current workspace 76 | const currentWorkspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; 77 | 78 | if (!currentWorkspacePath) { 79 | // No workspace is open 80 | return { 81 | workspaces: [], 82 | summary: { 83 | total: 0, 84 | active: 0, 85 | available: 0, 86 | cleaned: 0 87 | } 88 | }; 89 | } 90 | 91 | // Get IDE type 92 | const ideType = await detectIdeType(); 93 | 94 | // Create workspace info for the current workspace 95 | const currentWorkspace: WorkspaceInfo = { 96 | workspace_path: currentWorkspacePath, 97 | workspace_name: getWorkspaceName(), 98 | workspace_type: getWorkspaceType(), 99 | folders: getWorkspaceFolders(), 100 | status: 'active', 101 | extension_version: packageJson.version, 102 | vscode_version: vscode.version, 103 | ide_type: ideType, 104 | // Additional info is not available in extension context without importing server logic 105 | }; 106 | 107 | return { 108 | workspaces: [currentWorkspace], 109 | summary: { 110 | total: 1, 111 | active: 1, 112 | available: 0, 113 | cleaned: 0 114 | } 115 | }; 116 | } catch (error) { 117 | throw new Error(`Failed to list workspaces: ${error instanceof Error ? error.message : String(error)}`); 118 | } 119 | }; -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/utils/resolve-symbol-position.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * Resolve symbol name to precise position in a file 5 | */ 6 | export async function resolveSymbolPosition( 7 | uri: vscode.Uri, 8 | symbol: string, 9 | codeSnippet?: string 10 | ): Promise { 11 | const document = await vscode.workspace.openTextDocument(uri); 12 | const text = document.getText(); 13 | 14 | if (!codeSnippet) { 15 | // No codeSnippet: find symbol directly in entire file 16 | return findSymbolDirectly(text, symbol, document); 17 | } 18 | 19 | // With codeSnippet: find symbol within the specified code snippet 20 | return findSymbolInCodeSnippet(text, symbol, codeSnippet, document); 21 | } 22 | 23 | /** 24 | * Find symbol directly in the entire file 25 | */ 26 | function findSymbolDirectly(text: string, symbol: string, document: vscode.TextDocument): vscode.Position { 27 | const matches = findAllSymbolMatches(text, symbol, document); 28 | 29 | if (matches.length === 0) { 30 | throw new Error(`Symbol "${symbol}" not found in file`); 31 | } 32 | 33 | if (matches.length > 1) { 34 | throw new Error(`Multiple occurrences of "${symbol}" found. Please provide codeSnippet to disambiguate.`); 35 | } 36 | 37 | return matches[0]; 38 | } 39 | 40 | /** 41 | * Find symbol within a specific code snippet 42 | */ 43 | function findSymbolInCodeSnippet(text: string, symbol: string, codeSnippet: string, document: vscode.TextDocument): vscode.Position { 44 | // 1. Find all occurrences of codeSnippet 45 | const snippetMatches: Array<{ start: number; end: number }> = []; 46 | let startIndex = 0; 47 | 48 | while (true) { 49 | const index = text.indexOf(codeSnippet, startIndex); 50 | if (index === -1) break; 51 | 52 | snippetMatches.push({ 53 | start: index, 54 | end: index + codeSnippet.length 55 | }); 56 | startIndex = index + 1; 57 | } 58 | 59 | if (snippetMatches.length === 0) { 60 | throw new Error(`Code snippet "${codeSnippet}" not found in file`); 61 | } 62 | 63 | if (snippetMatches.length > 1) { 64 | throw new Error(`Code snippet "${codeSnippet}" appears ${snippetMatches.length} times in file. Please be more specific.`); 65 | } 66 | 67 | // 2. Find symbol within the unique code snippet 68 | const snippetRange = snippetMatches[0]; 69 | const snippetText = text.slice(snippetRange.start, snippetRange.end); 70 | 71 | const symbolRegex = new RegExp(`\\b${escapeRegExp(symbol)}\\b`, 'g'); 72 | const symbolMatches: vscode.Position[] = []; 73 | let match: RegExpExecArray | null = symbolRegex.exec(snippetText); 74 | 75 | while (match !== null) { 76 | const absoluteIndex = snippetRange.start + match.index; 77 | symbolMatches.push(document.positionAt(absoluteIndex)); 78 | match = symbolRegex.exec(snippetText); 79 | } 80 | 81 | if (symbolMatches.length === 0) { 82 | throw new Error(`Symbol "${symbol}" not found in code snippet "${codeSnippet}"`); 83 | } 84 | 85 | if (symbolMatches.length > 1) { 86 | throw new Error(`Multiple occurrences of "${symbol}" found in code snippet. Please use a more specific snippet.`); 87 | } 88 | 89 | return symbolMatches[0]; 90 | } 91 | 92 | /** 93 | * Find all occurrences of a symbol in text 94 | */ 95 | function findAllSymbolMatches(text: string, symbol: string, document: vscode.TextDocument): vscode.Position[] { 96 | const matches: vscode.Position[] = []; 97 | const regex = new RegExp(`\\b${escapeRegExp(symbol)}\\b`, 'g'); 98 | let match: RegExpExecArray | null = regex.exec(text); 99 | 100 | while (match !== null) { 101 | matches.push(document.positionAt(match.index)); 102 | match = regex.exec(text); 103 | } 104 | 105 | return matches; 106 | } 107 | 108 | /** 109 | * Escape special regex characters 110 | */ 111 | function escapeRegExp(string: string): string { 112 | return string.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); 113 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/health-check.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { createDispatcher, HealthCheckInputSchema } from "@vscode-mcp/vscode-mcp-ipc"; 3 | 4 | import { VscodeMcpToolName } from "../constants.js"; 5 | import { formatToolCallError } from "../utils/format-tool-call-error.js"; 6 | import { workspacePathInputSchema } from "../utils/workspace-schema.js"; 7 | 8 | const inputSchema = { 9 | ...workspacePathInputSchema, 10 | ...HealthCheckInputSchema.shape 11 | }; 12 | 13 | const DESCRIPTION = `Test connection to VSCode MCP Bridge extension. Troubleshoot when other VSCode MCP tools return connection errors or timeouts`; 14 | 15 | export function registerHealthCheck(server: McpServer, serverVersion: string) { 16 | server.registerTool(VscodeMcpToolName.HEALTH_CHECK, { 17 | title: "Health Check", 18 | description: DESCRIPTION, 19 | inputSchema, 20 | annotations: { 21 | title: "Health Check", 22 | readOnlyHint: true, 23 | destructiveHint: false, 24 | idempotentHint: true, 25 | openWorldHint: false 26 | } 27 | }, async ({ workspace_path }) => { 28 | try { 29 | const dispatcher = await createDispatcher(workspace_path); 30 | const result = await dispatcher.dispatch("health", {}); 31 | 32 | // Version compatibility check 33 | const versionMatch = result.extension_version === serverVersion; 34 | const statusIcon = result.status === 'ok' ? '✅' : '❌'; 35 | const versionIcon = versionMatch ? '✅' : '⚠️'; 36 | 37 | let statusMessage = `${statusIcon} Health Check Result:\n`; 38 | statusMessage += ` • Status: ${result.status}\n`; 39 | statusMessage += ` • Extension Version: ${result.extension_version}\n`; 40 | statusMessage += ` • Server Version: ${serverVersion}\n`; 41 | statusMessage += ` • ${versionIcon} Version Match: ${versionMatch ? 'Yes' : 'No'}\n`; 42 | statusMessage += ` • Workspace: ${result.workspace || 'None'}\n`; 43 | statusMessage += ` • Timestamp: ${result.timestamp}\n`; 44 | 45 | if (result.system_info) { 46 | statusMessage += ` • Platform: ${result.system_info.platform}\n`; 47 | statusMessage += ` • Node.js: ${result.system_info.node_version}\n`; 48 | if (result.system_info.vscode_version) { 49 | statusMessage += ` • VSCode: ${result.system_info.vscode_version}\n`; 50 | } 51 | if (result.system_info.ide_type) { 52 | statusMessage += ` • IDE Type: ${result.system_info.ide_type}\n`; 53 | } 54 | } 55 | 56 | if (result.error) { 57 | statusMessage += ` • Error: ${result.error}\n`; 58 | } 59 | 60 | if (!versionMatch) { 61 | statusMessage += `\n⚠️ Warning: Version mismatch detected!\n`; 62 | statusMessage += ` This may cause compatibility issues. Please update both components to the same version:\n\n`; 63 | statusMessage += ` 📦 Update MCP Server:\n`; 64 | statusMessage += ` npx @vscode-mcp/vscode-mcp-server@latest --version\n\n`; 65 | statusMessage += ` 🔌 Update VSCode Extension:\n`; 66 | statusMessage += ` Please update the "VSCode MCP Bridge" extension (YuTengjing.vscode-mcp-bridge) to the latest version in VSCode Extensions marketplace\n\n`; 67 | statusMessage += ` 🔄 Important: After updating both components, please restart your editor to ensure the changes take effect.`; 68 | } 69 | 70 | // statusMessage += `\n\n Tips: 71 | // 1. Check if the "VSCode MCP Bridge" extension is installed and activated: https://marketplace.visualstudio.com/items?itemName=YuTengjing.vscode-mcp-bridge 72 | // 2. Check the "VSCode MCP Bridge" extension log in VSCode Output panel → "VSCode MCP Bridge" 73 | // `; 74 | 75 | 76 | return { 77 | content: [{ 78 | type: "text", 79 | text: statusMessage 80 | }] 81 | }; 82 | } catch (error) { 83 | return formatToolCallError("Health Check", error, `💡 Tips: 84 | 1. Check if the "VSCode MCP Bridge" extension is installed and activated: https://marketplace.visualstudio.com/items?itemName=YuTengjing.vscode-mcp-bridge 85 | 2. Check the "VSCode MCP Bridge" extension log in VSCode Output panel → "VSCode MCP Bridge" 86 | `); 87 | } 88 | }); 89 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/get-diagnostics.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { createDispatcher, GetDiagnosticsInputSchema } from "@vscode-mcp/vscode-mcp-ipc"; 3 | 4 | import { VscodeMcpToolName } from "../constants.js"; 5 | import { formatToolCallError } from "../utils/format-tool-call-error.js"; 6 | import { workspacePathInputSchema } from "../utils/workspace-schema.js"; 7 | 8 | const inputSchema = { 9 | ...workspacePathInputSchema, 10 | ...GetDiagnosticsInputSchema.shape 11 | }; 12 | 13 | const DESCRIPTION = `Get diagnostic information from vscode language servers. 14 | 15 | Ideal quality-check tool for AI coding agents, much faster than 'tsc --noEmit' and 'eslint .' 16 | 17 | **Parameter Examples:** 18 | - Check modified files: __NOT_RECOMMEND__filePaths: [] (auto-detects git changes) 19 | - Filter ESLint errors only: sources: ['eslint'], severities: ['error'] 20 | 21 | **Return Format:** 22 | Structured diagnostic results with severity levels, positions, and detailed error messages. 23 | Severity levels: 0=ERROR, 1=WARNING, 2=INFO, 3=HINT (matches VSCode DiagnosticSeverity enum) 24 | 25 | **Note:** 26 | - In most cases, leave __NOT_RECOMMEND__filePaths parameter empty to auto-detect git modified files. Modifying one file often causes diagnostics in other related files. 27 | 28 | `; 29 | 30 | export function registerGetDiagnostics(server: McpServer) { 31 | server.registerTool(VscodeMcpToolName.GET_DIAGNOSTICS, { 32 | title: "Get Diagnostics", 33 | description: DESCRIPTION, 34 | inputSchema, 35 | annotations: { 36 | title: "Get Diagnostics", 37 | readOnlyHint: true, 38 | destructiveHint: false, 39 | idempotentHint: false, 40 | openWorldHint: false 41 | } 42 | }, async ({ workspace_path, __NOT_RECOMMEND__filePaths, sources, severities }) => { 43 | const dispatcher = await createDispatcher(workspace_path); 44 | 45 | try { 46 | const result = await dispatcher.dispatch("getDiagnostics", { __NOT_RECOMMEND__filePaths, sources, severities }); 47 | 48 | // Handle case where no files were found 49 | if (result.files.length === 0) { 50 | return { 51 | content: [{ 52 | type: "text", 53 | text: __NOT_RECOMMEND__filePaths.length === 0 54 | ? "📄 No git modified files found in the workspace." 55 | : "📄 No files found or no diagnostics available." 56 | }] 57 | }; 58 | } 59 | 60 | // Filter out files with no diagnostics and format output 61 | const filesWithDiagnostics = result.files.filter(file => file.diagnostics.length > 0); 62 | 63 | const output = filesWithDiagnostics.map(file => { 64 | const diagnosticsList = file.diagnostics.map(diag => { 65 | const severity = diag.severity.toUpperCase(); 66 | const range = `${diag.range.start.line}:${diag.range.start.character}`; 67 | const source = diag.source ? `[${diag.source}] ` : ""; 68 | const code = diag.code ? `(${diag.code}) ` : ""; 69 | 70 | return ` ${severity} at ${range}: ${source}${code}${diag.message}`; 71 | }).join('\n'); 72 | 73 | return `❌ ${file.uri}\n Found ${file.diagnostics.length} diagnostic(s):\n${diagnosticsList}`; 74 | }).join('\n\n'); 75 | 76 | // Create summary based on diagnostic results 77 | let summary: string; 78 | 79 | if (filesWithDiagnostics.length === 0) { 80 | summary = __NOT_RECOMMEND__filePaths.length === 0 81 | ? `✅ All git modified files (${result.files.length} files) are clean - no diagnostics found!` 82 | : `✅ All checked files are clean - no diagnostics found!`; 83 | } else { 84 | const header = __NOT_RECOMMEND__filePaths.length === 0 85 | ? `⚠️ Found diagnostics in ${filesWithDiagnostics.length} of ${result.files.length} git modified files:` 86 | : `⚠️ Found diagnostics in ${filesWithDiagnostics.length} file(s):`; 87 | 88 | summary = `${header}\n\n${output}`; 89 | } 90 | 91 | return { 92 | content: [{ 93 | type: "text", 94 | text: summary 95 | }] 96 | }; 97 | } catch (error) { 98 | return formatToolCallError("Get Diagnostics", error); 99 | } 100 | }); 101 | } -------------------------------------------------------------------------------- /.github/workflows/vscode-extension-ci.yml: -------------------------------------------------------------------------------- 1 | name: VSCode Extension CI 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - 'packages/vscode-mcp-bridge/**' 12 | - '.github/workflows/vscode-extension-ci.yml' 13 | - 'pnpm-lock.yaml' 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | matrix: 19 | os: [macos-latest, ubuntu-latest, windows-latest] 20 | runs-on: ${{ matrix.os }} 21 | defaults: 22 | run: 23 | working-directory: packages/vscode-mcp-bridge 24 | outputs: 25 | GIT_TAG: ${{ steps.set-tag.outputs.GIT_TAG }} 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - uses: pnpm/action-setup@v4 31 | name: Install pnpm 32 | with: 33 | run_install: false 34 | 35 | - name: Install Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: '22' 39 | cache: pnpm 40 | 41 | - name: Install dependencies 42 | run: pnpm install 43 | working-directory: . 44 | 45 | - name: Build IPC package 46 | run: pnpm --filter @vscode-mcp/vscode-mcp-ipc build 47 | working-directory: . 48 | 49 | - name: Get the date on Ubuntu/MacOS 50 | id: date_unix 51 | if: runner.os != 'Windows' 52 | run: echo "DATE=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT 53 | 54 | - name: Get the date on Windows 55 | id: date_windows 56 | if: runner.os == 'Windows' 57 | run: echo "DATE=$(Get-Date -Format 'yyyyMMdd')" >> $GITHUB_OUTPUT 58 | 59 | - name: Cache .vscode-test 60 | uses: actions/cache@v4 61 | env: 62 | CACHE_PREFIX: ${{ runner.os }}-vscode-test-${{ steps.date_unix.outputs.DATE || steps.date_windows.outputs.DATE }} 63 | with: 64 | path: packages/vscode-mcp-bridge/.vscode-test 65 | key: ${{ env.CACHE_PREFIX }}-${{ hashFiles('packages/vscode-mcp-bridge/test/runTests.ts') }} 66 | restore-keys: ${{ env.CACHE_PREFIX }} 67 | 68 | - run: xvfb-run -a pnpm test 69 | if: runner.os == 'Linux' 70 | - run: pnpm test 71 | if: runner.os != 'Linux' 72 | 73 | - name: Set GIT_TAG 74 | id: set-tag 75 | if: runner.os == 'Linux' 76 | run: | 77 | git fetch --tags origin 78 | GIT_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") 79 | if [ -n "$GIT_TAG" ] && [ "$(git rev-list -n 1 $GIT_TAG 2>/dev/null || echo "")" = "$(git rev-parse HEAD)" ]; then 80 | echo "GIT_TAG=${GIT_TAG}" >> $GITHUB_OUTPUT 81 | echo "Git tag: ${GIT_TAG}" 82 | else 83 | echo "GIT_TAG=" >> $GITHUB_OUTPUT 84 | echo "No matching tag found for current commit" 85 | fi 86 | 87 | publish: 88 | needs: test 89 | if: startsWith(needs.test.outputs.GIT_TAG, 'v') 90 | runs-on: ubuntu-latest 91 | defaults: 92 | run: 93 | working-directory: packages/vscode-mcp-bridge 94 | steps: 95 | - name: Checkout 96 | uses: actions/checkout@v4 97 | with: 98 | fetch-depth: 0 99 | 100 | - uses: pnpm/action-setup@v4 101 | name: Install pnpm 102 | with: 103 | run_install: false 104 | 105 | - name: Install Node.js 106 | uses: actions/setup-node@v4 107 | with: 108 | node-version: '22' 109 | cache: pnpm 110 | 111 | - name: Install dependencies 112 | run: pnpm install 113 | working-directory: . 114 | 115 | - name: Build IPC package 116 | run: pnpm --filter @vscode-mcp/vscode-mcp-ipc build 117 | working-directory: . 118 | 119 | - name: Build extension 120 | run: pnpm esbuild:base --minify 121 | 122 | - name: Publish to VS Marketplace 123 | run: pnpm publish:vs-marketplace 124 | env: 125 | VSCE_PAT: ${{ secrets.VS_MARKETPLACE_TOKEN }} 126 | 127 | - name: Publish to Open VSX 128 | run: pnpm publish:open-vsx 129 | env: 130 | OVSX_PAT: ${{ secrets.OPEN_VSX_TOKEN }} 131 | 132 | - name: Generate changelog 133 | run: npx changelogithub 134 | working-directory: . 135 | env: 136 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 137 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-mcp-bridge", 3 | "displayName": "Vscode Mcp Bridge", 4 | "version": "4.5.0", 5 | "description": "A vscode extension which connect vscode and mcp", 6 | "publisher": "YuTengjing", 7 | "private": true, 8 | "preview": true, 9 | "author": { 10 | "name": "YuTengjing", 11 | "email": "ytj2713151713@gmail.com", 12 | "url": "https://github.com/tjx666" 13 | }, 14 | "license": "MIT", 15 | "homepage": "https://github.com/tjx666/vscode-mcp-bridge/blob/master/README.md", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/tjx666/vscode-mcp-bridge" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/tjx666/vscode-mcp-bridge/issues", 22 | "email": "ytj2713151713@gmail.com" 23 | }, 24 | "keywords": [ 25 | "cursor", 26 | "windsurf", 27 | "mcp", 28 | "lsp", 29 | "command" 30 | ], 31 | "categories": [ 32 | "Other" 33 | ], 34 | "main": "./out/src/extension.js", 35 | "icon": "assets/logo.png", 36 | "engines": { 37 | "vscode": "^1.99.1" 38 | }, 39 | "badges": [ 40 | { 41 | "url": "https://img.shields.io/badge/PRs-welcome-brightgreen.svg", 42 | "description": "PRs Welcome", 43 | "href": "https://github.com/tjx666/vscode-mcp-bridge/fork" 44 | } 45 | ], 46 | "activationEvents": [ 47 | "onStartupFinished" 48 | ], 49 | "contributes": { 50 | "configuration": { 51 | "type": "object", 52 | "title": "VSCode MCP Bridge", 53 | "properties": { 54 | "vscode-mcp-bridge.enableLog": { 55 | "type": "boolean", 56 | "default": true, 57 | "description": "Enable logging for VSCode MCP Bridge operations" 58 | } 59 | } 60 | }, 61 | "commands": [ 62 | { 63 | "command": "vscode-mcp-bridge.sleep", 64 | "title": "Sleep", 65 | "category": "VSCode MCP Bridge" 66 | }, 67 | { 68 | "command": "vscode-mcp-bridge.copyOpenedFilesPath", 69 | "title": "Copy Opened Files Path", 70 | "category": "VSCode MCP Bridge" 71 | }, 72 | { 73 | "command": "vscode-mcp-bridge.copyCurrentSelectionReference", 74 | "title": "Copy Current Selection Reference", 75 | "category": "VSCode MCP Bridge" 76 | } 77 | ], 78 | "keybindings": [ 79 | { 80 | "key": "alt+cmd+o", 81 | "command": "vscode-mcp-bridge.copyOpenedFilesPath", 82 | "args": { 83 | "isSendToActiveTerminal": true, 84 | "includeAtSymbol": true, 85 | "addQuotes": false, 86 | "focusTerminal": true 87 | } 88 | } 89 | ] 90 | }, 91 | "scripts": { 92 | "vscode:prepublish": "pnpm esbuild:base --minify", 93 | "clean": "npx rimraf -rf ./out", 94 | "esbuild:base": "tsx scripts/esbuild.ts", 95 | "esbuild:watch": "pnpm esbuild:base --sourcemap --watch", 96 | "esbuild:analyze": "pnpm esbuild:base --minify --metafile --analyze && esbuild-visualizer --metadata ./meta.json --open", 97 | "compile:test": "pnpm clean && tsc -b ./test/tsconfig.json", 98 | "lint": "eslint src --ext ts", 99 | "test": "pnpm compile:test && node ./out/test/runTests.js", 100 | "package": "vsce package --no-dependencies", 101 | "release": "tsx scripts/release.ts", 102 | "publish:vs-marketplace": "vsce publish --no-dependencies", 103 | "publish:open-vsx": "ovsx publish --no-dependencies" 104 | }, 105 | "dependencies": { 106 | "@vscode-mcp/vscode-mcp-ipc": "workspace:*", 107 | "zod": "^3.25.76" 108 | }, 109 | "devDependencies": { 110 | "@types/glob": "^9.0.0", 111 | "@types/mocha": "^10.0.10", 112 | "@types/vscode": "1.99.1", 113 | "@vscode/test-electron": "^2.4.1", 114 | "@vscode/vsce": "^3.6.0", 115 | "esbuild": "~0.25.10", 116 | "esbuild-visualizer": "^0.7.0", 117 | "glob": "^11.0.3", 118 | "mocha": "^11.7.2", 119 | "ovsx": "^0.10.5", 120 | "rimraf": "^6.0.1" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /.cursor/rules/vscode-mcp-project-architecture.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: VSCode MCP Bridge 项目架构说明,包含项目背景、核心使命、架构设计和通信协议 3 | alwaysApply: true 4 | --- 5 | 6 | # VSCode MCP Bridge 项目架构 7 | 8 | ## 项目背景 9 | 10 | **VSCode MCP Bridge** 是一个 monorepo 项目,提供了连接 VSCode 与 MCP (Model Context Protocol) 的完整解决方案,使 MCP 客户端能够访问丰富的 VSCode 上下文信息。 11 | 12 | ## 核心使命 13 | 14 | **VSCode MCP Bridge 主要服务于 AI IDEs(如 Cursor)和 AI 编码助手**,帮助他们更高效地开发和分析代码。 15 | 16 | ### 设计动机 17 | 18 | 传统的 AI 编码助手在验证代码修改时经常需要执行耗时的命令: 19 | 20 | - `tsc --noEmit` - TypeScript 类型检查 21 | - `eslint .` - 代码风格检查 22 | - `npm run build` - 项目构建 23 | 24 | 这些命令在大项目中运行缓慢,严重影响 AI 开发效率。VSCode MCP Bridge 提供实时 LSP (Language Server Protocol) 信息,让 AI 助手能够: 25 | 26 | - **快速获取诊断** (`get-diagnostics`) - 替代耗时的类型检查和 lint 命令 27 | - **访问实时类型信息** (`get-hover`) - 获取准确的类型定义,无需编译 28 | - **高效代码导航** (`get-definition`, `get-references`) - 理解代码结构和依赖关系 29 | - **智能文件操作** (`open-files`, `open-diff`) - 高效的代码审查和比较 30 | 31 | ### 核心优势 32 | 33 | 1. **实时性**: 利用 VSCode 的 LSP 获取实时代码状态,无需执行缓慢命令 34 | 2. **准确性**: 基于语言服务器的精确分析,比静态分析更可靠 35 | 3. **效率**: 显著减少 AI 编码助手的等待时间 36 | 4. **集成性**: 与 VSCode 生态系统深度集成,支持多种语言和扩展 37 | 38 | ## Monorepo 架构 39 | 40 | ```plaintext 41 | MCP Client ↔ MCP Server ↔ IPC Layer ↔ VSCode Extension ↔ VSCode API 42 | stdio Unix Socket Types Extension API 43 | ``` 44 | 45 | ### 组件职责 46 | 47 | **1. vscode-mcp-ipc** (IPC 通信层): 48 | 49 | - **事件定义**: 使用 Zod 定义所有事件类型和 Schema 验证 50 | - **类型安全**: 导出类型安全的 EventMap, EventParams, EventResult 类型 51 | - **通信**: 提供 EventDispatcher 用于 Unix Socket 通信,支持超时处理 52 | - **Socket 管理**: 基于工作区 hash 生成跨平台 socket 路径 53 | - **Schema 验证**: 所有事件的集中化输入/输出验证 Schema 54 | - **跨平台支持**: 处理 Windows 命名管道和 Unix socket 55 | 56 | **2. vscode-mcp-bridge** (VSCode 扩展): 57 | 58 | - **Socket 服务器**: SocketServer 类,支持服务注册和 Schema 验证 59 | - **服务注册**: 模块化服务架构,支持类型安全的处理器 60 | - **请求路由**: 将传入请求路由到相应的服务处理器 61 | - **Schema 验证**: 验证请求负载和响应结果 62 | - **错误处理**: 全面的错误处理和详细错误信息 63 | - **生命周期管理**: 扩展激活/停用和资源清理 64 | - **日志记录**: 结构化日志记录,用于调试和监控服务调用 65 | 66 | **3. vscode-mcp-server** (MCP 服务器): 67 | 68 | - **MCP 协议**: 实现标准 MCP 协议,使用 stdio 传输 69 | - **工具注册**: 注册所有可用工具并定义适当的 Schema 70 | - **请求转换**: 将 MCP 工具调用转换为 VSCode 扩展请求 71 | - **Schema 复用**: 使用 `...InputSchema.shape` 模式复用 IPC Schema 72 | - **错误处理**: 优雅的错误处理和用户友好的错误信息 73 | - **输出格式化**: 使用状态图标和结构化输出格式化响应 74 | 75 | ### 通信协议 76 | 77 | - **MCP Client ↔ MCP Server**: stdio + JSON-RPC 2.0 78 | - **MCP Server ↔ IPC Layer**: TypeScript 导入和函数调用 79 | - **IPC Layer ↔ VSCode Extension**: Unix Domain Sockets + JSON (无认证) 80 | 81 | ## 多窗口支持 82 | 83 | ### Socket 路径生成 84 | 85 | Socket 路径在 IPC 层和 Extension 之间一致生成: 86 | 87 | ```typescript 88 | // IPC 层:packages/vscode-mcp-ipc/src/dispatch.ts 89 | export function getSocketPath(workspacePath: string): string { 90 | const hash = createHash('md5').update(workspacePath).digest('hex').slice(0, 8); 91 | return process.platform === 'win32' 92 | ? `\\\\.\\pipe\\vscode-mcp-${hash}` 93 | : join(tmpdir(), `vscode-mcp-${hash}.sock`); 94 | } 95 | ``` 96 | 97 | **示例**: 98 | 99 | - `/Users/user/frontend` → `/tmp/vscode-mcp-a1b2c3d4.sock` 100 | - `/Users/user/backend` → `/tmp/vscode-mcp-e5f6g7h8.sock` 101 | 102 | ### 工作区定位 103 | 104 | 所有 MCP 工具都需要 `workspace_path` 参数来定位特定的 VSCode 实例。 105 | 106 | ## 开发工作流程 107 | 108 | ### 开发顺序(关键) 109 | 110 | 添加/修改工具时,必须按照以下顺序: 111 | 112 | 1. **IPC 层** ([packages/vscode-mcp-ipc/](mdc:packages/vscode-mcp-ipc/)) 113 | - 定义 Schema 和类型 114 | - 更新 EventMap 115 | - 构建包: `npm run build` 116 | 117 | 2. **Extension 层** ([packages/vscode-mcp-bridge/](mdc:packages/vscode-mcp-bridge/)) 118 | - 实现服务逻辑 119 | - 注册并验证 120 | - 类型检查: `npx tsc --noEmit --project src/tsconfig.json` 121 | 122 | 3. **MCP Server 层** ([packages/vscode-mcp-server/](mdc:packages/vscode-mcp-server/)) 123 | - 实现工具并复用 Schema 124 | - 在服务器中注册 125 | - 类型检查: `npx tsc --noEmit --project tsconfig.json` 126 | 127 | ## 安全考虑 128 | 129 | - **本地通信**: Unix socket 本地设计,仅限本地使用 130 | - **文件权限**: Socket 文件仅限当前用户访问 131 | - **无认证**: 简化的本地开发设计,无需认证 132 | - **进程隔离**: 每个 VSCode 窗口创建自己的 socket 133 | - **Schema 验证**: 输入/输出验证防止注入攻击 134 | 135 | ## 使用示例 136 | 137 | ### 基本工具使用 138 | 139 | ```json 140 | { 141 | "workspace_path": "/path/to/workspace", 142 | "uri": "file:///path/to/file.ts", 143 | "line": 10, 144 | "character": 5 145 | } 146 | ``` 147 | 148 | ### 错误处理 149 | 150 | 工具提供详细的错误信息: 151 | 152 | - **连接错误**: Socket 连接失败 153 | - **验证错误**: Schema 验证失败,包含字段详情 154 | - **VSCode 错误**: 扩展操作失败 155 | - **超时错误**: 请求超时处理 156 | 157 | ## 性能考虑 158 | 159 | - **批量操作**: 大部分工具支持批量处理 160 | - **连接复用**: EventDispatcher 高效管理 socket 连接 161 | - **后台加载**: 文件可以在不显示编辑器的情况下加载 162 | - **Schema 验证**: 快速 Zod 验证确保类型安全 163 | - **工作区定位**: 高效的多窗口支持 164 | 165 | ## 故障排除 166 | 167 | ### 常见问题 168 | 169 | 1. **类型错误**: 确保在 Schema 变更后构建 IPC 包 170 | 2. **未知方法**: 检查服务在所有层中的注册情况 171 | 3. **连接失败**: 验证工作区路径和 socket 权限 172 | 4. **Schema 验证**: 检查参数类型是否符合 Schema 定义 173 | 174 | ### 调试信息 175 | 176 | - **扩展日志**: VSCode 输出面板 → "VSCode MCP Bridge" 177 | - **Socket 路径**: 检查每个工作区生成的 socket 路径 178 | - **服务注册**: 验证所有服务是否正确注册 179 | -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/utils/workspace-discovery.ts: -------------------------------------------------------------------------------- 1 | import { access, constants, readdir } from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | 4 | import type {WorkspaceInfo} from '@vscode-mcp/vscode-mcp-ipc'; 5 | import { EventDispatcher, getAppDataDir, getLegacyAppDataDir } from '@vscode-mcp/vscode-mcp-ipc'; 6 | 7 | /** 8 | * Extract workspace path from socket file name 9 | */ 10 | function extractWorkspaceFromSocketName(socketFileName: string): string { 11 | // Socket file format: vscode-mcp-{hash}.sock 12 | // We can't reverse the hash, so we need to connect to get the real path 13 | // For now, return the hash as identifier 14 | const match = socketFileName.match(/vscode-mcp-([a-f0-9]+)\.sock$/); 15 | return match ? match[1] : 'unknown'; 16 | } 17 | 18 | /** 19 | * Discover available workspaces in a directory 20 | */ 21 | async function discoverInDirectory(appDir: string, isLegacy: boolean = false): Promise { 22 | try { 23 | await access(appDir, constants.F_OK); 24 | } catch { 25 | return []; 26 | } 27 | 28 | try { 29 | const files = await readdir(appDir); 30 | return files 31 | .filter((f: string) => f.startsWith('vscode-mcp-') && f.endsWith('.sock')) 32 | .map((f: string) => join(appDir, f)); 33 | } catch (error) { 34 | if (!isLegacy) { 35 | console.error('Failed to read socket directory:', error); 36 | } 37 | return []; 38 | } 39 | } 40 | 41 | /** 42 | * Discover all available workspaces 43 | */ 44 | export async function discoverAvailableWorkspaces(options: { 45 | cleanZombieSockets?: boolean; 46 | testConnection?: boolean; 47 | includeDetails?: boolean; 48 | } = {}): Promise<{workspaces: WorkspaceInfo[], hasLegacyWorkspaces: boolean}> { 49 | const { 50 | cleanZombieSockets = true, 51 | testConnection = true 52 | } = options; 53 | 54 | // Try new app directory first 55 | const appDir = getAppDataDir(); 56 | let socketFiles = await discoverInDirectory(appDir, false); 57 | 58 | // If no sockets found in new directory, try legacy directory 59 | let hasLegacyWorkspaces = false; 60 | if (socketFiles.length === 0) { 61 | const legacyAppDir = getLegacyAppDataDir(); 62 | if (legacyAppDir) { 63 | const legacySocketFiles = await discoverInDirectory(legacyAppDir, true); 64 | if (legacySocketFiles.length > 0) { 65 | console.warn(`⚠️ Found workspaces using legacy socket paths in: ${legacyAppDir}`); 66 | console.warn(` Please upgrade VSCode MCP Bridge extension to use new path: ${appDir}`); 67 | socketFiles = legacySocketFiles; 68 | hasLegacyWorkspaces = true; 69 | } 70 | } 71 | } 72 | 73 | // Process all sockets in parallel for better performance 74 | const promises = socketFiles.map(async (socketPath) => { 75 | if (!testConnection) { 76 | // Just list files without testing 77 | return { 78 | workspace_path: extractWorkspaceFromSocketName(socketPath), 79 | status: 'available' as const, 80 | socket_path: socketPath 81 | }; 82 | } 83 | 84 | // Test if socket is active and get real workspace info 85 | try { 86 | // Create a temporary dispatcher for the specific socket 87 | const tempDispatcher = new EventDispatcher('temp', 2000); 88 | // Override the socket path to use the discovered socket 89 | (tempDispatcher as any).socketPath = socketPath; 90 | const healthResult = await tempDispatcher.dispatch('health', {}); 91 | 92 | // Successfully connected, populate with real workspace info 93 | const workspaceInfo: WorkspaceInfo = { 94 | workspace_path: healthResult.workspace || extractWorkspaceFromSocketName(socketPath), 95 | status: 'active', 96 | socket_path: socketPath, 97 | extension_version: healthResult.extension_version, 98 | last_seen: healthResult.timestamp 99 | }; 100 | 101 | // Always add detailed info 102 | workspaceInfo.vscode_version = healthResult.system_info?.vscode_version; 103 | 104 | return workspaceInfo; 105 | } catch (error) { 106 | // Connection failed, this is a zombie socket 107 | if (cleanZombieSockets) { 108 | // Socket is dead, clean it up 109 | try { 110 | const { unlink } = await import('node:fs/promises'); 111 | await unlink(socketPath); 112 | console.log(`Cleaned zombie socket: ${socketPath}`); 113 | } catch (cleanError) { 114 | console.warn(`Failed to clean zombie socket ${socketPath}:`, cleanError); 115 | } 116 | return null; // Cleaned socket, don't include in results 117 | } else { 118 | // Mark as error but don't clean 119 | return { 120 | workspace_path: extractWorkspaceFromSocketName(socketPath), 121 | status: 'error' as const, 122 | socket_path: socketPath, 123 | error: error instanceof Error ? error.message : String(error) 124 | }; 125 | } 126 | } 127 | }); 128 | 129 | const results = await Promise.all(promises); 130 | 131 | // Filter out null results (cleaned or inactive sockets) 132 | const workspaces = results.filter((result): result is WorkspaceInfo => result !== null); 133 | 134 | return { 135 | workspaces, 136 | hasLegacyWorkspaces 137 | }; 138 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecuteCommandInputSchema, 3 | ExecuteCommandOutputSchema, 4 | GetDiagnosticsInputSchema, 5 | GetDiagnosticsOutputSchema, 6 | GetReferencesInputSchema, 7 | GetReferencesOutputSchema, 8 | GetSymbolLSPInfoInputSchema, 9 | GetSymbolLSPInfoOutputSchema, 10 | HealthCheckInputSchema, 11 | HealthCheckOutputSchema, 12 | ListWorkspacesOutputSchema, 13 | OpenFilesInputSchema, 14 | OpenFilesOutputSchema, 15 | RenameSymbolInputSchema, 16 | RenameSymbolOutputSchema, 17 | } from '@vscode-mcp/vscode-mcp-ipc'; 18 | import * as vscode from 'vscode'; 19 | 20 | import type {CopyCurrentSelectionReferenceOptions} from './commands/copy-current-selection-reference'; 21 | import { copyCurrentSelectionReferenceCommand } from './commands/copy-current-selection-reference'; 22 | import type {CopyOpenedFilesPathOptions} from './commands/copy-opened-files-path'; 23 | import { copyOpenedFilesPathCommand } from './commands/copy-opened-files-path'; 24 | import { sleepCommand } from './commands/sleep'; 25 | import { logger } from './logger'; 26 | import { 27 | executeCommand, 28 | getCurrentWorkspacePath, 29 | getDiagnostics, 30 | getReferences, 31 | getSymbolLSPInfo, 32 | health, 33 | listWorkspaces, 34 | openFiles, 35 | renameSymbol, 36 | } from './services'; 37 | import { SocketServer } from './socket-server'; 38 | 39 | // Global socket server instance 40 | let socketServer: SocketServer | null = null; 41 | 42 | /** 43 | * Extension activation 44 | */ 45 | export async function activate(context: vscode.ExtensionContext) { 46 | logger.info('VSCode MCP Bridge extension is being activated'); 47 | 48 | // Get current workspace path 49 | const workspacePath = getCurrentWorkspacePath(); 50 | if (!workspacePath) { 51 | logger.info('No workspace folder found, extension will not start socket server'); 52 | return; 53 | } 54 | 55 | logger.info(`Current workspace: ${workspacePath}`); 56 | 57 | try { 58 | // Create socket server 59 | socketServer = new SocketServer(workspacePath); 60 | 61 | socketServer.register('health', { 62 | handler: health, 63 | payloadSchema: HealthCheckInputSchema, 64 | resultSchema: HealthCheckOutputSchema 65 | }); 66 | 67 | 68 | socketServer.register('getDiagnostics', { 69 | handler: getDiagnostics, 70 | payloadSchema: GetDiagnosticsInputSchema, 71 | resultSchema: GetDiagnosticsOutputSchema 72 | }); 73 | 74 | socketServer.register('getSymbolLSPInfo', { 75 | handler: getSymbolLSPInfo, 76 | payloadSchema: GetSymbolLSPInfoInputSchema, 77 | resultSchema: GetSymbolLSPInfoOutputSchema 78 | }); 79 | 80 | 81 | socketServer.register('getReferences', { 82 | handler: getReferences, 83 | payloadSchema: GetReferencesInputSchema, 84 | resultSchema: GetReferencesOutputSchema 85 | }); 86 | 87 | socketServer.register('executeCommand', { 88 | handler: executeCommand, 89 | payloadSchema: ExecuteCommandInputSchema, 90 | resultSchema: ExecuteCommandOutputSchema 91 | }); 92 | 93 | 94 | socketServer.register('openFiles', { 95 | handler: openFiles, 96 | payloadSchema: OpenFilesInputSchema, 97 | resultSchema: OpenFilesOutputSchema 98 | }); 99 | 100 | 101 | 102 | socketServer.register('renameSymbol', { 103 | handler: renameSymbol, 104 | payloadSchema: RenameSymbolInputSchema, 105 | resultSchema: RenameSymbolOutputSchema 106 | }); 107 | 108 | socketServer.register('listWorkspaces', { 109 | handler: listWorkspaces, 110 | resultSchema: ListWorkspacesOutputSchema 111 | }); 112 | 113 | // Start socket server 114 | await socketServer.start(); 115 | 116 | logger.info(`Socket server started successfully at: ${socketServer.getSocketPath()}`); 117 | logger.info(`Registered ${socketServer.getServicesCount()} services`); 118 | 119 | // Register VSCode commands 120 | const sleepCommandDisposable = vscode.commands.registerCommand('vscode-mcp-bridge.sleep', async (duration: number) => { 121 | await sleepCommand(duration); 122 | }); 123 | context.subscriptions.push(sleepCommandDisposable); 124 | 125 | const copyOpenedFilesPathDisposable = vscode.commands.registerCommand('vscode-mcp-bridge.copyOpenedFilesPath', async (options?: CopyOpenedFilesPathOptions) => { 126 | await copyOpenedFilesPathCommand(options); 127 | }); 128 | context.subscriptions.push(copyOpenedFilesPathDisposable); 129 | 130 | const copyCurrentSelectionReferenceDisposable = vscode.commands.registerCommand('vscode-mcp-bridge.copyCurrentSelectionReference', async (options?: CopyCurrentSelectionReferenceOptions) => { 131 | await copyCurrentSelectionReferenceCommand(options); 132 | }); 133 | context.subscriptions.push(copyCurrentSelectionReferenceDisposable); 134 | 135 | // Register cleanup on extension deactivation 136 | context.subscriptions.push({ 137 | dispose: () => { 138 | if (socketServer) { 139 | socketServer.cleanup(); 140 | socketServer = null; 141 | } 142 | } 143 | }); 144 | 145 | } catch (error) { 146 | logger.error(`Failed to start socket server: ${error}`); 147 | vscode.window.showErrorMessage(`VSCode MCP Bridge: Failed to start socket server - ${error}`); 148 | } 149 | } 150 | 151 | /** 152 | * Extension deactivation 153 | */ 154 | export function deactivate() { 155 | logger.info('VSCode MCP Bridge extension is being deactivated'); 156 | 157 | if (socketServer) { 158 | socketServer.cleanup(); 159 | socketServer = null; 160 | } 161 | 162 | logger.dispose(); 163 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/get-symbol-lsp-info.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { createDispatcher, GetSymbolLSPInfoInputSchema } from "@vscode-mcp/vscode-mcp-ipc"; 3 | 4 | import { VscodeMcpToolName } from "../constants.js"; 5 | import { formatToolCallError } from "../utils/format-tool-call-error.js"; 6 | import { workspacePathInputSchema } from "../utils/workspace-schema.js"; 7 | 8 | /** 9 | * Format location info with optional code content 10 | */ 11 | function formatLocationInfo(location: { uri: string; range: { start: { line: number; character: number }; end: { line: number; character: number } }; usageCode?: string }): string { 12 | const positionInfo = `${location.uri} @ ${location.range.start.line}:${location.range.start.character}-${location.range.end.line}:${location.range.end.character}`; 13 | 14 | if (location.usageCode) { 15 | return ` • ${positionInfo}\n \`\`\`\n${location.usageCode.split('\n').map(line => ` ${line}`).join('\n')}\n \`\`\``; 16 | } 17 | 18 | return ` • ${positionInfo}`; 19 | } 20 | 21 | const inputSchema = { 22 | ...workspacePathInputSchema, 23 | ...GetSymbolLSPInfoInputSchema.shape 24 | }; 25 | 26 | export function registerGetSymbolLSPInfo(server: McpServer) { 27 | server.registerTool(VscodeMcpToolName.GET_SYMBOL_LSP_INFO, { 28 | title: "Get Symbol LSP Info", 29 | description: `Retrieve comprehensive LSP information for a symbol, including type definitions, documentation, and usage details. 30 | Essential for fixing type errors and understanding symbol declarations. 31 | Typical use cases: 32 | - Fix TypeScript type checking errors 33 | - Extract function parameters and return types from symbols when encapsulating code blocks into functions 34 | - Obtain variable types from nearby code symbols when declaring variables or type assertions, avoiding the use of 'any' 35 | `, 36 | inputSchema, 37 | annotations: { 38 | title: "Get Symbol LSP Info", 39 | readOnlyHint: true, 40 | destructiveHint: false, 41 | idempotentHint: true, 42 | openWorldHint: false 43 | } 44 | }, async ({ workspace_path, filePath, symbol, codeSnippet, infoType }) => { 45 | try { 46 | const dispatcher = await createDispatcher(workspace_path); 47 | const result = await dispatcher.dispatch("getSymbolLSPInfo", { filePath, symbol, codeSnippet, infoType }); 48 | 49 | // Format the comprehensive results 50 | let output = `🔍 **Symbol LSP Information: \`${symbol}\`**\n\n`; 51 | output += `📍 **File**: ${filePath}\n`; 52 | if (codeSnippet) { 53 | output += `📝 **Context**: \`${codeSnippet}\`\n`; 54 | } 55 | output += `🎯 **Info Type**: ${infoType || 'all'}\n\n`; 56 | 57 | const sections: string[] = []; 58 | 59 | // Hover results 60 | if (result.hover && result.hover.length > 0) { 61 | sections.push(`**💡 Hover Information** (${result.hover.length} entries):`); 62 | result.hover.forEach((hover, index) => { 63 | sections.push(` **${index + 1}.** ${Array.isArray(hover.contents) ? hover.contents.join('\n') : hover.contents}`); 64 | if (hover.range) { 65 | sections.push(` Range: ${hover.range.start.line}:${hover.range.start.character}-${hover.range.end.line}:${hover.range.end.character}`); 66 | } 67 | }); 68 | } else if (result.hover) { 69 | sections.push(`**💡 Hover Information**: No hover info available`); 70 | } 71 | 72 | // Signature Help results 73 | if (result.signature_help) { 74 | sections.push(`**✍️ Signature Help**:`); 75 | sections.push(` Active Signature: ${result.signature_help.activeSignature ?? 'N/A'}`); 76 | sections.push(` Active Parameter: ${result.signature_help.activeParameter ?? 'N/A'}`); 77 | sections.push(` Signatures (${result.signature_help.signatures.length}):`); 78 | result.signature_help.signatures.forEach((sig, index) => { 79 | sections.push(` ${index + 1}. \`${sig.label}\``); 80 | if (sig.documentation) { 81 | sections.push(` ${sig.documentation}`); 82 | } 83 | if (sig.parameters && sig.parameters.length > 0) { 84 | sections.push(` Parameters: ${sig.parameters.map(p => p.label).join(', ')}`); 85 | } 86 | }); 87 | } else if (result.signature_help === null && (infoType === 'signature_help' || infoType === 'all')) { 88 | sections.push(`**✍️ Signature Help**: No signature help available`); 89 | } 90 | 91 | // Type Definition results 92 | if (result.type_definition && result.type_definition.length > 0) { 93 | sections.push(`**🏷️ Type Definition** (${result.type_definition.length} locations):\n${result.type_definition.map(def => 94 | formatLocationInfo(def) 95 | ).join('\n')}`); 96 | } else if (result.type_definition) { 97 | sections.push(`**🏷️ Type Definition**: No type definitions found`); 98 | } 99 | 100 | // Definition results 101 | if (result.definition && result.definition.length > 0) { 102 | sections.push(`**📋 Definition** (${result.definition.length} locations):\n${result.definition.map(def => 103 | formatLocationInfo(def) 104 | ).join('\n')}`); 105 | } else if (result.definition) { 106 | sections.push(`**📋 Definition**: No definitions found`); 107 | } 108 | 109 | // Implementation results 110 | if (result.implementation && result.implementation.length > 0) { 111 | sections.push(`**⚙️ Implementation** (${result.implementation.length} locations):\n${result.implementation.map(impl => 112 | formatLocationInfo(impl) 113 | ).join('\n')}`); 114 | } else if (result.implementation) { 115 | sections.push(`**⚙️ Implementation**: No implementations found`); 116 | } 117 | 118 | if (sections.length === 0) { 119 | output += `❌ No LSP information available for symbol "\`${symbol}\`"`; 120 | output += `\n\n💡 **Troubleshooting Tips:**`; 121 | output += `\n- Make sure the symbol name is spelled correctly`; 122 | output += `\n- Try providing a codeSnippet if there are multiple symbols with the same name`; 123 | output += `\n- Verify the file URI is correct and the file exists`; 124 | output += `\n- Ensure the language server extension is installed and running`; 125 | } else { 126 | output += sections.join('\n\n'); 127 | } 128 | 129 | return { 130 | content: [{ 131 | type: "text" as const, 132 | text: output 133 | }] 134 | }; 135 | } catch (error) { 136 | return formatToolCallError("Get Symbol LSP Info", error); 137 | } 138 | }); 139 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/services/get-diagnostics.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import * as path from 'path'; 3 | import { promisify } from 'util'; 4 | 5 | import type { EventParams, EventResult } from '@vscode-mcp/vscode-mcp-ipc'; 6 | import * as vscode from 'vscode'; 7 | 8 | import { logger } from '../logger.js'; 9 | import { ensureFileIsOpen, resolveFilePaths } from '../utils/workspace.js'; 10 | 11 | const execAsync = promisify(exec); 12 | 13 | /** 14 | * Get all git modified files in the workspace (staged, unstaged, and untracked) 15 | * Returns absolute file paths 16 | */ 17 | async function getModifiedFiles(): Promise { 18 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 19 | if (!workspaceFolder) { 20 | logger.info('No workspace folder found, returning empty modified files list'); 21 | return []; 22 | } 23 | 24 | const workspaceRoot = workspaceFolder.uri.fsPath; 25 | logger.info(`Getting git modified files from workspace: ${workspaceRoot}`); 26 | const modifiedFiles: string[] = []; 27 | 28 | try { 29 | // Get unstaged changes 30 | const { stdout: unstagedFiles } = await execAsync('git diff --name-only', { 31 | cwd: workspaceRoot 32 | }); 33 | 34 | // Get staged changes 35 | const { stdout: stagedFiles } = await execAsync('git diff --cached --name-only', { 36 | cwd: workspaceRoot 37 | }); 38 | 39 | // Get untracked files (new files not yet added to git) 40 | const { stdout: untrackedFiles } = await execAsync('git ls-files --others --exclude-standard', { 41 | cwd: workspaceRoot 42 | }); 43 | 44 | // Combine and process file paths 45 | const allFiles = new Set(); 46 | 47 | // Process unstaged files 48 | if (unstagedFiles.trim()) { 49 | unstagedFiles.trim().split('\n').forEach(filePath => { 50 | if (filePath.trim()) { 51 | allFiles.add(filePath.trim()); 52 | } 53 | }); 54 | } 55 | 56 | // Process staged files 57 | if (stagedFiles.trim()) { 58 | stagedFiles.trim().split('\n').forEach(filePath => { 59 | if (filePath.trim()) { 60 | allFiles.add(filePath.trim()); 61 | } 62 | }); 63 | } 64 | 65 | // Process untracked files 66 | if (untrackedFiles.trim()) { 67 | untrackedFiles.trim().split('\n').forEach(filePath => { 68 | if (filePath.trim()) { 69 | allFiles.add(filePath.trim()); 70 | } 71 | }); 72 | } 73 | 74 | // Convert relative paths to absolute paths 75 | for (const relativePath of allFiles) { 76 | const absolutePath = path.resolve(workspaceRoot, relativePath); 77 | modifiedFiles.push(absolutePath); 78 | } 79 | 80 | logger.info(`Found ${modifiedFiles.length} git modified files: ${modifiedFiles.map(f => path.basename(f)).join(', ')}`); 81 | 82 | } catch (error) { 83 | logger.error(`Error getting git modified files: ${error}`); 84 | // Fallback: return empty array if git commands fail 85 | return []; 86 | } 87 | 88 | return modifiedFiles; 89 | } 90 | 91 | /** 92 | * Handle get diagnostics for multiple files 93 | */ 94 | export const getDiagnostics = async ( 95 | payload: EventParams<'getDiagnostics'> 96 | ): Promise> => { 97 | logger.info(`getDiagnostics called with ${payload.__NOT_RECOMMEND__filePaths.length} file paths, sources: [${payload.sources.join(', ')}], severities: [${payload.severities.join(', ')}]`); 98 | 99 | let targetFilePaths = payload.__NOT_RECOMMEND__filePaths; 100 | const { sources, severities } = payload; 101 | 102 | // If empty array is provided, get all git modified files 103 | if (targetFilePaths.length === 0) { 104 | logger.info('No file paths provided, getting git modified files'); 105 | targetFilePaths = await getModifiedFiles(); 106 | } 107 | 108 | // Convert file paths to URIs 109 | const targetUris = resolveFilePaths(targetFilePaths); 110 | 111 | // Severity mapping from VSCode number to string 112 | const severityNumberToString = { 113 | 0: 'error', 114 | 1: 'warning', 115 | 2: 'info', 116 | 3: 'hint' 117 | } as const; 118 | 119 | logger.info(`Processing diagnostics for ${targetUris.length} files`); 120 | 121 | const files = await Promise.all( 122 | targetUris.map(async (uri: vscode.Uri) => { 123 | // Ensure file is open to get accurate diagnostics 124 | await ensureFileIsOpen(uri.toString()); 125 | 126 | // uri is already a VSCode Uri object 127 | const allDiagnostics = vscode.languages.getDiagnostics(uri); 128 | 129 | logger.info(`File ${path.basename(uri.fsPath)}: found ${allDiagnostics.length} total diagnostics`); 130 | 131 | // Filter diagnostics based on sources and severities 132 | const filteredDiagnostics = allDiagnostics.filter(diag => { 133 | // Filter by source (empty array means include all sources) 134 | const sourceMatches = sources.length === 0 || 135 | (diag.source ? sources.some(s => diag.source!.toLowerCase().includes(s.toLowerCase())) : false); 136 | 137 | // Filter by severity (empty array means include all severities) 138 | const diagSeverityString = severityNumberToString[diag.severity]; 139 | const severityMatches = severities.length === 0 || severities.includes(diagSeverityString); 140 | 141 | return sourceMatches && severityMatches; 142 | }); 143 | 144 | logger.info(`File ${path.basename(uri.fsPath)}: after filtering - ${filteredDiagnostics.length} diagnostics (sources: ${filteredDiagnostics.map(d => d.source || 'unknown').join(', ') || 'none'})`); 145 | 146 | return { 147 | uri: uri.fsPath, 148 | diagnostics: filteredDiagnostics.map(diag => ({ 149 | range: { 150 | start: { line: diag.range.start.line, character: diag.range.start.character }, 151 | end: { line: diag.range.end.line, character: diag.range.end.character } 152 | }, 153 | message: diag.message, 154 | severity: severityNumberToString[diag.severity], 155 | source: diag.source, 156 | code: typeof diag.code === 'object' ? diag.code.value : diag.code 157 | })) 158 | }; 159 | }) 160 | ); 161 | 162 | const totalDiagnostics = files.reduce((sum, file) => sum + file.diagnostics.length, 0); 163 | logger.info(`getDiagnostics completed: ${totalDiagnostics} total diagnostics across ${files.length} files`); 164 | 165 | return { files }; 166 | }; -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | 5 | import packageJson from "../package.json" with { type: "json" }; 6 | import { getAllToolNames } from "./constants.js"; 7 | import { createVSCodeMCPServer } from "./server.js"; 8 | 9 | // Package info from package.json 10 | const PACKAGE_NAME = Object.keys(packageJson.bin)[0]; // Get the command name from bin field 11 | const PACKAGE_VERSION = packageJson.version; 12 | 13 | 14 | /** 15 | * Parse enabled tools from CLI arguments or environment variables 16 | */ 17 | function parseEnabledTools(args: string[]): string[] { 18 | const enabledTools = new Set(); 19 | 20 | // Check environment variable first 21 | const envEnabled = process.env.VSCODE_MCP_ENABLED_TOOLS; 22 | if (envEnabled) { 23 | envEnabled.split(',').forEach(tool => { 24 | const trimmed = tool.trim(); 25 | if (trimmed) enabledTools.add(trimmed); 26 | }); 27 | } 28 | 29 | // Check CLI arguments (overrides environment variable) 30 | const enableToolsIndex = args.indexOf('--enable-tools'); 31 | if (enableToolsIndex !== -1 && enableToolsIndex + 1 < args.length) { 32 | const toolsArg = args[enableToolsIndex + 1]; 33 | toolsArg.split(',').forEach(tool => { 34 | const trimmed = tool.trim(); 35 | if (trimmed) enabledTools.add(trimmed); 36 | }); 37 | } 38 | 39 | // If no enabled tools specified, return empty array (all tools enabled by default) 40 | if (enabledTools.size === 0) { 41 | return []; 42 | } 43 | 44 | const availableTools = getAllToolNames(); 45 | 46 | // Validate tool names 47 | const invalidTools = [...enabledTools].filter(tool => !availableTools.includes(tool)); 48 | if (invalidTools.length > 0) { 49 | console.error(`Warning: Invalid tool names will be ignored: ${invalidTools.join(', ')}`); 50 | console.error(`Available tools: ${availableTools.join(', ')}`); 51 | } 52 | 53 | // Return only valid tools 54 | return [...enabledTools].filter(tool => availableTools.includes(tool)); 55 | } 56 | 57 | /** 58 | * Parse disabled tools from CLI arguments or environment variables 59 | */ 60 | function parseDisabledTools(args: string[]): string[] { 61 | const disabledTools = new Set(); 62 | 63 | // Check environment variable first 64 | const envDisabled = process.env.VSCODE_MCP_DISABLED_TOOLS; 65 | if (envDisabled) { 66 | envDisabled.split(',').forEach(tool => { 67 | const trimmed = tool.trim(); 68 | if (trimmed) disabledTools.add(trimmed); 69 | }); 70 | } 71 | 72 | // Check CLI arguments (overrides environment variable) 73 | const disableToolsIndex = args.indexOf('--disable-tools'); 74 | if (disableToolsIndex !== -1 && disableToolsIndex + 1 < args.length) { 75 | const toolsArg = args[disableToolsIndex + 1]; 76 | toolsArg.split(',').forEach(tool => { 77 | const trimmed = tool.trim(); 78 | if (trimmed) disabledTools.add(trimmed); 79 | }); 80 | } 81 | 82 | const availableTools = getAllToolNames(); 83 | 84 | // Validate tool names 85 | const invalidTools = [...disabledTools].filter(tool => !availableTools.includes(tool)); 86 | if (invalidTools.length > 0) { 87 | console.error(`Warning: Invalid tool names will be ignored: ${invalidTools.join(', ')}`); 88 | console.error(`Available tools: ${availableTools.join(', ')}`); 89 | } 90 | 91 | // Return only valid tools 92 | return [...disabledTools].filter(tool => availableTools.includes(tool)); 93 | } 94 | 95 | /** 96 | * Handle command line arguments 97 | */ 98 | function handleCliArgs(): { shouldExit: boolean; enabledTools: string[]; disabledTools: string[] } { 99 | const args = process.argv.slice(2); 100 | const argSet = new Set(args); 101 | 102 | const enabledTools = parseEnabledTools(args); 103 | const disabledTools = parseDisabledTools(args); 104 | 105 | if (argSet.has("--version") || argSet.has("-v")) { 106 | console.log(`${PACKAGE_NAME} v${PACKAGE_VERSION}`); 107 | return { shouldExit: true, enabledTools: [], disabledTools: [] }; 108 | } 109 | 110 | if (argSet.has("--help") || argSet.has("-h")) { 111 | console.log(` 112 | ${PACKAGE_NAME} v${PACKAGE_VERSION} 113 | 114 | A Model Context Protocol (MCP) server that provides access to VSCode workspace information. 115 | 116 | Usage: 117 | ${PACKAGE_NAME} [options] 118 | 119 | Options: 120 | -v, --version Show version information 121 | -h, --help Show this help message 122 | --enable-tools Comma-separated list of tools to enable (if specified, only these tools will be available) 123 | --disable-tools Comma-separated list of tools to disable (applied after --enable-tools) 124 | 125 | Description: 126 | This server communicates with VSCode extensions through Unix Domain Sockets 127 | to provide real-time code intelligence information to MCP clients. 128 | 129 | The server expects stdin/stdout communication using the MCP protocol. 130 | It requires the VSCode MCP Bridge extension to be installed and running. 131 | 132 | Available Tools: 133 | ${getAllToolNames().join(', ')} 134 | 135 | Examples: 136 | # Start the server (for MCP client usage) 137 | ${PACKAGE_NAME} 138 | 139 | # Enable only specific tools 140 | ${PACKAGE_NAME} --enable-tools get_diagnostics,get_symbol_lsp_info 141 | 142 | # Disable specific tools 143 | ${PACKAGE_NAME} --disable-tools execute_command,list_workspaces 144 | 145 | # Using environment variables 146 | VSCODE_MCP_ENABLED_TOOLS="get_diagnostics,get_symbol_lsp_info" ${PACKAGE_NAME} 147 | VSCODE_MCP_DISABLED_TOOLS="execute_command,list_workspaces" ${PACKAGE_NAME} 148 | 149 | # Show version 150 | ${PACKAGE_NAME} --version 151 | `); 152 | return { shouldExit: true, enabledTools: [], disabledTools: [] }; 153 | } 154 | 155 | return { shouldExit: false, enabledTools, disabledTools }; 156 | } 157 | 158 | /** 159 | * Main entry point for the MCP server 160 | */ 161 | async function main(): Promise { 162 | // Handle CLI arguments first 163 | const { shouldExit, enabledTools, disabledTools } = handleCliArgs(); 164 | if (shouldExit) { 165 | return; 166 | } 167 | 168 | // Log enabled/disabled tools if any 169 | if (enabledTools.length > 0) { 170 | console.error(`Enabled tools: ${enabledTools.join(', ')}`); 171 | } 172 | if (disabledTools.length > 0) { 173 | console.error(`Disabled tools: ${disabledTools.join(', ')}`); 174 | } 175 | 176 | // Create the server using the new architecture 177 | const server = createVSCodeMCPServer(PACKAGE_NAME, PACKAGE_VERSION, enabledTools, disabledTools); 178 | 179 | // Start the server 180 | const transport = new StdioServerTransport(); 181 | await server.connect(transport); 182 | console.error("VSCode MCP Server started on stdio"); 183 | } 184 | 185 | // Handle graceful shutdown 186 | process.on('SIGINT', async () => { 187 | console.error("Received SIGINT, shutting down gracefully..."); 188 | process.exit(0); 189 | }); 190 | 191 | process.on('SIGTERM', async () => { 192 | console.error("Received SIGTERM, shutting down gracefully..."); 193 | process.exit(0); 194 | }); 195 | 196 | // Start the server 197 | main().catch((error) => { 198 | console.error("Fatal error:", error); 199 | process.exit(1); 200 | }); -------------------------------------------------------------------------------- /packages/vscode-mcp-server/src/tools/list-workspaces.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | 3 | import { VscodeMcpToolName } from "../constants.js"; 4 | import { formatToolCallError } from "../utils/format-tool-call-error.js"; 5 | import { discoverAvailableWorkspaces } from "../utils/workspace-discovery.js"; 6 | 7 | // MCP tools don't have workspace_path parameter for list_workspaces 8 | // since it discovers all workspaces 9 | const inputSchema = {}; 10 | 11 | const DESCRIPTION = `List all available VSCode workspaces that can be connected to use vscode mcp tools 12 | 13 | **Return Format:** 14 | Array of workspace objects with paths, names, status, and optional details. 15 | Includes summary statistics about discovered workspaces. 16 | 17 | **Important Notes:** 18 | - **Active status** means successfully connected and verified 19 | - **Available status** means socket exists but not tested`; 20 | 21 | export function registerListWorkspaces(server: McpServer) { 22 | server.registerTool(VscodeMcpToolName.LIST_WORKSPACES, { 23 | title: "List Available Workspaces", 24 | description: DESCRIPTION, 25 | inputSchema, 26 | annotations: { 27 | title: "List Available Workspaces", 28 | readOnlyHint: true, 29 | destructiveHint: false, 30 | idempotentHint: false, // May clean zombie sockets 31 | openWorldHint: false 32 | } 33 | }, async () => { 34 | try { 35 | // Directly call the discovery function with hardcoded true values 36 | const result = await discoverAvailableWorkspaces({ 37 | cleanZombieSockets: true, 38 | includeDetails: true, 39 | testConnection: true 40 | }); 41 | 42 | const workspaces = result.workspaces; 43 | const hasLegacyWorkspaces = result.hasLegacyWorkspaces; 44 | 45 | // Calculate summary statistics 46 | const activeCount = workspaces.filter(w => w.status === 'active').length; 47 | const availableCount = workspaces.filter(w => w.status === 'available').length; 48 | 49 | const summary = { 50 | total: workspaces.length, 51 | active: activeCount, 52 | available: availableCount, 53 | cleaned: 0 // We can't track exact cleaned count from the current implementation 54 | }; 55 | 56 | // Format the response for display 57 | let response = "🔍 Available VSCode Workspaces:\n\n"; 58 | 59 | if (workspaces.length === 0) { 60 | response += "No active VSCode workspaces found.\n\n"; 61 | response += "💡 Tips:\n"; 62 | response += "- Make sure VSCode/Cursor/Windsurf is running\n"; 63 | response += "- Ensure VSCode MCP Bridge extension is installed and activated\n"; 64 | response += "- Check the extension output panel for any errors\n"; 65 | } else { 66 | // Group workspaces by status 67 | const activeWorkspaces = workspaces.filter(w => w.status === 'active'); 68 | const availableWorkspaces = workspaces.filter(w => w.status === 'available'); 69 | const errorWorkspaces = workspaces.filter(w => w.status === 'error'); 70 | 71 | // Show active workspaces first 72 | if (activeWorkspaces.length > 0) { 73 | response += "✅ Active Workspaces:\n"; 74 | activeWorkspaces.forEach((workspace, index) => { 75 | response += `\n${index + 1}. ${workspace.workspace_path}\n`; 76 | if (workspace.workspace_name) { 77 | response += ` 📁 Name: ${workspace.workspace_name}\n`; 78 | } 79 | if (workspace.workspace_type) { 80 | response += ` 📦 Type: ${workspace.workspace_type}\n`; 81 | } 82 | if (workspace.folders && workspace.folders.length > 1) { 83 | response += ` 📂 Folders:\n`; 84 | workspace.folders.forEach(folder => { 85 | response += ` - ${folder}\n`; 86 | }); 87 | } 88 | if (workspace.extension_version) { 89 | response += ` 🔌 Extension: v${workspace.extension_version}\n`; 90 | } 91 | if (workspace.vscode_version) { 92 | response += ` 💻 VSCode: v${workspace.vscode_version}\n`; 93 | } 94 | if (workspace.socket_path) { 95 | response += ` 🔧 Socket: ${workspace.socket_path}\n`; 96 | } 97 | }); 98 | } 99 | 100 | // Show available but untested workspaces 101 | if (availableWorkspaces.length > 0) { 102 | response += "\n📋 Available (not tested):\n"; 103 | availableWorkspaces.forEach((workspace, index) => { 104 | response += `\n${activeWorkspaces.length + index + 1}. ${workspace.workspace_path}\n`; 105 | if (workspace.socket_path) { 106 | response += ` 🔧 Socket: ${workspace.socket_path}\n`; 107 | } 108 | }); 109 | } 110 | 111 | // Show error workspaces 112 | if (errorWorkspaces.length > 0) { 113 | response += "\n❌ Workspaces with errors:\n"; 114 | errorWorkspaces.forEach(workspace => { 115 | response += `\n- ${workspace.workspace_path}\n`; 116 | if (workspace.error) { 117 | response += ` Error: ${workspace.error}\n`; 118 | } 119 | }); 120 | } 121 | } 122 | 123 | // Add summary 124 | response += `\n📊 Summary:\n`; 125 | response += `- Total workspaces: ${summary.total}\n`; 126 | response += `- Active: ${summary.active}\n`; 127 | if (summary.available > 0) { 128 | response += `- Available (untested): ${summary.available}\n`; 129 | } 130 | if (summary.cleaned > 0) { 131 | response += `- Zombie sockets cleaned: ${summary.cleaned}\n`; 132 | } 133 | 134 | // Add legacy workspace warning 135 | if (hasLegacyWorkspaces) { 136 | response += `\n⚠️ Legacy Socket Paths Detected:\n`; 137 | response += ` These workspaces are using the old socket path format.\n`; 138 | response += ` Please upgrade your VSCode MCP Bridge extension to the latest version:\n`; 139 | response += ` Extension: YuTengjing.vscode-mcp-bridge\n`; 140 | response += ` Marketplace: https://marketplace.visualstudio.com/items?itemName=YuTengjing.vscode-mcp-bridge\n`; 141 | } 142 | 143 | // Add usage hint 144 | if (workspaces.length > 0) { 145 | response += `\n💡 To use a workspace with other MCP tools, copy the workspace_path parameter from above.`; 146 | } 147 | 148 | return { 149 | content: [{ 150 | type: "text", 151 | text: response 152 | }] 153 | }; 154 | } catch (error) { 155 | // Check if it's a connection error 156 | if (error instanceof Error && error.message.includes('connect')) { 157 | // Special handling for no workspaces available 158 | return { 159 | content: [{ 160 | type: "text", 161 | text: `🔍 No VSCode workspaces currently available. 162 | 163 | This could mean: 164 | 1. No VSCode/Cursor/Windsurf instances are running 165 | 2. VSCode MCP Bridge extension is not installed or activated 166 | 3. All VSCode instances were recently closed (sockets cleaned up) 167 | 168 | 💡 To make workspaces available: 169 | 1. Open VSCode/Cursor/Windsurf with a project 170 | 2. Ensure VSCode MCP Bridge extension is installed: https://marketplace.visualstudio.com/items?itemName=YuTengjing.vscode-mcp-bridge 171 | 3. Check the extension is activated (see Output panel → "VSCode MCP Bridge") 172 | 4. Try running this command again` 173 | }] 174 | }; 175 | } 176 | 177 | return formatToolCallError("List Workspaces", error); 178 | } 179 | }); 180 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VSCode MCP 2 | 3 |

4 | VSCode MCP 5 |

6 | 7 |

8 | Connect VSCode with MCP (Model Context Protocol) for enhanced AI assistant capabilities 9 |

10 | 11 |

12 | Design Motivation • 13 | Available Tools • 14 | Installation • 15 | Architecture • 16 | License 17 |

18 | 19 | [![MCP Badge](https://lobehub.com/badge/mcp/tjx666-vscode-mcp)](https://lobehub.com/mcp/tjx666-vscode-mcp) ![CI](https://github.com/tjx666/vscode-mcp/actions/workflows/vscode-extension-ci.yml/badge.svg) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) [![Github Open Issues](https://img.shields.io/github/issues/tjx666/vscode-mcp)](https://github.com/tjx666/vscode-mcp/issues) [![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg?style=flat-square)](https://github.com/996icu/996.ICU/blob/master/LICENSE) 20 | 21 | ## Overview 22 | 23 | VSCode MCP is a comprehensive monorepo solution that enables MCP (Model Context Protocol) clients to access rich VSCode context information in real-time. This project bridges the gap between AI assistants and your development environment, providing accurate code analysis, diagnostics, and intelligent code navigation. 24 | 25 | ## Design Motivation 26 | 27 | **VSCode MCP Bridge primarily serves AI IDEs (like Cursor) and AI coding agents**, helping them develop and analyze code more efficiently. 28 | 29 | Traditional AI coding agents often need to execute time-consuming commands when validating code modifications: 30 | 31 | - `tsc --noEmit` - TypeScript type checking 32 | - `eslint .` - Code style checking 33 | - `npm run build` - Project building 34 | 35 | These commands run slowly in large projects, severely impacting AI development efficiency. VSCode MCP Bridge provides real-time LSP (Language Server Protocol) information, allowing AI agents to: 36 | 37 | - **Get fast diagnostics** (`get-diagnostics`) - Replace time-consuming type checking and lint commands 38 | - **Access comprehensive LSP information** (`get-symbol-lsp-info`) - Get definition, hover, signatures, and type info in one call 39 | - **Navigate code efficiently** (`get-references`) - Understand code structure and dependencies with usage context 40 | - **Safe file operations** - Rename symbols across files with automatic import updates 41 | 42 | ### Core Advantages 43 | 44 | 1. **Real-time**: Leverage VSCode's LSP for real-time code state without executing slow commands 45 | 2. **Accuracy**: Precise analysis based on language servers, more reliable than static analysis 46 | 3. **Efficiency**: Significantly reduce AI coding agent wait times 47 | 4. **Integration**: Deep integration with VSCode ecosystem, supporting multiple languages and extensions 48 | 49 | ## Available Tools 50 | 51 | VSCode MCP provides the following tools through the MCP protocol: 52 | 53 | | Tool | Description | 54 | | ----------------------- | ---------------------------------------------------------------- | 55 | | **execute_command** | ⚠️ Execute VSCode commands with JSON string arguments | 56 | | **get_symbol_lsp_info** | Get comprehensive LSP info (definition, hover, signatures, etc.) | 57 | | **get_diagnostics** | Get real-time diagnostics, replace slow tsc/eslint | 58 | | **get_references** | Find symbol references with usage context code | 59 | | **health_check** | Test connection to VSCode MCP Bridge extension | 60 | | **list_workspaces** | List all available VSCode workspaces | 61 | | **open_files** | Open multiple files with optional editor display | 62 | | **rename_symbol** | Rename symbols across all files in workspace | 63 | 64 | > **⚠️ Security Warning**: The `execute_command` tool can execute arbitrary VSCode commands and potentially trigger dangerous operations. Use with extreme caution and only with trusted AI models. 65 | 66 | ## Installation 67 | 68 | > **🚨 IMPORTANT**: Before installing the MCP server, you must first install the VSCode MCP Bridge extension in your VSCode instance. The extension is required for the MCP server to communicate with VSCode. 69 | 70 | ### Step 1: Install VSCode Extension 71 | 72 | Install the VSCode MCP Bridge extension using ID: `YuTengjing.vscode-mcp-bridge` 73 | 74 | [![Install VSCode Extension](https://img.shields.io/badge/VSCode%20Marketplace-Install%20Extension-007ACC?style=for-the-badge&logo=visualstudiocode)](https://marketplace.visualstudio.com/items?itemName=YuTengjing.vscode-mcp-bridge) 75 | 76 | Or search for "VSCode MCP Bridge" in the VSCode Extensions marketplace. 77 | 78 | ### Step 2: Install MCP Server 79 | 80 | #### Codex 81 | 82 | Add the following configuration to your `~/.codex/config.toml`: 83 | 84 | ```toml 85 | [mcp_servers.vscode-mcp] 86 | command = "bunx" 87 | args = ["-y", "@vscode-mcp/vscode-mcp-server@latest"] 88 | env = { "VSCODE_MCP_DISABLED_TOOLS" = "health_check,list_workspaces,open_files" } 89 | startup_timeout_ms = 16_000 90 | ``` 91 | 92 | #### Claude Code 93 | 94 | Claude Code (claude.ai/code) provides built-in MCP support. Simply run: 95 | 96 | ```bash 97 | claude mcp add vscode-mcp -- npx -y @vscode-mcp/vscode-mcp-server@latest 98 | ``` 99 | 100 | This command will automatically configure the MCP server in your Claude Code environment. 101 | 102 | #### Cursor 103 | 104 | ##### Click the button to install 105 | 106 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=vscode-mcp&config=eyJjb21tYW5kIjoibnB4IEB2c2NvZGUtbWNwL3ZzY29kZS1tY3Atc2VydmVyQGxhdGVzdCJ9) 107 | 108 | ##### Or install manually 109 | 110 | Go to `Cursor Settings` -> `Tools & Integrations` -> `New MCP Server`. Name to your liking, use `command` type with the command `npx @vscode-mcp/vscode-mcp-server@latest`. You can also verify config or add command line arguments via clicking `Edit`. 111 | 112 | ```json 113 | { 114 | "mcpServers": { 115 | "vscode-mcp": { 116 | "command": "npx", 117 | "args": ["@vscode-mcp/vscode-mcp-server@latest"] 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | #### Gemini CLI 124 | 125 | Add the following configuration to your `~/.gemini/settings.json`: 126 | 127 | ```json 128 | { 129 | "mcpServers": { 130 | "vscode-mcp": { 131 | "command": "npx", 132 | "args": ["-y", "@vscode-mcp/vscode-mcp-server@latest"], 133 | "env": {}, 134 | "includeTools": [ 135 | "get_symbol_lsp_info", 136 | "get_diagnostics", 137 | "get_references", 138 | "health_check", 139 | "rename_symbol" 140 | ] 141 | } 142 | } 143 | } 144 | ``` 145 | 146 | ## Tool Filtering 147 | 148 | You can control which tools are available using command-line arguments or environment variables: 149 | 150 | **Command-line arguments:** 151 | 152 | - `--enable-tools` - Comma-separated list of tools to enable (whitelist mode). If specified, only these tools will be available. 153 | - `--disable-tools` - Comma-separated list of tools to disable (blacklist mode). Applied after `--enable-tools`. 154 | 155 | **Environment variables:** 156 | 157 | - `VSCODE_MCP_ENABLED_TOOLS` - Same as `--enable-tools` 158 | - `VSCODE_MCP_DISABLED_TOOLS` - Same as `--disable-tools` 159 | 160 | ## Architecture 161 | 162 | Once installed and configured, VSCode MCP works seamlessly with MCP-compatible clients: 163 | 164 | 1. **VSCode Extension**: Runs in your VSCode instance and provides access to LSP data 165 | 2. **MCP Server**: Translates MCP protocol calls to VSCode extension requests 166 | 167 | All tools require the `workspace_path` parameter to target specific VSCode instances. Each VSCode workspace gets its own socket connection for multi-window support. 168 | 169 | ## License 170 | 171 | This project is licensed under the [Anti 996](https://github.com/996icu/996.ICU/blob/master/LICENSE) License. 172 | -------------------------------------------------------------------------------- /.cursor/rules/vscode-mcp-development-guide.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: VSCode MCP 工具开发指南,包含开发流程、架构、步骤、质量标准和最佳实践 3 | globs: packages/vscode-mcp-ipc/src/**/*,packages/vscode-mcp-bridge/src/**/*,packages/vscode-mcp-server/src/**/* 4 | --- 5 | 6 | # VSCode MCP 工具开发指南 7 | 8 | 本指南总结了在 VSCode MCP Bridge 项目中开发和维护工具的完整流程,包括从基础实现到质量优化的全套最佳实践。 9 | 10 | ## 开发流程 11 | 12 | ### 核心原则 13 | 14 | **开发顺序**: 15 | 16 | ```plaintext 17 | 接口定义 (IPC) → 实现层 (Extension) → 工具层 (MCP Server) → 质量优化 18 | ``` 19 | 20 | **为什么要按这个顺序?** 21 | 22 | - IPC 层定义了类型契约,必须最先完成 23 | - Extension 层依赖 IPC 的类型定义 24 | - MCP Server 层调用 Extension 的服务 25 | - 质量优化确保符合 MCP 官方标准 26 | 27 | **重要提醒**: 每个开发阶段完成后,都必须进行 **编译和构建验证**,确保代码质量和依赖关系正确。 28 | 29 | ## 工具架构 30 | 31 | ### 三层架构 32 | 33 | ```plaintext 34 | MCP Client ↔ MCP Server ↔ IPC Layer ↔ VSCode Extension ↔ VSCode API 35 | ``` 36 | 37 | **职责分工**: 38 | 39 | - **IPC Layer**: 定义类型契约和通信协议 40 | - **Extension Layer**: 实现具体的 VSCode 操作逻辑 41 | - **MCP Server Layer**: 提供标准化的 MCP 工具接口 42 | 43 | ## 开发步骤 44 | 45 | ### 1. IPC 层:定义接口 46 | 47 | #### 1.1 创建事件定义文件 48 | 49 | 在 [packages/vscode-mcp-ipc/src/events/](mdc:packages/vscode-mcp-ipc/src/events/) 目录中创建事件定义文件: 50 | 51 | ```typescript 52 | // packages/vscode-mcp-ipc/src/events/your-tool.ts 53 | import { z } from 'zod'; 54 | 55 | export const YourToolInputSchema = z 56 | .object({ 57 | param1: z.string().describe('参数描述'), 58 | param2: z.boolean().optional().default(true).describe('可选参数'), 59 | }) 60 | .strict(); 61 | 62 | export const YourToolOutputSchema = z 63 | .object({ 64 | result: z.string().describe('结果描述'), 65 | }) 66 | .strict(); 67 | 68 | export type YourToolPayload = z.infer; 69 | export type YourToolResult = z.infer; 70 | ``` 71 | 72 | #### 1.2 注册到事件映射 73 | 74 | 更新 [packages/vscode-mcp-ipc/src/events/index.ts](mdc:packages/vscode-mcp-ipc/src/events/index.ts): 75 | 76 | ```typescript 77 | import type { YourToolPayload, YourToolResult } from './your-tool.js'; 78 | 79 | export * from './your-tool.js'; 80 | 81 | export interface EventMap { 82 | yourTool: { 83 | params: YourToolPayload; 84 | result: YourToolResult; 85 | }; 86 | } 87 | ``` 88 | 89 | #### 1.3 构建 IPC 包 90 | 91 | ```bash 92 | cd packages/vscode-mcp-ipc && npm run build 93 | ``` 94 | 95 | ### 2. Extension 层:实现服务 96 | 97 | #### 2.1 创建服务实现 98 | 99 | 在 [packages/vscode-mcp-bridge/src/services/](mdc:packages/vscode-mcp-bridge/src/services/) 目录中实现服务: 100 | 101 | ```typescript 102 | // packages/vscode-mcp-bridge/src/services/your-tool.ts 103 | import type { EventParams, EventResult } from '@vscode-mcp/vscode-mcp-ipc'; 104 | 105 | export const yourTool = async ( 106 | payload: EventParams<'yourTool'>, 107 | ): Promise> => { 108 | try { 109 | const result = await someVSCodeOperation(payload.param1); 110 | return { result }; 111 | } catch (error) { 112 | throw new Error(`操作失败: ${error}`); 113 | } 114 | }; 115 | ``` 116 | 117 | #### 2.2 注册服务 118 | 119 | 更新 [packages/vscode-mcp-bridge/src/extension.ts](mdc:packages/vscode-mcp-bridge/src/extension.ts): 120 | 121 | ```typescript 122 | import { YourToolInputSchema, YourToolOutputSchema } from '@vscode-mcp/vscode-mcp-ipc'; 123 | import { yourTool } from './services'; 124 | 125 | socketServer.register('yourTool', { 126 | handler: yourTool, 127 | payloadSchema: YourToolInputSchema, 128 | resultSchema: YourToolOutputSchema, 129 | }); 130 | ``` 131 | 132 | ### 3. MCP Server 层:创建工具 133 | 134 | #### 3.1 创建工具文件 135 | 136 | 在 [packages/vscode-mcp-server/src/tools/](mdc:packages/vscode-mcp-server/src/tools/) 目录中实现工具: 137 | 138 | ```typescript 139 | // packages/vscode-mcp-server/src/tools/your-tool.ts 140 | import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 141 | import { createDispatcher, YourToolInputSchema } from '@vscode-mcp/vscode-mcp-ipc'; 142 | import { z } from 'zod'; 143 | import { formatToolCallError } from './utils.js'; 144 | 145 | const inputSchema = { 146 | workspace_path: z.string().describe('VSCode workspace path to target'), 147 | ...YourToolInputSchema.shape, 148 | }; 149 | 150 | export function registerYourTool(server: McpServer) { 151 | server.registerTool( 152 | 'your_tool', 153 | { 154 | title: 'Your Tool Title', 155 | description: 'Detailed description with usage scenarios and examples', 156 | inputSchema, 157 | annotations: { 158 | title: 'Your Tool Title', 159 | readOnlyHint: true, 160 | destructiveHint: false, 161 | idempotentHint: true, 162 | openWorldHint: false, 163 | }, 164 | }, 165 | async ({ workspace_path, param1, param2 }) => { 166 | try { 167 | const dispatcher = createDispatcher(workspace_path); 168 | const result = await dispatcher.dispatch('yourTool', { param1, param2 }); 169 | 170 | return { 171 | content: [ 172 | { 173 | type: 'text' as const, 174 | text: `✅ 操作成功: ${result.result}`, 175 | }, 176 | ], 177 | }; 178 | } catch (error) { 179 | return formatToolCallError('Your Tool Title', error); 180 | } 181 | }, 182 | ); 183 | } 184 | ``` 185 | 186 | ## 质量标准 187 | 188 | ### 错误处理标准 189 | 190 | 遵循 MCP 官方错误处理规范,使用统一的错误处理函数: 191 | 192 | ```typescript 193 | // packages/vscode-mcp-server/src/tools/utils.ts 194 | export function formatToolCallError(toolName: string, error: unknown) { 195 | return { 196 | isError: true, // MCP 官方要求 197 | content: [ 198 | { 199 | type: 'text' as const, 200 | text: `❌ ${toolName} failed: ${String(error)}`, 201 | }, 202 | ], 203 | }; 204 | } 205 | ``` 206 | 207 | ### Tool Annotations 标准 208 | 209 | 根据 MCP 官方规范配置: 210 | 211 | | Annotation | 类型 | 默认值 | 描述 | 212 | | ----------------- | ------- | ------ | ------------------ | 213 | | `title` | string | - | 人性化标题 | 214 | | `readOnlyHint` | boolean | false | 是否只读操作 | 215 | | `destructiveHint` | boolean | true | 是否可能破坏性操作 | 216 | | `idempotentHint` | boolean | false | 是否幂等操作 | 217 | | `openWorldHint` | boolean | true | 是否与外部世界交互 | 218 | 219 | ## 最佳实践 220 | 221 | ### 1. 命名规范 222 | 223 | - **事件名**: camelCase (`getDiagnostics`, `openFiles`) 224 | - **工具名**: snake_case (`get_diagnostics`, `open_files`) 225 | - **文件名**: kebab-case (`get-diagnostics.ts`, `open-files.ts`) 226 | 227 | ### 2. 错误处理 228 | 229 | - 统一使用 `formatToolCallError` 函数 230 | - 必须设置 `isError: true` 231 | - 提供有意义的错误信息 232 | 233 | ### 3. Schema 设计 234 | 235 | - 使用 `.describe()` 为所有参数添加描述 236 | - 设置合理的默认值 237 | - 使用 `.strict()` 确保类型安全 238 | - MCP 工具层必须复用 IPC 层的 Schema 239 | 240 | ### 4. 构建验证 241 | 242 | 每个开发阶段完成后必须进行验证: 243 | 244 | ```bash 245 | # IPC 层 246 | cd packages/vscode-mcp-ipc && npm run build 247 | 248 | # Extension 层 249 | cd packages/vscode-mcp-bridge && npm run compile:test 250 | 251 | # MCP Server 层 252 | cd packages/vscode-mcp-server && npm run build 253 | ``` 254 | 255 | ## 开发检查清单 256 | 257 | ### 新工具开发 258 | 259 | **基础实现:** 260 | 261 | - [ ] 在 IPC 层定义 Schema 和类型 262 | - [ ] 添加到 EventMap 并导出 263 | - [ ] 构建 IPC 包 264 | - [ ] 实现 Extension 服务逻辑 265 | - [ ] 在 Extension 中注册服务 266 | - [ ] Extension 层编译验证 267 | - [ ] 创建 MCP 工具实现(正确复用 IPC Schema) 268 | - [ ] 导出并注册 MCP 工具 269 | - [ ] MCP Server 层构建 270 | 271 | **质量优化:** 272 | 273 | - [ ] 添加统一错误处理 274 | - [ ] 配置合适的 Tool Annotations 275 | - [ ] 优化 Description 276 | - [ ] 验证符合 MCP 官方标准 277 | 278 | **验证测试:** 279 | 280 | - [ ] 编译检查所有包 281 | - [ ] 功能测试验证 282 | - [ ] 错误处理测试 283 | - [ ] LLM 使用效果验证 284 | 285 | ## 常见问题解决 286 | 287 | ### 问题 1: 类型错误 288 | 289 | **症状**: `Property 'xxx' does not exist on type` 290 | 291 | **解决方案**: 292 | 293 | 1. 确保已构建 IPC 包: `cd packages/vscode-mcp-ipc && npm run build` 294 | 2. 检查事件是否已添加到 `EventMap` 295 | 3. 确保导入了正确的类型 296 | 297 | ### 问题 2: Schema 重复定义 298 | 299 | **症状**: MCP 工具层重新定义了 IPC 层已有的参数 300 | 301 | **解决方案**: 正确复用 IPC 层的 Schema 302 | 303 | ```typescript 304 | // ❌ 错误:重新定义参数 305 | const inputSchema = { 306 | workspace_path: z.string(), 307 | uri: z.string(), 308 | line: z.number(), 309 | }; 310 | 311 | // ✅ 正确:复用 IPC 层的 Schema 312 | import { GetDefinitionInputSchema } from '@vscode-mcp/vscode-mcp-ipc'; 313 | 314 | const inputSchema = { 315 | workspace_path: z.string().describe('VSCode workspace path to target'), 316 | ...GetDefinitionInputSchema.shape, 317 | }; 318 | ``` 319 | 320 | ### 问题 3: 带验证的 Schema 无法复用 321 | 322 | **症状**: 使用 `.refine()` 的 Schema 变成 `ZodEffects` 类型,没有 `.shape` 属性 323 | 324 | **解决方案**: 分离基础 Schema 和验证 Schema 325 | 326 | ```typescript 327 | // IPC 层:分离基础 Schema 和验证 Schema 328 | export const YourToolBaseInputSchema = z 329 | .object({ 330 | param1: z.string().describe('参数1'), 331 | param2: z.string().optional().describe('参数2'), 332 | }) 333 | .strict(); 334 | 335 | export const YourToolInputSchema = YourToolBaseInputSchema.refine( 336 | (data) => { 337 | return data.param1 && data.param2; 338 | }, 339 | { message: '验证失败' }, 340 | ); 341 | 342 | // MCP Server 层:复用基础 Schema 343 | import { YourToolBaseInputSchema } from '@vscode-mcp/vscode-mcp-ipc'; 344 | 345 | const inputSchema = { 346 | workspace_path: z.string().describe('VSCode workspace path to target'), 347 | ...YourToolBaseInputSchema.shape, 348 | }; 349 | ``` 350 | 351 | 遵循这个指南可以确保工具开发的一致性、可维护性和符合 MCP 官方标准。 352 | -------------------------------------------------------------------------------- /packages/vscode-mcp-bridge/src/socket-server.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as net from 'net'; 3 | 4 | import type { BaseRequest, BaseResponse, EventName } from '@vscode-mcp/vscode-mcp-ipc'; 5 | import { getSocketPath } from '@vscode-mcp/vscode-mcp-ipc'; 6 | import { z } from 'zod'; 7 | 8 | import { logger } from './logger'; 9 | 10 | type ServiceFunction = ( 11 | payload: any 12 | ) => Promise; 13 | 14 | /** 15 | * Service registration information 16 | */ 17 | interface ServiceRegistration { 18 | handler: ServiceFunction; 19 | payloadSchema?: z.ZodSchema; 20 | resultSchema?: z.ZodSchema; 21 | } 22 | 23 | /** 24 | * Service registration options 25 | */ 26 | interface ServiceRegistrationOptions { 27 | handler: ServiceFunction; 28 | payloadSchema?: z.ZodSchema; 29 | resultSchema?: z.ZodSchema; 30 | } 31 | 32 | /** 33 | * Socket Server for handling MCP communication 34 | */ 35 | export class SocketServer { 36 | private server: net.Server | null = null; 37 | private socketPath: string | null = null; 38 | private services: Map = new Map(); 39 | 40 | constructor(workspacePath: string) { 41 | this.socketPath = getSocketPath(workspacePath); 42 | } 43 | 44 | /** 45 | * Register a service handler with optional schema validation 46 | */ 47 | register(method: EventName, options: ServiceRegistrationOptions): void { 48 | this.services.set(method, { 49 | handler: options.handler, 50 | payloadSchema: options.payloadSchema, 51 | resultSchema: options.resultSchema 52 | }); 53 | logger.info(`Registered service: ${method}`); 54 | } 55 | 56 | /** 57 | * Handle incoming request and route to appropriate service 58 | */ 59 | private async handleRequest(request: BaseRequest): Promise { 60 | const { id, method, params } = request; 61 | 62 | logger.info(`Processing request: ${method}`); 63 | 64 | // Look up service registration 65 | const registration = this.services.get(method); 66 | if (!registration) { 67 | return { 68 | id, 69 | error: { 70 | code: 404, 71 | message: `Unknown method: ${method}` 72 | } 73 | }; 74 | } 75 | 76 | try { 77 | // Validate payload if schema is provided 78 | let validatedPayload = params || {}; 79 | if (registration.payloadSchema) { 80 | try { 81 | validatedPayload = registration.payloadSchema.parse(params || {}); 82 | } catch (validationError) { 83 | return { 84 | id, 85 | error: { 86 | code: 400, 87 | message: `Invalid payload for ${method}`, 88 | details: validationError instanceof z.ZodError 89 | ? validationError.errors.map((e: z.ZodIssue) => `${e.path.join('.')}: ${e.message}`).join(', ') 90 | : String(validationError) 91 | } 92 | }; 93 | } 94 | } 95 | 96 | // Call service function 97 | const result = await registration.handler(validatedPayload); 98 | 99 | // Validate result if schema is provided 100 | if (registration.resultSchema) { 101 | try { 102 | registration.resultSchema.parse(result); 103 | } catch (validationError) { 104 | // Log the actual result value that failed validation 105 | logger.error(`Result validation failed for ${method}:`); 106 | logger.error(`Actual result: ${JSON.stringify(result, null, 2)}`); 107 | logger.error(`Validation error: ${validationError}`); 108 | return { 109 | id, 110 | error: { 111 | code: 500, 112 | message: `Invalid result from ${method}`, 113 | details: validationError instanceof z.ZodError 114 | ? validationError.errors.map((e: z.ZodIssue) => `${e.path.join('.')}: ${e.message}`).join(', ') 115 | : String(validationError) 116 | } 117 | }; 118 | } 119 | } 120 | 121 | // Log successful service call 122 | logger.logServiceCall(method, params, result); 123 | 124 | return { id, result }; 125 | } catch (error) { 126 | // Log service error 127 | logger.logServiceError(method, params, error); 128 | 129 | return { 130 | id, 131 | error: { 132 | code: 500, 133 | message: `Internal error in ${method}`, 134 | details: String(error) 135 | } 136 | }; 137 | } 138 | } 139 | 140 | /** 141 | * Handle socket data 142 | */ 143 | private async handleSocketData(socket: net.Socket, data: any): Promise { 144 | try { 145 | const message = JSON.parse(data.toString()); 146 | logger.info(`Received message: ${JSON.stringify(message)}`); 147 | 148 | // Validate request format 149 | if (!message.id || !message.method) { 150 | const response: BaseResponse = { 151 | id: message.id || 'unknown', 152 | error: { 153 | code: 400, 154 | message: 'Invalid request format: missing id or method' 155 | } 156 | }; 157 | socket.write(JSON.stringify(response)); 158 | return; 159 | } 160 | 161 | // Handle request 162 | const response = await this.handleRequest(message); 163 | 164 | // Send response 165 | socket.write(JSON.stringify(response)); 166 | logger.info(`Sent response for ${message.method}: ${response.result ? 'success' : 'error'}`); 167 | 168 | } catch (error) { 169 | logger.error(`Error handling socket data: ${error}`); 170 | 171 | const errorResponse: BaseResponse = { 172 | id: 'unknown', 173 | error: { 174 | code: 500, 175 | message: 'Internal server error', 176 | details: String(error) 177 | } 178 | }; 179 | 180 | socket.write(JSON.stringify(errorResponse)); 181 | } 182 | } 183 | 184 | /** 185 | * Start the socket server 186 | */ 187 | async start(): Promise { 188 | if (this.server) { 189 | throw new Error('Socket server is already running'); 190 | } 191 | 192 | // Clean up existing socket file if it exists (Unix-like systems only) 193 | if (this.socketPath && process.platform !== 'win32' && fs.existsSync(this.socketPath)) { 194 | try { 195 | fs.unlinkSync(this.socketPath); 196 | logger.info('Removed existing socket file'); 197 | } catch (error) { 198 | logger.error(`Error removing existing socket file: ${error}`); 199 | // If the file is in use by another process, fail fast 200 | if ((error as NodeJS.ErrnoException).code === 'EBUSY' || (error as NodeJS.ErrnoException).code === 'EADDRINUSE') { 201 | throw new Error('Socket is already in use by another VSCode instance'); 202 | } 203 | } 204 | } 205 | 206 | return new Promise((resolve, reject) => { 207 | this.server = net.createServer(); 208 | 209 | this.server.on('connection', (socket) => { 210 | logger.info(`Client connected to socket: ${this.socketPath}`); 211 | 212 | socket.on('data', (data) => { 213 | this.handleSocketData(socket, data); 214 | }); 215 | 216 | socket.on('close', () => { 217 | logger.info('Client disconnected'); 218 | }); 219 | 220 | socket.on('error', (error) => { 221 | logger.error(`Socket error: ${error}`); 222 | }); 223 | }); 224 | 225 | this.server.on('error', (error) => { 226 | logger.error(`Server error: ${error}`); 227 | reject(error); 228 | }); 229 | 230 | this.server.listen(this.socketPath!, () => { 231 | logger.info(`Socket server listening on: ${this.socketPath}`); 232 | resolve(); 233 | }); 234 | }); 235 | } 236 | 237 | /** 238 | * Stop the socket server and cleanup 239 | */ 240 | cleanup(): void { 241 | if (this.server) { 242 | this.server.close(); 243 | this.server = null; 244 | logger.info('Socket server closed'); 245 | } 246 | 247 | if (this.socketPath && process.platform !== 'win32' && fs.existsSync(this.socketPath)) { 248 | try { 249 | fs.unlinkSync(this.socketPath); 250 | logger.info(`Socket file removed: ${this.socketPath}`); 251 | } catch (error) { 252 | logger.error(`Error removing socket file: ${error}`); 253 | } 254 | } 255 | 256 | this.socketPath = null; 257 | } 258 | 259 | /** 260 | * Get the socket path 261 | */ 262 | getSocketPath(): string | null { 263 | return this.socketPath; 264 | } 265 | 266 | /** 267 | * Get registered services count 268 | */ 269 | getServicesCount(): number { 270 | return this.services.size; 271 | } 272 | } -------------------------------------------------------------------------------- /packages/vscode-mcp-ipc/src/dispatch.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'node:crypto'; 2 | import { mkdir, stat } from 'node:fs/promises'; 3 | import { Socket } from 'node:net'; 4 | import { homedir, tmpdir } from 'node:os'; 5 | import { isAbsolute, join } from 'node:path'; 6 | 7 | import type { BaseRequest, BaseResponse, EventName, EventParams, EventResult } from './events/index.js'; 8 | 9 | /** 10 | * Check if a path exists 11 | */ 12 | async function pathExists(path: string): Promise { 13 | try { 14 | await stat(path); 15 | return true; 16 | } catch { 17 | return false; 18 | } 19 | } 20 | 21 | /** 22 | * Get application data directory for storing socket files 23 | */ 24 | export function getAppDataDir(): string { 25 | const appName = 'YuTengjing.vscode-mcp'; 26 | const homeDir = homedir(); 27 | 28 | switch (process.platform) { 29 | case 'darwin': 30 | // macOS: Use Application Support directory 31 | return join(homeDir, 'Library', 'Application Support', appName); 32 | 33 | case 'win32': 34 | // Windows: Using named pipes, no directory needed 35 | return ''; 36 | 37 | default: { 38 | // Linux and other Unix-like: Follow XDG Base Directory spec 39 | const linuxAppName = appName.toLowerCase().replaceAll('.', '-'); 40 | const xdgData = process.env.XDG_DATA_HOME || join(homeDir, '.local', 'share'); 41 | return join(xdgData, linuxAppName); 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Generate socket path based on workspace path 48 | */ 49 | export function getSocketPath(workspacePath: string): string { 50 | const hash = createHash('md5').update(workspacePath).digest('hex').slice(0, 8); 51 | 52 | if (process.platform === 'win32') { 53 | // Windows: Use named pipes 54 | return `\\\\.\\pipe\\vscode-mcp-${hash}`; 55 | } 56 | 57 | // Unix-like systems: Use socket files in app data directory 58 | const appDir = getAppDataDir(); 59 | return join(appDir, `vscode-mcp-${hash}.sock`); 60 | } 61 | 62 | /** 63 | * Ensure socket directory exists 64 | */ 65 | async function ensureSocketDir(): Promise { 66 | if (process.platform === 'win32') { 67 | return; // Windows uses named pipes, no directory needed 68 | } 69 | 70 | const appDir = getAppDataDir(); 71 | if (!(await pathExists(appDir))) { 72 | await mkdir(appDir, { recursive: true, mode: 0o700 }); 73 | } 74 | } 75 | 76 | /** 77 | * Get legacy application data directory (using tmpdir) 78 | */ 79 | export function getLegacyAppDataDir(): string { 80 | if (process.platform === 'win32') { 81 | return ''; // Windows uses named pipes, no directory 82 | } 83 | return tmpdir(); 84 | } 85 | 86 | /** 87 | * Generate legacy socket path (for backward compatibility) 88 | */ 89 | function getLegacySocketPath(workspacePath: string): string { 90 | const hash = createHash('md5').update(workspacePath).digest('hex').slice(0, 8); 91 | 92 | return process.platform === 'win32' 93 | ? `\\\\.\\pipe\\vscode-mcp-${hash}` 94 | : join(tmpdir(), `vscode-mcp-${hash}.sock`); 95 | } 96 | 97 | /** 98 | * Generate unique request ID 99 | */ 100 | function generateRequestId(): string { 101 | return `${Date.now()}-${Math.random().toString(36).slice(2)}`; 102 | } 103 | 104 | /** 105 | * Dispatcher class for sending events to VSCode extension via Unix Socket 106 | */ 107 | export class EventDispatcher { 108 | private workspacePath: string; 109 | private socketPath: string; 110 | private legacySocketPath: string; 111 | private requestTimeout: number; 112 | 113 | constructor(workspacePath: string, requestTimeout: number = 30000) { 114 | this.workspacePath = workspacePath; 115 | this.socketPath = getSocketPath(workspacePath); 116 | this.legacySocketPath = getLegacySocketPath(workspacePath); 117 | this.requestTimeout = requestTimeout; 118 | } 119 | 120 | /** 121 | * Send event to VSCode extension and wait for response 122 | */ 123 | async dispatch( 124 | eventName: T, 125 | params: EventParams, 126 | ): Promise> { 127 | // Try new socket path first 128 | try { 129 | return await this.tryConnect(eventName, params, this.socketPath); 130 | } catch (newPathError) { 131 | // Check if this is a connection error or business error 132 | const isConnectionError = this.isConnectionError(newPathError as Error); 133 | 134 | if (!isConnectionError) { 135 | // This is a business error (e.g., file not found, invalid params), not connection issue 136 | // Don't retry, let outer layer handle it 137 | throw newPathError; 138 | } 139 | 140 | // This is a connection error, try legacy path 141 | try { 142 | const result = await this.tryConnect(eventName, params, this.legacySocketPath); 143 | 144 | // If legacy path works, show upgrade warning 145 | console.warn( 146 | `⚠️ Connected using legacy socket path. Please upgrade your VSCode MCP Bridge extension to the latest version.\n` + 147 | ` Extension: YuTengjing.vscode-mcp-bridge\n` + 148 | ` Current connection: ${this.legacySocketPath}\n` + 149 | ` Expected new path: ${this.socketPath}\n` + 150 | ` Workspace: ${this.workspacePath}` 151 | ); 152 | 153 | return result; 154 | } catch (legacyPathError) { 155 | const isLegacyConnectionError = this.isConnectionError(legacyPathError as Error); 156 | 157 | if (!isLegacyConnectionError) { 158 | // Legacy path connected but returned business error, throw it 159 | throw legacyPathError; 160 | } 161 | 162 | // Both paths failed with connection errors 163 | throw new Error( 164 | `Failed to connect to VSCode extension at both locations:\n` + 165 | ` New path: ${this.socketPath} - ${(newPathError as Error).message}\n` + 166 | ` Legacy path: ${this.legacySocketPath} - ${(legacyPathError as Error).message}\n\n` + 167 | `Please ensure:\n` + 168 | ` 1. VSCode MCP Bridge extension is installed and activated\n` + 169 | ` 2. A workspace is open in VSCode\n` + 170 | ` 3. Extension is updated to the latest version` 171 | ); 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * Check if error is a connection error (vs business logic error from VSCode extension) 178 | */ 179 | private isConnectionError(error: Error): boolean { 180 | const { message } = error; 181 | return message.includes('connect ENOENT') && message.includes('.sock'); 182 | } 183 | 184 | /** 185 | * Try to connect to a specific socket path 186 | */ 187 | private async tryConnect( 188 | eventName: T, 189 | params: EventParams, 190 | socketPath: string, 191 | ): Promise> { 192 | // Ensure socket directory exists before connecting 193 | await ensureSocketDir(); 194 | 195 | return new Promise((resolve, reject) => { 196 | const socket = new Socket(); 197 | const requestId = generateRequestId(); 198 | let responseReceived = false; 199 | 200 | // Set timeout 201 | const timeoutId = setTimeout(() => { 202 | if (!responseReceived) { 203 | socket.destroy(); 204 | reject( 205 | new Error( 206 | `Request timeout after ${this.requestTimeout}ms for event: ${eventName}`, 207 | ), 208 | ); 209 | } 210 | }, this.requestTimeout); 211 | 212 | // Handle socket connection 213 | socket.on('connect', () => { 214 | const request: BaseRequest = { 215 | id: requestId, 216 | method: eventName, 217 | params: params as Record, 218 | }; 219 | 220 | socket.write(`${JSON.stringify(request)}\n`); 221 | }); 222 | 223 | // Handle socket data 224 | socket.on('data', (data) => { 225 | try { 226 | const response: BaseResponse = JSON.parse(data.toString()); 227 | 228 | if (response.id === requestId) { 229 | responseReceived = true; 230 | clearTimeout(timeoutId); 231 | socket.destroy(); 232 | 233 | if (response.error) { 234 | reject( 235 | new Error( 236 | `VSCode extension error: ${response.error.message}${ 237 | response.error.details ? ` - ${response.error.details}` : '' 238 | }`, 239 | ), 240 | ); 241 | } else { 242 | resolve(response.result); 243 | } 244 | } 245 | } catch (error) { 246 | responseReceived = true; 247 | clearTimeout(timeoutId); 248 | socket.destroy(); 249 | reject(new Error(`Failed to parse response: ${error instanceof Error ? error.message : String(error)}`)); 250 | } 251 | }); 252 | 253 | // Handle socket errors 254 | socket.on('error', (error) => { 255 | responseReceived = true; 256 | clearTimeout(timeoutId); 257 | reject( 258 | new Error( 259 | `Failed to connect to VSCode extension at ${socketPath}: ${error.message}`, 260 | ), 261 | ); 262 | }); 263 | 264 | // Handle socket close 265 | socket.on('close', () => { 266 | if (!responseReceived) { 267 | clearTimeout(timeoutId); 268 | reject( 269 | new Error( 270 | `Connection closed unexpectedly for event: ${eventName}`, 271 | ), 272 | ); 273 | } 274 | }); 275 | 276 | // Connect to socket 277 | socket.connect(socketPath); 278 | }); 279 | } 280 | 281 | /** 282 | * Test connection to VSCode extension 283 | */ 284 | async testConnection(): Promise { 285 | try { 286 | await this.dispatch('health', {}); 287 | return true; 288 | } catch { 289 | return false; 290 | } 291 | } 292 | 293 | /** 294 | * Get workspace path 295 | */ 296 | getWorkspacePath(): string { 297 | return this.workspacePath; 298 | } 299 | 300 | /** 301 | * Get socket path 302 | */ 303 | getSocketPath(): string { 304 | return this.socketPath; 305 | } 306 | } 307 | 308 | /** 309 | * Create a new event dispatcher instance 310 | */ 311 | export async function createDispatcher( 312 | workspacePath: string, 313 | requestTimeout?: number, 314 | ): Promise { 315 | // Validate that workspace path is absolute 316 | if (!isAbsolute(workspacePath)) { 317 | throw new Error( 318 | `workspace_path must be an absolute path, got: ${workspacePath}` 319 | ); 320 | } 321 | 322 | // Validate that workspace path is a directory 323 | if (await pathExists(workspacePath)) { 324 | const stats = await stat(workspacePath); 325 | if (!stats.isDirectory()) { 326 | throw new Error( 327 | `workspace_path must be a directory, got a file: ${workspacePath}` 328 | ); 329 | } 330 | } 331 | 332 | return new EventDispatcher(workspacePath, requestTimeout); 333 | } 334 | 335 | 336 | --------------------------------------------------------------------------------