├── tests ├── fixtures │ ├── python │ │ ├── .gitignore │ │ ├── pyrightconfig.json │ │ ├── pyproject.toml │ │ └── main.py │ ├── fsharp │ │ ├── .gitignore │ │ ├── FSharpFixture.fsproj │ │ ├── Types.fs │ │ ├── Program.fs │ │ └── CommentedTypes.fs │ ├── rust │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── Cargo.lock │ │ └── src │ │ │ └── main.rs │ ├── moonbit │ │ ├── .gitignore │ │ ├── moon.mod.json │ │ └── main.mbt │ ├── go │ │ ├── go.mod │ │ └── main.go │ ├── haskell │ │ ├── hie.yaml │ │ └── Main.hs │ ├── ruby │ │ ├── Gemfile │ │ └── main.rb │ ├── veryl │ │ ├── Veryl.toml │ │ ├── Veryl.lock │ │ ├── adder.veryl │ │ └── counter.veryl │ ├── lsp-rename │ │ ├── simple-variable.input.ts │ │ ├── simple-variable.expected.ts │ │ ├── function.input.ts │ │ ├── function.expected.ts │ │ ├── class.input.ts │ │ ├── class.expected.ts │ │ ├── cross-file-export.input.ts │ │ ├── cross-file-export.expected.ts │ │ ├── cross-file-import.input.ts │ │ ├── cross-file-import.expected.ts │ │ ├── type-alias.input.ts │ │ └── type-alias.expected.ts │ ├── ocaml │ │ ├── .lsmcp │ │ │ └── config.json │ │ └── main.ml │ ├── typescript │ │ ├── simple.ts │ │ ├── tsconfig.json │ │ └── index.ts │ ├── typescript-with-src │ │ ├── tsconfig.json │ │ ├── src │ │ │ └── main.ts │ │ └── tests │ │ │ └── main.test.ts │ ├── deno │ │ ├── deno.json │ │ └── main.ts │ ├── lsp-definitions │ │ ├── utils.ts │ │ ├── main.ts │ │ └── calculator.ts │ ├── lsp-document-symbols │ │ └── symbols.ts │ ├── document-symbols-test.ts │ ├── neverthrow-example │ │ ├── README.md │ │ └── index.ts │ └── variables-test.ts ├── setup.ts ├── languages │ ├── language-tests │ │ └── haskell-simple.test.ts │ └── diagnosticProcessors.ts ├── integration │ └── README.md └── helpers │ └── test-lsp-startup.mjs ├── .mcp.json ├── examples ├── python-project │ ├── .gitignore │ ├── .lsmcp │ │ ├── cache │ │ │ └── symbols.db │ │ └── memories │ │ │ └── pyright_test_results.md │ ├── .mcp.json │ ├── pyproject.toml │ ├── README.md │ ├── errors.py │ └── uv.lock ├── fsharp-project │ ├── .gitignore │ ├── fsharp-test.config.json │ ├── fsharp-project.fsproj │ ├── .mcp.json │ ├── README.md │ ├── Program.fs │ └── Calculator.fs ├── rust-project │ ├── target │ │ ├── debug │ │ │ ├── .cargo-lock │ │ │ ├── deps │ │ │ │ ├── librust_project-0110108930012809.rmeta │ │ │ │ ├── librust_project-177dffeea0f5a51b.rmeta │ │ │ │ ├── rust_project-0110108930012809.d │ │ │ │ └── rust_project-177dffeea0f5a51b.d │ │ │ ├── incremental │ │ │ │ ├── rust_project-1z3ywfls92enr │ │ │ │ │ ├── s-h8mgmtxv5r-0xpb9sn.lock │ │ │ │ │ └── s-h8mgmtxv5r-0xpb9sn-5pz6qv1h79tp7rwnaxuwheek1 │ │ │ │ │ │ ├── work-products.bin │ │ │ │ │ │ ├── dep-graph.bin │ │ │ │ │ │ └── query-cache.bin │ │ │ │ └── rust_project-3sl79htn2jl9j │ │ │ │ │ ├── s-h8mgmtxvf1-0mccyr4.lock │ │ │ │ │ └── s-h8mgmtxvf1-0mccyr4-bg1r822sjoday9i9f7d6kbkg7 │ │ │ │ │ ├── work-products.bin │ │ │ │ │ ├── dep-graph.bin │ │ │ │ │ └── query-cache.bin │ │ │ └── .fingerprint │ │ │ │ ├── rust-project-177dffeea0f5a51b │ │ │ │ ├── bin-rust-project │ │ │ │ ├── invoked.timestamp │ │ │ │ ├── dep-bin-rust-project │ │ │ │ └── bin-rust-project.json │ │ │ │ └── rust-project-0110108930012809 │ │ │ │ ├── test-bin-rust-project │ │ │ │ ├── invoked.timestamp │ │ │ │ ├── dep-test-bin-rust-project │ │ │ │ └── test-bin-rust-project.json │ │ ├── CACHEDIR.TAG │ │ └── .rustc_info.json │ ├── .gitignore │ ├── Cargo.toml │ ├── .mcp.json │ ├── Cargo.lock │ ├── src │ │ ├── main.rs │ │ ├── errors.rs │ │ ├── lib.rs │ │ └── test_diagnostics.rs │ └── .lsmcp │ │ └── config.json ├── moonbit-project │ ├── src │ │ ├── lib │ │ │ ├── moon.pkg.json │ │ │ ├── hello.mbt │ │ │ └── fixed.mbt │ │ ├── test │ │ │ ├── target │ │ │ │ ├── .moon-lock │ │ │ │ ├── wasm-gc │ │ │ │ │ └── release │ │ │ │ │ │ └── check │ │ │ │ │ │ ├── check.moon_db │ │ │ │ │ │ └── single │ │ │ │ │ │ ├── single.mi │ │ │ │ │ │ └── single.blackbox_test.mi │ │ │ │ └── packages.json │ │ │ ├── moon.pkg.json │ │ │ ├── test.mbt │ │ │ ├── error.mbt │ │ │ └── test_diagnostics.mbt │ │ └── main │ │ │ ├── moon.pkg.json │ │ │ └── main.mbt │ ├── .gitignore │ ├── moon.mod.json │ ├── .mcp.json │ └── README.md ├── go │ ├── go.mod │ └── hello.go ├── tsgo-project │ └── .mcp.json └── full-lsmcp-config.json ├── packages ├── lsp-client │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── protocol │ │ └── types │ │ │ ├── index.ts │ │ │ ├── notifications.ts │ │ │ └── base.ts │ │ ├── utils │ │ ├── validation.ts │ │ ├── debug.ts │ │ ├── lineResolver.ts │ │ ├── helpers.ts │ │ ├── documentManager.ts │ │ ├── fileContext.ts │ │ └── textEdits.ts │ │ ├── managers │ │ └── workspace.ts │ │ ├── interfaces.ts │ │ ├── core │ │ └── state.ts │ │ └── providers.ts ├── code-indexer │ ├── tsconfig.json │ ├── src │ │ ├── engine │ │ │ ├── testHelpers.ts │ │ │ ├── contentHash.ts │ │ │ ├── adapterDefaults.ts │ │ │ ├── types.ts │ │ │ └── NodeFileSystem.ts │ │ ├── cache │ │ │ └── MemoryCache.ts │ │ └── utils │ │ │ └── autoIndex.ts │ └── package.json └── types │ ├── src │ ├── domain │ │ ├── index.ts │ │ ├── adapters.ts │ │ ├── mcp.ts │ │ ├── project.ts │ │ └── tools.ts │ ├── shared │ │ ├── index.ts │ │ ├── result.ts │ │ └── errors.ts │ ├── constants │ │ ├── lsp.ts │ │ ├── config.ts │ │ ├── indexer.ts │ │ └── diagnostics.ts │ ├── common │ │ └── logger.ts │ ├── validators │ │ ├── index.ts │ │ └── memory.ts │ └── lsp │ │ ├── index.ts │ │ └── diagnostics.ts │ ├── tsconfig.json │ └── package.json ├── pnpm-workspace.yaml ├── src ├── constants │ └── diagnostics.ts ├── tools │ ├── highlevel │ │ ├── fileSystemTools.ts │ │ ├── symbolTools.ts │ │ └── symbolTools.simple.test.ts │ ├── toolLists.ts │ ├── getAllTools.ts │ └── lsp │ │ └── createLspTools.ts ├── presets │ ├── fsharp.ts │ ├── deno.ts │ ├── moonbit.ts │ ├── gopls.ts │ ├── rust-analyzer.ts │ ├── ocaml.ts │ ├── ruff.ts │ ├── hls.ts │ ├── typescript-language-server.ts │ ├── pyright.ts │ └── tsgo.ts ├── features │ ├── ts │ │ └── utils │ │ │ ├── findNodeModulesBin.ts │ │ │ ├── findSymbolOccurrences.ts │ │ │ ├── findSymbolInLine.ts │ │ │ ├── validation.ts │ │ │ └── parseLineNumber.ts │ └── memory │ │ └── onboarding │ │ ├── onboardingTools.ts │ │ └── onboardingTools.test.ts ├── config │ └── presets.ts └── utils │ ├── applyTextEdits.ts │ ├── filePatternParser.ts │ └── toolFilters.ts ├── .husky ├── pre-push └── pre-commit ├── .lsmcp ├── config.json └── memories │ ├── task_completion_checklist.md │ ├── suggested_commands.md │ └── project_overview.md ├── .claude ├── settings.json └── commands │ └── dce-by-tsr.md ├── tsdown.config.ts ├── biome.json ├── .versionrc.json ├── .github ├── workflows │ ├── ci-unit.yml │ ├── ci-integration.yml │ ├── release.yml │ └── security.yml └── dependabot.yml ├── LICENSE ├── scripts ├── setup-haskell.sh ├── generate-schema.ts └── check-ci.sh ├── tsconfig.json ├── TODO.md ├── docs └── ja │ └── lsif-spec.md └── benchmarks └── simple-search-benchmark.ts /tests/fixtures/python/.gitignore: -------------------------------------------------------------------------------- 1 | .venv -------------------------------------------------------------------------------- /.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": {} 3 | } -------------------------------------------------------------------------------- /examples/python-project/.gitignore: -------------------------------------------------------------------------------- 1 | .venv -------------------------------------------------------------------------------- /tests/fixtures/fsharp/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /tests/fixtures/rust/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /examples/fsharp-project/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.cargo-lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/moonbit-project/src/lib/moon.pkg.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/target/.moon-lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/lsp-client/.gitignore: -------------------------------------------------------------------------------- 1 | tests/integration/tmp-** -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - . 3 | - packages/* -------------------------------------------------------------------------------- /tests/fixtures/moonbit/.gitignore: -------------------------------------------------------------------------------- 1 | .mooncakes 2 | target -------------------------------------------------------------------------------- /examples/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mizchi/lsmcp/examples/go 2 | 3 | go 1.21 -------------------------------------------------------------------------------- /examples/rust-project/target/debug/deps/librust_project-0110108930012809.rmeta: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/deps/librust_project-177dffeea0f5a51b.rmeta: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/example/go-fixture 2 | 3 | go 1.19 -------------------------------------------------------------------------------- /tests/fixtures/haskell/hie.yaml: -------------------------------------------------------------------------------- 1 | cradle: 2 | direct: 3 | arguments: [] -------------------------------------------------------------------------------- /tests/fixtures/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'solargraph' -------------------------------------------------------------------------------- /tests/fixtures/veryl/Veryl.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "test_project" 3 | version = "0.1.0" -------------------------------------------------------------------------------- /examples/moonbit-project/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .mooncakes 3 | # lsmcp cache 4 | .lsmcp/cache 5 | -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": ["moonbit-test/src/lib"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/incremental/rust_project-1z3ywfls92enr/s-h8mgmtxv5r-0xpb9sn.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/incremental/rust_project-3sl79htn2jl9j/s-h8mgmtxvf1-0mccyr4.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/moonbit-project/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moonbit-test", 3 | "version": "0.1.0" 4 | } 5 | -------------------------------------------------------------------------------- /examples/rust-project/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | target 4 | # lsmcp cache 5 | .lsmcp/cache 6 | -------------------------------------------------------------------------------- /tests/fixtures/moonbit/moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moonbit-fixture", 3 | "version": "0.1.0" 4 | } 5 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.fingerprint/rust-project-177dffeea0f5a51b/bin-rust-project: -------------------------------------------------------------------------------- 1 | c307576ce154391c -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.fingerprint/rust-project-0110108930012809/test-bin-rust-project: -------------------------------------------------------------------------------- 1 | b637b0d378d5a198 -------------------------------------------------------------------------------- /examples/moonbit-project/src/main/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": ["moonbit-test/src/lib"], 3 | "is-main": true 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-fixture" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] -------------------------------------------------------------------------------- /examples/rust-project/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-project" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.fingerprint/rust-project-0110108930012809/invoked.timestamp: -------------------------------------------------------------------------------- 1 | This file has an mtime of when this was started. -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.fingerprint/rust-project-177dffeea0f5a51b/invoked.timestamp: -------------------------------------------------------------------------------- 1 | This file has an mtime of when this was started. -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/simple-variable.input.ts: -------------------------------------------------------------------------------- 1 | const foo = 1; 2 | console.log(foo); 3 | const result = foo + 2; 4 | export { foo }; 5 | -------------------------------------------------------------------------------- /tests/fixtures/ocaml/.lsmcp/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["**/*.ml", "**/*.mli"], 3 | "excludePatterns": ["_build/**", "_opam/**"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/code-indexer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": ["src/**/*"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/python-project/.lsmcp/cache/symbols.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/python-project/.lsmcp/cache/symbols.db -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/simple-variable.expected.ts: -------------------------------------------------------------------------------- 1 | const bar = 1; 2 | console.log(bar); 3 | const result = bar + 2; 4 | export { bar as foo }; 5 | -------------------------------------------------------------------------------- /src/constants/diagnostics.ts: -------------------------------------------------------------------------------- 1 | export const DIAGNOSTICS_BATCH_SIZE = 10; 2 | export const MAX_FILES_TO_SHOW = 100; 3 | export const MAX_DIAGNOSTICS_PER_FILE = 20; 4 | -------------------------------------------------------------------------------- /tests/fixtures/veryl/Veryl.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Veryl. 2 | # It is not intended for manual editing. 3 | version = 1 4 | projects = [] 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run unit tests only in pre-push (fast tests) 4 | echo "Running unit tests..." 5 | pnpm test:unit 6 | 7 | # Integration tests run in CI only -------------------------------------------------------------------------------- /examples/rust-project/target/debug/incremental/rust_project-1z3ywfls92enr/s-h8mgmtxv5r-0xpb9sn-5pz6qv1h79tp7rwnaxuwheek1/work-products.bin: -------------------------------------------------------------------------------- 1 | RSIC1.87.0 (17067e9ac 2025-05-09)rust-end-file -------------------------------------------------------------------------------- /examples/rust-project/target/debug/incremental/rust_project-3sl79htn2jl9j/s-h8mgmtxvf1-0mccyr4-bg1r822sjoday9i9f7d6kbkg7/work-products.bin: -------------------------------------------------------------------------------- 1 | RSIC1.87.0 (17067e9ac 2025-05-09)rust-end-file -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/function.input.ts: -------------------------------------------------------------------------------- 1 | function foo(x: number): number { 2 | return x * 2; 3 | } 4 | 5 | const value = foo(5); 6 | console.log(foo(10)); 7 | 8 | export { foo }; 9 | -------------------------------------------------------------------------------- /examples/moonbit-project/.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "moonbit": { 4 | "command": "node", 5 | "args": ["../../dist/lsmcp.js", "-p", "moonbit"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/rust-project/.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "rust": { 4 | "command": "npx", 5 | "args": ["-y", "@mizchi/lsmcp", "-p", "rust-analyzer"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/python-project/.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "python-pyright": { 4 | "command": "node", 5 | "args": ["../../dist/lsmcp.js", "-p", "pyright"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/lsp-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": false, 5 | "noEmit": true 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/types/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | // Domain types exports 2 | 3 | export * from "./filesystem.ts"; 4 | export * from "./project.ts"; 5 | export * from "./tools.ts"; 6 | export * from "./mcp.ts"; 7 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/function.expected.ts: -------------------------------------------------------------------------------- 1 | function bar(x: number): number { 2 | return x * 2; 3 | } 4 | 5 | const value = bar(5); 6 | console.log(bar(10)); 7 | 8 | export { bar as foo }; 9 | -------------------------------------------------------------------------------- /.lsmcp/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "typescript", 3 | "files": ["**/*.ts", "**/*.tsx"], 4 | "symbolFilter": { 5 | "excludeKinds": ["Variable", "Constant", "String", "Number", "Boolean"] 6 | } 7 | } -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/target/wasm-gc/release/check/check.moon_db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/moonbit-project/src/test/target/wasm-gc/release/check/check.moon_db -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/target/wasm-gc/release/check/single/single.mi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/moonbit-project/src/test/target/wasm-gc/release/check/single/single.mi -------------------------------------------------------------------------------- /examples/rust-project/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "rust-project" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /src/tools/highlevel/fileSystemTools.ts: -------------------------------------------------------------------------------- 1 | import { createListDirTool } from "./fileSystemToolsFactory"; 2 | 3 | // Export default instances using Node.js filesystem 4 | export const listDirTool = createListDirTool(); 5 | -------------------------------------------------------------------------------- /tests/fixtures/rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "rust-fixture" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /tests/fixtures/typescript/simple.ts: -------------------------------------------------------------------------------- 1 | // Simple file to test tsgo diagnostics 2 | const x: number = "hello"; // Type error 3 | 4 | function test() { 5 | return 42; 6 | } 7 | 8 | const result: string = test(); // Type error 9 | -------------------------------------------------------------------------------- /examples/rust-project/target/CACHEDIR.TAG: -------------------------------------------------------------------------------- 1 | Signature: 8a477f597d28d172789f06886806bc55 2 | # This file is a cache directory tag created by cargo. 3 | # For information about cache directory tags see https://bford.info/cachedir/ 4 | -------------------------------------------------------------------------------- /tests/fixtures/veryl/adder.veryl: -------------------------------------------------------------------------------- 1 | module Adder #( 2 | parameter WIDTH: u32 = 8 3 | ) ( 4 | a: input logic, 5 | b: input logic, 6 | sum: output logic 7 | ) { 8 | assign sum = a + b; 9 | } -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/target/wasm-gc/release/check/single/single.blackbox_test.mi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/moonbit-project/src/test/target/wasm-gc/release/check/single/single.blackbox_test.mi -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/test.mbt: -------------------------------------------------------------------------------- 1 | fn hello() -> Int { 2 | 42 3 | } 4 | 5 | let x : String = "world" 6 | 7 | // Code with syntax error 8 | fn syntax_error() { 9 | let x = 42 + 10 | // Incomplete expression 11 | } 12 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.fingerprint/rust-project-177dffeea0f5a51b/dep-bin-rust-project: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/rust-project/target/debug/.fingerprint/rust-project-177dffeea0f5a51b/dep-bin-rust-project -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.fingerprint/rust-project-0110108930012809/dep-test-bin-rust-project: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/rust-project/target/debug/.fingerprint/rust-project-0110108930012809/dep-test-bin-rust-project -------------------------------------------------------------------------------- /packages/types/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | // Shared utility types 2 | 3 | export * from "./errors.ts"; 4 | export * from "./result.ts"; 5 | export * from "./utils.ts"; 6 | // export * from "./types.ts"; 7 | export * from "./resultBuilders.ts"; 8 | // export * from "./schemas.ts"; 9 | -------------------------------------------------------------------------------- /tests/fixtures/typescript-with-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/fsharp-project/fsharp-test.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "fsharp-test", 3 | "name": "F# Test Configuration", 4 | "bin": "fsautocomplete", 5 | "args": [], 6 | "initializationOptions": { 7 | "AutomaticWorkspaceInit": true 8 | }, 9 | "unsupported": ["get_all_diagnostics"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/incremental/rust_project-1z3ywfls92enr/s-h8mgmtxv5r-0xpb9sn-5pz6qv1h79tp7rwnaxuwheek1/dep-graph.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/rust-project/target/debug/incremental/rust_project-1z3ywfls92enr/s-h8mgmtxv5r-0xpb9sn-5pz6qv1h79tp7rwnaxuwheek1/dep-graph.bin -------------------------------------------------------------------------------- /examples/rust-project/target/debug/incremental/rust_project-3sl79htn2jl9j/s-h8mgmtxvf1-0mccyr4-bg1r822sjoday9i9f7d6kbkg7/dep-graph.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/rust-project/target/debug/incremental/rust_project-3sl79htn2jl9j/s-h8mgmtxvf1-0mccyr4-bg1r822sjoday9i9f7d6kbkg7/dep-graph.bin -------------------------------------------------------------------------------- /examples/rust-project/target/debug/incremental/rust_project-1z3ywfls92enr/s-h8mgmtxv5r-0xpb9sn-5pz6qv1h79tp7rwnaxuwheek1/query-cache.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/rust-project/target/debug/incremental/rust_project-1z3ywfls92enr/s-h8mgmtxv5r-0xpb9sn-5pz6qv1h79tp7rwnaxuwheek1/query-cache.bin -------------------------------------------------------------------------------- /examples/rust-project/target/debug/incremental/rust_project-3sl79htn2jl9j/s-h8mgmtxvf1-0mccyr4-bg1r822sjoday9i9f7d6kbkg7/query-cache.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/typescript-mcp/HEAD/examples/rust-project/target/debug/incremental/rust_project-3sl79htn2jl9j/s-h8mgmtxvf1-0mccyr4-bg1r822sjoday9i9f7d6kbkg7/query-cache.bin -------------------------------------------------------------------------------- /examples/moonbit-project/src/main/main.mbt: -------------------------------------------------------------------------------- 1 | fn main { 2 | let message = @lib.hello() 3 | println(message) 4 | 5 | let result = @lib.add(10, 20) 6 | println("10 + 20 = " + result.to_string()) 7 | 8 | let user = @lib.create_user("Alice", 25) 9 | println("User: " + user.name + ", Age: " + user.age.to_string()) 10 | } -------------------------------------------------------------------------------- /tests/fixtures/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "esnext", 5 | "lib": ["es2020"], 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(pnpm:*)", 5 | "Bash(ls:*)", 6 | "Bash(grep:*)", 7 | "mcp__readability__read_url_content_as_markdown", 8 | "mcp__typescript__lsmcp_*", 9 | "mcp__lsmcp__lsmcp_*" 10 | ], 11 | "deny": ["Bash(npm:*)"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/moonbit-project/src/lib/hello.mbt: -------------------------------------------------------------------------------- 1 | pub fn hello() -> String { 2 | "Hello, Moonbit!" 3 | } 4 | 5 | pub fn add(x: Int, y: Int) -> Int { 6 | x + y 7 | } 8 | 9 | pub struct User { 10 | name: String 11 | age: Int 12 | } derive(Show) 13 | 14 | pub fn create_user(name: String, age: Int) -> User { 15 | { name, age } 16 | } -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist", 6 | "composite": true, 7 | "declaration": true, 8 | "declarationMap": true 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/typescript-with-src/src/main.ts: -------------------------------------------------------------------------------- 1 | export class Calculator { 2 | add(a: number, b: number): number { 3 | return a + b; 4 | } 5 | 6 | subtract(a: number, b: number): number { 7 | return a - b; 8 | } 9 | } 10 | 11 | export function createCalculator(): Calculator { 12 | return new Calculator(); 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/veryl/counter.veryl: -------------------------------------------------------------------------------- 1 | module Counter ( 2 | clk: input clock, 3 | rst: input reset, 4 | en: input logic, 5 | cnt: output logic<8> 6 | ) { 7 | always_ff (clk, rst) { 8 | if_reset { 9 | cnt = 0; 10 | } else if en { 11 | cnt = cnt + 1; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/class.input.ts: -------------------------------------------------------------------------------- 1 | class Foo { 2 | private value: number; 3 | 4 | constructor(value: number) { 5 | this.value = value; 6 | } 7 | 8 | getValue(): number { 9 | return this.value; 10 | } 11 | } 12 | 13 | const instance = new Foo(42); 14 | console.log(instance.getValue()); 15 | 16 | export { Foo }; 17 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/deps/rust_project-0110108930012809.d: -------------------------------------------------------------------------------- 1 | /home/mizchi/mizchi/typescript-mcp/playground/rust-project/target/debug/deps/librust_project-0110108930012809.rmeta: src/main.rs 2 | 3 | /home/mizchi/mizchi/typescript-mcp/playground/rust-project/target/debug/deps/rust_project-0110108930012809.d: src/main.rs 4 | 5 | src/main.rs: 6 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/deps/rust_project-177dffeea0f5a51b.d: -------------------------------------------------------------------------------- 1 | /home/mizchi/mizchi/typescript-mcp/playground/rust-project/target/debug/deps/librust_project-177dffeea0f5a51b.rmeta: src/main.rs 2 | 3 | /home/mizchi/mizchi/typescript-mcp/playground/rust-project/target/debug/deps/rust_project-177dffeea0f5a51b.d: src/main.rs 4 | 5 | src/main.rs: 6 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/class.expected.ts: -------------------------------------------------------------------------------- 1 | class Bar { 2 | private value: number; 3 | 4 | constructor(value: number) { 5 | this.value = value; 6 | } 7 | 8 | getValue(): number { 9 | return this.value; 10 | } 11 | } 12 | 13 | const instance = new Bar(42); 14 | console.log(instance.getValue()); 15 | 16 | export { Bar as Foo }; 17 | -------------------------------------------------------------------------------- /tests/fixtures/deno/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true 4 | }, 5 | "lint": { 6 | "rules": { 7 | "tags": ["recommended"] 8 | } 9 | }, 10 | "fmt": { 11 | "options": { 12 | "useTabs": false, 13 | "lineWidth": 80, 14 | "indentWidth": 2, 15 | "singleQuote": false 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/fixtures/fsharp/FSharpFixture.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/cross-file-export.input.ts: -------------------------------------------------------------------------------- 1 | // Export a function that will be imported in another file 2 | export function processData(data: string): string { 3 | return data.toUpperCase(); 4 | } 5 | 6 | export class DataProcessor { 7 | process(data: string): string { 8 | return processData(data); 9 | } 10 | } 11 | 12 | export const DEFAULT_VALUE = "default"; 13 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/cross-file-export.expected.ts: -------------------------------------------------------------------------------- 1 | // Export a function that will be imported in another file 2 | export function transformData(data: string): string { 3 | return data.toUpperCase(); 4 | } 5 | 6 | export class DataProcessor { 7 | process(data: string): string { 8 | return transformData(data); 9 | } 10 | } 11 | 12 | export const DEFAULT_VALUE = "default"; 13 | -------------------------------------------------------------------------------- /tests/fixtures/typescript-with-src/tests/main.test.ts: -------------------------------------------------------------------------------- 1 | import { Calculator } from "../src/main"; 2 | 3 | describe("Calculator", () => { 4 | const calc = new Calculator(); 5 | 6 | test("should add numbers", () => { 7 | expect(calc.add(2, 3)).toBe(5); 8 | }); 9 | 10 | test("should subtract numbers", () => { 11 | expect(calc.subtract(5, 3)).toBe(2); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/fixtures/fsharp/Types.fs: -------------------------------------------------------------------------------- 1 | module Types 2 | 3 | type User = { 4 | Id: int 5 | Name: string 6 | Email: string option 7 | } 8 | 9 | type UserService() = 10 | member this.CreateUser(id: int, name: string, email: string option) = 11 | { Id = id; Name = name; Email = email } 12 | 13 | member this.GreetUser(user: User) = 14 | sprintf "Hello, %s!" user.Name -------------------------------------------------------------------------------- /tests/fixtures/python/pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "venvPath": ".", 3 | "venv": ".venv", 4 | "pythonVersion": "3.12", 5 | "typeCheckingMode": "basic", 6 | "useLibraryCodeForTypes": true, 7 | "reportMissingImports": true, 8 | "reportMissingTypeStubs": false, 9 | "executionEnvironments": [ 10 | { 11 | "root": ".", 12 | "pythonVersion": "3.12" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | dependencies = [ 3 | "pyright>=1.1.402", 4 | "nodejs-wheel", 5 | ] 6 | name = "test" 7 | requires-python = ">=3.12" 8 | version = "0.1.0" 9 | 10 | [tool.pyright] 11 | pythonVersion = "3.12" 12 | typeCheckingMode = "strict" 13 | 14 | [tool.ruff] 15 | line-length = 88 16 | target-version = "py38" 17 | 18 | [tool.ruff.lint] 19 | select = ["E", "F", "I"] 20 | -------------------------------------------------------------------------------- /examples/fsharp-project/fsharp-project.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | fsharp_project 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/types/src/domain/adapters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for LSP client 3 | * Simplified configuration without unnecessary abstractions 4 | */ 5 | export interface LspClientConfig { 6 | languageId: string; 7 | rootPath: string; 8 | command?: string; 9 | args?: string[]; 10 | initializationOptions?: Record; 11 | serverCapabilities?: unknown; 12 | env?: Record; 13 | } 14 | -------------------------------------------------------------------------------- /src/presets/fsharp.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * F# Autocomplete (fsautocomplete) adapter 5 | */ 6 | export const fsharpAdapter: Preset = { 7 | presetId: "fsharp", 8 | bin: "fsautocomplete", 9 | args: [], 10 | files: ["**/*.fs", "**/*.fsi", "**/*.fsx"], 11 | initializationOptions: { 12 | AutomaticWorkspaceInit: true, 13 | }, 14 | disable: ["get_all_diagnostics"], 15 | }; 16 | -------------------------------------------------------------------------------- /examples/moonbit-project/src/lib/fixed.mbt: -------------------------------------------------------------------------------- 1 | pub fn fixed_function() -> String { 2 | let x : String = "hello" 3 | let y : Int = 42 4 | // Use string concatenation 5 | x + y.to_string() 6 | } 7 | 8 | pub fn another_fixed() -> String { 9 | // Convert Int to String 10 | "123" 11 | } 12 | 13 | pub fn fixed_variable() -> Unit { 14 | // Define variable before using 15 | let defined_var = "This is defined" 16 | println(defined_var) 17 | } -------------------------------------------------------------------------------- /src/tools/highlevel/symbolTools.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createGetSymbolsOverviewTool, 3 | getFilesRecursively, 4 | } from "./symbolToolsFactory.ts"; 5 | 6 | // Export for backward compatibility and testing 7 | export { getFilesRecursively }; 8 | 9 | // Create default instances with node filesystem 10 | export const getSymbolsOverviewTool = createGetSymbolsOverviewTool(); 11 | // querySymbolsTool removed - functionality now in search_symbols tool 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Check formatting (don't modify files) 4 | echo "Checking code format..." 5 | if ! pnpm biome check --linter-enabled=false --formatter-enabled=true src/ tests/ 2>/dev/null; then 6 | echo "❌ Code is not formatted. Run 'pnpm biome format --write src/ tests/' to fix." 7 | exit 1 8 | fi 9 | 10 | # Run lint 11 | echo "Running lint..." 12 | pnpm lint 13 | 14 | # Run type check 15 | echo "Running type check..." 16 | pnpm typecheck -------------------------------------------------------------------------------- /examples/fsharp-project/.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "fsharp-preset": { 4 | "command": "node", 5 | "args": ["../../dist/lsmcp.js", "-p", "fsharp"] 6 | }, 7 | "fsharp-direct": { 8 | "command": "node", 9 | "args": [ 10 | "../../dist/lsmcp.js", 11 | "--bin", 12 | "fsautocomplete", 13 | "--initializationOptions", 14 | "{\"AutomaticWorkspaceInit\": true}" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/tsgo-project/.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "lsmcp": { 4 | "command": "npx", 5 | "args": ["-y", "@mizchi/lsmcp", "--preset", "typescript"] 6 | }, 7 | "lsmcp-tsgo-bin-examples": { 8 | "command": "npx", 9 | "args": [ 10 | "-y", 11 | "@mizchi/lsmcp", 12 | "--preset", 13 | "typescript", 14 | "--bin", 15 | "tsgo --lsp --stdio" 16 | ], 17 | "disabled": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/code-indexer/src/engine/testHelpers.ts: -------------------------------------------------------------------------------- 1 | import type { FileSymbols } from "./types.ts"; 2 | 3 | /** 4 | * Create a minimal FileSymbols object for testing 5 | */ 6 | export function createTestFileSymbols(overrides?: Partial): FileSymbols { 7 | return { 8 | uri: "file:///test.ts", 9 | symbols: [], 10 | lastModified: Date.now(), 11 | lastIndexed: Date.now(), 12 | gitHash: undefined, 13 | contentHash: undefined, 14 | ...overrides, 15 | }; 16 | } -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/cross-file-import.input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataProcessor, 3 | DEFAULT_VALUE, 4 | processData, 5 | } from "./cross-file-export.input.ts"; 6 | 7 | const result = processData("hello"); 8 | console.log(result); 9 | 10 | const processor = new DataProcessor(); 11 | const output = processor.process(DEFAULT_VALUE); 12 | console.log(output); 13 | 14 | // Use processData in another context 15 | function wrapper(input: string): string { 16 | return processData(input); 17 | } 18 | -------------------------------------------------------------------------------- /examples/rust-project/src/main.rs: -------------------------------------------------------------------------------- 1 | use rust_project::{Calculator, greet}; 2 | 3 | fn main() { 4 | // Example with type error (uncomment to test diagnostics) 5 | // let foo: i32 = "xx"; 6 | 7 | // Using the Calculator 8 | let mut calc = Calculator::new(); 9 | calc.add(10.0).subtract(3.0); 10 | println!("Calculator result: {}", calc.get_value()); 11 | 12 | // Using the greet function 13 | let message = greet("Rust MCP"); 14 | println!("{}", message); 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/cross-file-import.expected.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataProcessor, 3 | DEFAULT_VALUE, 4 | transformData, 5 | } from "./cross-file-export.input.ts"; 6 | 7 | const result = transformData("hello"); 8 | console.log(result); 9 | 10 | const processor = new DataProcessor(); 11 | const output = processor.process(DEFAULT_VALUE); 12 | console.log(output); 13 | 14 | // Use transformData in another context 15 | function wrapper(input: string): string { 16 | return transformData(input); 17 | } 18 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-definitions/utils.ts: -------------------------------------------------------------------------------- 1 | export function formatResult(operation: string, result: number): string { 2 | return `${operation} result: ${result}`; 3 | } 4 | 5 | export function roundTo(value: number, decimals: number): number { 6 | return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals); 7 | } 8 | 9 | export function isEven(num: number): boolean { 10 | return num % 2 === 0; 11 | } 12 | 13 | export const CONSTANTS = { 14 | PI: 3.14159, 15 | E: 2.71828, 16 | } as const; 17 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown"; 2 | 3 | export default defineConfig({ 4 | entry: { 5 | lsmcp: "src/cli/lsmcp.ts", 6 | }, 7 | define: { 8 | "import.meta.vitest": "undefined", 9 | }, 10 | // Bundle workspace packages to avoid TypeScript enum issues at runtime 11 | noExternal: [ 12 | "@internal/lsp-client", 13 | "@internal/code-indexer", 14 | "@internal/types", 15 | "vscode-languageserver-types", 16 | "vscode-languageserver-protocol", 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /packages/types/src/constants/lsp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LSP-related constants 3 | */ 4 | 5 | // Timeout constants 6 | export const LSP_OPERATION_TIMEOUT = 30000; // 30 seconds 7 | export const LSP_INITIALIZATION_TIMEOUT = 10000; // 10 seconds 8 | export const LSP_SHUTDOWN_TIMEOUT = 5000; // 5 seconds 9 | 10 | // Default server characteristics 11 | export const DEFAULT_DOCUMENT_OPEN_DELAY = 1000; // ms 12 | export const DEFAULT_READINESS_CHECK_TIMEOUT = 500; // ms 13 | export const DEFAULT_OPERATION_TIMEOUT = 10000; // ms 14 | -------------------------------------------------------------------------------- /packages/lsp-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/lsp-client", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "exports": { 8 | ".": "./src/index.ts" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "@internal/types": "workspace:*", 13 | "vscode-languageserver-protocol": "^3.17.5", 14 | "neverthrow": "^8.2.0", 15 | "zod": "^3.22.4" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20.10.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/types/src/constants/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration-related constants 3 | */ 4 | 5 | // Config file paths 6 | export const CONFIG_FILE_NAME = "config.json"; 7 | export const CONFIG_DIR_NAME = ".lsmcp"; 8 | export const MEMORIES_DIR_NAME = "memories"; 9 | 10 | // Memory templates 11 | export const DEFAULT_MEMORY_TEMPLATES = { 12 | projectOverview: "project_overview.md", 13 | codeStyleConventions: "code_style_conventions.md", 14 | taskCompletionChecklist: "task_completion_checklist.md", 15 | } as const; 16 | -------------------------------------------------------------------------------- /packages/code-indexer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/code-indexer", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "exports": { 8 | ".": "./src/index.ts", 9 | "./symbolIndex": "./src/symbolIndex.ts", 10 | "./mcp/globalClient": "./src/mcp/globalClient.ts", 11 | "./*": "./src/*" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@internal/lsp-client": "workspace:*", 16 | "@internal/types": "workspace:*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/types/src/constants/indexer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Symbol indexer constants 3 | */ 4 | 5 | // Cache constants 6 | export const SYMBOL_CACHE_SCHEMA_VERSION = 2; 7 | export const INDEX_BATCH_SIZE = 10; // Files to process in parallel 8 | export const INDEX_CONCURRENCY_DEFAULT = 5; 9 | export const INDEX_CONCURRENCY_MAX = 20; 10 | export const INDEX_CONCURRENCY_MIN = 1; 11 | 12 | // Token compression 13 | export const AVERAGE_TOKEN_COMPRESSION_RATIO = 0.97; // 97% compression 14 | 15 | // Symbol kinds are now in ../lsp/symbols.ts 16 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-definitions/main.ts: -------------------------------------------------------------------------------- 1 | import { Calculator } from "./calculator.ts"; 2 | import { formatResult } from "./utils.ts"; 3 | 4 | const calc = new Calculator(); 5 | const result = calc.add(5, 3); 6 | console.log(formatResult("Addition", result)); 7 | 8 | export interface User { 9 | id: number; 10 | name: string; 11 | } 12 | 13 | export function createUser(name: string): User { 14 | return { 15 | id: Math.random(), 16 | name, 17 | }; 18 | } 19 | 20 | export type UserAction = "create" | "update" | "delete"; 21 | -------------------------------------------------------------------------------- /examples/python-project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "testproj" 3 | requires-python = ">=3.12" 4 | version = "0.0.1" 5 | 6 | [tool.pyright] 7 | exclude = ["**/node_modules", "**/__pycache__"] 8 | include = ["**/*.py"] 9 | pythonPlatform = "Linux" 10 | pythonVersion = "3.12" 11 | reportMissingImports = true 12 | reportMissingTypeStubs = false 13 | 14 | [tool.pyright.analysis] 15 | autoSearchPaths = true 16 | diagnosticMode = "workspace" 17 | useLibraryCodeForTypes = true 18 | 19 | [dependency-groups] 20 | dev = [ 21 | "pyright>=1.1.403", 22 | ] -------------------------------------------------------------------------------- /tests/fixtures/ruby/main.rb: -------------------------------------------------------------------------------- 1 | # Ruby test fixture for LSMCP 2 | 3 | module Greeting 4 | def self.say_hello(name) 5 | "Hello, #{name}!" 6 | end 7 | end 8 | 9 | class Person 10 | attr_reader :name, :age 11 | 12 | def initialize(name, age) 13 | @name = name 14 | @age = age 15 | end 16 | 17 | def greet 18 | Greeting.say_hello(@name) 19 | end 20 | 21 | def adult? 22 | @age >= 18 23 | end 24 | end 25 | 26 | def main 27 | person = Person.new("Alice", 25) 28 | puts person.greet 29 | end 30 | 31 | if __FILE__ == $0 32 | main 33 | end -------------------------------------------------------------------------------- /examples/rust-project/src/errors.rs: -------------------------------------------------------------------------------- 1 | // This file contains intentional errors for testing diagnostics 2 | 3 | fn type_error_example() { 4 | let number: i32 = "not a number"; // Type error 5 | } 6 | 7 | fn undefined_variable() { 8 | println!("{}", undefined_var); // Undefined variable 9 | } 10 | 11 | fn missing_semicolon() { 12 | let x = 5 // Missing semicolon 13 | let y = 10; 14 | } 15 | 16 | fn unused_variable() { 17 | let unused = 42; // Warning: unused variable 18 | } 19 | 20 | // Missing return type 21 | fn missing_return() -> i32 { 22 | // Function should return i32 but doesn't 23 | } -------------------------------------------------------------------------------- /packages/lsp-client/src/protocol/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LSP Protocol types aggregation 3 | */ 4 | 5 | // Re-export base types 6 | export * from "./base.ts"; 7 | 8 | // Re-export request types 9 | export * from "./requests.ts"; 10 | 11 | // Re-export response types 12 | export * from "./responses.ts"; 13 | 14 | // Re-export notification types 15 | export * from "./notifications.ts"; 16 | 17 | // Re-export client types from @internal/types 18 | export type { 19 | LSPClient, 20 | LSPClientConfig, 21 | LSPClientState, 22 | HoverContents, 23 | DocumentDiagnosticReport, 24 | } from "@internal/types"; 25 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.fingerprint/rust-project-177dffeea0f5a51b/bin-rust-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "rustc": 15597765236515928571, 3 | "features": "[]", 4 | "declared_features": "[]", 5 | "target": 3286384029616100458, 6 | "profile": 17672942494452627365, 7 | "path": 4942398508502643691, 8 | "deps": [], 9 | "local": [ 10 | { 11 | "CheckDepInfo": { 12 | "dep_info": "debug/.fingerprint/rust-project-177dffeea0f5a51b/dep-bin-rust-project", 13 | "checksum": false 14 | } 15 | } 16 | ], 17 | "rustflags": [], 18 | "config": 2069994364910194474, 19 | "compile_kind": 0 20 | } 21 | -------------------------------------------------------------------------------- /examples/rust-project/target/debug/.fingerprint/rust-project-0110108930012809/test-bin-rust-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "rustc": 15597765236515928571, 3 | "features": "[]", 4 | "declared_features": "[]", 5 | "target": 3286384029616100458, 6 | "profile": 3316208278650011218, 7 | "path": 4942398508502643691, 8 | "deps": [], 9 | "local": [ 10 | { 11 | "CheckDepInfo": { 12 | "dep_info": "debug/.fingerprint/rust-project-0110108930012809/dep-test-bin-rust-project", 13 | "checksum": false 14 | } 15 | } 16 | ], 17 | "rustflags": [], 18 | "config": 2069994364910194474, 19 | "compile_kind": 0 20 | } 21 | -------------------------------------------------------------------------------- /packages/code-indexer/src/cache/MemoryCache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * In-memory cache implementation 3 | */ 4 | 5 | import type { SymbolCache, IndexedSymbol } from "../engine/types.ts"; 6 | 7 | export class MemoryCache implements SymbolCache { 8 | private cache: Map = new Map(); 9 | 10 | async get(filePath: string): Promise { 11 | return this.cache.get(filePath) || null; 12 | } 13 | 14 | async set(filePath: string, symbols: IndexedSymbol[]): Promise { 15 | this.cache.set(filePath, symbols); 16 | } 17 | 18 | async clear(): Promise { 19 | this.cache.clear(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll } from "vitest"; 2 | import { findNodeModulesBin } from "../src/features/ts/utils/findNodeModulesBin.js"; 3 | 4 | // Set up environment variables for all tests 5 | beforeAll(() => { 6 | // Find typescript-language-server in node_modules for faster test execution 7 | const tsLanguageServerPath = findNodeModulesBin( 8 | __dirname, 9 | "typescript-language-server", 10 | ); 11 | if (tsLanguageServerPath) { 12 | process.env.TYPESCRIPT_LANGUAGE_SERVER_PATH = tsLanguageServerPath; 13 | } 14 | 15 | // Set project root for finding binaries 16 | process.env.LSMCP_PROJECT_ROOT = process.cwd(); 17 | }); 18 | -------------------------------------------------------------------------------- /src/presets/deno.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * Deno language server adapter 5 | */ 6 | export const denoAdapter: Preset = { 7 | presetId: "deno", 8 | bin: "deno", 9 | args: ["lsp"], 10 | files: ["**/*.ts", "**/*.tsx", "**/*.d.ts"], 11 | initializationOptions: { 12 | enable: true, 13 | lint: true, 14 | unstable: true, 15 | }, 16 | serverCharacteristics: { 17 | documentOpenDelay: 1500, 18 | readinessCheckTimeout: 1000, 19 | initialDiagnosticsTimeout: 2500, 20 | requiresProjectInit: false, 21 | sendsInitialDiagnostics: true, 22 | operationTimeout: 10000, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /examples/fsharp-project/README.md: -------------------------------------------------------------------------------- 1 | # Fsharp Example 2 | 3 | Install `fsautocomplete` 4 | 5 | https://github.com/ionide/FsAutoComplete 6 | 7 | ## How to use 8 | 9 | ```bash 10 | claude mcp add fsharp npx -- -y @mizchi/lsmcp -l fsharp --bin "fsautocomplete --adaptive-lsp-server-enabled" 11 | ``` 12 | 13 | ## Develop 14 | 15 | ```bash 16 | pnpm build # build dist/lsmcp.js 17 | claude --mcp-config=.mcp.json 18 | ``` 19 | 20 | ```json 21 | { 22 | "mcpServers": { 23 | "fsharp": { 24 | "command": "node", 25 | "args": [ 26 | "../../dist/lsmcp.js", 27 | "--bin", 28 | "fsautocomplete" 29 | ] 30 | } 31 | } 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/error.mbt: -------------------------------------------------------------------------------- 1 | // This file contains intentional errors for testing diagnostics 2 | 3 | fn test_type_error() { 4 | let x: String = 42 // Type error: Int cannot be assigned to String 5 | println(x) 6 | } 7 | 8 | fn test_undefined_variable() { 9 | println(undefined_var) // Error: undefined variable 10 | } 11 | 12 | fn test_wrong_arity() { 13 | let result = add(1) // Error: wrong number of arguments 14 | println(result) 15 | } 16 | 17 | fn test_missing_return() -> Int { 18 | println("forgot to return") 19 | // Error: missing return value 20 | } 21 | 22 | pub fn test_unused_function() { 23 | // Warning: unused function 24 | let unused = 42 25 | } -------------------------------------------------------------------------------- /examples/rust-project/.lsmcp/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "files": ["**/*.rs"], 4 | "settings": { 5 | "autoIndex": false, 6 | "indexConcurrency": 5, 7 | "autoIndexDelay": 500, 8 | "enableWatchers": true, 9 | "memoryLimit": 1024 10 | }, 11 | "ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/.git/**"], 12 | "adapter": { 13 | "id": "rust-analyzer", 14 | "name": "rust-analyzer", 15 | "baseLanguage": "rust", 16 | "description": "Official Rust language server", 17 | "bin": "rust-analyzer", 18 | "args": [], 19 | "initializationOptions": { 20 | "cargo": { 21 | "features": "all" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-document-symbols/symbols.ts: -------------------------------------------------------------------------------- 1 | // Test file with various symbols 2 | export interface User { 3 | id: number; 4 | name: string; 5 | } 6 | 7 | export class UserService { 8 | private users: User[] = []; 9 | 10 | addUser(user: User): void { 11 | this.users.push(user); 12 | } 13 | 14 | getUser(id: number): User | undefined { 15 | return this.users.find((u) => u.id === id); 16 | } 17 | } 18 | 19 | export function createUser(name: string): User { 20 | return { 21 | id: Math.random(), 22 | name, 23 | }; 24 | } 25 | 26 | export const DEFAULT_USER: User = { 27 | id: 0, 28 | name: "Default", 29 | }; 30 | 31 | export type UserRole = "admin" | "user" | "guest"; 32 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "includes": [ 11 | "**", 12 | "!**/.venv/**", 13 | "!node_modules/**", 14 | "!dist/**", 15 | "!coverage/**", 16 | "!**/*.min.js", 17 | "!pnpm-lock.yaml" 18 | ] 19 | }, 20 | "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 }, 21 | "linter": { 22 | "enabled": false 23 | }, 24 | "javascript": { "formatter": { "quoteStyle": "double" } }, 25 | "assist": { 26 | "enabled": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/presets/moonbit.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * MoonBit language server adapter 5 | * 6 | * Known issues: 7 | * - May have slower response times for some operations 8 | * - Hover operations may timeout on large files 9 | */ 10 | export const moonbitAdapter: Preset = { 11 | presetId: "moonbit", 12 | bin: "moonbit-lsp", 13 | args: [], 14 | files: ["**/*.mbt", "**/*.mbti"], 15 | binFindStrategy: { 16 | strategies: [ 17 | { type: "node_modules", names: ["@moonbit/moonbit-lsp"] }, 18 | { type: "global", names: ["moonbit-lsp"] }, 19 | ], 20 | }, 21 | disable: [ 22 | // "get_hover", // May be slow/timeout on some files 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /packages/types/src/common/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common logging types and constants 3 | */ 4 | 5 | export enum LogLevel { 6 | ERROR = 0, 7 | WARN = 1, 8 | INFO = 2, 9 | DEBUG = 3, 10 | TRACE = 4, 11 | } 12 | 13 | export interface LogEntry { 14 | timestamp: string; 15 | level: LogLevel; 16 | component: string; 17 | message: string; 18 | data?: unknown; 19 | error?: Error; 20 | } 21 | 22 | export interface DebugSession { 23 | sessionId: string; 24 | adapter: string; 25 | startTime: Date; 26 | endTime?: Date; 27 | logEntries: LogEntry[]; 28 | metrics: { 29 | totalRequests: number; 30 | failedRequests: number; 31 | totalResponseTime: number; 32 | avgResponseTime?: number; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /tests/fixtures/document-symbols-test.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | name: string; 4 | email?: string; 5 | } 6 | 7 | export class UserService { 8 | private users: User[] = []; 9 | 10 | constructor() { 11 | this.users = []; 12 | } 13 | 14 | addUser(user: User): void { 15 | this.users.push(user); 16 | } 17 | 18 | getUser(id: number): User | undefined { 19 | return this.users.find((u) => u.id === id); 20 | } 21 | 22 | get userCount(): number { 23 | return this.users.length; 24 | } 25 | } 26 | 27 | export function processUser(user: User): string { 28 | return `Processing user: ${user.name}`; 29 | } 30 | 31 | export const defaultUser: User = { 32 | id: 0, 33 | name: "Guest", 34 | email: undefined, 35 | }; 36 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | { "type": "feat", "section": "Features" }, 4 | { "type": "fix", "section": "Bug Fixes" }, 5 | { "type": "chore", "hidden": false, "section": "Chores" }, 6 | { "type": "docs", "section": "Documentation" }, 7 | { "type": "style", "hidden": true }, 8 | { "type": "refactor", "section": "Code Refactoring" }, 9 | { "type": "perf", "section": "Performance Improvements" }, 10 | { "type": "test", "section": "Tests" }, 11 | { "type": "build", "section": "Build System" }, 12 | { "type": "ci", "section": "Continuous Integration" } 13 | ], 14 | "commitUrlFormat": "https://github.com/mizchi/lsmcp/commit/{{hash}}", 15 | "compareUrlFormat": "https://github.com/mizchi/lsmcp/compare/{{previousTag}}...{{currentTag}}" 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/type-alias.input.ts: -------------------------------------------------------------------------------- 1 | // Type alias definition 2 | type UserData = { 3 | id: number; 4 | name: string; 5 | email: string; 6 | }; 7 | 8 | // Use type in function parameters 9 | function processUser(user: UserData): UserData { 10 | return { 11 | ...user, 12 | name: user.name.toUpperCase(), 13 | }; 14 | } 15 | 16 | // Use type in variable declaration 17 | const testUser: UserData = { 18 | id: 1, 19 | name: "John", 20 | email: "john@example.com", 21 | }; 22 | 23 | // Use type in class 24 | class UserService { 25 | private users: UserData[] = []; 26 | 27 | addUser(user: UserData): void { 28 | this.users.push(user); 29 | } 30 | 31 | getUser(id: number): UserData | undefined { 32 | return this.users.find((u) => u.id === id); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.claude/commands/dce-by-tsr.md: -------------------------------------------------------------------------------- 1 | # Dead Code Elimination with tsr 2 | 3 | このコマンドは[tsr](https://github.com/nadeesha/ts-remove-unused)を使用して、TypeScript プロジェクトの未使用コードを検出します。 4 | 5 | ## 実行方法 6 | 7 | ```bash 8 | npx tsr 'src/mcp/(lsmcp|typescript-mcp|generic-lsp-mcp)\.ts$' 9 | ``` 10 | 11 | ## オプション 12 | 13 | - `--write`: 検出された未使用コードを自動的に削除します(注意:テストファイルも削除される可能性があります) 14 | - `--recursive`: プロジェクトがクリーンになるまで再帰的にチェックします 15 | - `--project `: カスタム tsconfig.json のパスを指定します 16 | - `--include-d-ts`: .d.ts ファイルの未使用コードもチェックします 17 | 18 | ## 注意事項 19 | 20 | - tsr はエントリーポイントから到達可能なコードのみを保持します 21 | - テストファイルは通常のエントリーポイントから参照されないため、未使用として検出される可能性があります 22 | - `--write`オプションを使用する前に、必ず結果を確認してください 23 | 24 | ## このプロジェクトでの使用例 25 | 26 | - `src/mcp/lsmcp.ts` - メインの LSP MCP CLI 27 | 28 | 実行すると、これらのエントリーポイントから到達できないエクスポートやファイルが表示されます。 29 | -------------------------------------------------------------------------------- /.github/workflows/ci-unit.yml: -------------------------------------------------------------------------------- 1 | name: CI - Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | unit-tests: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Install pnpm 16 | uses: pnpm/action-setup@v4 17 | - name: Use Node.js 22 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 22 21 | cache: "pnpm" 22 | - name: Install dependencies 23 | run: pnpm install --frozen-lockfile 24 | - name: Build 25 | run: pnpm build 26 | - name: Type check 27 | run: pnpm typecheck 28 | - name: Lint 29 | run: pnpm lint 30 | - name: Run unit tests 31 | run: pnpm test:unit 32 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-rename/type-alias.expected.ts: -------------------------------------------------------------------------------- 1 | // Type alias definition 2 | type PersonData = { 3 | id: number; 4 | name: string; 5 | email: string; 6 | }; 7 | 8 | // Use type in function parameters 9 | function processUser(user: PersonData): PersonData { 10 | return { 11 | ...user, 12 | name: user.name.toUpperCase(), 13 | }; 14 | } 15 | 16 | // Use type in variable declaration 17 | const testUser: PersonData = { 18 | id: 1, 19 | name: "John", 20 | email: "john@example.com", 21 | }; 22 | 23 | // Use type in class 24 | class UserService { 25 | private users: PersonData[] = []; 26 | 27 | addUser(user: PersonData): void { 28 | this.users.push(user); 29 | } 30 | 31 | getUser(id: number): PersonData | undefined { 32 | return this.users.find((u) => u.id === id); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/fixtures/fsharp/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open Types 3 | 4 | [] 5 | let main argv = 6 | let service = UserService() 7 | 8 | let users = [ 9 | service.CreateUser(1, "Alice", Some "alice@example.com") 10 | service.CreateUser(2, "Bob", None) 11 | service.CreateUser(3, "Charlie", Some "charlie@example.com") 12 | ] 13 | 14 | users |> List.iter (fun user -> 15 | printfn "%s" (service.GreetUser(user)) 16 | ) 17 | 18 | // This should cause a type error 19 | let invalidUser = service.CreateUser("not-a-number", "Invalid", None) // Type error: string is not int 20 | 21 | // This will also cause a type error 22 | let result: int = service.GreetUser(users.[0]) // Type error: string is not int 23 | 24 | 0 // return an integer exit code -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for npm 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | time: "00:00" 10 | open-pull-requests-limit: 10 11 | groups: 12 | development: 13 | patterns: 14 | - "@types/*" 15 | - "vitest*" 16 | - "tsx" 17 | - "tsdown" 18 | - "@biomejs/*" 19 | production: 20 | patterns: 21 | - "@modelcontextprotocol/*" 22 | - "vscode-*" 23 | 24 | # Enable version updates for GitHub Actions 25 | - package-ecosystem: "github-actions" 26 | directory: "/" 27 | schedule: 28 | interval: "weekly" 29 | day: "monday" 30 | time: "00:00" 31 | open-pull-requests-limit: 5 -------------------------------------------------------------------------------- /packages/types/src/constants/diagnostics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Diagnostics-related constants 3 | */ 4 | 5 | // Batch processing 6 | export const DIAGNOSTICS_BATCH_SIZE = 5; // Files to process in parallel 7 | export const DIAGNOSTICS_POLL_INTERVAL = 50; // ms 8 | export const DIAGNOSTICS_MAX_POLLS = 100; 9 | export const DIAGNOSTICS_DEFAULT_TIMEOUT = 5000; // ms 10 | 11 | // Display limits 12 | export const MAX_FILES_TO_SHOW = 20; 13 | export const MAX_DIAGNOSTICS_PER_FILE = 10; 14 | 15 | // Language-specific timeouts 16 | export const LANGUAGE_SPECIFIC_TIMEOUTS = { 17 | moonbit: { initialWait: 1000, maxPolls: 200, timeout: 5000 }, 18 | deno: { initialWait: 800, maxPolls: 100, timeout: 3000 }, 19 | default: { initialWait: 200, maxPolls: 60, timeout: 1000 }, 20 | largeFile: { initialWait: 500, maxPolls: 100, timeout: 3000 }, 21 | } as const; 22 | -------------------------------------------------------------------------------- /examples/full-lsmcp-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../lsmcp.schema.json", 3 | 4 | "preset": "tsgo", 5 | 6 | "files": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 7 | 8 | "settings": { 9 | "autoIndex": false, 10 | "indexConcurrency": 5, 11 | "autoIndexDelay": 500, 12 | "enableWatchers": true, 13 | "memoryLimit": 1024 14 | }, 15 | 16 | "ignorePatterns": [ 17 | "**/node_modules/**", 18 | "**/dist/**", 19 | "**/build/**", 20 | "**/.git/**", 21 | "**/coverage/**" 22 | ], 23 | 24 | "symbolFilter": { 25 | "excludeKinds": [ 26 | "Variable", 27 | "Constant", 28 | "String", 29 | "Number", 30 | "Boolean", 31 | "Array", 32 | "Object", 33 | "Key", 34 | "Null" 35 | ], 36 | "excludePatterns": ["callback", "temp", "tmp", "_", "^[a-z]$"], 37 | "includeOnlyTopLevel": false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/tools/toolLists.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tool list exports 3 | */ 4 | 5 | import type { McpToolDef } from "@internal/types"; 6 | 7 | // Import analysis tools 8 | import { indexTools } from "./highlevel/indexTools.ts"; 9 | 10 | // Import serenity tools 11 | import { getSerenityToolsList } from "./index.ts"; 12 | 13 | // Import onboarding tools 14 | import { indexOnboardingTools } from "../features/memory/onboarding/onboardingTools.ts"; 15 | 16 | // Define high-level analysis tools (not affected by LSP capabilities) 17 | export const highLevelTools: McpToolDef[] = [ 18 | ...indexTools, // Includes search_symbols and get_project_overview 19 | ]; 20 | 21 | // Define serenity tools (use function to get proper tools list) 22 | export const serenityToolsList: McpToolDef[] = getSerenityToolsList(); 23 | 24 | // Define onboarding tools 25 | export const onboardingToolsList: McpToolDef[] = indexOnboardingTools; 26 | -------------------------------------------------------------------------------- /tests/fixtures/fsharp/CommentedTypes.fs: -------------------------------------------------------------------------------- 1 | module CommentedTypes 2 | 3 | /// This is a document comment for the function 4 | /// It spans multiple lines to test position handling 5 | let myFunction x = 6 | x + 1 7 | 8 | /// Another comment for a type 9 | type MyType = { 10 | /// Field comment for Value 11 | Value: int 12 | /// Another field comment for Name 13 | Name: string 14 | } 15 | 16 | /// Comment for a class 17 | type MyClass() = 18 | /// Method comment 19 | member this.GetValue() = 42 20 | 21 | /// Property comment 22 | member this.Property = "test" 23 | 24 | /// Record type with multiple commented fields 25 | type Person = { 26 | /// The person's first name 27 | FirstName: string 28 | /// The person's last name 29 | LastName: string 30 | /// The person's age 31 | Age: int 32 | /// The person's email address 33 | Email: string option 34 | } -------------------------------------------------------------------------------- /.github/workflows/ci-integration.yml: -------------------------------------------------------------------------------- 1 | name: CI - Integration Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | integration-tests: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 # Give integration tests more time 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v4 18 | - name: Use Node.js 22 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | cache: "pnpm" 23 | - name: Install dependencies 24 | run: pnpm install --frozen-lockfile 25 | - name: Install typescript-language-server globally 26 | run: npm install -g typescript typescript-language-server 27 | - name: Build 28 | run: pnpm build 29 | - name: Run integration tests 30 | run: pnpm test:integration 31 | -------------------------------------------------------------------------------- /examples/go/hello.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Person represents a person with a name and age 9 | type Person struct { 10 | Name string 11 | Age int 12 | } 13 | 14 | // NewPerson creates a new Person instance 15 | func NewPerson(name string, age int) *Person { 16 | return &Person{ 17 | Name: name, 18 | Age: age, 19 | } 20 | } 21 | 22 | // Greet returns a greeting message 23 | func (p *Person) Greet() string { 24 | return fmt.Sprintf("Hello, my name is %s and I am %d years old", p.Name, p.Age) 25 | } 26 | 27 | // Helper functions 28 | func formatName(name string) string { 29 | return strings.Title(strings.ToLower(name)) 30 | } 31 | 32 | func main() { 33 | person := NewPerson("Alice", 30) 34 | fmt.Println(person.Greet()) 35 | 36 | // Example of using the helper function 37 | formattedName := formatName("JOHN DOE") 38 | fmt.Println("Formatted name:", formattedName) 39 | } -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/types", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "main": "./src/index.ts", 6 | "types": "./src/index.ts", 7 | "exports": { 8 | ".": "./src/index.ts", 9 | "./lsp": "./src/lsp/index.ts", 10 | "./domain": "./src/domain/index.ts", 11 | "./shared": "./src/shared/index.ts", 12 | "./validators": "./src/validators/index.ts", 13 | "./constants/lsp": "./src/constants/lsp.ts", 14 | "./constants/config": "./src/constants/config.ts", 15 | "./constants/diagnostics": "./src/constants/diagnostics.ts", 16 | "./constants/indexer": "./src/constants/indexer.ts" 17 | }, 18 | "scripts": { 19 | "typecheck": "tsc --noEmit" 20 | }, 21 | "dependencies": { 22 | "vscode-languageserver-types": "^3.17.5", 23 | "zod": "^3.24.1" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^22.10.7", 27 | "typescript": "^5.8.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/fixtures/lsp-definitions/calculator.ts: -------------------------------------------------------------------------------- 1 | export class Calculator { 2 | private history: number[] = []; 3 | 4 | add(a: number, b: number): number { 5 | const result = a + b; 6 | this.history.push(result); 7 | return result; 8 | } 9 | 10 | subtract(a: number, b: number): number { 11 | const result = a - b; 12 | this.history.push(result); 13 | return result; 14 | } 15 | 16 | multiply(a: number, b: number): number { 17 | const result = a * b; 18 | this.history.push(result); 19 | return result; 20 | } 21 | 22 | divide(a: number, b: number): number { 23 | if (b === 0) { 24 | throw new Error("Division by zero"); 25 | } 26 | const result = a / b; 27 | this.history.push(result); 28 | return result; 29 | } 30 | 31 | getHistory(): number[] { 32 | return [...this.history]; 33 | } 34 | 35 | clearHistory(): void { 36 | this.history = []; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/lsp-client/src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common validation utilities for LSP tools 3 | */ 4 | 5 | export function validateLineAndSymbol( 6 | content: string, 7 | line: number | string, 8 | symbolName: string, 9 | filePath: string, 10 | ) { 11 | const lines = content.split("\n"); 12 | let lineIndex: number; 13 | 14 | if (typeof line === "number") { 15 | lineIndex = line - 1; 16 | } else { 17 | lineIndex = lines.findIndex((l) => l.includes(line)); 18 | if (lineIndex === -1) { 19 | throw new Error(`Line containing "${line}" not found in ${filePath}`); 20 | } 21 | } 22 | 23 | const lineContent = lines[lineIndex]; 24 | const symbolIndex = lineContent.indexOf(symbolName); 25 | 26 | if (symbolIndex === -1) { 27 | throw new Error( 28 | `Symbol "${symbolName}" not found on line ${lineIndex + 1} in ${filePath}`, 29 | ); 30 | } 31 | 32 | return { lineIndex, symbolIndex }; 33 | } 34 | -------------------------------------------------------------------------------- /tests/fixtures/moonbit/main.mbt: -------------------------------------------------------------------------------- 1 | struct User { 2 | id : Int 3 | name : String 4 | email : String? 5 | } 6 | 7 | fn greet_user(user : User) -> String { 8 | "Hello, \(user.name)!" 9 | } 10 | 11 | fn process_users(users : Array[User]) -> Map[String, Int] { 12 | let result = Map::new() 13 | for user in users { 14 | result[user.name] = user.id 15 | } 16 | result 17 | } 18 | 19 | fn main() { 20 | let users = [ 21 | { id: 1, name: "Alice", email: Some("alice@example.com") }, 22 | { id: 2, name: "Bob", email: None }, 23 | { id: 3, name: "Charlie", email: Some("charlie@example.com") } 24 | ] 25 | 26 | let id_map = process_users(users) 27 | println("User ID map: \(id_map)") 28 | 29 | // This should cause a type error 30 | let invalid_id : String = 123 // Type error: Int is not String 31 | 32 | // This will also cause a type error 33 | let result : Int = process_users(users) // Type error: Map[String, Int] is not Int 34 | } -------------------------------------------------------------------------------- /tests/fixtures/neverthrow-example/README.md: -------------------------------------------------------------------------------- 1 | # Neverthrow Example Fixture 2 | 3 | This directory contains example TypeScript files that use the `neverthrow` library for testing external library symbol resolution. 4 | 5 | ## Purpose 6 | 7 | - Test import parsing for external libraries 8 | - Test symbol resolution from node_modules 9 | - Test filtering of external vs internal symbols 10 | - Verify that symbols like `fromThrowable`, `Result`, `Ok`, `Err` can be found 11 | 12 | ## Key Symbols to Test 13 | 14 | From `neverthrow`: 15 | - `ok` - function to create Ok result 16 | - `err` - function to create Err result 17 | - `Result` - main Result type 18 | - `ResultAsync` - async version of Result 19 | - `fromThrowable` - convert throwing functions to Result 20 | - `fromAsyncThrowable` - async version 21 | - `Ok` - Ok type 22 | - `Err` - Err type 23 | 24 | ## Requirements 25 | 26 | The main project should have `neverthrow` installed as a dev dependency: 27 | ```bash 28 | pnpm add neverthrow --save-dev 29 | ``` -------------------------------------------------------------------------------- /src/presets/gopls.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * Gopls adapter for Go language support 5 | * @see https://pkg.go.dev/golang.org/x/tools/gopls 6 | */ 7 | export const goplsAdapter: Preset = { 8 | presetId: "gopls", 9 | bin: "gopls", 10 | args: ["serve"], 11 | files: ["**/*.go", "go.mod", "go.sum"], 12 | initializationOptions: { 13 | // Enable all gopls features 14 | codelenses: { 15 | gc_details: true, 16 | generate: true, 17 | regenerate_cgo: true, 18 | run_govulncheck: true, 19 | test: true, 20 | tidy: true, 21 | upgrade_dependency: true, 22 | vendor: true, 23 | }, 24 | analyses: { 25 | unusedparams: true, 26 | unusedwrite: true, 27 | useany: true, 28 | }, 29 | staticcheck: true, 30 | gofumpt: true, 31 | semanticTokens: true, 32 | noSemanticString: false, 33 | usePlaceholders: true, 34 | completeUnimported: true, 35 | completionBudget: "500ms", 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/features/ts/utils/findNodeModulesBin.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join, resolve } from "path"; 3 | 4 | /** 5 | * Find a binary in node_modules/.bin directory 6 | * This is more reliable than using npx in test environments 7 | * @param rootPath The root path to start searching from 8 | * @param binName The binary name to search for 9 | * @returns The path to the binary, or null if not found 10 | */ 11 | export function findNodeModulesBin( 12 | rootPath: string, 13 | binName: string, 14 | ): string | null { 15 | const isWindows = process.platform === "win32"; 16 | const binFileName = isWindows ? `${binName}.cmd` : binName; 17 | 18 | // Search up to 10 levels up for node_modules 19 | for (let i = 0; i <= 10; i++) { 20 | const searchPath = 21 | i === 0 ? rootPath : resolve(rootPath, ...Array(i).fill("..")); 22 | 23 | const binPath = join(searchPath, "node_modules", ".bin", binFileName); 24 | 25 | if (existsSync(binPath)) { 26 | return binPath; 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | -------------------------------------------------------------------------------- /tests/fixtures/ocaml/main.ml: -------------------------------------------------------------------------------- 1 | (* Type definitions *) 2 | type user = { 3 | name: string; 4 | age: int; 5 | email: string option; 6 | } 7 | 8 | (* Functions *) 9 | let greet_user user = 10 | "Hello, " ^ user.name ^ "!" 11 | 12 | let process_users users = 13 | List.map (fun u -> (u.name, u.age)) users 14 | 15 | (* Example with type error *) 16 | let invalid_age = 17 | { name = "Alice"; age = "not a number"; email = None } (* Type error: string instead of int *) 18 | 19 | (* Another type error *) 20 | let wrong_type : int = "hello" (* Type error: string instead of int *) 21 | 22 | (* Main *) 23 | let () = 24 | let users = [ 25 | { name = "Alice"; age = 30; email = Some "alice@example.com" }; 26 | { name = "Bob"; age = 25; email = None }; 27 | { name = "Charlie"; age = 35; email = Some "charlie@example.com" }; 28 | ] in 29 | let user_list = process_users users in 30 | Printf.printf "User list: %s\n" (String.concat ", " (List.map (fun (n, a) -> Printf.sprintf "(%s, %d)" n a) user_list)); 31 | print_endline (greet_user (List.hd users)) -------------------------------------------------------------------------------- /tests/fixtures/haskell/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | -- Type definitions 4 | data User = User 5 | { userName :: String 6 | , userAge :: Int 7 | , userEmail :: Maybe String 8 | } deriving (Show, Eq) 9 | 10 | -- Functions 11 | greetUser :: User -> String 12 | greetUser user = "Hello, " ++ userName user ++ "!" 13 | 14 | processUsers :: [User] -> [(String, Int)] 15 | processUsers users = map (\u -> (userName u, userAge u)) users 16 | 17 | -- Example with type error 18 | invalidAge :: User 19 | invalidAge = User "Alice" "not a number" Nothing -- Type error: String instead of Int 20 | 21 | -- Another type error 22 | wrongType :: Int 23 | wrongType = "hello" -- Type error: String instead of Int 24 | 25 | -- Main function 26 | main :: IO () 27 | main = do 28 | let users = [ User "Alice" 30 (Just "alice@example.com") 29 | , User "Bob" 25 Nothing 30 | , User "Charlie" 35 (Just "charlie@example.com") 31 | ] 32 | putStrLn $ "User list: " ++ show (processUsers users) 33 | putStrLn $ greetUser (head users) -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/target/packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "source_dir": "/home/mizchi/mizchi/lsmcp/examples/moonbit-project/src/test", 3 | "name": "moon/test", 4 | "packages": [ 5 | { 6 | "is-main": false, 7 | "is-third-party": false, 8 | "root-path": "/home/mizchi/mizchi/lsmcp/examples/moonbit-project/src/test", 9 | "root": "moon/test", 10 | "rel": "single", 11 | "files": {}, 12 | "wbtest-files": {}, 13 | "test-files": { 14 | "/home/mizchi/mizchi/lsmcp/examples/moonbit-project/src/test/error.mbt": { 15 | "backend": ["Wasm", "WasmGC", "Js", "Native", "LLVM"], 16 | "optlevel": ["Debug", "Release"] 17 | } 18 | }, 19 | "mbt-md-files": {}, 20 | "deps": [], 21 | "wbtest-deps": [], 22 | "test-deps": [], 23 | "artifact": "/home/mizchi/mizchi/lsmcp/examples/moonbit-project/src/test/target/wasm-gc/release/check/single/single.mi" 24 | } 25 | ], 26 | "deps": [], 27 | "backend": "wasm-gc", 28 | "opt_level": "release", 29 | "source": null 30 | } 31 | -------------------------------------------------------------------------------- /packages/lsp-client/src/utils/debug.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Debug and logging utilities 3 | * 4 | * This file provides backward compatibility with existing code 5 | * while using the new independent LSP logger 6 | */ 7 | 8 | import { lspDebug, lspDebugWithPrefix } from "./lsp-logger.ts"; 9 | 10 | export function debug(...args: any[]): void { 11 | lspDebug(...args); 12 | } 13 | 14 | export function debugLog(message: string, data?: any): void { 15 | if (data) { 16 | lspDebugWithPrefix("Debug", message, JSON.stringify(data, null, 2)); 17 | } else { 18 | lspDebugWithPrefix("Debug", message); 19 | } 20 | } 21 | 22 | // Re-export error handling utilities from main utils 23 | export { 24 | formatError, 25 | getErrorMessage, 26 | } from "../../../../src/utils/errorHandler.ts"; 27 | export type { ErrorContext } from "../../../../src/utils/errorHandler.ts"; 28 | 29 | export function isErrorWithCode( 30 | error: unknown, 31 | ): error is Error & { code: number } { 32 | return ( 33 | error instanceof Error && 34 | "code" in error && 35 | typeof (error as any).code === "number" 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 mizchi 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. -------------------------------------------------------------------------------- /examples/fsharp-project/Program.fs: -------------------------------------------------------------------------------- 1 | // For more information see https://aka.ms/fsharp-console-apps 2 | module Program 3 | 4 | let add x y = x + y 5 | let multiply x y = x * y 6 | 7 | // Type error test 8 | let (_typeError: string) = 123 // Error: assigning int to string 9 | 10 | let main() = 11 | printfn "=== F# Calculator Demo ===" 12 | 13 | // Basic calculations 14 | let addResult = Calculator.add 10 5 15 | let subResult = Calculator.subtract 10 5 16 | let mulResult = Calculator.multiply 10 5 17 | let divResult = Calculator.divide 10 5 18 | 19 | printfn "10 + 5 = %d" addResult 20 | printfn "10 - 5 = %d" subResult 21 | printfn "10 * 5 = %d" mulResult 22 | printfn "10 / 5 = %d" divResult 23 | 24 | // List sum 25 | let numbers = [1; 2; 3; 4; 5] 26 | let sum = Calculator.sumList numbers 27 | printfn "Sum of %A = %d" numbers sum 28 | 29 | // Pattern matching 30 | printfn "10 is %s" (Calculator.describeNumber 10) 31 | printfn "-5 is %s" (Calculator.describeNumber -5) 32 | printfn "0 is %s" (Calculator.describeNumber 0) 33 | 34 | 0 35 | 36 | main() |> ignore 37 | -------------------------------------------------------------------------------- /src/presets/rust-analyzer.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * rust-analyzer adapter 5 | */ 6 | export const rustAnalyzerAdapter: Preset = { 7 | presetId: "rust-analyzer", 8 | name: "rust-analyzer", 9 | description: "Language Server for Rust", 10 | binFindStrategy: { 11 | strategies: [ 12 | // 1. Check global installation (most common for Rust) 13 | { type: "global", names: ["rust-analyzer"] }, 14 | // 2. Check cargo install location 15 | { type: "path", path: "~/.cargo/bin/rust-analyzer" }, 16 | // 3. Check system package manager installations 17 | { type: "path", path: "/usr/bin/rust-analyzer" }, 18 | { type: "path", path: "/usr/local/bin/rust-analyzer" }, 19 | ], 20 | defaultArgs: [], 21 | }, 22 | files: ["**/*.rs"], 23 | initializationOptions: { 24 | cargo: { 25 | features: "all", 26 | }, 27 | // procMacro: { 28 | // enable: true, 29 | // }, 30 | }, 31 | 32 | // Language-specific features 33 | languageFeatures: { 34 | rust: { 35 | enabled: true, 36 | indexCargo: true, 37 | }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /examples/moonbit-project/README.md: -------------------------------------------------------------------------------- 1 | # Moonbit Test Project 2 | 3 | This directory is a test project for demonstrating Moonbit MCP server functionality through lsmcp. The Moonbit LSP server is fully functional and provides hover information, diagnostics, and reference finding capabilities. 4 | 5 | ## Structure 6 | 7 | - `moon.mod.json` - Moonbit project configuration 8 | - `src/lib/` - Library code 9 | - `src/main/` - Main entry point 10 | - `src/utils/` - TypeScript utilities (to test mixed language support) 11 | 12 | ## Testing MCP Tools 13 | 14 | 1. Build the MCP servers: 15 | ```bash 16 | cd ../.. 17 | pnpm build 18 | ``` 19 | 20 | 2. Start Claude in this directory: 21 | ```bash 22 | claude 23 | ``` 24 | 25 | 3. Test Moonbit tools: 26 | ``` 27 | Use moonbit_get_hover on the hello function in src/lib/hello.mbt 28 | Use moonbit_find_references to find all uses of the User struct 29 | Use moonbit_get_diagnostics on src/main/main.mbt 30 | Use moonbit_get_diagnostics on src/test/error.mbt to see all errors 31 | ``` 32 | 33 | 4. Test TypeScript tools on mixed files: 34 | ``` 35 | Use mcp__typescript__get_type_at_symbol on MoonbitProjectManager in src/utils/project.ts 36 | ``` -------------------------------------------------------------------------------- /src/tools/getAllTools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get all available MCP tools based on configuration 3 | */ 4 | 5 | import type { McpToolDef } from "@internal/types"; 6 | import type { LSPClient } from "@internal/lsp-client"; 7 | import { createLSPTools } from "./lsp/createLspTools.ts"; 8 | import { 9 | highLevelTools, 10 | serenityToolsList, 11 | onboardingToolsList, 12 | } from "./toolLists.ts"; 13 | 14 | /** 15 | * Get all available tools for the current configuration 16 | */ 17 | export async function getAllAvailableTools( 18 | _config?: any, 19 | client?: LSPClient, 20 | ): Promise[]> { 21 | const tools: McpToolDef[] = []; 22 | 23 | // Add high-level analysis tools (always available) 24 | tools.push(...highLevelTools); 25 | 26 | // Create client if not provided 27 | const lspClient = client || ({} as any); 28 | 29 | // Add low-level LSP tools (subject to capability filtering) 30 | const lspTools = createLSPTools(lspClient); 31 | tools.push(...lspTools); 32 | 33 | // Add serenity tools 34 | tools.push(...serenityToolsList); 35 | 36 | // Add onboarding tools 37 | tools.push(...onboardingToolsList); 38 | 39 | return tools; 40 | } 41 | -------------------------------------------------------------------------------- /tests/fixtures/python/main.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional 2 | 3 | 4 | class User: 5 | def __init__(self, name: str, age: int, email: Optional[str] = None): 6 | self.name = name 7 | self.age = age 8 | self.email = email 9 | 10 | def greet(self) -> str: 11 | return f"Hello, my name is {self.name} and I'm {self.age} years old." 12 | 13 | 14 | def process_users(users: List[User]) -> Dict[str, int]: 15 | """Process a list of users and return a dict of names to ages.""" 16 | result = {} 17 | for user in users: 18 | result[user.name] = user.age 19 | return result 20 | 21 | 22 | # Example usage 23 | if __name__ == "__main__": 24 | users = [ 25 | User("Alice", 30, "alice@example.com"), 26 | User("Bob", 25), 27 | User("Charlie", 35, "charlie@example.com"), 28 | ] 29 | 30 | age_map = process_users(users) 31 | print(f"Age map: {age_map}") 32 | 33 | # This should cause a type error 34 | invalid_user = User("Dave", "not-a-number") # Type error: str is not int 35 | 36 | # This will also cause a type error 37 | result: int = process_users(users) # Type error: Dict[str, int] is not int 38 | -------------------------------------------------------------------------------- /packages/types/src/validators/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Centralized Zod validation schemas for the entire codebase 3 | */ 4 | 5 | // Export all validator modules 6 | export * from "./lsp.ts"; 7 | export * from "./indexer.ts"; 8 | export * from "./config.ts"; 9 | export * from "./memory.ts"; 10 | 11 | // Re-export commonly used schemas at top level for convenience 12 | export { 13 | commonSchemas, 14 | lspSchemas, 15 | fileLocationSchema, 16 | symbolLocationSchema, 17 | definitionSchema, 18 | hoverSchema, 19 | diagnosticsSchema, 20 | formattingOptionsSchema, 21 | } from "./lsp.ts"; 22 | 23 | export { 24 | SymbolKindSchema, 25 | // Note: SymbolKind type is already exported from lsp/index.ts 26 | indexFilesSchema, 27 | searchSymbolSchema, 28 | clearIndexSchema, 29 | } from "./indexer.ts"; 30 | 31 | export { 32 | configSchema, 33 | adapterConfigSchema, 34 | serverCharacteristicsSchema, 35 | memoryConfigSchema, 36 | indexConfigSchema, 37 | } from "./config.ts"; 38 | 39 | export { 40 | listMemoriesSchema, 41 | readMemorySchema, 42 | writeMemorySchema, 43 | deleteMemorySchema, 44 | searchMemoriesSchema, 45 | mergeMemoriesSchema, 46 | compressMemorySchema, 47 | } from "./memory.ts"; 48 | -------------------------------------------------------------------------------- /examples/rust-project/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// A simple calculator struct 2 | pub struct Calculator { 3 | value: f64, 4 | } 5 | 6 | impl Calculator { 7 | /// Creates a new Calculator with initial value 8 | pub fn new() -> Self { 9 | Calculator { value: 0.0 } 10 | } 11 | 12 | /// Adds a number to the current value 13 | pub fn add(&mut self, num: f64) -> &mut Self { 14 | self.value += num; 15 | self 16 | } 17 | 18 | /// Subtracts a number from the current value 19 | pub fn subtract(&mut self, num: f64) -> &mut Self { 20 | self.value -= num; 21 | self 22 | } 23 | 24 | /// Gets the current value 25 | pub fn get_value(&self) -> f64 { 26 | self.value 27 | } 28 | } 29 | 30 | /// Example function to demonstrate refactoring 31 | pub fn greet(name: &str) -> String { 32 | format!("Hello, {}!", name) 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | 39 | #[test] 40 | fn test_calculator() { 41 | let mut calc = Calculator::new(); 42 | calc.add(5.0).subtract(2.0); 43 | assert_eq!(calc.get_value(), 3.0); 44 | } 45 | 46 | #[test] 47 | fn test_greet() { 48 | assert_eq!(greet("World"), "Hello, World!"); 49 | } 50 | } -------------------------------------------------------------------------------- /packages/types/src/domain/mcp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MCP (Model Context Protocol) related types 3 | */ 4 | 5 | import type { z, ZodType } from "zod"; 6 | import type { FileSystemApi } from "./filesystem.ts"; 7 | 8 | /** 9 | * MCP execution context passed to tools 10 | */ 11 | export interface McpContext { 12 | /** LSP client instance (any type that implements LSP operations) */ 13 | lspClient: any; // Using 'any' to avoid circular dependency with LSPClient 14 | /** File system API (required) */ 15 | fs: FileSystemApi; 16 | /** Configuration */ 17 | config?: Record; 18 | /** Language ID or preset ID for language-specific handling */ 19 | languageId?: string; 20 | } 21 | 22 | /** 23 | * MCP Tool definition interface 24 | */ 25 | export interface McpToolDef { 26 | name: string; 27 | description: string; 28 | schema: TSchema; 29 | execute: (args: z.infer, context?: McpContext) => Promise; 30 | } 31 | 32 | /** 33 | * MCP Server configuration options 34 | */ 35 | export interface McpServerOptions { 36 | name: string; 37 | version: string; 38 | description?: string; 39 | capabilities?: { 40 | tools?: boolean; 41 | resources?: boolean; 42 | prompts?: boolean; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /scripts/setup-haskell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup script for Haskell development environment using ghcup 4 | 5 | set -e 6 | 7 | echo "Setting up Haskell development environment..." 8 | 9 | # Check if ghcup is installed 10 | if ! command -v ghcup &> /dev/null; then 11 | echo "Installing ghcup..." 12 | curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh 13 | 14 | # Source ghcup environment 15 | [ -f "$HOME/.ghcup/env" ] && source "$HOME/.ghcup/env" 16 | else 17 | echo "ghcup is already installed" 18 | fi 19 | 20 | # Install GHC and HLS 21 | echo "Installing GHC and HLS..." 22 | ghcup install ghc recommended --set 23 | ghcup install hls latest --set 24 | ghcup install cabal latest --set 25 | ghcup install stack latest --set 26 | 27 | # Verify installations 28 | echo "Verifying installations..." 29 | echo "GHC version: $(ghc --version)" 30 | echo "HLS version: $(haskell-language-server-wrapper --version || echo 'HLS not found')" 31 | echo "Cabal version: $(cabal --version | head -n1)" 32 | echo "Stack version: $(stack --version)" 33 | 34 | echo "Haskell environment setup complete!" 35 | echo "" 36 | echo "To use this environment, make sure to add the following to your shell profile:" 37 | echo ' [ -f "$HOME/.ghcup/env" ] && source "$HOME/.ghcup/env"' -------------------------------------------------------------------------------- /packages/code-indexer/src/engine/contentHash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Content hash utilities for file caching 3 | */ 4 | 5 | import { createHash } from "crypto"; 6 | import { readFile } from "fs/promises"; 7 | 8 | /** 9 | * Calculate SHA1 hash of file content 10 | * SHA1 is faster than SHA256 and sufficient for content comparison 11 | */ 12 | export async function getFileContentHash(filePath: string): Promise { 13 | try { 14 | const content = await readFile(filePath, "utf-8"); 15 | return createHash("sha1").update(content).digest("hex"); 16 | } catch (error) { 17 | throw new Error(`Failed to calculate hash for ${filePath}: ${error}`); 18 | } 19 | } 20 | 21 | /** 22 | * Calculate SHA1 hash of string content 23 | * SHA1 is faster than SHA256 and sufficient for content comparison 24 | */ 25 | export function getContentHash(content: string): string { 26 | return createHash("sha1").update(content).digest("hex"); 27 | } 28 | 29 | /** 30 | * File metadata with content hash 31 | */ 32 | export interface FileMetadata { 33 | path: string; 34 | contentHash: string; 35 | mtime: Date; 36 | gitHash?: string; 37 | } 38 | 39 | /** 40 | * Cache entry with metadata 41 | */ 42 | export interface CacheEntry { 43 | data: T; 44 | metadata: FileMetadata; 45 | timestamp: Date; 46 | } 47 | -------------------------------------------------------------------------------- /tests/fixtures/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type User struct { 8 | ID int 9 | Name string 10 | Email *string 11 | } 12 | 13 | func NewUser(id int, name string, email *string) *User { 14 | return &User{ 15 | ID: id, 16 | Name: name, 17 | Email: email, 18 | } 19 | } 20 | 21 | func (u *User) Greet() string { 22 | return fmt.Sprintf("Hello, I'm %s!", u.Name) 23 | } 24 | 25 | func processUsers(users []*User) map[string]int { 26 | result := make(map[string]int) 27 | for _, user := range users { 28 | result[user.Name] = user.ID 29 | } 30 | return result 31 | } 32 | 33 | func main() { 34 | email1 := "alice@example.com" 35 | email3 := "charlie@example.com" 36 | 37 | users := []*User{ 38 | NewUser(1, "Alice", &email1), 39 | NewUser(2, "Bob", nil), 40 | NewUser(3, "Charlie", &email3), 41 | } 42 | 43 | idMap := processUsers(users) 44 | fmt.Printf("User ID map: %v\n", idMap) 45 | 46 | // This should cause a type error 47 | var invalidID string = 123 // Type error: cannot use 123 (untyped int constant) as string value 48 | 49 | // This will also cause a type error 50 | var result int = processUsers(users) // Type error: cannot use processUsers(users) (value of type map[string]int) as int value 51 | 52 | fmt.Println(invalidID, result) 53 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "allowImportingTsExtensions": true, 11 | "paths": { 12 | "@internal/code-indexer": ["./packages/code-indexer/src/index.ts"], 13 | "@internal/code-indexer/*": ["./packages/code-indexer/src/*"], 14 | "@internal/lsp-client": ["./packages/lsp-client/src/index.ts"], 15 | "@internal/lsp-client/*": ["./packages/lsp-client/src/*"], 16 | "@internal/types": ["./packages/types/src/index.ts"], 17 | "@internal/types/*": ["./packages/types/src/*"] 18 | }, 19 | "lib": ["esnext", "dom"], 20 | "types": ["vitest/importMeta"], 21 | "strict": true, 22 | "noImplicitAny": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true 25 | }, 26 | "include": ["src/**/*", "tests/**/*", "packages/**/*"], 27 | "exclude": [ 28 | "tests/fixtures/**/*", 29 | "tests/languages/fixtures/**/*", 30 | "tests/integration/tmp-lsp-errors-*/**/*", 31 | "tests/integration/tmp-variables-*/**/*", 32 | "packages/lsp-client/tests/integration/tmp-lsp-rename-*/**/*" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/lsp-client/src/utils/lineResolver.ts: -------------------------------------------------------------------------------- 1 | import { resolveLineParameter } from "./container-helpers.ts"; 2 | import { ErrorContext, formatError } from "./container-helpers.ts"; 3 | 4 | /** 5 | * Resolve a line parameter to a line index, throwing on error 6 | * 7 | * @param content - File content 8 | * @param line - Line number (1-based) or string to search for 9 | * @param filePath - File path for error context 10 | * @returns Zero-based line index 11 | * @throws Error if line cannot be resolved 12 | */ 13 | export function resolveLineIndexOrThrow( 14 | content: string, 15 | line: string | number, 16 | filePath: string, 17 | ): number { 18 | const lines = content.split("\n"); 19 | 20 | try { 21 | const lineIndex = resolveLineParameter(lines, line); 22 | return lineIndex; 23 | } catch (error) { 24 | const context: ErrorContext = { 25 | operation: "line resolution", 26 | filePath, 27 | details: { 28 | line, 29 | error: error instanceof Error ? error.message : String(error), 30 | }, 31 | }; 32 | throw new Error( 33 | formatError( 34 | new Error( 35 | `Failed to resolve line: ${error instanceof Error ? error.message : String(error)}`, 36 | ), 37 | context, 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/fixtures/rust/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[derive(Debug)] 4 | struct User { 5 | id: u32, 6 | name: String, 7 | email: Option, 8 | } 9 | 10 | impl User { 11 | fn new(id: u32, name: String, email: Option) -> Self { 12 | User { id, name, email } 13 | } 14 | 15 | fn greet(&self) -> String { 16 | format!("Hello, I'm {}!", self.name) 17 | } 18 | } 19 | 20 | fn process_users(users: Vec) -> HashMap { 21 | let mut result = HashMap::new(); 22 | for user in users { 23 | result.insert(user.name.clone(), user.id); 24 | } 25 | result 26 | } 27 | 28 | fn main() { 29 | let users = vec![ 30 | User::new(1, "Alice".to_string(), Some("alice@example.com".to_string())), 31 | User::new(2, "Bob".to_string(), None), 32 | User::new(3, "Charlie".to_string(), Some("charlie@example.com".to_string())), 33 | ]; 34 | 35 | let id_map = process_users(users); 36 | println!("User ID map: {:?}", id_map); 37 | 38 | // This should cause a type error 39 | let invalid_id: String = 123; // Type error: expected String, found integer 40 | 41 | // This will also cause a type error 42 | let result: i32 = process_users(vec![]); // Type error: expected i32, found HashMap 43 | } -------------------------------------------------------------------------------- /src/presets/ocaml.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * OCaml Language Server (ocaml-lsp) adapter 5 | */ 6 | export const ocamlAdapter: Preset = { 7 | presetId: "ocaml", 8 | name: "OCaml Language Server", 9 | description: "Official language server for OCaml", 10 | baseLanguage: "ocaml", 11 | binFindStrategy: { 12 | strategies: [ 13 | // 1. Check opam installation 14 | { type: "global", names: ["ocamllsp"] }, 15 | // 2. Check in _opam directory (local switch) 16 | { type: "path", path: "_opam/bin/ocamllsp" }, 17 | // 3. Check in ~/.opam//bin 18 | { type: "path", path: "~/.opam/default/bin/ocamllsp" }, 19 | // 4. Check esy installation 20 | { type: "path", path: "_esy/default/build/install/default/bin/ocamllsp" }, 21 | ], 22 | defaultArgs: ["--stdio"], 23 | }, 24 | files: ["**/*.ml", "**/*.mli", "**/*.mll", "**/*.mly"], 25 | initializationOptions: { 26 | codelens: { 27 | enable: true, 28 | }, 29 | extendedHover: { 30 | enable: true, 31 | }, 32 | }, 33 | serverCharacteristics: { 34 | documentOpenDelay: 1500, 35 | readinessCheckTimeout: 800, 36 | initialDiagnosticsTimeout: 3000, 37 | requiresProjectInit: true, 38 | sendsInitialDiagnostics: true, 39 | operationTimeout: 12000, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/presets/ruff.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * Ruff adapter - Fast Python linter and formatter with LSP support 5 | */ 6 | export const ruffAdapter: Preset = { 7 | presetId: "ruff", 8 | name: "Ruff LSP", 9 | description: "Fast Python linter and formatter with LSP", 10 | binFindStrategy: { 11 | strategies: [ 12 | // 1. Try UV run first (preferred for Python projects) 13 | { type: "uv", tool: "ruff" }, 14 | // 2. Check global installation 15 | { type: "global", names: ["ruff", "ruff-lsp"] }, 16 | // 3. Check Python virtual environments 17 | { 18 | type: "venv", 19 | names: ["ruff", "ruff-lsp"], 20 | venvDirs: [".venv", "venv"], 21 | }, 22 | ], 23 | defaultArgs: ["server"], 24 | }, 25 | files: ["**/*.py", "**/*.pyi"], 26 | initializationOptions: { 27 | settings: { 28 | // Ruff configuration 29 | lineLength: 88, 30 | lint: { 31 | enable: true, 32 | }, 33 | format: { 34 | enable: true, 35 | }, 36 | }, 37 | }, 38 | serverCharacteristics: { 39 | documentOpenDelay: 300, 40 | readinessCheckTimeout: 500, 41 | initialDiagnosticsTimeout: 1000, 42 | requiresProjectInit: false, 43 | sendsInitialDiagnostics: true, 44 | operationTimeout: 5000, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /examples/fsharp-project/Calculator.fs: -------------------------------------------------------------------------------- 1 | module Calculator 2 | 3 | // Basic arithmetic operations 4 | let add x y = x + y 5 | let subtract x y = x - y 6 | let multiply x y = x * y 7 | let divide x y = 8 | if y = 0 then 9 | failwith "Division by zero" 10 | else 11 | x / y 12 | 13 | // Higher-order function example 14 | let applyOperation op x y = op x y 15 | 16 | // Record type 17 | type CalculationResult = { 18 | Operation: string 19 | Result: int 20 | } 21 | 22 | // Execute calculation and return result 23 | let calculate opName x y = 24 | let result = 25 | match opName with 26 | | "add" -> add x y 27 | | "subtract" -> subtract x y 28 | | "multiply" -> multiply x y 29 | | "divide" -> divide x y 30 | | _ -> failwith "Unknown operation" 31 | { Operation = opName; Result = result } 32 | 33 | // Pattern matching example 34 | let describeNumber n = 35 | match n with 36 | | 0 -> "Zero" 37 | | n when n > 0 -> "Positive" 38 | | _ -> "Negative" 39 | 40 | // List operations 41 | let sumList numbers = 42 | List.fold (+) 0 numbers 43 | 44 | // Code with errors (for testing) 45 | let buggyFunction x = 46 | let y = unknownVariable // Error: unknownVariable is not defined 47 | x + y 48 | 49 | // Type error test 50 | let typeError: string = 123 // Error: assigning int to string -------------------------------------------------------------------------------- /src/config/presets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in preset adapters registration 3 | */ 4 | 5 | import { PresetRegistry } from "./loader.ts"; 6 | 7 | // Import all adapters 8 | import { typescriptAdapter } from "../presets/typescript-language-server.ts"; 9 | import { tsgoAdapter } from "../presets/tsgo.ts"; 10 | import { denoAdapter } from "../presets/deno.ts"; 11 | import { pyrightAdapter } from "../presets/pyright.ts"; 12 | import { ruffAdapter } from "../presets/ruff.ts"; 13 | import { rustAnalyzerAdapter } from "../presets/rust-analyzer.ts"; 14 | import { fsharpAdapter } from "../presets/fsharp.ts"; 15 | import { moonbitAdapter } from "../presets/moonbit.ts"; 16 | import { goplsAdapter } from "../presets/gopls.ts"; 17 | import { hlsAdapter } from "../presets/hls.ts"; 18 | import { ocamlAdapter } from "../presets/ocaml.ts"; 19 | 20 | /** 21 | * Register all built-in adapters to the registry 22 | */ 23 | export function registerBuiltinAdapters(registry: PresetRegistry): void { 24 | registry.register(typescriptAdapter); 25 | registry.register(tsgoAdapter); 26 | registry.register(denoAdapter); 27 | registry.register(pyrightAdapter); 28 | registry.register(ruffAdapter); 29 | registry.register(rustAnalyzerAdapter); 30 | registry.register(fsharpAdapter); 31 | registry.register(moonbitAdapter); 32 | registry.register(goplsAdapter); 33 | registry.register(hlsAdapter); 34 | registry.register(ocamlAdapter); 35 | } 36 | -------------------------------------------------------------------------------- /tests/fixtures/variables-test.ts: -------------------------------------------------------------------------------- 1 | // Test file for Variables and Constants indexing 2 | 3 | // Regular variables 4 | let myVariable = "test"; 5 | var oldStyleVar = 42; 6 | 7 | // Constants 8 | const MY_CONSTANT = "CONSTANT_VALUE"; 9 | const API_KEY = "secret"; 10 | const CONFIG = { 11 | port: 3000, 12 | host: "localhost", 13 | }; 14 | 15 | // Exported variables 16 | export let exportedVariable = "exported"; 17 | export const EXPORTED_CONSTANT = 100; 18 | 19 | // Module level variables 20 | const modulePrivateConst = "private"; 21 | let modulePrivateLet = 123; 22 | 23 | // Inside a function 24 | function testFunction() { 25 | const localConst = "local"; 26 | let localLet = 456; 27 | var localVar = true; 28 | 29 | return { localConst, localLet, localVar }; 30 | } 31 | 32 | // Inside a class 33 | class TestClass { 34 | static readonly STATIC_CONSTANT = "static"; 35 | private privateField = "private"; 36 | public publicField: string = "public"; 37 | 38 | constructor() { 39 | const constructorConst = "constructor"; 40 | let constructorLet = 789; 41 | } 42 | } 43 | 44 | // Destructured variables 45 | const { port, host } = CONFIG; 46 | const [first, second] = [1, 2]; 47 | 48 | // Arrow function with const 49 | const arrowFunction = () => { 50 | return "arrow"; 51 | }; 52 | 53 | // Type aliases and interfaces (not variables but often confused) 54 | type MyType = string; 55 | interface MyInterface { 56 | field: number; 57 | } 58 | -------------------------------------------------------------------------------- /tests/languages/language-tests/haskell-simple.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { join } from "path"; 3 | import { hlsAdapter } from "../../../src/presets/hls.ts"; 4 | import { testLspConnection } from "../testHelpers.ts"; 5 | 6 | const projectRoot = join(import.meta.dirname, "../../fixtures", "haskell"); 7 | 8 | describe("Haskell Language Server Basic Tests", () => { 9 | it("should connect to HLS and detect type errors", async () => { 10 | const checkFiles = ["Main.hs"]; 11 | const result = await testLspConnection(hlsAdapter, projectRoot, checkFiles); 12 | 13 | // HLS might not be installed in CI 14 | expect(result.connected).toBeDefined(); 15 | if (result.connected) { 16 | expect(result.diagnostics).toBeDefined(); 17 | 18 | const mainDiagnostics = (result.diagnostics as any)?.["Main.hs"]; 19 | if (mainDiagnostics && mainDiagnostics.length > 0) { 20 | // Should have at least 2 type errors 21 | expect(mainDiagnostics.length).toBeGreaterThanOrEqual(2); 22 | 23 | // Check for type errors 24 | const hasTypeErrors = mainDiagnostics.some( 25 | (d: any) => 26 | d.severity === 1 && d.message.toLowerCase().includes("type"), 27 | ); 28 | expect(hasTypeErrors).toBe(true); 29 | } 30 | } else { 31 | expect(result.error).toBeDefined(); 32 | console.warn("HLS not available, skipping test"); 33 | } 34 | }, 90000); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/fixtures/typescript/index.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: number; 3 | name: string; 4 | email: string; 5 | } 6 | 7 | function greetUser(user: User): string { 8 | return `Hello, ${user.name}!`; 9 | } 10 | 11 | const testUser: User = { 12 | id: 1, 13 | name: "Test User", 14 | email: "test@example.com", 15 | }; 16 | 17 | console.log(greetUser(testUser)); 18 | 19 | // This should cause a type error 20 | const _invalidUser: User = { 21 | id: "not-a-number", // Type error: string is not assignable to number 22 | name: "Invalid User", 23 | email: "invalid@example.com", 24 | }; 25 | 26 | // This will also cause a type error 27 | const _result: number = greetUser(testUser); // Type error: string is not assignable to number 28 | 29 | // Duplicate function implementation 30 | function greetUser(user: User): string { 31 | return `Hi, ${user.name}!`; 32 | } 33 | 34 | // Duplicate variable declaration 35 | const testUser2: User = { 36 | id: 2, 37 | name: "Another User", 38 | email: "another@example.com", 39 | }; 40 | 41 | // Another duplicate variable 42 | const _invalidUser2: User = { 43 | id: 3, 44 | name: "Yet Another User", 45 | email: "yet@example.com", 46 | }; 47 | 48 | // Another duplicate with type error 49 | const _invalidUser3: User = { 50 | id: "still-not-a-number", // Type error 51 | name: "Invalid Again", 52 | email: "invalid2@example.com", 53 | }; 54 | 55 | // Another duplicate const 56 | const _result2: number = greetUser(testUser2); // Type error 57 | -------------------------------------------------------------------------------- /examples/python-project/.lsmcp/memories/pyright_test_results.md: -------------------------------------------------------------------------------- 1 | # Pyright Language Server Test Results 2 | 3 | ## Test Date: 2025-08-14 4 | 5 | ## Configuration 6 | - Language: Python 7 | - LSP Server: Pyright v1.1.403 8 | - Project: Python example project with intentional errors 9 | 10 | ## Files Tested 11 | 12 | ### main.py 13 | - **Total Errors**: 2 14 | - **Error 1** (Line 68): Type error - passing string "5" instead of int to add() method 15 | - **Error 2** (Line 78): Undefined variable - `undefined_variable` is not defined 16 | 17 | ### errors.py 18 | - **Total Errors**: 7 19 | - **Error 1** (Line 12): Return type mismatch - returning int instead of str 20 | - **Error 2** (Line 18): Undefined variable - `undefined_var` is not defined 21 | - **Error 3** (Line 24): Import error - module `non_existent_module` could not be resolved 22 | - **Error 4** (Line 32): Attribute error - dict has no attribute `append` 23 | - **Error 5** (Line 44): Type error - passing string to function expecting int 24 | - **Error 6** (Line 58): Return type mismatch - returning str instead of int 25 | - **Error 7** (Line 60): Missing return - function must return value on all code paths 26 | 27 | ## Configuration Fix Applied 28 | - Fixed `pythonVersion` in pyproject.toml from ">=3.12" to "3.12" (Pyright doesn't support version ranges) 29 | 30 | ## Command Used 31 | ```bash 32 | uv run pyright 33 | ``` 34 | 35 | ## Status 36 | ✅ Pyright is working correctly and detecting all intentional errors in the test files. -------------------------------------------------------------------------------- /src/presets/hls.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * Haskell Language Server (HLS) adapter 5 | */ 6 | export const hlsAdapter: Preset = { 7 | presetId: "hls", 8 | name: "Haskell Language Server", 9 | description: "Official language server for Haskell", 10 | baseLanguage: "haskell", 11 | binFindStrategy: { 12 | strategies: [ 13 | // 1. Check GHCup installation (primary) 14 | { type: "path", path: "~/.ghcup/bin/haskell-language-server-wrapper" }, 15 | // 2. Check global installation 16 | { 17 | type: "global", 18 | names: ["haskell-language-server-wrapper", "haskell-language-server"], 19 | }, 20 | // 3. Check in project's .stack-work 21 | { 22 | type: "path", 23 | path: ".stack-work/install/*/bin/haskell-language-server-wrapper", 24 | }, 25 | // 4. Check Cabal installation 26 | { 27 | type: "path", 28 | path: "dist-newstyle/build/*/haskell-language-server-wrapper", 29 | }, 30 | ], 31 | defaultArgs: ["--lsp"], 32 | }, 33 | files: ["**/*.hs", "**/*.lhs"], 34 | initializationOptions: { 35 | haskell: { 36 | formattingProvider: "ormolu", 37 | checkProject: true, 38 | }, 39 | }, 40 | serverCharacteristics: { 41 | documentOpenDelay: 3000, 42 | readinessCheckTimeout: 5000, 43 | initialDiagnosticsTimeout: 10000, 44 | requiresProjectInit: true, 45 | sendsInitialDiagnostics: true, 46 | operationTimeout: 20000, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /tests/fixtures/deno/main.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: number; 3 | name: string; 4 | email?: string; 5 | } 6 | 7 | class UserService { 8 | greetUser(user: User): string { 9 | return `Hello, ${user.name}!`; 10 | } 11 | 12 | processUsers(users: User[]): Map { 13 | const result = new Map(); 14 | for (const user of users) { 15 | result.set(user.name, user.id); 16 | } 17 | return result; 18 | } 19 | } 20 | 21 | // Deno specific imports 22 | import { serve } from "https://deno.land/std@0.208.0/http/server.ts"; 23 | 24 | const service = new UserService(); 25 | 26 | const users: User[] = [ 27 | { id: 1, name: "Alice", email: "alice@example.com" }, 28 | { id: 2, name: "Bob" }, 29 | { id: 3, name: "Charlie", email: "charlie@example.com" }, 30 | ]; 31 | 32 | const idMap = service.processUsers(users); 33 | console.log("User ID map:", idMap); 34 | 35 | // This should cause a type error 36 | const _invalidUser: User = { 37 | id: "not-a-number", // Type error: string is not assignable to number 38 | name: "Invalid", 39 | }; 40 | 41 | // This will also cause a type error 42 | const _result: number = service.processUsers(users); // Type error: Map is not number 43 | 44 | // Deno HTTP server example 45 | const handler = (_req: Request): Response => { 46 | return new Response("Hello from Deno!", { 47 | headers: { "content-type": "text/plain" }, 48 | }); 49 | }; 50 | 51 | console.log("Server listening on http://localhost:8000"); 52 | await serve(handler, { port: 8000 }); 53 | -------------------------------------------------------------------------------- /src/presets/typescript-language-server.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * TypeScript Language Server adapter (default) 5 | */ 6 | export const typescriptAdapter: Preset = { 7 | presetId: "typescript", 8 | binFindStrategy: { 9 | strategies: [ 10 | // 1. Check node_modules first 11 | { type: "node_modules", names: ["typescript-language-server"] }, 12 | // 2. Check global installation 13 | { type: "global", names: ["typescript-language-server"] }, 14 | // 3. Fall back to npx 15 | { type: "npx", package: "typescript-language-server" }, 16 | ], 17 | defaultArgs: ["--stdio"], 18 | }, 19 | files: [ 20 | "**/*.ts", 21 | "**/*.tsx", 22 | "**/*.d.ts", 23 | "**/*.js", 24 | "**/*.jsx", 25 | "**/*.mjs", 26 | "**/*.mts", 27 | "**/*.cjs", 28 | ], 29 | initializationOptions: { 30 | preferences: { 31 | includeCompletionsForModuleExports: true, 32 | includeCompletionsWithInsertText: true, 33 | }, 34 | }, 35 | serverCharacteristics: { 36 | documentOpenDelay: 2000, 37 | readinessCheckTimeout: 1000, 38 | initialDiagnosticsTimeout: 3000, 39 | requiresProjectInit: true, 40 | sendsInitialDiagnostics: true, 41 | operationTimeout: 15000, 42 | }, 43 | 44 | // Language-specific features 45 | languageFeatures: { 46 | typescript: { 47 | enabled: true, 48 | indexNodeModules: true, 49 | maxFiles: 5000, 50 | }, 51 | }, 52 | 53 | // Unsupported features 54 | unsupported: [], 55 | }; 56 | -------------------------------------------------------------------------------- /src/presets/pyright.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * Pyright adapter - Microsoft's Python language server 5 | */ 6 | export const pyrightAdapter: Preset = { 7 | presetId: "pyright", 8 | name: "Pyright", 9 | description: "Microsoft's Python language server", 10 | binFindStrategy: { 11 | strategies: [ 12 | // 1. Try UV run first (preferred for Python projects) 13 | { type: "uv", tool: "pyright", command: "pyright-langserver" }, 14 | // 2. Check global installation 15 | { type: "global", names: ["pyright-langserver"] }, 16 | // 3. Check Python virtual environments 17 | { 18 | type: "venv", 19 | names: ["pyright-langserver"], 20 | venvDirs: [".venv", "venv"], 21 | }, 22 | // 5. Check node_modules (if installed via npm) 23 | { type: "node_modules", names: ["pyright-langserver"] }, 24 | // 6. Fall back to npx 25 | { type: "npx", package: "pyright" }, 26 | ], 27 | defaultArgs: ["--stdio"], 28 | }, 29 | files: ["**/*.py", "**/*.pyi"], 30 | initializationOptions: { 31 | python: { 32 | analysis: { 33 | autoSearchPaths: true, 34 | useLibraryCodeForTypes: true, 35 | diagnosticMode: "workspace", 36 | }, 37 | }, 38 | }, 39 | serverCharacteristics: { 40 | documentOpenDelay: 1500, 41 | readinessCheckTimeout: 800, 42 | initialDiagnosticsTimeout: 2500, 43 | requiresProjectInit: false, 44 | sendsInitialDiagnostics: true, 45 | operationTimeout: 12000, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | contents: write 14 | packages: write 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v4 23 | 24 | - name: Use Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 22 28 | cache: 'pnpm' 29 | registry-url: 'https://registry.npmjs.org' 30 | 31 | - name: Install dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: Build 35 | run: pnpm build 36 | 37 | - name: Run tests 38 | run: pnpm test 39 | 40 | - name: Generate changelog 41 | run: pnpm changelog 42 | 43 | - name: Publish to npm 44 | run: npm publish --access public 45 | env: 46 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | 48 | - name: Create GitHub Release 49 | uses: softprops/action-gh-release@v2 50 | with: 51 | files: | 52 | dist/** 53 | lsmcp.schema.json 54 | body_path: CHANGELOG.md 55 | draft: false 56 | prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }} 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /examples/python-project/README.md: -------------------------------------------------------------------------------- 1 | # Python Project - Pyright Language Server Test 2 | 3 | This directory contains a test Python project for the Pyright language server 4 | integration. 5 | 6 | ## Files 7 | 8 | - `main.py` - Main Python module with basic functionality 9 | - `errors.py` - Python file with intentional errors for testing diagnostics 10 | - `pyproject.toml` - Pyright configuration 11 | - `.mcp.json` - MCP server configuration 12 | 13 | ## Testing 14 | 15 | To test the Pyright integration: 16 | 17 | 1. Build the lsmcp project: 18 | ```bash 19 | cd ../.. 20 | pnpm build 21 | ``` 22 | 23 | 2. Test the server: 24 | ```bash 25 | node dist/lsmcp.js -p pyright 26 | ``` 27 | 28 | 3. Test with MCP client (like Claude Desktop): 29 | - Add the `.mcp.json` configuration to your MCP client 30 | - Test various LSP operations on the Python files 31 | 32 | ## Expected Functionality 33 | 34 | The pyright integration should support: 35 | 36 | - **Hover information**: Type information and documentation 37 | - **Diagnostics**: Type errors, undefined variables, import errors 38 | - **Go to definition**: Navigate to function/class definitions 39 | - **Find references**: Find all usages of symbols 40 | - **Code completion**: Auto-complete suggestions 41 | - **Document symbols**: List all symbols in a file 42 | - **Formatting**: Code formatting (if configured) 43 | 44 | ## Example Errors 45 | 46 | The `errors.py` file contains intentional errors that should be detected: 47 | 48 | - Type annotation errors 49 | - Undefined variables 50 | - Import errors 51 | - Attribute errors 52 | - Missing return statements 53 | -------------------------------------------------------------------------------- /packages/code-indexer/src/engine/adapterDefaults.ts: -------------------------------------------------------------------------------- 1 | import { globalPresetRegistry } from "../../../../src/config/loader.ts"; 2 | import { registerBuiltinAdapters } from "../../../../src/config/presets.ts"; 3 | 4 | /** 5 | * Default index patterns for different language adapters 6 | */ 7 | 8 | export interface AdapterIndexDefaults { 9 | patterns: string[]; 10 | concurrency?: number; 11 | } 12 | 13 | // Ensure built-in adapters are registered 14 | registerBuiltinAdapters(globalPresetRegistry); 15 | 16 | /** 17 | * Get default patterns for an adapter 18 | */ 19 | export function getAdapterDefaultPattern(adapterId: string): string { 20 | const preset = globalPresetRegistry.get(adapterId); 21 | 22 | // Return empty string if adapter not found - caller should handle this 23 | if (!preset || !preset.files) { 24 | return ""; 25 | } 26 | 27 | return preset.files.join(","); 28 | } 29 | 30 | /** 31 | * Get default concurrency for an adapter 32 | */ 33 | export function getAdapterDefaultConcurrency(_adapterId: string): number { 34 | // Default concurrency for all adapters 35 | // Could be extended to read from preset if we add concurrency settings there 36 | return 5; 37 | } 38 | 39 | /** 40 | * Get adapter defaults dynamically from preset registry 41 | */ 42 | export function getAdapterDefaults( 43 | adapterId: string, 44 | ): AdapterIndexDefaults | undefined { 45 | const preset = globalPresetRegistry.get(adapterId); 46 | 47 | if (!preset || !preset.files) { 48 | return undefined; 49 | } 50 | 51 | return { 52 | patterns: preset.files, 53 | concurrency: 5, // Default concurrency 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/features/ts/utils/findSymbolOccurrences.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds all occurrences of a symbol within a line 3 | * @param lineText The text of the line 4 | * @param symbolName The symbol to find 5 | * @returns Array of character indices where the symbol appears 6 | */ 7 | export function findSymbolOccurrences( 8 | lineText: string, 9 | symbolName: string, 10 | ): number[] { 11 | const occurrences: number[] = []; 12 | let searchIndex = 0; 13 | 14 | let foundIndex = lineText.indexOf(symbolName, searchIndex); 15 | while (foundIndex !== -1) { 16 | occurrences.push(foundIndex); 17 | searchIndex = foundIndex + 1; 18 | foundIndex = lineText.indexOf(symbolName, searchIndex); 19 | } 20 | 21 | return occurrences; 22 | } 23 | 24 | if (import.meta.vitest) { 25 | const { describe, it, expect } = import.meta.vitest; 26 | 27 | describe("findSymbolOccurrences", () => { 28 | it("should find single occurrence", () => { 29 | const result = findSymbolOccurrences("const foo = 1;", "foo"); 30 | expect(result).toEqual([6]); 31 | }); 32 | 33 | it("should find multiple occurrences", () => { 34 | const result = findSymbolOccurrences("const foo = foo + foo;", "foo"); 35 | expect(result).toEqual([6, 12, 18]); 36 | }); 37 | 38 | it("should return empty array if symbol not found", () => { 39 | const result = findSymbolOccurrences("const bar = 1;", "foo"); 40 | expect(result).toEqual([]); 41 | }); 42 | 43 | it("should handle overlapping occurrences", () => { 44 | const result = findSymbolOccurrences("aaaa", "aa"); 45 | expect(result).toEqual([0, 1, 2]); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /tests/fixtures/neverthrow-example/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example TypeScript file using neverthrow library 3 | * Used for testing external library symbol resolution 4 | */ 5 | 6 | import { ok, err, Result, ResultAsync, fromThrowable } from "neverthrow"; 7 | 8 | // Example function using Result type 9 | export function divide(a: number, b: number): Result { 10 | if (b === 0) { 11 | return err("Division by zero"); 12 | } 13 | return ok(a / b); 14 | } 15 | 16 | // Example function using ResultAsync 17 | export function fetchUserData(userId: string): ResultAsync { 18 | return ResultAsync.fromPromise( 19 | fetch(`/api/users/${userId}`).then((res) => res.json()), 20 | (error) => new FetchError(String(error)), 21 | ); 22 | } 23 | 24 | // Example using fromThrowable 25 | const parseJSON = fromThrowable( 26 | JSON.parse, 27 | (e) => new Error(`Parse error: ${e}`), 28 | ); 29 | 30 | export function safeParseJSON(jsonString: string): Result { 31 | return parseJSON(jsonString); 32 | } 33 | 34 | // Type definitions 35 | interface User { 36 | id: string; 37 | name: string; 38 | email: string; 39 | } 40 | 41 | class FetchError extends Error { 42 | constructor(message: string) { 43 | super(message); 44 | this.name = "FetchError"; 45 | } 46 | } 47 | 48 | // Usage example 49 | const result = divide(10, 2); 50 | if (result.isOk()) { 51 | console.log("Result:", result.value); 52 | } else { 53 | console.log("Error:", result.error); 54 | } 55 | 56 | // Chain operations 57 | const calculation = ok(10) 58 | .map((x) => x * 2) 59 | .andThen((x) => divide(x, 5)) 60 | .mapErr((e) => `Calculation failed: ${e}`); 61 | -------------------------------------------------------------------------------- /examples/moonbit-project/src/test/test_diagnostics.mbt: -------------------------------------------------------------------------------- 1 | // Test file with intentional errors for diagnostics testing 2 | 3 | // Type error: String assigned to Int 4 | let number_var : Int = "not a number" 5 | 6 | // Undefined variable 7 | fn use_undefined() -> Unit { 8 | println(undefined_variable) 9 | } 10 | 11 | // Type mismatch in function parameter 12 | fn expect_string(s : String) -> Unit { 13 | println(s) 14 | } 15 | 16 | fn test_type_mismatch() -> Unit { 17 | expect_string(123) // Error: Int is not assignable to String 18 | } 19 | 20 | // Function missing return value 21 | fn must_return_int() -> Int { 22 | println("forgot to return") 23 | // Missing return statement 24 | } 25 | 26 | // Incorrect array type 27 | let number_array : Array[Int] = ["one", "two", "three"] 28 | 29 | // Pattern matching not exhaustive 30 | enum Color { 31 | Red 32 | Green 33 | Blue 34 | } 35 | 36 | fn incomplete_match(color : Color) -> Unit { 37 | match color { 38 | Red => println("Red") 39 | Green => println("Green") 40 | // Missing Blue case 41 | } 42 | } 43 | 44 | // Wrong number of arguments 45 | fn takes_two_args(a : Int, b : Int) -> Int { 46 | a + b 47 | } 48 | 49 | fn test_wrong_args() -> Unit { 50 | let result = takes_two_args(1) // Error: missing argument 51 | println(result) 52 | } 53 | 54 | // Type inference error 55 | fn type_inference_error() -> Unit { 56 | let x = 42 57 | let y : String = x // Error: Int cannot be assigned to String 58 | println(y) 59 | } 60 | 61 | // Invalid operator usage 62 | fn invalid_operator() -> Unit { 63 | let a = "hello" 64 | let b = "world" 65 | let c = a - b // Error: operator - not defined for String 66 | println(c) 67 | } -------------------------------------------------------------------------------- /packages/lsp-client/src/protocol/types/notifications.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LSP Notification types 3 | */ 4 | 5 | import { DocumentUri, Diagnostic } from "@internal/types"; 6 | 7 | // Diagnostic notifications 8 | export interface PublishDiagnosticsParams { 9 | uri: DocumentUri; 10 | diagnostics: Diagnostic[]; 11 | } 12 | 13 | // Window notifications 14 | export interface ShowMessageParams { 15 | type: MessageType; 16 | message: string; 17 | } 18 | 19 | export enum MessageType { 20 | Error = 1, 21 | Warning = 2, 22 | Info = 3, 23 | Log = 4, 24 | } 25 | 26 | export interface LogMessageParams { 27 | type: MessageType; 28 | message: string; 29 | } 30 | 31 | // Progress notifications 32 | export interface ProgressParams { 33 | token: number | string; 34 | value: T; 35 | } 36 | 37 | export interface WorkDoneProgressBegin { 38 | kind: "begin"; 39 | title: string; 40 | cancellable?: boolean; 41 | message?: string; 42 | percentage?: number; 43 | } 44 | 45 | export interface WorkDoneProgressReport { 46 | kind: "report"; 47 | cancellable?: boolean; 48 | message?: string; 49 | percentage?: number; 50 | } 51 | 52 | export interface WorkDoneProgressEnd { 53 | kind: "end"; 54 | message?: string; 55 | } 56 | 57 | export type WorkDoneProgress = 58 | | WorkDoneProgressBegin 59 | | WorkDoneProgressReport 60 | | WorkDoneProgressEnd; 61 | 62 | // Type guards 63 | export function isPublishDiagnosticsParams( 64 | params: any, 65 | ): params is PublishDiagnosticsParams { 66 | return ( 67 | params && 68 | typeof params === "object" && 69 | "uri" in params && 70 | "diagnostics" in params && 71 | Array.isArray(params.diagnostics) 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/applyTextEdits.ts: -------------------------------------------------------------------------------- 1 | import { TextEdit } from "vscode-languageserver-types"; 2 | 3 | /** 4 | * Apply text edits to a document content. 5 | * Edits should be sorted in reverse order (last to first) before applying. 6 | */ 7 | export function applyTextEdits(content: string, edits: TextEdit[]): string { 8 | // Sort edits in reverse order 9 | const sortedEdits = [...edits].sort((a, b) => { 10 | const lineDiff = b.range.start.line - a.range.start.line; 11 | if (lineDiff !== 0) return lineDiff; 12 | return b.range.start.character - a.range.start.character; 13 | }); 14 | 15 | let lines = content.split("\n"); 16 | 17 | for (const edit of sortedEdits) { 18 | const startLine = edit.range.start.line; 19 | const startChar = edit.range.start.character; 20 | const endLine = edit.range.end.line; 21 | const endChar = edit.range.end.character; 22 | 23 | // Handle single line edit 24 | if (startLine === endLine) { 25 | const line = lines[startLine] || ""; 26 | lines[startLine] = 27 | line.substring(0, startChar) + edit.newText + line.substring(endChar); 28 | } else { 29 | // Handle multi-line edit 30 | const startLineText = lines[startLine] || ""; 31 | const endLineText = lines[endLine] || ""; 32 | 33 | // Create the new content 34 | const newContent = 35 | startLineText.substring(0, startChar) + 36 | edit.newText + 37 | endLineText.substring(endChar); 38 | 39 | // Replace the lines 40 | lines.splice( 41 | startLine, 42 | endLine - startLine + 1, 43 | ...newContent.split("\n"), 44 | ); 45 | } 46 | } 47 | 48 | return lines.join("\n"); 49 | } 50 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] integration: Add tests for get_definitions via MCP 2 | - Verify include_body behavior as well 3 | - [x] Change tests/adapters => tests/languages. Also change vitest project to languages 4 | - [x] Fix CI as well 5 | - [x] integration: Verify if MCP tools described in README can be called individually 6 | - Add tests for individual MCP get_project_overview, get_definitions in tests/languages 7 | - [x] Regenerate lsmcp.schema.json from zod-to-jsonschema 8 | - [x] Expand config loading tests 9 | - Can presets be loaded and merged correctly? 10 | - Can `.lsmcp/config.json` be loaded? 11 | - Are items specified in `disable: ['toolName']` disabled? 12 | - Are additional features enabled by `experiments.*` specification? 13 | - [x] integration: Automatic environment detection at startup 14 | - Don't auto-start. Guide to corresponding preset 15 | - [x] Replace console.error with debugLog. Unify to not output logs unless LSMCP_DEBUG=1 is set 16 | - [x] Refactoring 17 | 18 | - [x] Files for each language are defined in multiple places - Consolidated in languageDefinitions.ts 19 | - [x] Write in English except for docs/ja/. Comments too - Already in English 20 | - [ ] Consider reducing code as much as possible within the range where tests pass 21 | 22 | - [x] fix: directories: [] not displayed when list_dir is not recursive 23 | 24 | --- 25 | 26 | - [ ] Composed Lsp Client: これは既存の LspClient と全く API を実装するが、初期化時に LspClient を複数束ねて、統一的にあつかう。 27 | - createComposedLspClient(lspClients) 28 | - [ ] node:fs に直接アクセスしないようにして、FileSystemApi を経由するようにする 29 | 30 | --- 31 | 32 | - [ ] 言語固有機能を選択的に読み込めるようにする 33 | - [ ] typescript の外部パッケージのシンボル化機能 34 | - [ ] typescript export 分析機能 35 | - [ ] Rust 36 | -------------------------------------------------------------------------------- /examples/rust-project/target/.rustc_info.json: -------------------------------------------------------------------------------- 1 | {"rustc_fingerprint":18354981170381366981,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.88.0 (6b00bc388 2025-06-23)\nbinary: rustc\ncommit-hash: 6b00bc3880198600130e1cf62b8f8a93494488cc\ncommit-date: 2025-06-23\nhost: x86_64-unknown-linux-gnu\nrelease: 1.88.0\nLLVM version: 20.1.5\n","stderr":""},"11857020428658561806":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/mizchi/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/mizchi/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}} -------------------------------------------------------------------------------- /packages/lsp-client/src/managers/workspace.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Workspace edit management 3 | */ 4 | 5 | import type { WorkspaceEdit } from "../protocol/types/index.ts"; 6 | import type { IFileSystem } from "../interfaces.ts"; 7 | import { applyTextEdits } from "../utils/textEdits.ts"; 8 | 9 | export async function applyWorkspaceEditManually( 10 | edit: WorkspaceEdit, 11 | fileSystemApi: IFileSystem, 12 | ): Promise { 13 | if (!edit.changes) { 14 | return; 15 | } 16 | 17 | for (const [uri, edits] of Object.entries(edit.changes)) { 18 | if (!edits || edits.length === 0) { 19 | continue; 20 | } 21 | 22 | // Convert file:// URI to file path 23 | const filePath = uri.startsWith("file://") ? uri.slice(7) : uri; 24 | 25 | // Read current content 26 | const currentContent = await fileSystemApi.readFile(filePath); 27 | 28 | // Apply edits 29 | const newContent = applyTextEdits(currentContent, edits); 30 | 31 | // Write back 32 | await fileSystemApi.writeFile(filePath, newContent); 33 | } 34 | } 35 | 36 | export function createApplyWorkspaceEditParams( 37 | edit: WorkspaceEdit, 38 | label?: string, 39 | ): { edit: WorkspaceEdit; label?: string } { 40 | return { edit, label }; 41 | } 42 | 43 | export function handleApplyWorkspaceEditResponse(response: unknown): { 44 | applied: boolean; 45 | failureReason?: string; 46 | } { 47 | if (!response || typeof response !== "object") { 48 | return { applied: false, failureReason: "Invalid response from server" }; 49 | } 50 | 51 | const result = response as { applied?: boolean; failureReason?: string }; 52 | return { 53 | applied: result.applied ?? false, 54 | failureReason: result.failureReason, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | # Run at 00:00 UTC every Monday 10 | - cron: '0 0 * * 1' 11 | 12 | jobs: 13 | audit: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v4 21 | 22 | - name: Use Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 22 26 | cache: 'pnpm' 27 | 28 | - name: Install dependencies 29 | run: pnpm install --frozen-lockfile 30 | 31 | - name: Run audit 32 | run: pnpm audit 33 | continue-on-error: true 34 | 35 | - name: Check for known vulnerabilities 36 | run: | 37 | npx better-npm-audit audit --level high 38 | continue-on-error: true 39 | 40 | codeql: 41 | name: CodeQL Analysis 42 | runs-on: ubuntu-latest 43 | 44 | permissions: 45 | actions: read 46 | contents: read 47 | security-events: write 48 | 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | language: ['javascript', 'typescript'] 53 | 54 | steps: 55 | - name: Checkout repository 56 | uses: actions/checkout@v4 57 | 58 | - name: Initialize CodeQL 59 | uses: github/codeql-action/init@v3 60 | with: 61 | languages: ${{ matrix.language }} 62 | 63 | - name: Autobuild 64 | uses: github/codeql-action/autobuild@v3 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 -------------------------------------------------------------------------------- /packages/lsp-client/src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * General helper utilities 3 | */ 4 | 5 | export interface ServerCharacteristics { 6 | readinessCheckTimeout: number; 7 | supportsDidSave?: boolean; 8 | requiresFileWatching?: boolean; 9 | } 10 | 11 | export function getServerCharacteristics( 12 | languageId: string, 13 | customCharacteristics?: Record, 14 | ): ServerCharacteristics { 15 | // Use custom characteristics if provided 16 | if (customCharacteristics) { 17 | return { 18 | readinessCheckTimeout: customCharacteristics.readinessCheckTimeout || 500, 19 | supportsDidSave: customCharacteristics.supportsDidSave, 20 | requiresFileWatching: customCharacteristics.requiresFileWatching, 21 | }; 22 | } 23 | 24 | // Default characteristics by language 25 | const defaults: Record = { 26 | typescript: { 27 | readinessCheckTimeout: 1000, 28 | supportsDidSave: true, 29 | }, 30 | javascript: { 31 | readinessCheckTimeout: 1000, 32 | supportsDidSave: true, 33 | }, 34 | python: { 35 | readinessCheckTimeout: 2000, 36 | supportsDidSave: true, 37 | }, 38 | rust: { 39 | readinessCheckTimeout: 3000, 40 | supportsDidSave: true, 41 | requiresFileWatching: true, 42 | }, 43 | go: { 44 | readinessCheckTimeout: 1500, 45 | supportsDidSave: true, 46 | }, 47 | default: { 48 | readinessCheckTimeout: 500, 49 | }, 50 | }; 51 | 52 | return defaults[languageId] || defaults.default; 53 | } 54 | 55 | export function isObject(value: unknown): value is Record { 56 | return value !== null && typeof value === "object" && !Array.isArray(value); 57 | } 58 | -------------------------------------------------------------------------------- /scripts/generate-schema.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | /** 3 | * Generate JSON Schema from Zod schema 4 | */ 5 | 6 | import { writeFileSync } from "fs"; 7 | import { join } from "path"; 8 | import { zodToJsonSchema } from "zod-to-json-schema"; 9 | import { configSchema } from "../src/config/schema.ts"; 10 | 11 | // Generate JSON Schema 12 | const jsonSchema = zodToJsonSchema(configSchema, { 13 | name: "LSMCPConfig", 14 | $refStrategy: "none", 15 | errorMessages: true, 16 | markdownDescription: true, 17 | }); 18 | 19 | // Add additional metadata 20 | const schema = { 21 | $schema: "http://json-schema.org/draft-07/schema#", 22 | $id: "https://github.com/mizchi/lsmcp/lsmcp.schema.json", 23 | title: "LSMCP Configuration", 24 | description: "Configuration schema for Language Service MCP", 25 | ...(jsonSchema as any), 26 | }; 27 | 28 | // Add examples 29 | schema.examples = [ 30 | { 31 | preset: "tsgo", 32 | }, 33 | { 34 | preset: "pyright", 35 | files: ["**/*.py", "**/*.pyi"], 36 | }, 37 | { 38 | lsp: { 39 | bin: "deno", 40 | args: ["lsp"], 41 | }, 42 | files: ["**/*.ts", "**/*.tsx"], 43 | }, 44 | { 45 | preset: "tsgo", 46 | settings: { 47 | autoIndex: true, 48 | indexConcurrency: 10, 49 | }, 50 | symbolFilter: { 51 | excludeKinds: ["Variable", "Constant"], 52 | includeOnlyTopLevel: true, 53 | }, 54 | }, 55 | ]; 56 | 57 | // Write to file 58 | const outputPath = join(process.cwd(), "lsmcp.schema.json"); 59 | writeFileSync(outputPath, JSON.stringify(schema, null, 2) + "\n"); 60 | 61 | console.log(`✅ Schema generated at: ${outputPath}`); 62 | console.log("\nTo use this schema in your config file, add:"); 63 | console.log(` "$schema": "../lsmcp.schema.json"`); 64 | -------------------------------------------------------------------------------- /packages/lsp-client/src/utils/documentManager.ts: -------------------------------------------------------------------------------- 1 | import type { LSPClient } from "../protocol/types/index.ts"; 2 | import { ErrorContext, formatError } from "./container-helpers.ts"; 3 | 4 | /** 5 | * Execute an operation with a temporarily opened LSP document 6 | * 7 | * This function handles the lifecycle of opening and closing a document 8 | * in the LSP server, ensuring proper cleanup even if the operation fails. 9 | * 10 | * @param client - LSP client instance 11 | * @param fileUri - File URI for the document 12 | * @param content - Content of the document 13 | * @param operation - Async operation to execute while document is open 14 | * @param language - Optional language ID for the document 15 | * @returns Result of the operation 16 | */ 17 | export async function withTemporaryDocument( 18 | client: LSPClient, 19 | fileUri: string, 20 | content: string, 21 | operation: () => Promise, 22 | language?: string, 23 | ): Promise { 24 | if (!client) { 25 | const context: ErrorContext = { 26 | operation: "LSP document operation", 27 | language, 28 | }; 29 | throw new Error( 30 | formatError( 31 | new Error( 32 | "LSP client not initialized. Ensure the language server is started.", 33 | ), 34 | context, 35 | ), 36 | ); 37 | } 38 | 39 | // Open the document in LSP with language ID if provided 40 | client.openDocument(fileUri, content, language); 41 | 42 | try { 43 | // Wait a bit for LSP to process the document 44 | await new Promise((resolve) => setTimeout(resolve, 500)); 45 | 46 | // Execute the operation 47 | return await operation(); 48 | } finally { 49 | // Always close the document 50 | client.closeDocument(fileUri); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/tools/lsp/createLspTools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Factory for creating LSP tools with injected client 3 | */ 4 | 5 | import type { LSPClient } from "@internal/lsp-client"; 6 | import type { McpToolDef } from "@internal/types"; 7 | 8 | // Import individual tool creators 9 | import { createHoverTool } from "./hover.ts"; 10 | import { createReferencesTool } from "./references.ts"; 11 | import { createDefinitionsTool } from "./definitions.ts"; 12 | import { createDiagnosticsTool } from "./diagnostics.ts"; 13 | import { createRenameSymbolTool } from "./rename.ts"; 14 | import { createDocumentSymbolsTool } from "./documentSymbols.ts"; 15 | import { createCompletionTool } from "./completion.ts"; 16 | import { createSignatureHelpTool } from "./signatureHelp.ts"; 17 | import { createFormatDocumentTool } from "./formatting.ts"; 18 | import { createWorkspaceSymbolsTool } from "./workspaceSymbols.ts"; 19 | import { createCodeActionsTool } from "./codeActions.ts"; 20 | import { createCheckCapabilitiesTool } from "./checkCapabilities.ts"; 21 | import { createDeleteSymbolTool } from "./deleteSymbol.ts"; 22 | 23 | /** 24 | * Create all LSP tools with an injected client 25 | */ 26 | export function createLSPTools(client: LSPClient): McpToolDef[] { 27 | return [ 28 | createHoverTool(client), 29 | createReferencesTool(client), 30 | createDefinitionsTool(client), 31 | createDiagnosticsTool(client), 32 | createRenameSymbolTool(client), 33 | createDocumentSymbolsTool(client), 34 | createCompletionTool(client), 35 | createSignatureHelpTool(client), 36 | createFormatDocumentTool(client), 37 | createWorkspaceSymbolsTool(client), 38 | createCodeActionsTool(client), 39 | createCheckCapabilitiesTool(client), 40 | createDeleteSymbolTool(client), 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/filePatternParser.ts: -------------------------------------------------------------------------------- 1 | import { minimatch } from "minimatch"; 2 | 3 | /** 4 | * Parse file patterns string, handling both comma-separated patterns and brace expansion 5 | */ 6 | export function parseFilePatterns(patternsString: string): string[] { 7 | const patterns: string[] = []; 8 | 9 | // Split by comma, but need to handle commas inside braces 10 | const parts = splitByCommaOutsideBraces(patternsString); 11 | 12 | for (const part of parts) { 13 | const trimmed = part.trim(); 14 | if (!trimmed) continue; 15 | 16 | // Check if pattern contains brace expansion 17 | if (trimmed.includes("{") && trimmed.includes("}")) { 18 | // Use minimatch's brace expansion 19 | const expanded = minimatch.braceExpand(trimmed); 20 | patterns.push(...expanded); 21 | } else { 22 | patterns.push(trimmed); 23 | } 24 | } 25 | 26 | // Remove duplicates and return 27 | return [...new Set(patterns)]; 28 | } 29 | 30 | /** 31 | * Split string by commas, but ignore commas inside braces 32 | */ 33 | function splitByCommaOutsideBraces(str: string): string[] { 34 | const result: string[] = []; 35 | let current = ""; 36 | let braceDepth = 0; 37 | 38 | for (let i = 0; i < str.length; i++) { 39 | const char = str[i]; 40 | 41 | if (char === "{") { 42 | braceDepth++; 43 | current += char; 44 | } else if (char === "}") { 45 | braceDepth--; 46 | current += char; 47 | } else if (char === "," && braceDepth === 0) { 48 | // This is a separator comma 49 | result.push(current); 50 | current = ""; 51 | } else { 52 | current += char; 53 | } 54 | } 55 | 56 | // Don't forget the last part 57 | if (current) { 58 | result.push(current); 59 | } 60 | 61 | return result; 62 | } 63 | -------------------------------------------------------------------------------- /packages/lsp-client/src/protocol/types/base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base LSP protocol types 3 | */ 4 | 5 | // LSP Message types 6 | export interface LSPRequest { 7 | jsonrpc: "2.0"; 8 | id: number | string; 9 | method: string; 10 | params?: Record; 11 | } 12 | 13 | export interface LSPResponse { 14 | jsonrpc: "2.0"; 15 | id: number | string; 16 | result?: unknown; 17 | error?: { 18 | code: number; 19 | message: string; 20 | data?: unknown; 21 | }; 22 | } 23 | 24 | export interface LSPNotification { 25 | jsonrpc: "2.0"; 26 | method: string; 27 | params?: Record; 28 | } 29 | 30 | export type LSPMessage = LSPRequest | LSPResponse | LSPNotification; 31 | 32 | // Type guards 33 | export function isLSPRequest(message: LSPMessage): message is LSPRequest { 34 | return "method" in message && "id" in message; 35 | } 36 | 37 | export function isLSPResponse(message: LSPMessage): message is LSPResponse { 38 | return "id" in message && ("result" in message || "error" in message); 39 | } 40 | 41 | export function isLSPNotification( 42 | message: LSPMessage, 43 | ): message is LSPNotification { 44 | return "method" in message && !("id" in message); 45 | } 46 | 47 | // Re-export commonly used types from @internal/types 48 | export { 49 | CodeAction, 50 | Command, 51 | CompletionItem, 52 | CompletionList, 53 | Diagnostic, 54 | DocumentSymbol, 55 | DocumentUri, 56 | FormattingOptions, 57 | Hover, 58 | integer, 59 | Location, 60 | LocationLink, 61 | MarkedString, 62 | MarkupContent, 63 | Position, 64 | Range, 65 | SymbolInformation, 66 | TextEdit, 67 | WorkspaceEdit, 68 | } from "@internal/types"; 69 | 70 | // Type aliases need to be exported separately 71 | export type { Definition, SignatureHelp } from "@internal/types"; 72 | -------------------------------------------------------------------------------- /packages/types/src/shared/result.ts: -------------------------------------------------------------------------------- 1 | // Result type for error handling 2 | 3 | export type Result = 4 | | { ok: true; value: T } 5 | | { ok: false; error: E }; 6 | 7 | export function Ok(value: T): Result { 8 | return { ok: true, value }; 9 | } 10 | 11 | export function Err(error: E): Result { 12 | return { ok: false, error }; 13 | } 14 | 15 | export function isOk( 16 | result: Result, 17 | ): result is { ok: true; value: T } { 18 | return result.ok === true; 19 | } 20 | 21 | export function isErr( 22 | result: Result, 23 | ): result is { ok: false; error: E } { 24 | return result.ok === false; 25 | } 26 | 27 | export function unwrap(result: Result): T { 28 | if (isOk(result)) return result.value; 29 | throw result.error; 30 | } 31 | 32 | export function unwrapOr(result: Result, defaultValue: T): T { 33 | return isOk(result) ? result.value : defaultValue; 34 | } 35 | 36 | export function map( 37 | result: Result, 38 | fn: (value: T) => U, 39 | ): Result { 40 | return isOk(result) ? Ok(fn(result.value)) : result; 41 | } 42 | 43 | export function mapErr( 44 | result: Result, 45 | fn: (error: E) => F, 46 | ): Result { 47 | return isErr(result) ? Err(fn(result.error)) : result; 48 | } 49 | 50 | export async function tryAsync( 51 | fn: () => Promise, 52 | ): Promise> { 53 | try { 54 | return Ok(await fn()); 55 | } catch (error) { 56 | return Err(error instanceof Error ? error : new Error(String(error))); 57 | } 58 | } 59 | 60 | export function trySync(fn: () => T): Result { 61 | try { 62 | return Ok(fn()); 63 | } catch (error) { 64 | return Err(error instanceof Error ? error : new Error(String(error))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /scripts/check-ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check CI readiness script 4 | echo "🔍 Checking CI readiness..." 5 | 6 | # Color codes 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[1;33m' 10 | NC='\033[0m' # No Color 11 | 12 | # Track if all checks pass 13 | ALL_PASS=true 14 | 15 | # Function to check command 16 | check_command() { 17 | if command -v $1 &> /dev/null; then 18 | echo -e "${GREEN}✓${NC} $1 is installed" 19 | return 0 20 | else 21 | echo -e "${RED}✗${NC} $1 is not installed" 22 | return 1 23 | fi 24 | } 25 | 26 | # Function to run check 27 | run_check() { 28 | echo -e "\n${YELLOW}Running: $1${NC}" 29 | if eval $2; then 30 | echo -e "${GREEN}✓ $1 passed${NC}" 31 | else 32 | echo -e "${RED}✗ $1 failed${NC}" 33 | ALL_PASS=false 34 | fi 35 | } 36 | 37 | echo -e "\n📦 Checking dependencies..." 38 | check_command "node" 39 | check_command "pnpm" 40 | check_command "git" 41 | 42 | echo -e "\n🏗️ Running CI checks..." 43 | 44 | # Build 45 | run_check "Build" "pnpm build" 46 | 47 | # Type check 48 | run_check "Type check" "pnpm typecheck" 49 | 50 | # Lint 51 | run_check "Lint" "pnpm lint" 52 | 53 | # Format check 54 | run_check "Format check" "pnpm format:check" 55 | 56 | # Unit tests 57 | run_check "Unit tests" "pnpm test:unit" 58 | 59 | # Integration tests (with timeout) 60 | run_check "Integration tests" "timeout 300 pnpm test:integration" 61 | 62 | # Check examples 63 | run_check "Check examples" "pnpm test:examples" 64 | 65 | echo -e "\n📊 Summary:" 66 | if [ "$ALL_PASS" = true ]; then 67 | echo -e "${GREEN}✅ All CI checks passed!${NC}" 68 | echo "Ready for GitHub Actions CI/CD" 69 | exit 0 70 | else 71 | echo -e "${RED}❌ Some CI checks failed${NC}" 72 | echo "Please fix the issues before pushing to GitHub" 73 | exit 1 74 | fi -------------------------------------------------------------------------------- /.lsmcp/memories/task_completion_checklist.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: 2025-08-16T16:23:31.971Z 3 | updated: 2025-08-16T16:23:31.971Z 4 | --- 5 | 6 | # Task Completion Checklist 7 | 8 | When completing any coding task in the lsmcp project, ensure you: 9 | 10 | ## 1. Code Quality Checks 11 | - [ ] Run `pnpm typecheck` to ensure no TypeScript errors 12 | - [ ] Run `pnpm lint` to check for linting issues 13 | - [ ] Run `pnpm format` to ensure consistent formatting 14 | 15 | ## 2. Testing 16 | - [ ] Run `pnpm test:unit` for unit tests 17 | - [ ] Run `pnpm build` before integration tests 18 | - [ ] Run `pnpm test:integration` if changes affect MCP/LSP functionality 19 | - [ ] Run specific language tests if modifying language adapters 20 | - For Rust: `pnpm test:languages:rust` 21 | - For TypeScript: `pnpm test:languages:tsgo` 22 | - For F#: `pnpm test:languages:fsharp` 23 | 24 | ## 3. Documentation 25 | - [ ] Update relevant documentation if API changes 26 | - [ ] Ensure all new functions have proper TypeScript types 27 | - [ ] Add code comments in English for complex logic 28 | 29 | ## 4. Before Committing 30 | - [ ] Verify all tests pass 31 | - [ ] Check that build succeeds with `pnpm build` 32 | - [ ] Review changes with `git diff` 33 | - [ ] Use conventional commit format 34 | 35 | ## Important Reminders 36 | - **NEVER** modify test timeouts without permission 37 | - Always use `rg` (ripgrep) instead of `grep` for searching 38 | - Build before running integration tests 39 | - Use English for all documentation and comments 40 | - Follow lowerCamelCase for file naming 41 | - Add `.ts` extension to imports 42 | 43 | ## For Rust Example Project 44 | When working in `examples/rust-project/`: 45 | - [ ] Run `cargo check` to verify compilation 46 | - [ ] Run `cargo fmt` to format Rust code 47 | - [ ] Run `cargo clippy` for linting 48 | - [ ] Run `cargo test` for Rust tests -------------------------------------------------------------------------------- /examples/rust-project/src/test_diagnostics.rs: -------------------------------------------------------------------------------- 1 | // Test file with intentional errors for diagnostics testing 2 | 3 | // Unused import 4 | use std::collections::HashMap; 5 | 6 | // Function with wrong return type 7 | fn add_numbers(a: i32, b: i32) -> String { 8 | a + b // Error: expected String, found i32 9 | } 10 | 11 | // Undefined variable 12 | fn use_undefined() { 13 | println!("{}", undefined_var); 14 | } 15 | 16 | // Type mismatch 17 | fn type_mismatch() { 18 | let x: i32 = "not a number"; // Error: expected i32, found &str 19 | } 20 | 21 | // Borrowing error 22 | fn borrowing_error() { 23 | let s = String::from("hello"); 24 | let r1 = &s; 25 | let r2 = &mut s; // Error: cannot borrow as mutable 26 | println!("{} {}", r1, r2); 27 | } 28 | 29 | // Missing lifetime specifier 30 | fn lifetime_error(s: &str) -> &str { 31 | &s[..] // Error: missing lifetime specifier 32 | } 33 | 34 | // Unused variable 35 | fn unused_variable() { 36 | let unused = 42; // Warning: unused variable 37 | } 38 | 39 | // Unreachable code 40 | fn unreachable_code() -> i32 { 41 | return 42; 42 | println!("This is unreachable"); // Warning: unreachable code 43 | } 44 | 45 | // Match not exhaustive 46 | enum Color { 47 | Red, 48 | Green, 49 | Blue, 50 | } 51 | 52 | fn incomplete_match(color: Color) { 53 | match color { 54 | Color::Red => println!("Red"), 55 | Color::Green => println!("Green"), 56 | // Missing Blue case - Error: non-exhaustive patterns 57 | } 58 | } 59 | 60 | // Infinite loop without break 61 | fn infinite_loop() { 62 | loop { 63 | println!("This loops forever"); 64 | // Missing break 65 | } 66 | } 67 | 68 | // Using moved value 69 | fn use_after_move() { 70 | let s = String::from("hello"); 71 | let s2 = s; // s is moved here 72 | println!("{}", s); // Error: use of moved value 73 | } -------------------------------------------------------------------------------- /packages/code-indexer/src/utils/autoIndex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Auto-indexing utilities for updating the index after file modifications 3 | */ 4 | 5 | import { updateIndexIncremental } from "../mcp/IndexerAdapter.ts"; 6 | import { relative } from "path"; 7 | import { errorLog } from "../../../../src/utils/debugLog.ts"; 8 | 9 | // Track modified files that need re-indexing 10 | const modifiedFiles = new Set(); 11 | let updateTimer: NodeJS.Timeout | null = null; 12 | 13 | /** 14 | * Mark a file as modified and schedule an index update 15 | */ 16 | export function markFileModified(rootPath: string, filePath: string): void { 17 | const relativePath = relative(rootPath, filePath); 18 | modifiedFiles.add(relativePath); 19 | 20 | // Cancel existing timer 21 | if (updateTimer) { 22 | clearTimeout(updateTimer); 23 | } 24 | 25 | // Schedule update after a short delay to batch multiple changes 26 | updateTimer = setTimeout(() => { 27 | void performAutoIndex(rootPath); 28 | }, 500); // 500ms delay 29 | } 30 | 31 | /** 32 | * Perform automatic incremental index update 33 | */ 34 | async function performAutoIndex(rootPath: string): Promise { 35 | if (modifiedFiles.size === 0) return; 36 | 37 | try { 38 | // Clear the set before updating to handle concurrent modifications 39 | modifiedFiles.clear(); 40 | 41 | // Perform incremental update 42 | const result = await updateIndexIncremental(rootPath); 43 | 44 | if (!result.success) { 45 | errorLog("Auto-index failed:", result.errors); 46 | } 47 | } catch (error) { 48 | errorLog("Auto-index error:", error); 49 | } 50 | } 51 | 52 | /** 53 | * Force immediate index update without waiting 54 | */ 55 | export async function forceAutoIndex(rootPath: string): Promise { 56 | if (updateTimer) { 57 | clearTimeout(updateTimer); 58 | updateTimer = null; 59 | } 60 | 61 | await performAutoIndex(rootPath); 62 | } 63 | -------------------------------------------------------------------------------- /packages/lsp-client/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interfaces for dependency injection - following Dependency Inversion Principle 3 | */ 4 | 5 | // Logger interface 6 | export interface ILogger { 7 | debug(...args: any[]): void; 8 | info(...args: any[]): void; 9 | warn(...args: any[]): void; 10 | error(...args: any[]): void; 11 | } 12 | 13 | // Error handling interface 14 | export interface IErrorHandler { 15 | formatError(error: any, context?: Record): string; 16 | } 17 | 18 | // Re-export FileSystemApi as IFileSystem for backward compatibility 19 | export type { FileSystemApi as IFileSystem } from "@internal/types"; 20 | 21 | // Language detection interface 22 | export interface ILanguageDetector { 23 | getLanguageId(filePath: string): string | null; 24 | } 25 | 26 | // Line resolver interface 27 | export interface ILineResolver { 28 | resolveLineParameter(lines: string[], line: string | number): number; 29 | } 30 | 31 | // Server characteristics interface 32 | export interface IServerCharacteristics { 33 | documentOpenDelay: number; 34 | operationTimeout: number; 35 | supportsIncrementalSync?: boolean; 36 | supportsPullDiagnostics?: boolean; 37 | } 38 | 39 | export interface IServerCharacteristicsProvider { 40 | getCharacteristics( 41 | languageId: string, 42 | overrides?: Partial, 43 | ): IServerCharacteristics; 44 | } 45 | 46 | // LSP Client Config interface 47 | export interface ILspClientConfig { 48 | id: string; 49 | name: string; 50 | command: string[]; 51 | baseLanguage: string; 52 | bin: string; // Required for spawn 53 | args?: string[]; 54 | initializationOptions?: any; 55 | serverCharacteristics?: IServerCharacteristics; 56 | doctor?: () => Promise<{ ok: boolean; message?: string }>; 57 | unsupported?: string[]; // List of unsupported LSP features 58 | } 59 | 60 | // Result builder interface 61 | export interface IDiagnosticResultBuilder { 62 | build(): any; 63 | } 64 | -------------------------------------------------------------------------------- /packages/types/src/validators/memory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Memory tool validation schemas using Zod 3 | */ 4 | 5 | import { z } from "zod"; 6 | 7 | // Memory operations schemas 8 | export const listMemoriesSchema = z.object({ 9 | root: z.string().describe("Root directory of the project"), 10 | }); 11 | 12 | export const readMemorySchema = z.object({ 13 | root: z.string().describe("Root directory of the project"), 14 | memoryName: z.string().describe("Name of the memory to read"), 15 | }); 16 | 17 | export const writeMemorySchema = z.object({ 18 | root: z.string().describe("Root directory of the project"), 19 | memoryName: z.string().describe("Name of the memory to write"), 20 | content: z.string().describe("Content to save in the memory"), 21 | }); 22 | 23 | export const deleteMemorySchema = z.object({ 24 | root: z.string().describe("Root directory of the project"), 25 | memoryName: z.string().describe("Name of the memory to delete"), 26 | }); 27 | 28 | // Advanced memory operations 29 | export const searchMemoriesSchema = z.object({ 30 | root: z.string().describe("Root directory of the project"), 31 | query: z.string().describe("Search query"), 32 | caseSensitive: z.boolean().optional().describe("Case sensitive search"), 33 | regex: z.boolean().optional().describe("Use regex for search"), 34 | }); 35 | 36 | export const mergeMemoriesSchema = z.object({ 37 | root: z.string().describe("Root directory of the project"), 38 | sourceMemories: z.array(z.string()).describe("List of memory names to merge"), 39 | targetMemory: z.string().describe("Target memory name for merged content"), 40 | deleteSources: z 41 | .boolean() 42 | .optional() 43 | .describe("Delete source memories after merge"), 44 | }); 45 | 46 | export const compressMemorySchema = z.object({ 47 | root: z.string().describe("Root directory of the project"), 48 | memoryName: z.string().describe("Name of the memory to compress"), 49 | algorithm: z.enum(["gzip", "brotli", "deflate"]).optional(), 50 | }); 51 | -------------------------------------------------------------------------------- /src/features/memory/onboarding/onboardingTools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Onboarding tools for lsmcp symbol indexing 3 | */ 4 | 5 | import { z } from "zod"; 6 | import type { McpToolDef } from "@internal/types"; 7 | import { platform } from "node:os"; 8 | import { 9 | symbolIndexOnboardingPrompt, 10 | symbolSearchGuidancePrompt, 11 | compressionAnalysisPrompt, 12 | } from "./onboardingPrompts.ts"; 13 | 14 | const indexOnboardingSchema = z.object({ 15 | root: z.string().describe("Root directory of the project"), 16 | }); 17 | 18 | export const indexOnboardingTool: McpToolDef = { 19 | name: "index_onboarding", 20 | description: "Get instructions for onboarding the symbol index for a project", 21 | schema: indexOnboardingSchema, 22 | execute: async ({ root }) => { 23 | const systemInfo = `${platform()} ${process.version}`; 24 | return symbolIndexOnboardingPrompt({ system: systemInfo, rootPath: root }); 25 | }, 26 | }; 27 | 28 | const getSymbolSearchGuidanceSchema = z.object({}); 29 | 30 | export const getSymbolSearchGuidanceTool: McpToolDef< 31 | typeof getSymbolSearchGuidanceSchema 32 | > = { 33 | name: "get_symbol_search_guidance", 34 | description: "Get guidance on how to effectively search symbols in the index", 35 | schema: getSymbolSearchGuidanceSchema, 36 | execute: async () => { 37 | return symbolSearchGuidancePrompt(); 38 | }, 39 | }; 40 | 41 | const getCompressionGuidanceSchema = z.object({}); 42 | 43 | export const getCompressionGuidanceTool: McpToolDef< 44 | typeof getCompressionGuidanceSchema 45 | > = { 46 | name: "get_compression_guidance", 47 | description: "Get guidance on token compression analysis", 48 | schema: getCompressionGuidanceSchema, 49 | execute: async () => { 50 | return compressionAnalysisPrompt(); 51 | }, 52 | }; 53 | 54 | // Export all onboarding tools 55 | export const indexOnboardingTools = [ 56 | indexOnboardingTool, 57 | getSymbolSearchGuidanceTool, 58 | getCompressionGuidanceTool, 59 | ]; 60 | -------------------------------------------------------------------------------- /packages/lsp-client/src/utils/fileContext.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { pathToFileURL } from "url"; 3 | import { ErrorContext, formatError } from "./container-helpers.ts"; 4 | import type { FileSystemApi } from "./container-helpers.ts"; 5 | 6 | /** 7 | * File context information for LSP operations 8 | */ 9 | export interface FileContext { 10 | /** Absolute path to the file */ 11 | absolutePath: string; 12 | /** File URI in the format expected by LSP */ 13 | fileUri: string; 14 | /** Content of the file */ 15 | content: string; 16 | } 17 | 18 | /** 19 | * Load a file and prepare its context for LSP operations 20 | * 21 | * @param root - Root directory for resolving relative paths 22 | * @param filePath - File path (can be relative or absolute) 23 | * @param fs - FileSystem API instance 24 | * @returns File context with absolute path, URI, and content 25 | */ 26 | export async function loadFileContext( 27 | root: string, 28 | filePath: string, 29 | fs: FileSystemApi, 30 | ): Promise { 31 | // Convert to absolute path 32 | const absolutePath = path.isAbsolute(filePath) 33 | ? filePath 34 | : path.join(root, filePath); 35 | 36 | // Check if file exists 37 | if (!(await fs.exists(absolutePath))) { 38 | const context: ErrorContext = { 39 | operation: "file access", 40 | filePath: path.relative(root, absolutePath), 41 | }; 42 | throw new Error(formatError(new Error("File not found"), context)); 43 | } 44 | 45 | // Convert to file URI (pathToFileURL handles Windows paths correctly) 46 | const fileUri = pathToFileURL(absolutePath).toString(); 47 | 48 | // Read the file content 49 | try { 50 | const content = await fs.readFile(absolutePath); 51 | return { absolutePath, fileUri, content }; 52 | } catch (error) { 53 | const context: ErrorContext = { 54 | operation: "file read", 55 | filePath: path.relative(root, absolutePath), 56 | }; 57 | throw new Error(formatError(error, context)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | This directory contains integration tests for the LSMCP project. 4 | 5 | ## Test Categories 6 | 7 | - **LSP Integration**: Tests for Language Server Protocol functionality 8 | - **MCP Integration**: Tests for Model Context Protocol server behavior 9 | - **File System Operations**: Tests for real file system interactions 10 | - **Multi-language Support**: Tests for different language servers 11 | 12 | ## Known Issues 13 | 14 | ### Flaky Tests 15 | 16 | Some integration tests may be flaky due to: 17 | 18 | 1. **Timing-sensitive LSP operations**: Language servers may take varying 19 | amounts of time to initialize and process files 20 | 2. **File system race conditions**: Creating, modifying, and deleting files can 21 | have timing issues 22 | 3. **Concurrent test execution**: Tests running in parallel may interfere with 23 | each other 24 | 25 | To address these issues: 26 | 27 | - Vitest is configured to **retry failed tests up to 2 times** 28 | - Tests use appropriate delays and polling mechanisms 29 | - Temporary directories are used to isolate tests 30 | 31 | ### Specific Flaky Tests 32 | 33 | - `lsp-diagnostics-stale-content.test.ts`: May fail on first run due to LSP 34 | server initialization timing 35 | - `issue-8-file-system.test.ts`: File system operations may have race conditions 36 | 37 | ## Running Tests 38 | 39 | ```bash 40 | # Run all integration tests 41 | pnpm test:integration 42 | 43 | # Run a specific test file 44 | pnpm test:integration tests/integration/lsp-diagnostics-stale-content.test.ts 45 | 46 | # Run tests with verbose output 47 | DEBUG=1 pnpm test:integration 48 | ``` 49 | 50 | ## Writing New Tests 51 | 52 | When writing new integration tests: 53 | 54 | 1. Use proper setup/teardown to clean up resources 55 | 2. Add appropriate delays for LSP operations 56 | 3. Use polling instead of fixed delays where possible 57 | 4. Document any known flakiness in the test file 58 | 5. Consider isolating tests that may interfere with others 59 | -------------------------------------------------------------------------------- /tests/languages/diagnosticProcessors.ts: -------------------------------------------------------------------------------- 1 | import type { Diagnostic } from "vscode-languageserver-protocol"; 2 | 3 | interface ProcessedDiagnostic { 4 | file: string; 5 | severity: number; 6 | message: string; 7 | line: number; 8 | source?: string; 9 | } 10 | 11 | /** 12 | * Process diagnostics with deduplication and validation 13 | * Some LSP servers may have issues: 14 | * 1. Reports duplicate diagnostics 15 | * 2. Reports diagnostics for non-existent lines 16 | * 3. May report diagnostics multiple times with different line numbers 17 | */ 18 | export function processDeduplicatedDiagnostics( 19 | diagnostics: Diagnostic[], 20 | filePath: string, 21 | fileContent: string, 22 | ): ProcessedDiagnostic[] { 23 | const lineCount = fileContent.split("\n").length; 24 | const processed: ProcessedDiagnostic[] = []; 25 | const seen = new Set(); 26 | 27 | for (const diagnostic of diagnostics) { 28 | const line = diagnostic.range.start.line; 29 | 30 | // Skip diagnostics for lines that don't exist 31 | if (line >= lineCount) { 32 | continue; 33 | } 34 | 35 | // Create a unique key for deduplication 36 | const key = `${line}:${diagnostic.message}`; 37 | if (seen.has(key)) { 38 | continue; 39 | } 40 | seen.add(key); 41 | 42 | processed.push({ 43 | file: filePath, 44 | severity: diagnostic.severity || 1, 45 | message: diagnostic.message, 46 | line: line, 47 | source: diagnostic.source, 48 | }); 49 | } 50 | 51 | return processed; 52 | } 53 | /** 54 | * Default diagnostic processor 55 | */ 56 | export function processDefaultDiagnostics( 57 | diagnostics: Diagnostic[], 58 | filePath: string, 59 | _fileContent: string, 60 | ): ProcessedDiagnostic[] { 61 | return diagnostics.map((d) => ({ 62 | file: filePath, 63 | severity: d.severity || 1, 64 | message: d.message.substring(0, 80) + (d.message.length > 80 ? "..." : ""), 65 | line: d.range.start.line, 66 | source: d.source, 67 | })); 68 | } 69 | -------------------------------------------------------------------------------- /packages/types/src/lsp/index.ts: -------------------------------------------------------------------------------- 1 | // Re-export all LSP types 2 | // Export from client.ts first (has the main implementations) 3 | export * from "./client.ts"; 4 | // Only export non-duplicate items from protocol.ts 5 | export { LSPMethods, type LSPMethod } from "./protocol.ts"; 6 | export * from "./symbols.ts"; 7 | export * from "./diagnostics.ts"; 8 | 9 | // Re-export commonly used types from vscode-languageserver-types 10 | export { 11 | Diagnostic, 12 | DiagnosticSeverity, 13 | Range, 14 | Position, 15 | Location, 16 | LocationLink, 17 | integer, 18 | DocumentSymbol, 19 | SymbolKind, 20 | SymbolInformation, 21 | SymbolTag, 22 | CompletionItem, 23 | CompletionItemKind, 24 | CompletionList, 25 | TextEdit, 26 | WorkspaceEdit, 27 | DocumentUri, 28 | Hover, 29 | MarkupContent, 30 | MarkedString, 31 | SignatureInformation, 32 | ParameterInformation, 33 | CodeAction, 34 | CodeActionKind, 35 | Command, 36 | FormattingOptions, 37 | Color, 38 | ColorInformation, 39 | ColorPresentation, 40 | FoldingRange, 41 | FoldingRangeKind, 42 | SelectionRange, 43 | InlayHint, 44 | InlayHintKind, 45 | InlayHintLabelPart, 46 | SemanticTokens, 47 | DocumentHighlight, 48 | DocumentHighlightKind, 49 | DocumentLink, 50 | } from "vscode-languageserver-types"; 51 | 52 | // Type aliases that are not exported at runtime from vscode-languageserver-types 53 | export type Definition = Location | Location[]; 54 | import type { SignatureInformation as SignatureInfo } from "vscode-languageserver-types"; 55 | export type SignatureHelp = { 56 | signatures: SignatureInfo[]; 57 | activeSignature?: number; 58 | activeParameter?: number; 59 | }; 60 | export type CallHierarchyItem = any; // These types are not available in v3.17.5 61 | export type CallHierarchyIncomingCall = any; 62 | export type CallHierarchyOutgoingCall = any; 63 | export type TypeHierarchyItem = any; 64 | export type TypeHierarchySupertypeParams = any; 65 | export type TypeHierarchySubtypeParams = any; 66 | export type SemanticTokensEdit = any; 67 | -------------------------------------------------------------------------------- /packages/lsp-client/src/core/state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LSP Client state management 3 | */ 4 | 5 | import { ChildProcess } from "child_process"; 6 | import { EventEmitter } from "events"; 7 | import type { 8 | ServerCapabilities, 9 | Diagnostic, 10 | DocumentUri, 11 | } from "../protocol/types/index.ts"; 12 | import type { IFileSystem } from "../interfaces.ts"; 13 | import { nodeFileSystemApi } from "../utils/filesystem.ts"; 14 | 15 | export interface LSPProcessState { 16 | process: ChildProcess | null; 17 | messageId: number; 18 | responseHandlers: Map< 19 | number | string, 20 | { 21 | resolve: (value: any) => void; 22 | reject: (error: Error) => void; 23 | timer?: NodeJS.Timeout; 24 | } 25 | >; 26 | buffer: string; 27 | contentLength: number; 28 | diagnostics: Map; 29 | eventEmitter: EventEmitter; 30 | rootPath: string; 31 | languageId: string; 32 | serverCharacteristics?: Record; 33 | fileSystemApi: IFileSystem; 34 | serverCapabilities?: ServerCapabilities; 35 | } 36 | 37 | export interface LSPClientConfig { 38 | process: ChildProcess; 39 | rootPath: string; 40 | languageId?: string; 41 | serverCharacteristics?: Record; 42 | fileSystemApi?: IFileSystem; 43 | clientName?: string; 44 | clientVersion?: string; 45 | initializationOptions?: Record; 46 | } 47 | 48 | export function createInitialState(config: LSPClientConfig): LSPProcessState { 49 | return { 50 | process: config.process, 51 | messageId: 0, 52 | responseHandlers: new Map(), 53 | buffer: "", 54 | contentLength: -1, 55 | diagnostics: new Map(), 56 | eventEmitter: new EventEmitter(), 57 | rootPath: config.rootPath, 58 | languageId: config.languageId || "plaintext", 59 | serverCharacteristics: config.serverCharacteristics, 60 | fileSystemApi: config.fileSystemApi || createDefaultFileSystemApi(), 61 | }; 62 | } 63 | 64 | function createDefaultFileSystemApi(): IFileSystem { 65 | return nodeFileSystemApi; 66 | } 67 | -------------------------------------------------------------------------------- /tests/helpers/test-lsp-startup.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { spawn } from "child_process"; 4 | import { join, dirname } from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)); 8 | const projectRoot = join(__dirname, "../.."); 9 | 10 | console.log("Testing LSP server startup..."); 11 | 12 | const tsLspPath = join( 13 | projectRoot, 14 | "node_modules", 15 | ".bin", 16 | "typescript-language-server", 17 | ); 18 | console.log(`LSP path: ${tsLspPath}`); 19 | 20 | const lspProcess = spawn(tsLspPath, ["--stdio"], { 21 | cwd: __dirname, 22 | stdio: ["pipe", "pipe", "pipe"], 23 | }); 24 | 25 | let stderrBuffer = ""; 26 | let stdoutBuffer = ""; 27 | 28 | lspProcess.stdout.on("data", (data) => { 29 | stdoutBuffer += data.toString(); 30 | console.log("STDOUT:", data.toString()); 31 | }); 32 | 33 | lspProcess.stderr.on("data", (data) => { 34 | stderrBuffer += data.toString(); 35 | console.error("STDERR:", data.toString()); 36 | }); 37 | 38 | lspProcess.on("error", (error) => { 39 | console.error("Process error:", error); 40 | }); 41 | 42 | lspProcess.on("exit", (code, signal) => { 43 | console.log(`Process exited with code ${code}, signal ${signal}`); 44 | if (stderrBuffer) { 45 | console.log("Full stderr:", stderrBuffer); 46 | } 47 | if (stdoutBuffer) { 48 | console.log("Full stdout:", stdoutBuffer); 49 | } 50 | }); 51 | 52 | // Send initialize request 53 | const initRequest = JSON.stringify({ 54 | jsonrpc: "2.0", 55 | id: 1, 56 | method: "initialize", 57 | params: { 58 | processId: process.pid, 59 | rootUri: `file://${__dirname}`, 60 | capabilities: {}, 61 | }, 62 | }); 63 | 64 | const message = `Content-Length: ${Buffer.byteLength(initRequest)}\r\n\r\n${initRequest}`; 65 | console.log("Sending:", message); 66 | 67 | setTimeout(() => { 68 | lspProcess.stdin.write(message); 69 | 70 | // Wait for response 71 | setTimeout(() => { 72 | console.log("Killing process..."); 73 | lspProcess.kill(); 74 | }, 2000); 75 | }, 100); 76 | -------------------------------------------------------------------------------- /src/presets/tsgo.ts: -------------------------------------------------------------------------------- 1 | import type { Preset } from "../config/schema.ts"; 2 | 3 | /** 4 | * tsgo adapter - Fast TypeScript language server 5 | * 6 | * Known issues: 7 | * - May report duplicate diagnostics 8 | * - May report diagnostics for non-existent lines 9 | * - Diagnostics are deduplicated and filtered by the test helper 10 | */ 11 | export const tsgoAdapter: Preset = { 12 | presetId: "tsgo", 13 | binFindStrategy: { 14 | strategies: [ 15 | // 1. Check node_modules first (most common for JS/TS tools) 16 | { type: "node_modules", names: ["tsgo"] }, 17 | // 2. Check global installation 18 | { type: "global", names: ["tsgo"] }, 19 | // 3. Fall back to npx 20 | { type: "npx", package: "@typescript/native-preview" }, 21 | ], 22 | defaultArgs: ["--lsp", "--stdio"], 23 | }, 24 | files: ["**/*.ts", "**/*.tsx", "**/*.d.ts"], 25 | disable: ["get_code_actions", "rename_symbol", "delete_symbol"], 26 | needsDiagnosticDeduplication: true, 27 | 28 | serverCharacteristics: { 29 | documentOpenDelay: 500, 30 | readinessCheckTimeout: 200, 31 | initialDiagnosticsTimeout: 1000, 32 | requiresProjectInit: false, 33 | sendsInitialDiagnostics: false, 34 | operationTimeout: 5000, 35 | }, 36 | 37 | // Initialize with TypeScript preferences 38 | initializationOptions: { 39 | preferences: { 40 | includeInlayParameterNameHints: "none", 41 | includeInlayParameterNameHintsWhenArgumentMatchesName: false, 42 | includeInlayFunctionParameterTypeHints: false, 43 | includeInlayVariableTypeHints: false, 44 | includeInlayPropertyDeclarationTypeHints: false, 45 | includeInlayFunctionLikeReturnTypeHints: false, 46 | includeInlayEnumMemberValueHints: false, 47 | }, 48 | // Disable some features that might cause issues 49 | maxTsServerMemory: 4096, 50 | }, 51 | 52 | // Language-specific features 53 | languageFeatures: { 54 | typescript: { 55 | enabled: true, 56 | indexNodeModules: true, 57 | maxFiles: 5000, 58 | }, 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /src/features/ts/utils/findSymbolInLine.ts: -------------------------------------------------------------------------------- 1 | import { findSymbolOccurrences } from "./findSymbolOccurrences.ts"; 2 | 3 | /** 4 | * Finds the position of a symbol within a line 5 | * @param lineText The text of the line 6 | * @param symbolName The symbol to find 7 | * @param symbolIndex Optional index if symbol appears multiple times (0-based) 8 | * @returns Character index or error message 9 | */ 10 | export function findSymbolInLine( 11 | lineText: string, 12 | symbolName: string, 13 | symbolIndex = 0, 14 | ): { characterIndex: number } | { error: string } { 15 | const occurrences = findSymbolOccurrences(lineText, symbolName); 16 | 17 | if (occurrences.length === 0) { 18 | return { error: `Symbol "${symbolName}" not found` }; 19 | } 20 | 21 | if (symbolIndex < 0 || symbolIndex >= occurrences.length) { 22 | return { 23 | error: `Symbol "${symbolName}" occurrence ${symbolIndex} not found (only ${occurrences.length} occurrences)`, 24 | }; 25 | } 26 | 27 | return { characterIndex: occurrences[symbolIndex] }; 28 | } 29 | 30 | if (import.meta.vitest) { 31 | const { describe, it, expect } = import.meta.vitest; 32 | 33 | describe("findSymbolInLine", () => { 34 | it("should find symbol at first occurrence by default", () => { 35 | const result = findSymbolInLine("const foo = foo + foo;", "foo"); 36 | expect(result).toEqual({ characterIndex: 6 }); 37 | }); 38 | 39 | it("should find symbol at specific index", () => { 40 | const result = findSymbolInLine("const foo = foo + foo;", "foo", 2); 41 | expect(result).toEqual({ characterIndex: 18 }); 42 | }); 43 | 44 | it("should return error if symbol not found", () => { 45 | const result = findSymbolInLine("const bar = 1;", "foo"); 46 | expect(result).toEqual({ error: 'Symbol "foo" not found' }); 47 | }); 48 | 49 | it("should return error for invalid index", () => { 50 | const result = findSymbolInLine("const foo = 1;", "foo", 1); 51 | expect(result).toEqual({ 52 | error: 'Symbol "foo" occurrence 1 not found (only 1 occurrences)', 53 | }); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /packages/types/src/lsp/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver-types"; 2 | 3 | // Diagnostic-related types 4 | 5 | // Pull diagnostics support (LSP 3.17+) 6 | export interface DocumentDiagnosticReport { 7 | kind: "full" | "unchanged"; 8 | items?: Diagnostic[]; 9 | resultId?: string; 10 | } 11 | 12 | export interface DiagnosticResult { 13 | uri: string; 14 | diagnostics: Diagnostic[]; 15 | } 16 | 17 | export interface DiagnosticStats { 18 | total: number; 19 | errors: number; 20 | warnings: number; 21 | informations: number; 22 | hints: number; 23 | } 24 | 25 | export interface DiagnosticFilter { 26 | severity?: DiagnosticSeverity | DiagnosticSeverity[]; 27 | source?: string; 28 | code?: string | number; 29 | message?: string | RegExp; 30 | } 31 | 32 | export interface FormattedDiagnostic { 33 | severity: "error" | "warning" | "info" | "hint"; 34 | line: number; 35 | column: number; 36 | endLine?: number; 37 | endColumn?: number; 38 | message: string; 39 | source?: string; 40 | code?: string | number; 41 | } 42 | 43 | // Diagnostic support capabilities 44 | export interface DiagnosticSupport { 45 | pushDiagnostics: boolean; 46 | pullDiagnostics: boolean; 47 | relatedInformation?: boolean; 48 | tags?: boolean; 49 | codeDescription?: boolean; 50 | data?: boolean; 51 | } 52 | 53 | // Constants 54 | export const DIAGNOSTIC_SEVERITY_NAMES = { 55 | [DiagnosticSeverity.Error]: "error", 56 | [DiagnosticSeverity.Warning]: "warning", 57 | [DiagnosticSeverity.Information]: "info", 58 | [DiagnosticSeverity.Hint]: "hint", 59 | } as const; 60 | 61 | export function getSeverityName(severity: DiagnosticSeverity): string { 62 | return DIAGNOSTIC_SEVERITY_NAMES[severity] || "unknown"; 63 | } 64 | 65 | export function parseSeverity(value: string): DiagnosticSeverity | undefined { 66 | const normalized = value.toLowerCase(); 67 | for (const [severity, name] of Object.entries(DIAGNOSTIC_SEVERITY_NAMES)) { 68 | if (name === normalized) { 69 | return Number(severity) as DiagnosticSeverity; 70 | } 71 | } 72 | return undefined; 73 | } 74 | -------------------------------------------------------------------------------- /docs/ja/lsif-spec.md: -------------------------------------------------------------------------------- 1 | # LSIF 概要 (短縮版) 2 | 3 | 本書は Microsoft の公式 Overview を要約した日本語の短縮版です。後続の詳細版で図や仕様項目の網羅を追加予定です。 4 | 5 | ## これは何か 6 | 7 | Language Server Index Format (LSIF) は、リポジトリをローカルにクローンせずとも Web UI やツール上で Hover、Go to Definition、Find All References などのリッチなコードナビゲーションを提供するための交換フォーマットです。言語サーバーや他のツールがワークスペースに関する知識を事前に出力し、後からそのデータを使って LSP 相当の問いに答えます。 8 | 9 | ## なぜ必要か 10 | 11 | LSP の言語サーバーは全ファイルをローカルに保持し解析する前提ですが、PR レビューやブラウザ上の閲覧ではそれが重い/不可能な場合があります。LSIF は事前計算済みの結果を永続化し、実行時には言語サーバーを起動せずに結果を提供します。 12 | 13 | ## 仕組みの要点 14 | 15 | - LSP のデータ型を再利用し、LSP リクエストの結果をモデル化して永続化する 16 | - 位置(Position)ではなく範囲(Range)を基本単位にして重複を圧縮する 17 | - グラフ表現で記述する: 頂点(vertex)=document, range, 各種 result; 辺(edge)=contains, textDocument/hover など 18 | - データはストリーミング出力可能で、大規模コードでもメモリ効率よく生成できる 19 | - 言語の意味論や「何が定義か」などのシンボル意味付けは対象外(LSP と同様に非目標) 20 | 21 | ## 代表的な例(文章による要約) 22 | 23 | - Hover: ある識別子の範囲に対して hoverResult 頂点を結び付け、textDocument/hover 辺で関連付ける 24 | - Folding Range: document 頂点に foldingRangeResult 頂点を結び付け、textDocument/foldingRange 辺で関連付ける 25 | 26 | ## サポートされる主なリクエスト種別(Overview 時点) 27 | 28 | Document Symbols、Document Links、Go to Declaration、Go to Definition、Go to Type Definition、Find All References、Go to Implementation、Hover、Folding Range。Moniker によるシンボル関連付けも仕様に含まれ、LSP の textDocument/moniker と整合するように計算することが推奨されます。 29 | 30 | ## LSP との関係 31 | 32 | - LSIF は LSP を置き換えるものではなく、LSP の型と概念を流用して「事前計算済みの結果を配布する」ためのフォーマット 33 | - 既存の LSP 対応クライアント/サービスに統合しやすい 34 | 35 | ## 主な適用シナリオ 36 | 37 | - PR レビューやブラウザでのコード閲覧時に、定義ジャンプや参照検索を提供 38 | - CI で LSIF を生成・配布して、エディタや Web サービスで高速に応答 39 | 40 | ## 参考リンク 41 | 42 | - Overview: https://microsoft.github.io/language-server-protocol/overviews/lsif/overview/ 43 | - 仕様 0.4.0: https://microsoft.github.io/language-server-protocol/specifications/lsif/0.4.0/specification 44 | - LSIF Index for TypeScript: https://github.com/Microsoft/lsif-node 45 | - VS Code 拡張 (LSIF): https://github.com/Microsoft/vscode-lsif-extension 46 | - フィードバック Issue: https://github.com/Microsoft/language-server-protocol/issues/623 47 | 48 | ## 関連ドキュメント 49 | 50 | - LSP の Moniker 解説は [../lsp-spec.md](../lsp-spec.md) を参照 51 | 52 | 本文は概要のみを扱っています。詳細版ではグラフ要素の一覧、サンプル、Mermaid 図、最適化、制約事項などを追記します。 53 | -------------------------------------------------------------------------------- /packages/types/src/domain/project.ts: -------------------------------------------------------------------------------- 1 | // Project and memory domain types 2 | 3 | export interface ProjectOverview { 4 | projectName: string; 5 | projectType?: string; 6 | description?: string; 7 | rootPath: string; 8 | mainLanguages: string[]; 9 | dependencies?: Record; 10 | devDependencies?: Record; 11 | structure: ProjectStructure; 12 | statistics: ProjectStatistics; 13 | keyComponents?: KeyComponent[]; 14 | } 15 | 16 | export interface ProjectStructure { 17 | directories: DirectoryInfo[]; 18 | configFiles: string[]; 19 | entryPoints?: string[]; 20 | } 21 | 22 | export interface DirectoryInfo { 23 | path: string; 24 | description?: string; 25 | fileCount: number; 26 | purpose?: string; 27 | } 28 | 29 | export interface ProjectStatistics { 30 | totalFiles: number; 31 | totalLines: number; 32 | filesByExtension: Record; 33 | languageBreakdown: LanguageStats[]; 34 | } 35 | 36 | export interface LanguageStats { 37 | language: string; 38 | files: number; 39 | lines: number; 40 | percentage: number; 41 | extensions: string[]; 42 | } 43 | 44 | export interface KeyComponent { 45 | name: string; 46 | type: "interface" | "class" | "function" | "module" | "component" | "service"; 47 | path: string; 48 | description?: string; 49 | exports?: string[]; 50 | dependencies?: string[]; 51 | } 52 | 53 | // Memory-related types 54 | export interface Memory { 55 | name: string; 56 | content: string; 57 | metadata?: MemoryMetadata; 58 | } 59 | 60 | export interface MemoryMetadata { 61 | createdAt: Date; 62 | updatedAt: Date; 63 | version?: number; 64 | tags?: string[]; 65 | author?: string; 66 | } 67 | 68 | export interface MemoryDatabase { 69 | listMemories(projectRoot: string): Promise; 70 | readMemory(projectRoot: string, name: string): Promise; 71 | writeMemory( 72 | projectRoot: string, 73 | name: string, 74 | content: string, 75 | ): Promise; 76 | deleteMemory(projectRoot: string, name: string): Promise; 77 | hasMemory(projectRoot: string, name: string): Promise; 78 | } 79 | -------------------------------------------------------------------------------- /packages/code-indexer/src/engine/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Core types for symbol indexing 3 | */ 4 | 5 | import { SymbolKind, Location } from "vscode-languageserver-types"; 6 | 7 | /** 8 | * Indexed symbol information 9 | */ 10 | export interface IndexedSymbol { 11 | name: string; 12 | kind: SymbolKind; 13 | location: Location; 14 | containerName?: string; 15 | deprecated?: boolean; 16 | detail?: string; 17 | children?: IndexedSymbol[]; 18 | } 19 | 20 | /** 21 | * File symbol information 22 | */ 23 | export interface FileSymbols { 24 | uri: string; 25 | lastModified: number; 26 | lastIndexed: number; 27 | gitHash?: string; 28 | contentHash?: string; 29 | symbols: IndexedSymbol[]; 30 | } 31 | 32 | /** 33 | * Symbol query parameters 34 | */ 35 | export interface SymbolQuery { 36 | name?: string; 37 | kind?: SymbolKind | SymbolKind[]; 38 | file?: string; 39 | containerName?: string; 40 | includeChildren?: boolean; 41 | } 42 | 43 | /** 44 | * Index statistics 45 | */ 46 | export interface IndexStats { 47 | totalFiles: number; 48 | totalSymbols: number; 49 | indexingTime: number; 50 | lastUpdated: Date; 51 | lastGitHash?: string; 52 | } 53 | 54 | /** 55 | * Symbol provider interface 56 | */ 57 | export interface SymbolProvider { 58 | getDocumentSymbols(uri: string): Promise; 59 | } 60 | 61 | /** 62 | * Re-export FileSystemApi as FileSystem for backward compatibility 63 | */ 64 | export type { FileSystemApi as FileSystem } from "@internal/types"; 65 | 66 | /** 67 | * Cache interface 68 | */ 69 | export interface SymbolCache { 70 | get(filePath: string): Promise; 71 | set(filePath: string, symbols: IndexedSymbol[]): Promise; 72 | clear(): Promise; 73 | } 74 | 75 | /** 76 | * Index event types 77 | */ 78 | export type IndexEvent = 79 | | { 80 | type: "fileIndexed"; 81 | uri: string; 82 | symbolCount: number; 83 | fromCache: boolean; 84 | } 85 | | { type: "fileRemoved"; uri: string } 86 | | { type: "indexError"; uri: string; error: Error } 87 | | { type: "indexingStarted"; fileCount: number } 88 | | { type: "indexingCompleted"; duration: number }; 89 | -------------------------------------------------------------------------------- /src/tools/highlevel/symbolTools.simple.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { getFilesRecursively } from "./symbolTools.ts"; 3 | import { resolve, relative } from "node:path"; 4 | import { stat } from "node:fs/promises"; 5 | 6 | async function firstExistingDir( 7 | rootPath: string, 8 | candidates: string[], 9 | ): Promise { 10 | for (const rel of candidates) { 11 | const abs = resolve(rootPath, rel); 12 | try { 13 | const s = await stat(abs); 14 | if (s.isDirectory()) return abs; 15 | } catch { 16 | // ignore 17 | } 18 | } 19 | // fallback to rootPath to avoid hard failures 20 | return rootPath; 21 | } 22 | 23 | describe("getFilesRecursively simple test", () => { 24 | it("should find TypeScript files in current project", async () => { 25 | const rootPath = process.cwd(); 26 | 27 | // Try multiple stable directories in this repo; pick the first that exists 28 | const testDir = await firstExistingDir(rootPath, [ 29 | "src/tools", // this repo has src/tools/* 30 | "src/config", 31 | "packages/code-indexer/src", 32 | "src", 33 | ]); 34 | const expectedPrefix = relative(rootPath, testDir); 35 | 36 | const files = await getFilesRecursively(testDir, rootPath); 37 | // Should find some TypeScript-like files under the chosen directory 38 | expect(files.length).toBeGreaterThan(0); 39 | expect(files.some((f) => f.endsWith(".ts"))).toBe(true); 40 | 41 | // Returned paths should be relative to rootPath and start with the chosen directory prefix 42 | expect(files.every((f) => f.startsWith(expectedPrefix))).toBe(true); 43 | }); 44 | 45 | it("should handle path replacement correctly", async () => { 46 | const rootPath = "/test/root"; 47 | const testPath = "/test/root/src/file.ts"; 48 | 49 | // Test the path replacement logic 50 | const relativePath = testPath.replace(rootPath + "/", ""); 51 | expect(relativePath).toBe("src/file.ts"); 52 | 53 | // Edge case: without trailing slash 54 | const relativePath2 = testPath.replace(rootPath, ""); 55 | expect(relativePath2).toBe("/src/file.ts"); // This shows the issue! 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/code-indexer/src/engine/NodeFileSystem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Node.js file system implementation 3 | */ 4 | 5 | import * as fs from "fs/promises"; 6 | import { existsSync } from "fs"; 7 | import * as path from "path"; 8 | import type { FileSystemApi } from "@internal/types"; 9 | 10 | export class NodeFileSystem implements FileSystemApi { 11 | async readFile(path: string): Promise { 12 | return await fs.readFile(path, "utf-8"); 13 | } 14 | 15 | async writeFile( 16 | path: string, 17 | data: string | Buffer, 18 | encoding?: BufferEncoding, 19 | ): Promise { 20 | await fs.writeFile(path, data, encoding || "utf-8"); 21 | } 22 | 23 | async readdir(path: string, options?: any): Promise { 24 | if (options?.withFileTypes) { 25 | return fs.readdir(path, options); 26 | } 27 | return fs.readdir(path); 28 | } 29 | 30 | async exists(path: string): Promise { 31 | return existsSync(path); 32 | } 33 | 34 | async stat(path: string): Promise { 35 | return await fs.stat(path); 36 | } 37 | 38 | async lstat(path: string): Promise { 39 | return await fs.lstat(path); 40 | } 41 | 42 | async mkdir( 43 | dirPath: string, 44 | options?: { recursive?: boolean }, 45 | ): Promise { 46 | return (await fs.mkdir(dirPath, options)) as any; 47 | } 48 | 49 | async rm( 50 | dirPath: string, 51 | options?: { recursive?: boolean; force?: boolean }, 52 | ): Promise { 53 | await fs.rm(dirPath, options); 54 | } 55 | 56 | async realpath(dirPath: string): Promise { 57 | return await fs.realpath(dirPath); 58 | } 59 | 60 | async cwd(): Promise { 61 | return process.cwd(); 62 | } 63 | 64 | async resolve(...paths: string[]): Promise { 65 | return path.resolve(...paths); 66 | } 67 | 68 | async isDirectory(dirPath: string): Promise { 69 | try { 70 | const stats = await fs.stat(dirPath); 71 | return stats.isDirectory(); 72 | } catch { 73 | return false; 74 | } 75 | } 76 | 77 | async listDirectory(dirPath: string): Promise { 78 | return await fs.readdir(dirPath); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/lsp-client/src/utils/textEdits.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Text edit utilities 3 | */ 4 | 5 | import type { TextEdit, Position } from "../protocol/types/index.ts"; 6 | 7 | export function applyTextEdits(text: string, edits: TextEdit[]): string { 8 | // Sort edits in reverse order (from end to start) to avoid position shifts 9 | const sortedEdits = [...edits].sort((a, b) => { 10 | if (a.range.start.line !== b.range.start.line) { 11 | return b.range.start.line - a.range.start.line; 12 | } 13 | return b.range.start.character - a.range.start.character; 14 | }); 15 | 16 | let lines = text.split("\n"); 17 | 18 | for (const edit of sortedEdits) { 19 | const startLine = edit.range.start.line; 20 | const startChar = edit.range.start.character; 21 | const endLine = edit.range.end.line; 22 | const endChar = edit.range.end.character; 23 | 24 | // Get the text before and after the edit range 25 | const beforeEdit = 26 | lines[startLine].substring(0, startChar) + 27 | edit.newText + 28 | lines[endLine].substring(endChar); 29 | 30 | // Remove the lines in the range and replace with the new text 31 | const removedLines = endLine - startLine; 32 | lines.splice(startLine, removedLines + 1, ...beforeEdit.split("\n")); 33 | } 34 | 35 | return lines.join("\n"); 36 | } 37 | 38 | export function positionToOffset(text: string, position: Position): number { 39 | const lines = text.split("\n"); 40 | let offset = 0; 41 | 42 | for (let i = 0; i < position.line && i < lines.length; i++) { 43 | offset += lines[i].length + 1; // +1 for newline 44 | } 45 | 46 | if (position.line < lines.length) { 47 | offset += Math.min(position.character, lines[position.line].length); 48 | } 49 | 50 | return offset; 51 | } 52 | 53 | export function offsetToPosition(text: string, offset: number): Position { 54 | const lines = text.split("\n"); 55 | let currentOffset = 0; 56 | let line = 0; 57 | 58 | while ( 59 | line < lines.length && 60 | currentOffset + lines[line].length + 1 <= offset 61 | ) { 62 | currentOffset += lines[line].length + 1; 63 | line++; 64 | } 65 | 66 | return { 67 | line, 68 | character: offset - currentOffset, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/features/ts/utils/validation.ts: -------------------------------------------------------------------------------- 1 | import { parseLineNumber } from "./parseLineNumber.ts"; 2 | import { findSymbolInLine } from "./findSymbolInLine.ts"; 3 | import { errors } from "../../../domain/errors/index.ts"; 4 | 5 | interface LineAndSymbolResult { 6 | lineIndex: number; 7 | lineContent: string; 8 | symbolIndex: number; 9 | } 10 | 11 | /** 12 | * Validate and find line and symbol position in file content 13 | * @param fileContent The file content 14 | * @param line Line number (1-based) or string to match 15 | * @param symbolName Symbol name to find 16 | * @param filePath File path for error messages 17 | * @returns LineAndSymbolResult with line index, content, and symbol index 18 | * @throws LSMCPError if validation fails 19 | */ 20 | export function validateLineAndSymbol( 21 | fileContent: string, 22 | line: string | number, 23 | symbolName: string, 24 | filePath: string, 25 | ): LineAndSymbolResult { 26 | // Parse line number 27 | const lineResult = parseLineNumber(fileContent, line); 28 | if ("error" in lineResult) { 29 | throw errors.lineNotFound(line, filePath); 30 | } 31 | 32 | const { targetLine, lineText } = lineResult; 33 | 34 | // Find symbol in line 35 | const symbolResult = findSymbolInLine(lineText, symbolName); 36 | if ("error" in symbolResult) { 37 | throw errors.symbolNotFound(symbolName, targetLine + 1); 38 | } 39 | 40 | return { 41 | lineIndex: targetLine, 42 | lineContent: lineText, 43 | symbolIndex: symbolResult.characterIndex, 44 | }; 45 | } 46 | 47 | /** 48 | * Validate line number only 49 | * @param fileContent The file content 50 | * @param line Line number (1-based) or string to match 51 | * @param filePath File path for error messages 52 | * @returns Line index and content 53 | * @throws LSMCPError if validation fails 54 | */ 55 | export function validateLine( 56 | fileContent: string, 57 | line: string | number, 58 | filePath: string, 59 | ): { lineIndex: number; lineContent: string } { 60 | const lineResult = parseLineNumber(fileContent, line); 61 | if ("error" in lineResult) { 62 | throw errors.lineNotFound(line, filePath); 63 | } 64 | 65 | return { 66 | lineIndex: lineResult.targetLine, 67 | lineContent: lineResult.lineText, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /packages/types/src/domain/tools.ts: -------------------------------------------------------------------------------- 1 | // Tool and MCP-related domain types 2 | 3 | import { z } from "zod"; 4 | 5 | export interface ToolDefinition { 6 | name: string; 7 | description: string; 8 | schema: TSchema; 9 | execute: (params: z.infer) => Promise; 10 | examples?: ToolExample[]; 11 | } 12 | 13 | export interface ToolExample { 14 | description: string; 15 | params: any; 16 | result?: any; 17 | } 18 | 19 | export interface ToolRegistry { 20 | register(tool: ToolDefinition): void; 21 | get(name: string): ToolDefinition | undefined; 22 | list(): ToolDefinition[]; 23 | has(name: string): boolean; 24 | execute(name: string, params: any): Promise; 25 | } 26 | 27 | export interface MCPServer { 28 | start(): Promise; 29 | stop(): Promise; 30 | registerTool(tool: ToolDefinition): void; 31 | handleRequest(request: MCPRequest): Promise; 32 | } 33 | 34 | export interface MCPRequest { 35 | jsonrpc: "2.0"; 36 | id: string | number; 37 | method: string; 38 | params?: any; 39 | } 40 | 41 | export interface MCPResponse { 42 | jsonrpc: "2.0"; 43 | id: string | number; 44 | result?: any; 45 | error?: MCPError; 46 | } 47 | 48 | export interface MCPError { 49 | code: number; 50 | message: string; 51 | data?: any; 52 | } 53 | 54 | export interface MCPCapabilities { 55 | tools?: boolean; 56 | prompts?: boolean; 57 | resources?: boolean; 58 | logging?: boolean; 59 | experimental?: Record; 60 | } 61 | 62 | // Common tool parameter schemas 63 | export const toolSchemas = { 64 | root: z.string().describe("Root directory for resolving relative paths"), 65 | filePath: z.string().describe("File path (relative to root)"), 66 | line: z.union([ 67 | z.number().describe("Line number (1-based)"), 68 | z.string().describe("String to match in the line"), 69 | ]), 70 | symbolName: z.string().describe("Name of the symbol"), 71 | includeBody: z 72 | .boolean() 73 | .optional() 74 | .describe("Include the full body of the symbol"), 75 | before: z.number().optional().describe("Number of lines to show before"), 76 | after: z.number().optional().describe("Number of lines to show after"), 77 | } as const; 78 | -------------------------------------------------------------------------------- /.lsmcp/memories/suggested_commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: 2025-08-16T16:22:59.010Z 3 | updated: 2025-08-16T16:22:59.010Z 4 | --- 5 | 6 | # Suggested Commands for lsmcp Development 7 | 8 | ## Build and Development 9 | - `pnpm build` - Build the project (required before running integration tests) 10 | - `pnpm watch` - Watch mode for development 11 | - `pnpm generate-schema` - Generate JSON schema for configuration 12 | 13 | ## Testing 14 | - `pnpm test` - Run all unit and integration tests 15 | - `pnpm test:unit` - Run unit tests only 16 | - `pnpm test:integration` - Run integration tests (requires build first) 17 | - `pnpm test:languages` - Run language-specific tests (local only) 18 | - `pnpm test:languages:rust` - Test Rust language support specifically 19 | - `pnpm test:watch` - Run tests in watch mode 20 | - `pnpm coverage` - Generate test coverage report 21 | 22 | ## Code Quality 23 | - `pnpm typecheck` - Type check with tsgo (fast TypeScript checker) 24 | - `pnpm typecheck:tsc` - Type check with native TypeScript compiler 25 | - `pnpm lint` - Run oxlint in quiet mode 26 | - `pnpm lint:refactor` - Run oxlint with detailed output 27 | - `pnpm format` - Format code with Biome 28 | - `pnpm format:check` - Check formatting without changing files 29 | 30 | ## Rust-specific (for examples/rust-project) 31 | - `cargo build` - Build the Rust project 32 | - `cargo check` - Check for compilation errors 33 | - `cargo test` - Run Rust tests 34 | - `cargo fmt` - Format Rust code 35 | - `cargo clippy` - Run Rust linter 36 | 37 | ## Git and Version Control 38 | - Use conventional commits: `feat:`, `fix:`, `docs:`, `chore:`, etc. 39 | - `pnpm changelog` - Generate changelog from commits 40 | - `git` - Standard git commands available 41 | 42 | ## System Utilities 43 | - `ls` - List directory contents 44 | - `cd` - Change directory 45 | - `rg` (ripgrep) - Fast text search (preferred over grep) 46 | - `find` - Find files and directories 47 | 48 | ## MCP Server 49 | - `npx @mizchi/lsmcp -p tsgo` - Run lsmcp with tsgo preset 50 | - `npx @mizchi/lsmcp --bin "rust-analyzer"` - Run with rust-analyzer 51 | 52 | ## Important Notes 53 | - Always run `pnpm build` before integration tests 54 | - Use `rg` instead of `grep` for searching 55 | - The project uses strict TypeScript mode 56 | - Tests may fail if timeouts are insufficient - DO NOT modify timeouts without permission -------------------------------------------------------------------------------- /src/features/memory/onboarding/onboardingTools.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for onboarding tools 3 | */ 4 | 5 | import { describe, it, expect, beforeEach, afterEach } from "vitest"; 6 | import { rmSync, mkdirSync } from "node:fs"; 7 | import { join } from "node:path"; 8 | import { 9 | indexOnboardingTool, 10 | getSymbolSearchGuidanceTool, 11 | getCompressionGuidanceTool, 12 | } from "./onboardingTools.ts"; 13 | 14 | describe("Onboarding Tools", () => { 15 | const testRoot = join(process.cwd(), "test-onboarding"); 16 | 17 | beforeEach(() => { 18 | // Create test directory 19 | mkdirSync(testRoot, { recursive: true }); 20 | }); 21 | 22 | afterEach(() => { 23 | // Clean up test directory 24 | rmSync(testRoot, { recursive: true, force: true }); 25 | }); 26 | 27 | describe("indexOnboardingTool", () => { 28 | it("should return onboarding instructions", async () => { 29 | const result = await indexOnboardingTool.execute({ root: testRoot }); 30 | 31 | expect(result).toContain("setting up lsmcp's symbol indexing"); 32 | expect(result).toContain("Project: " + testRoot); 33 | expect(result).toContain("1. Explore Project Structure"); 34 | expect(result).toContain("2. Index the Codebase"); 35 | expect(result).toContain("3. Verify Index Success"); 36 | expect(result).toContain("4. Test Symbol Search"); 37 | }); 38 | }); 39 | 40 | describe("getSymbolSearchGuidanceTool", () => { 41 | it("should return symbol search guidance", async () => { 42 | const result = await getSymbolSearchGuidanceTool.execute({}); 43 | 44 | expect(result).toContain("searching for symbols"); 45 | expect(result).toContain("search_symbol"); 46 | expect(result).toContain("Symbol kinds"); 47 | expect(result).toContain("5: Class"); 48 | expect(result).toContain("12: Function"); 49 | }); 50 | }); 51 | 52 | describe("getCompressionGuidanceTool", () => { 53 | it("should return compression analysis guidance", async () => { 54 | const result = await getCompressionGuidanceTool.execute({}); 55 | 56 | expect(result).toContain("token compression effectiveness"); 57 | expect(result).toContain("measure_compression"); 58 | expect(result).toContain("90-98%"); 59 | expect(result).toContain("compressed format includes"); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/utils/toolFilters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tool filtering utilities 3 | */ 4 | 5 | import type { McpToolDef } from "@internal/types"; 6 | import type { ServerCapabilities } from "vscode-languageserver-protocol"; 7 | 8 | /** 9 | * Filter out tools that are in the unsupported/disabled list 10 | */ 11 | export function filterUnsupportedTools( 12 | tools: McpToolDef[], 13 | unsupportedList: string[], 14 | ): McpToolDef[] { 15 | if (!unsupportedList || unsupportedList.length === 0) { 16 | return tools; 17 | } 18 | 19 | return tools.filter((tool) => !unsupportedList.includes(tool.name)); 20 | } 21 | 22 | /** 23 | * Filter tools based on LSP server capabilities 24 | */ 25 | export function filterToolsByCapabilities( 26 | tools: McpToolDef[], 27 | capabilities: ServerCapabilities, 28 | ): McpToolDef[] { 29 | return tools.filter((tool) => { 30 | const name = tool.name; 31 | 32 | // Check capability requirements for each tool 33 | if (name === "get_hover" && !capabilities.hoverProvider) { 34 | return false; 35 | } 36 | if (name === "find_references" && !capabilities.referencesProvider) { 37 | return false; 38 | } 39 | if (name === "get_definitions" && !capabilities.definitionProvider) { 40 | return false; 41 | } 42 | if (name === "get_diagnostics" && !capabilities.diagnosticProvider) { 43 | return false; 44 | } 45 | if (name === "rename_symbol" && !capabilities.renameProvider) { 46 | return false; 47 | } 48 | if ( 49 | name === "get_document_symbols" && 50 | !capabilities.documentSymbolProvider 51 | ) { 52 | return false; 53 | } 54 | if ( 55 | name === "get_workspace_symbols" && 56 | !capabilities.workspaceSymbolProvider 57 | ) { 58 | return false; 59 | } 60 | if (name === "get_completion" && !capabilities.completionProvider) { 61 | return false; 62 | } 63 | if (name === "get_signature_help" && !capabilities.signatureHelpProvider) { 64 | return false; 65 | } 66 | if ( 67 | name === "format_document" && 68 | !capabilities.documentFormattingProvider 69 | ) { 70 | return false; 71 | } 72 | if (name === "get_code_actions" && !capabilities.codeActionProvider) { 73 | return false; 74 | } 75 | 76 | // Tool is supported 77 | return true; 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/features/ts/utils/parseLineNumber.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses a line number from either a number or a string match 3 | * @param fullText Full text content of the file 4 | * @param line Line number (1-based) or string to match 5 | * @returns 0-based line index with line text or error message 6 | */ 7 | export function parseLineNumber( 8 | fullText: string, 9 | line: number | string, 10 | ): { targetLine: number; lineText: string } | { error: string } { 11 | const lines = fullText.split("\n"); 12 | 13 | if (typeof line === "string") { 14 | const lineIndex = lines.findIndex((l) => l.includes(line)); 15 | if (lineIndex === -1) { 16 | return { error: `Line containing "${line}" not found` }; 17 | } 18 | return { targetLine: lineIndex, lineText: lines[lineIndex] }; 19 | } else { 20 | const lineIndex = line - 1; // Convert to 0-based 21 | if (lineIndex < 0 || lineIndex >= lines.length) { 22 | return { error: `Line ${line} not found` }; 23 | } 24 | return { targetLine: lineIndex, lineText: lines[lineIndex] }; 25 | } 26 | } 27 | 28 | if (import.meta.vitest) { 29 | const { describe, it, expect } = import.meta.vitest; 30 | 31 | describe("parseLineNumber", () => { 32 | it("should parse line number to 0-based index", () => { 33 | const fullText = "line 1\nline 2\nline 3"; 34 | 35 | const result = parseLineNumber(fullText, 2); 36 | expect(result).toEqual({ targetLine: 1, lineText: "line 2" }); 37 | }); 38 | 39 | it("should find line by string match", () => { 40 | const fullText = "const foo = 1;\nconst bar = 2;\nconst baz = 3;"; 41 | 42 | const result = parseLineNumber(fullText, "bar = 2"); 43 | expect(result).toEqual({ targetLine: 1, lineText: "const bar = 2;" }); 44 | }); 45 | 46 | it("should return error if string not found", () => { 47 | const fullText = "const foo = 1;\nconst bar = 2;"; 48 | 49 | const result = parseLineNumber(fullText, "not found"); 50 | expect(result).toEqual({ 51 | error: 'Line containing "not found" not found', 52 | }); 53 | }); 54 | 55 | it("should return error if line number out of bounds", () => { 56 | const fullText = "line 1\nline 2"; 57 | 58 | const result = parseLineNumber(fullText, 5); 59 | expect(result).toEqual({ 60 | error: "Line 5 not found", 61 | }); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /packages/types/src/shared/errors.ts: -------------------------------------------------------------------------------- 1 | // Error types and utilities 2 | 3 | export class LSMCPError extends Error { 4 | constructor( 5 | message: string, 6 | public code?: string, 7 | public details?: any, 8 | ) { 9 | super(message); 10 | this.name = "LSMCPError"; 11 | } 12 | } 13 | 14 | export class LSPClientError extends LSMCPError { 15 | constructor(message: string, details?: any) { 16 | super(message, "LSP_CLIENT_ERROR", details); 17 | this.name = "LSPClientError"; 18 | } 19 | } 20 | 21 | export class FileSystemError extends LSMCPError { 22 | constructor( 23 | message: string, 24 | public path?: string, 25 | details?: any, 26 | ) { 27 | super(message, "FILESYSTEM_ERROR", { path, ...details }); 28 | this.name = "FileSystemError"; 29 | } 30 | } 31 | 32 | export class IndexingError extends LSMCPError { 33 | constructor( 34 | message: string, 35 | public file?: string, 36 | details?: any, 37 | ) { 38 | super(message, "INDEXING_ERROR", { file, ...details }); 39 | this.name = "IndexingError"; 40 | } 41 | } 42 | 43 | export class ValidationError extends LSMCPError { 44 | constructor( 45 | message: string, 46 | public field?: string, 47 | details?: any, 48 | ) { 49 | super(message, "VALIDATION_ERROR", { field, ...details }); 50 | this.name = "ValidationError"; 51 | } 52 | } 53 | 54 | export class TimeoutError extends LSMCPError { 55 | constructor( 56 | message: string, 57 | public timeout: number, 58 | details?: any, 59 | ) { 60 | super(message, "TIMEOUT_ERROR", { timeout, ...details }); 61 | this.name = "TimeoutError"; 62 | } 63 | } 64 | 65 | // Error utilities 66 | export function getErrorMessage(error: unknown): string { 67 | if (error instanceof Error) return error.message; 68 | if (typeof error === "string") return error; 69 | return String(error); 70 | } 71 | 72 | export function isErrorWithCode( 73 | error: unknown, 74 | ): error is Error & { code: string | number } { 75 | return error instanceof Error && "code" in error; 76 | } 77 | 78 | export function formatError(error: unknown, context?: any): string { 79 | const message = getErrorMessage(error); 80 | if (!context) return message; 81 | 82 | const contextStr = 83 | typeof context === "string" ? context : JSON.stringify(context, null, 2); 84 | 85 | return `${message}\nContext: ${contextStr}`; 86 | } 87 | -------------------------------------------------------------------------------- /examples/python-project/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python file with intentional errors for testing diagnostics. 3 | """ 4 | 5 | from typing import List, Dict 6 | import json 7 | 8 | 9 | def function_with_type_error(x: int) -> str: 10 | """Function that will cause type errors.""" 11 | # Type error: returning int instead of str 12 | return x + 1 13 | 14 | 15 | def function_with_undefined_variable() -> None: 16 | """Function with undefined variable.""" 17 | # NameError: undefined variable 18 | print(undefined_var) 19 | 20 | 21 | def function_with_import_error() -> None: 22 | """Function with import error.""" 23 | # ImportError: non-existent module 24 | import non_existent_module 25 | non_existent_module.some_function() 26 | 27 | 28 | def function_with_attribute_error() -> None: 29 | """Function with attribute error.""" 30 | data = {"key": "value"} 31 | # AttributeError: dict has no attribute 'append' 32 | data.append("item") 33 | 34 | 35 | def function_with_wrong_type_annotation() -> None: 36 | """Function with wrong type usage.""" 37 | # Type error: List should be List[something] 38 | items: List = [1, 2, 3] 39 | 40 | # Type error: Dict should be Dict[key_type, value_type] 41 | mapping: Dict = {"a": 1, "b": 2} 42 | 43 | # Type error: passing wrong type to function 44 | result = function_with_type_error("not an int") 45 | 46 | print(items, mapping, result) 47 | 48 | 49 | class ClassWithErrors: 50 | """Class with various errors.""" 51 | 52 | def __init__(self, value: str) -> None: 53 | self.value = value 54 | 55 | def method_with_error(self) -> int: 56 | """Method that returns wrong type.""" 57 | # Type error: returning str instead of int 58 | return self.value 59 | 60 | def method_with_missing_return(self) -> str: 61 | """Method that should return but doesn't.""" 62 | # Missing return statement 63 | print("This method should return a string") 64 | 65 | 66 | def main() -> None: 67 | """Main function with errors.""" 68 | # Call functions with errors 69 | function_with_type_error(42) 70 | function_with_wrong_type_annotation() 71 | 72 | # Create instance with errors 73 | obj = ClassWithErrors("test") 74 | result = obj.method_with_error() 75 | 76 | print(f"Result: {result}") 77 | 78 | 79 | if __name__ == "__main__": 80 | main() -------------------------------------------------------------------------------- /.lsmcp/memories/project_overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: 2025-08-16T16:22:42.833Z 3 | updated: 2025-08-16T16:22:42.833Z 4 | --- 5 | 6 | # lsmcp Project Overview 7 | 8 | ## Purpose 9 | lsmcp (Language Service Protocol MCP) is a unified Model Context Protocol server that provides advanced code manipulation and analysis capabilities for multiple programming languages through Language Server Protocol integration. 10 | 11 | ## Tech Stack 12 | - **Language**: TypeScript (ES modules) 13 | - **Runtime**: Node.js 14 | - **Package Manager**: pnpm (9.15.0) 15 | - **Build Tool**: tsdown (Rolldown-based bundler) 16 | - **Testing**: Vitest (unit, integration, language tests) 17 | - **Linting**: oxlint 18 | - **Type Checking**: tsgo (TypeScript Go-based checker) 19 | - **Formatting**: Biome 20 | - **Core Dependencies**: 21 | - @modelcontextprotocol/sdk - MCP implementation 22 | - vscode-languageserver-protocol - LSP client implementation 23 | - zod - Schema validation 24 | 25 | ## Project Structure 26 | ``` 27 | lsmcp/ 28 | ├── src/ # Main source code 29 | ├── packages/ # Internal packages 30 | │ ├── code-indexer/ # Symbol indexing functionality 31 | │ ├── lsp-client/ # LSP client implementation 32 | │ └── types/ # Shared TypeScript types 33 | ├── tests/ # Test files 34 | │ ├── unit/ # Unit tests 35 | │ ├── integration/ # Integration tests 36 | │ └── languages/ # Language-specific tests 37 | ├── examples/ # Example projects for different languages 38 | │ ├── rust-project/ # Current working directory (Rust example) 39 | │ └── ... # Other language examples 40 | └── scripts/ # Build and utility scripts 41 | ``` 42 | 43 | ## Current Working Directory 44 | We are currently in `/home/mizchi/mizchi/lsmcp/examples/rust-project/` which is a Rust example project demonstrating how to use lsmcp with rust-analyzer. 45 | 46 | ### Rust Project Structure 47 | ``` 48 | rust-project/ 49 | ├── src/ 50 | │ ├── main.rs # Main entry point 51 | │ ├── lib.rs # Library with Calculator struct and greet function 52 | │ ├── errors.rs # File with intentional errors for testing 53 | │ └── test_diagnostics.rs # Various diagnostic test cases 54 | ├── Cargo.toml # Rust project configuration 55 | └── README.md # Usage documentation 56 | ``` 57 | 58 | ## Version 59 | - lsmcp: v0.10.0-rc.3 60 | - TypeScript/Native: 7.0.0-dev 61 | - Node.js environment on Linux -------------------------------------------------------------------------------- /examples/python-project/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.12" 4 | 5 | [[package]] 6 | name = "nodeenv" 7 | version = "1.9.1" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, 12 | ] 13 | 14 | [[package]] 15 | name = "pyright" 16 | version = "1.1.403" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "nodeenv" }, 20 | { name = "typing-extensions" }, 21 | ] 22 | sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526 } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504 }, 25 | ] 26 | 27 | [[package]] 28 | name = "testproj" 29 | version = "0.0.1" 30 | source = { virtual = "." } 31 | 32 | [package.dev-dependencies] 33 | dev = [ 34 | { name = "pyright" }, 35 | ] 36 | 37 | [package.metadata] 38 | 39 | [package.metadata.requires-dev] 40 | dev = [{ name = "pyright", specifier = ">=1.1.403" }] 41 | 42 | [[package]] 43 | name = "typing-extensions" 44 | version = "4.14.1" 45 | source = { registry = "https://pypi.org/simple" } 46 | sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } 47 | wheels = [ 48 | { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, 49 | ] 50 | -------------------------------------------------------------------------------- /packages/lsp-client/src/providers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LSP-based symbol provider for code indexing 3 | */ 4 | 5 | import type { LSPClient } from "./protocol/types/index.ts"; 6 | import type { DocumentSymbol } from "@internal/types"; 7 | import { fixFSharpSymbolPositions } from "./utils/fsharp-position-fix.ts"; 8 | 9 | /** 10 | * Symbol provider interface required by code-indexer 11 | */ 12 | export interface SymbolProvider { 13 | getDocumentSymbols(uri: string): Promise; 14 | } 15 | 16 | /** 17 | * LSP implementation of SymbolProvider 18 | */ 19 | export class LSPSymbolProvider implements SymbolProvider { 20 | constructor( 21 | private client: LSPClient, 22 | private fileContentProvider: (uri: string) => Promise, 23 | private languageId?: string, 24 | ) {} 25 | 26 | async getDocumentSymbols(uri: string): Promise { 27 | try { 28 | // Get file content 29 | const content = await this.fileContentProvider(uri); 30 | 31 | // Open document temporarily 32 | this.client.openDocument(uri, content); 33 | 34 | try { 35 | // Wait a bit for LSP to process 36 | await new Promise((resolve) => setTimeout(resolve, 200)); 37 | 38 | // Get symbols 39 | let symbols = await this.client.getDocumentSymbols(uri); 40 | 41 | // Apply F# position fix if needed 42 | if ( 43 | this.languageId === "fsharp" && 44 | (uri.endsWith(".fs") || uri.endsWith(".fsi") || uri.endsWith(".fsx")) 45 | ) { 46 | symbols = fixFSharpSymbolPositions( 47 | symbols as DocumentSymbol[], 48 | content, 49 | ); 50 | } 51 | 52 | // Cast to DocumentSymbol[] as code-indexer expects 53 | return symbols as DocumentSymbol[]; 54 | } finally { 55 | // Always close document 56 | this.client.closeDocument(uri); 57 | } 58 | } catch (error) { 59 | // Return empty array for files that can't be processed 60 | // This prevents cascading failures during incremental updates 61 | return []; 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Create an LSP-based symbol provider 68 | */ 69 | export function createLSPSymbolProvider( 70 | client: LSPClient, 71 | fileContentProvider: (uri: string) => Promise, 72 | languageId?: string, 73 | ): SymbolProvider { 74 | return new LSPSymbolProvider(client, fileContentProvider, languageId); 75 | } 76 | -------------------------------------------------------------------------------- /benchmarks/simple-search-benchmark.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { performance } from "node:perf_hooks"; 3 | import { createSearchForPatternTool } from "../src/tools/highlevel/fileSystemToolsFactory.ts"; 4 | import { createFastSearchForPatternTool } from "../src/tools/highlevel/fastSearchForPatternTool.ts"; 5 | 6 | async function main() { 7 | console.log("🚀 Quick Search Pattern Benchmark\n"); 8 | 9 | // Test with a simple pattern in a small directory 10 | const pattern = "function"; 11 | const path = "src/tools/finder"; 12 | 13 | console.log(`Pattern: "${pattern}"`); 14 | console.log(`Path: ${path}\n`); 15 | 16 | // Original implementation 17 | const original = createSearchForPatternTool(); 18 | console.log("Testing original implementation..."); 19 | const originalStart = performance.now(); 20 | const originalResult = await original.execute({ 21 | substringPattern: pattern, 22 | relativePath: path, 23 | restrictSearchToCodeFiles: true, 24 | contextLinesBefore: 0, 25 | contextLinesAfter: 0, 26 | maxAnswerChars: 200000, 27 | }); 28 | const originalTime = performance.now() - originalStart; 29 | const originalMatches = Object.keys(JSON.parse(originalResult)).length; 30 | console.log(` Time: ${originalTime.toFixed(2)}ms`); 31 | console.log(` Files with matches: ${originalMatches}\n`); 32 | 33 | // Fast implementation 34 | const fast = createFastSearchForPatternTool(); 35 | console.log("Testing fast implementation..."); 36 | const fastStart = performance.now(); 37 | const fastResult = await fast.execute({ 38 | substringPattern: pattern, 39 | relativePath: path, 40 | restrictSearchToCodeFiles: true, 41 | contextLinesBefore: 0, 42 | contextLinesAfter: 0, 43 | maxAnswerChars: 200000, 44 | }); 45 | const fastTime = performance.now() - fastStart; 46 | const fastMatches = Object.keys(JSON.parse(fastResult)).length; 47 | console.log(` Time: ${fastTime.toFixed(2)}ms`); 48 | console.log(` Files with matches: ${fastMatches}\n`); 49 | 50 | // Results 51 | console.log("=".repeat(50)); 52 | console.log("RESULTS:"); 53 | console.log(` Original: ${originalTime.toFixed(2)}ms`); 54 | console.log(` Fast: ${fastTime.toFixed(2)}ms`); 55 | const speedup = originalTime / fastTime; 56 | const improvement = ((originalTime - fastTime) / originalTime) * 100; 57 | console.log( 58 | ` ⚡ Speedup: ${speedup.toFixed(2)}x (${improvement.toFixed(1)}% faster)`, 59 | ); 60 | } 61 | 62 | main().catch(console.error); 63 | --------------------------------------------------------------------------------