diff --git a/.claude/lsp.json b/.claude/lsp.json deleted file mode 100644 index 1dd214f..0000000 --- a/.claude/lsp.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "servers": { - "ansible": { - "command": "ansible-language-server", - "args": [ - "--stdio" - ], - "configuration": { - "ansible": { - "ansible": { - "path": "/opt/homebrew/bin/ansible" - }, - "completion": { - "provideRedirectModules": true, - "provideModuleOptionAliases": true - }, - "python": { - "interpreterPath": "/opt/homebrew/bin/python3" - }, - "validation": { - "enabled": false - } - } - }, - "extensions": [ - ".yaml" - ], - "projects": [ - { - "name": "k3s-cluster", - "description": "A high-availability K3s cluster deployed with Ansible.", - "url": "https://github.com/axivo/k3s-cluster", - "path": "/Users/username/github/k3s-cluster", - "patterns": { - "exclude": [ - "**/Chart.yaml" - ] - } - } - ], - "settings": { - "messageRequest": false, - "registrationRequest": false, - "workspace": false - } - }, - "go": { - "command": "gopls", - "args": [ - "serve" - ], - "extensions": [ - ".go" - ], - "projects": [ - { - "name": "helm", - "description": "A Kubernetes package manager.", - "url": "https://github.com/helm/helm", - "path": "/Users/username/github/helm" - } - ] - }, - "helm": { - "command": "helm_ls", - "args": [ - "serve" - ], - "extensions": [ - ".tpl", - ".yaml" - ], - "projects": [ - { - "name": "cert-manager", - "description": "A Helm chart to automatically provision and manage TLS certificates in Kubernetes.", - "url": "https://github.com/cert-manager/cert-manager", - "path": "/Users/username/github/cert-manager/deploy/charts/cert-manager", - "patterns": { - "exclude": [ - "**/values.yaml" - ] - } - } - ], - "configuration": { - "helm-ls": { - "yamlls": { - "config": { - "documentSymbol": true, - "schemas": { - "kubernetes": [ - "**/*.yaml" - ] - }, - "schemaStore": { - "enable": true - }, - "validate": true - }, - "path": "/opt/homebrew/bin/yaml-language-server" - } - } - } - }, - "kotlin": { - "command": "kotlin-lsp", - "args": [ - "--stdio" - ], - "env": { - "JAVA_HOME": "/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home", - "JAVA_TOOL_OPTIONS": "--enable-native-access=ALL-UNNAMED", - "MAVEN_HOME": "/opt/homebrew/opt/maven/libexec" - }, - "extensions": [ - ".kt" - ], - "projects": [ - { - "name": "ktor", - "description": "A framework for quickly creating connected applications in Kotlin.", - "url": "https://github.com/ktorio/ktor", - "path": "/Users/username/github/ktor" - } - ] - }, - "python": { - "command": "pyright-langserver", - "args": [ - "--stdio" - ], - "configuration": { - "settings": { - "python": { - "analysis": { - "autoSearchPaths": true, - "diagnosticMode": "workspace" - } - } - } - }, - "extensions": [ - ".py" - ], - "projects": [ - { - "name": "fastapi", - "description": "A modern, fast (high-performance), web framework for building APIs.", - "url": "https://github.com/fastapi/fastapi", - "path": "/Users/username/github/fastapi", - "patterns": { - "exclude": [ - "**/docs_src" - ] - } - } - ] - }, - "terraform": { - "command": "terraform-ls", - "args": [ - "serve" - ], - "configuration": { - "terraform": { - "path": "/opt/homebrew/bin/terraform" - }, - "validation": { - "enableEnhancedValidation": true - } - }, - "extensions": [ - ".tf", - ".tfvars" - ], - "init": [ - "/opt/homebrew/bin/terraform init" - ], - "projects": [ - { - "name": "terraform-aws-eks", - "description": "A Terraform module to create Amazon EKS resources.", - "url": "https://github.com/terraform-aws-modules/terraform-aws-eks", - "path": "/Users/username/github/terraform-aws-eks" - } - ], - "settings": { - "preloadFiles": false, - "workspace": false - } - }, - "typescript": { - "command": "typescript-language-server", - "args": [ - "--stdio" - ], - "configuration": { - "preferences": { - "includeInlayEnumMemberValueHints": true, - "includeInlayFunctionLikeReturnTypeHints": true, - "includeInlayFunctionParameterTypeHints": true, - "includeInlayParameterNameHints": "all", - "includeInlayPropertyDeclarationTypeHints": true, - "includeInlayVariableTypeHints": true - } - }, - "extensions": [ - ".js", - ".ts" - ], - "projects": [ - { - "name": "k3s-cluster-actions", - "description": "A GitHub Actions workflow for K3s cluster deployed with Ansible.", - "url": "https://github.com/axivo/k3s-cluster", - "path": "/Users/username/github/k3s-cluster/.github/actions" - }, - { - "name": "mcp-lsp", - "description": "A MCP server for interacting with multiple language server protocols.", - "url": "https://github.com/axivo/mcp-lsp", - "path": "/Users/username/github/mcp-lsp" - }, - { - "name": "typescript-sdk", - "description": "Official TypeScript SDK for Model Context Protocol servers and clients.", - "url": "https://github.com/modelcontextprotocol/typescript-sdk", - "path": "/Users/username/github/typescript-sdk" - } - ] - } - } -} diff --git a/.claude/templates/README.md b/.claude/templates/README.md deleted file mode 100644 index e0cb1fc..0000000 --- a/.claude/templates/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Language Server Templates - -Professional templates for systematic code analysis and development workflows using Language Server Protocol (LSP) tools across multiple programming languages. - -## Available Templates - -The following templates are available: - -- [`code-review.md`](code-review.md) - Systematic 9-phase code review methodology with capability-aware adaptation - -### Templates Usage - -Ask Claude to use a workflow template with a specific project: - -- *Start the TypeScript language server with `typescript-sdk` project and check the server capabilities.* -- *Read the `/Users/username/github/mcp-lsp/.claude/templates/code-review.md` template prior code review.* -- *Perform a detailed review of project source code using the LSP tools and let me know your findings.* - -## Collaboration Platform Integration - -The templates are designed for use with [Claude Collaboration Platform](https://github.com/axivo/claude), which transforms Claude from a generic AI assistant into a professional developer through behavioral programming. Visit the [platform documentation](https://axivo.com/claude/) for setup instructions. diff --git a/.claude/templates/code-review.md b/.claude/templates/code-review.md deleted file mode 100644 index 594e12d..0000000 --- a/.claude/templates/code-review.md +++ /dev/null @@ -1,605 +0,0 @@ -# Source Code Review Template - -A template providing systematic methodology for conducting comprehensive code reviews using Language Server Protocol (LSP) tools. The review process spans 9 phases, utilizing 39+ LSP tools to provide tool-verified insights rather than speculation. - -## Review Methodology - -This template provides a universal code review methodology that works across all programming languages supported by Language Server Protocol (LSP). However, **different language servers support different LSP capabilities**, which means the specific tools available will vary by language. - -### Understanding Language Server Capabilities - -Before starting the review, validate what tools are supported for the target language by running: - -``` -language-server:get_server_capabilities(language_id: "your-language") -``` - -This returns a capabilities object showing which LSP features are supported. Look for the `supported: true/false` flag in each capability section: - -**Example: TypeScript LSP (26 tools)** - -```json -{ - "callHierarchyProvider": { "supported": true }, - "typeDefinitionProvider": { "supported": true }, - "inlayHintProvider": { "supported": true }, - "renameProvider": { "supported": true }, - "foldingRangeProvider": { "supported": true } -} -``` - -**Example: Terraform LSP (Limited - 14 tools)** - -```json -{ - "callHierarchyProvider": { "supported": false }, - "typeDefinitionProvider": { "supported": false }, - "inlayHintProvider": { "supported": false }, - "renameProvider": { "supported": false }, - "foldingRangeProvider": { "supported": false } -} -``` - -### Adapting the Review Process - -The 9-phase methodology remains the same for all server languages, **adapt which LSP tools are used in each phase** based on the capabilities response: - -1. **Phase 1 (Project Discovery)** - Always available for all languages -2. **Phase 2 (Structural Analysis)** - Requires `documentSymbolProvider` (universally supported) -3. **Phase 3 (Dependency Mapping)** - Adapt based on: - - ✅ Full analysis if `callHierarchyProvider` is supported - - ⚠️ Limited to symbol definitions/references if not supported -4. **Phase 4 (Type Safety)** - Adapt based on: - - ✅ Comprehensive if `typeDefinitionProvider` and `inlayHintProvider` supported - - ⚠️ Basic hover-only analysis if not supported -5. **Phase 5 (Usage Analysis)** - Requires `referencesProvider` (widely supported) -6. **Phase 6 (Code Quality)** - Adapt based on `codeActionProvider` and `diagnosticsProvider` -7. **Phase 7 (Refactoring Safety)** - Adapt based on: - - ✅ Full rename testing if `renameProvider` is supported - - ⚠️ Manual refactoring assessment if not supported -8. **Phase 8 (Consistency)** - Uses basic symbol and formatting tools (widely supported) -9. **Phase 9 (Report)** - Always available, synthesizes findings from previous phases - -### Handling Unsupported Tools - -When a phase requires tools that aren't supported for server language: - -- **DO NOT skip the phase** - the analysis objective is still valid -- **DO adapt your approach** - use alternative tools or manual analysis -- **DO document limitations** - note which analyses couldn't be performed -- **DO NOT fail the review** - work with what's available - -**Example: Phase 3 for Terraform** - -``` -Original requirement: Use call hierarchy tools -Terraform reality: callHierarchyProvider not supported -Adaptation: Use symbol definitions and references to trace module dependencies manually -``` - -### Fallback Strategies for Unsupported Tools - -When LSP tools are unavailable, adapt your analysis approach: - -- Use `claude:Read` to manually inspect file contents and structure -- Use `claude:Grep` to search for patterns across the codebase -- Document which analyses couldn't be performed due to missing capabilities -- Focus analysis on what the available tools can verify rather than speculation - -The quality of code review will vary based on server capabilities. A review with 5 available tools (Ansible) will be more limited than one with 26 tools (TypeScript). Document these limitations in your final report. - -**Example: Phase 2 for Ansible (No document symbols)** - -``` -Original requirement: Use get_symbols to list all symbols in files -Ansible reality: documentSymbolProvider not supported -Adaptation: Use claude:Read on key files and manually catalog tasks, roles, variables, and handlers -``` - -The template assumes maximum capabilities (~39 tools), expect a different number of available tools depending on used language server. - -### Prerequisites - -Before starting the review: - -1. Verify LSP server is running for target language -2. Confirm project path and entry points - -### Phase Structure and Execution Protocol - -Each phase uses specific LSP tools to gather verified data. The phases build on each other - do not skip phases even if you think you have enough information. - -Complete one phase at a time using the project files and LSP tools listed in each phase. After completing a phase, acknowledge completion and wait for approval before proceeding to the next phase. Report code review results only after all phases are completed. Report only what you observe from the tools - do not speculate or invent problems. - -## Phase 1: Project Discovery - -Understand project structure, file organization, and technology stack. - -### LSP Tools to Use - -- `language-server:get_server_status` - Verify LSP server availability -- `language-server:get_server_capabilities` - Determine which LSP features are supported for this language -- `language-server:get_server_projects` - List available projects -- `language-server:get_project_files` - Enumerate all source files -- `claude:Glob` - Find files by pattern (e.g., `**/*.ts`, `**/*.py`) -- `claude:Read` - Read key files (package.json, setup.py, etc.) - -### Data to Collect - -- Server capabilities (which LSP features are available) -- Total number of files -- File size distribution (lines per file) -- Entry point identification -- Package dependencies (from package.json, requirements.txt, etc.) -- Build/test configuration files -- Documentation files (README, CONTRIBUTING, etc.) - -**Deliverable**: Project inventory with file counts, entry points, and technology stack summary. - -### Related Framework Observations - -- "Always analyze source code with language-server tools" -- "Monitor internally positioning guesswork" -- "Read current file state before making changes" - -## Phase 2: Structural Analysis - -Analyze code organization, module structure, and architectural patterns. - -### LSP Tools to Use - -- `language-server:get_symbols` - List symbols in each file -- `language-server:get_project_symbols` - Search symbols across project -- `language-server:get_folding_ranges` - Identify collapsible code sections -- `language-server:get_semantic_tokens` - Analyze syntax structure - -### Data to Collect - -- Total symbol count (functions, classes, interfaces, etc.) -- Symbol distribution per file -- Class/interface definitions count -- Function/method complexity indicators -- Code folding structure (nesting depth) -- Module organization patterns - -**Deliverable**: Structural overview showing symbol distribution, nesting depth, and organizational patterns. - -### Related Framework Observations - -- "Consider symbol boundaries when calculating character positions" -- "Monitor internally symbol boundary miscalculation" -- "Test tools systematically with precise positioning" - -## Phase 3: Dependency Mapping - -Map import relationships, call hierarchies, and dependency flow. - -### LSP Tools to Use - -- `language-server:get_symbol_definitions` - Find where symbols are defined -- `language-server:get_symbol_references` - Find all symbol usage locations -- `language-server:get_call_hierarchy` - Build call relationship maps -- `language-server:get_incoming_calls` - Find callers of functions -- `language-server:get_outgoing_calls` - Find functions called by target -- `language-server:get_implementations` - Find interface implementations -- `language-server:get_type_hierarchy` - Map type inheritance -- `language-server:get_supertypes` - Find parent types -- `language-server:get_subtypes` - Find child types - -### Data to Collect - -- Import dependency graph (which files import what) -- Call hierarchy for main entry points -- Circular dependency detection -- Leaf modules (no internal dependencies) -- Central modules (highly connected) -- Type hierarchy depth -- Implementation patterns (interface → concrete classes) - -**Deliverable**: Dependency graph showing import relationships, call chains, and type hierarchies. Flag any circular dependencies. - -### Related Framework Observations - -- "Monitor internally systematic approach" -- "Perform root cause analysis before symptom treatment" -- "Apply complete systematic analysis to technical problems" - -## Phase 4: Type Safety Review - -Assess type coverage, identify type safety issues, and verify type inference. - -### LSP Tools to Use - -- `language-server:get_hover` - Retrieve type information -- `language-server:get_inlay_hints` - Show implicit types and parameter names -- `language-server:get_inlay_hint` - Resolve individual inlay hint details -- `language-server:get_signature` - Analyze function signatures -- `language-server:get_type_definitions` - Navigate to type definitions -- `language-server:get_diagnostics` - Find type errors and warnings -- `language-server:get_completions` - Check type inference at specific points -- `language-server:get_resolves` - Resolve completion item details - -### Data to Collect - -- Explicit type annotation coverage -- Usage of `any` or equivalent unsafe types -- Type inference quality (implicit types) -- Generic type usage patterns -- Union/intersection type patterns -- Optional parameter handling -- Type guard implementations -- Diagnostic errors/warnings count - -**Deliverable**: Type safety assessment with coverage percentage, list of unsafe type usage (with file:line), and type inference quality evaluation. - -### Related Framework Observations - -- "Monitor internally character counting shortcuts" -- "Provide accurate code analysis using properly positioned requests" -- "State technical conclusions definitively when evidence supports them" - -## Phase 5: Usage Analysis - -Analyze how symbols are used throughout the codebase. - -### LSP Tools to Use - -- `language-server:get_symbol_references` - Count exact usage of symbols -- `language-server:get_highlights` - Find symbol occurrences in context -- `language-server:get_linked_editing_range` - Check linked identifier patterns - -### Data to Collect - -- Symbol usage frequency (functions, classes, variables) -- Unused symbols (defined but never referenced) -- Over-used symbols (potential code smell) -- Single-use functions (potential inline candidates) -- Symmetric patterns (e.g., open/close, create/destroy pairs) -- Naming consistency patterns -- Cross-module usage patterns - -**Deliverable**: Usage analysis showing reference counts for key symbols, identification of unused code, and naming consistency assessment. - -### Related Framework Observations - -- "Monitor internally symbol identifier imprecision" -- "Monitor internally arbitrary positioning targeting" -- "Remove dead code and unused variables" - -## Phase 6: Code Quality - -Evaluate error handling, resource management, and code maintainability. - -### LSP Tools to Use - -- `language-server:get_code_actions` - Find refactoring opportunities -- `language-server:get_code_resolves` - Resolve code action details -- `language-server:get_diagnostics` - Identify errors and warnings -- `language-server:get_document_formatting` - Check formatting capabilities -- `language-server:get_range_format` - Test range formatting -- `language-server:get_colors` - Extract color definitions (if applicable) -- `language-server:get_links` - Find documentation links -- `language-server:get_link_resolves` - Resolve document link targets - -### Data to Collect - -- Available refactorings per file -- Error handling patterns (try-catch coverage) -- Resource management patterns (cleanup, disposal) -- Code action suggestions count -- Formatting consistency -- Magic numbers and string literals -- Comment quality and coverage -- Diagnostic issues (errors/warnings/info) - -**Deliverable**: Code quality assessment identifying strengths, issues requiring attention, and available automated refactorings. - -### Code Quality Checklist - -- ✅ Comprehensive error handling -- ✅ Proper resource cleanup (finally blocks, disposal patterns) -- ✅ Defensive programming (null checks, validation) -- ✅ Documented public APIs -- ⚠️ Complex method length (identify methods >50 lines) -- ⚠️ Magic numbers (identify hardcoded values) -- ⚠️ Deep nesting (identify >4 levels) -- 🔴 Critical issues (security, data loss risks) - -### Related Framework Observations - -- "Focus on functional correctness over style preferences" -- "Handle edge cases and validate input parameters" -- "Keep functions small and focused on single tasks" -- "Use constants for magic numbers and configuration values" - -## Phase 7: Refactoring Safety - -Test rename operations and assess refactoring risk before recommending changes. - -### LSP Tools to Use - -- `language-server:get_symbol_renames` - Preview rename impact -- `language-server:get_selection_range` - Understand code boundaries -- `language-server:get_code_actions` - Review available refactorings - -### Rename Tests to Perform (minimum 5) - -1. Local function rename (private method) -2. Public class/interface rename -3. Shared type/interface rename -4. Parameter rename -5. Property rename - -### Data to Collect - -- Rename scope (single file vs. cross-module) -- Number of locations affected per rename -- Import/export updates required -- Breaking change risk assessment -- Selection range depth (nesting levels) - -**Deliverable**: Refactoring safety matrix showing rename operation impacts, safe refactoring candidates, and risk assessments. - -### Safety Assessment - -- ✅ **Very Safe**: Local scope, single file, private symbols -- ✅ **Safe with Awareness**: Cross-module, tracked by LSP -- ⚠️ **Medium Risk**: Shared interfaces, multiple consumers -- 🔴 **High Risk**: Public API changes, external consumers - -### Related Framework Observations - -- "Validate before implementation" -- "Flag breaking changes explicitly" -- "Confirm execution parameters before file modifications" - -## Phase 8: Consistency Verification - -Verify naming conventions, style consistency, and architectural patterns. - -### LSP Tools to Use - -- `language-server:get_project_symbols` - Search for naming patterns -- `language-server:get_symbols` - Verify per-file consistency -- `language-server:get_semantic_tokens` - Analyze token usage -- `language-server:get_folding_ranges` - Check structural consistency - -### Data to Collect - -- Naming convention adherence (camelCase, PascalCase, snake_case) -- Prefix/suffix patterns (get*, set*, is*, has*) -- File organization consistency -- Import ordering patterns -- Indentation and formatting consistency -- Documentation style consistency - -**Deliverable**: Consistency report identifying deviations from established patterns and style guidelines. - -### Related Framework Observations - -- "Choose meaningful variable and function names that express intent" -- "Follow consistent indentation and formatting standards" -- "Organize imports and dependencies logically" - -## Phase 9: Final Report Compilation - -Synthesize findings into actionable report with prioritized recommendations. - -**No LSP Tools** - This phase synthesizes data from all previous phases. - -### Report Structure - -#### Executive Summary - -- Overall assessment (rating out of 5) -- Critical issues count -- High priority improvements count -- Code quality score - -#### Critical Issues (Must Fix) - -- Issue description -- Exact location (file:line) -- Impact assessment -- Solution approach with code examples -- Verification steps - -#### High Priority Improvements - -- Improvement description -- Affected locations -- Rationale -- Implementation approach - -#### Medium Priority Enhancements - -- Enhancement description -- Benefits -- Implementation effort estimate - -#### Strengths to Maintain - -- Architectural patterns working well -- Code quality highlights -- Best practices observed - -#### Metrics Summary - -| Metric | Value | Assessment | -|--------|-------|------------| -| Total Lines | X | [Assessment] | -| Files | X | [Assessment] | -| Symbols | X | [Assessment] | -| Type Coverage | X% | [Assessment] | -| Documentation | X% | [Assessment] | -| Circular Dependencies | X | [Assessment] | -| Code Quality | X/5 | [Assessment] | - -#### Refactoring Safety Analysis - -- Safe refactoring candidates -- High-risk operations to avoid -- LSP support quality assessment - -#### Immediate Actions - -- Specific files to modify -- Exact changes needed -- Code examples -- Testing requirements - -### Related Framework Observations - -- "Provide complete technical solutions" -- "Present analysis findings before file modifications" -- "Provide technical analysis before executing operations" - -## Session Guidelines - -### Before Starting Phase 1 - -Verify these steps are complete: - -- ✅ Session setup executed (memory:read_graph, time:get_current_time) -- ✅ DEVELOPER profile loaded -- ✅ LSP server running for target language -- ✅ Project path identified -- ✅ Entry points confirmed - -### During Each Phase - -#### DO - -- Use LSP tools systematically, not selectively -- Record exact numbers (don't estimate) -- Note tool failures (explain what didn't work and why) -- Take time for thorough analysis (patience over speed) -- Verify findings with multiple tools when possible - -#### DON'T - -- Skip phases even if you think you have enough information -- Make statements without tool verification -- Assume patterns without checking references -- Rush to conclusions before completing analysis -- Suggest fixes before finishing all phases - -### After Phase 9 - -Create conversation log with: -- Complete findings documented -- Immediate actions section for PR -- Code examples for critical fixes -- Testing considerations -- Next session preparation guidance - -### Related Framework Observations - -#### From DEVELOPER profile - -- "Follow analyze → discuss → implement sequence" -- "Require explicit approval before implement phase" -- "Perform root cause analysis before symptom treatment" -- "State technical conclusions definitively when evidence supports them" -- "Treat uncertainty as technical data" - -#### From language_server_protocol section - -- "Always analyze source code with language-server tools" -- "Consider symbol boundaries when calculating character positions" -- "Test tools systematically with precise positioning" -- "Monitor internally positioning assumption making" - -#### From tools section - -- "Use language-server tools for code-related operations" -- "Use claude:TodoWrite for systematic code reviews" - -## LSP Tools Reference - -### Navigation Tools (9 tools) - -- `get_symbol_definitions` - Navigate to symbol definition -- `get_symbol_references` - Find all symbol usage -- `get_type_definitions` - Navigate to type definition -- `get_implementations` - Find interface implementations -- `get_call_hierarchy` - Build call hierarchy -- `get_incoming_calls` - Find callers -- `get_outgoing_calls` - Find callees -- `get_type_hierarchy` - Build type hierarchy -- `get_supertypes` - Find parent types -- `get_subtypes` - Find child types - -### Code Intelligence Tools (10 tools) - -- `get_hover` - Get type information at position -- `get_signature` - Get signature help -- `get_completions` - Get completions at position -- `get_code_actions` - Get available refactorings -- `get_diagnostics` - Get errors/warnings -- `get_inlay_hints` - Get implicit type information -- `get_highlights` - Highlight symbol occurrences -- `get_linked_editing_range` - Find linked editing ranges -- `get_selection_range` - Get logical selection ranges -- `get_colors` - Extract color definitions - -### Symbol Tools (3 tools) - -- `get_symbols` - List symbols in document -- `get_project_symbols` - Search symbols in project -- `get_symbol_renames` - Preview rename impact - -### Formatting Tools (3 tools) - -- `get_format` - Format entire document -- `get_range_format` - Format code range -- `get_folding_ranges` - Get foldable regions - -### Additional Tools (8 tools) - -- `get_semantic_tokens` - Get semantic token information -- `get_links` - Get document links -- `get_server_capabilities` - Get server capabilities -- `get_server_projects` - List available projects -- `get_server_status` - Check server status -- `get_project_files` - List project files -- `start_server` - Start language server -- `stop_server` - Stop language server - -**Total: 39+ LSP tools available** - -## Quality Checklist - -Before finalizing the review, verify: - -- All 9 phases completed systematically -- At least 20+ LSP tool invocations performed -- Every claim backed by tool-verified data -- Exact file:line locations for all issues -- At least 5 rename operations tested -- Critical issues have code examples for fixes -- Metrics table populated with real numbers -- Refactoring safety assessed for major changes -- Conversation log created for next session -- Immediate actions clearly defined - -## Template Usage Instructions - -When starting a new code review session: - -1. Read this template completely before beginning -2. Copy the phase structure but fill with actual project data -3. Use the LSP tools listed in each phase systematically -4. Don't skip phases even if you think you have enough information -5. Record all findings as you discover them -6. Create conversation log after Phase 9 completion - -Framework will enforce: - -- Systematic tool usage (no speculation) -- Complete phase execution (no shortcuts) -- Tool-verified claims (no guesswork) -- Precise locations (file:line numbers) -- Actionable recommendations (code examples) - -**This template embodies**: Analyze → Verify → Document → Deliver diff --git a/README.md b/README.md index 288f773..37a31cb 100644 --- a/README.md +++ b/README.md @@ -78,15 +78,12 @@ A language server configuration has the following format: } ], "settings": { # Optional language server settings + "loggingLevel": "info", "maxConcurrentFileReads": 10, - "messageRequest": true, - "preloadFiles": true, "rateLimitMaxRequests": 100, "rateLimitWindowMs": 60000, - "registrationRequest": true, "shutdownGracePeriodMs": 100, - "timeoutMs": 600000, - "workspace": true + "timeoutMs": 600000 } } } @@ -116,15 +113,12 @@ For example, `pyright-langserver` requires the following settings: These settings control LSP protocol behavior and server compatibility: +- `loggingLevel` - language server logging verbosity with `debug`, `info`, `warning` and `error` supported levels (default: `info`) - `maxConcurrentFileReads` - maximum number of files to read concurrently when opening project files, controls memory usage and performance during project initialization (default: `10`) -- `messageRequest` - controls whether the language server can send `window/showMessage` requests to display user dialogs, disable for headless operation or automated environments (default: `true`) -- `preloadFiles` - controls whether project files are loaded during or after project initialization (default: `true`) - `rateLimitMaxRequests` - maximum number of requests allowed per rate limit window, prevents overwhelming the language server with too many concurrent requests (default: `100`) -- `rateLimitWindowMs` - time window in milliseconds for rate limiting, requests are counted within this sliding window (default: `60000` - 1 minute) -- `registrationRequest` - controls whether the language server can send `client/registerCapability` requests to dynamically register capabilities, disable for servers that ignore client capability declarations (default: `true`) +- `rateLimitWindowMs` - time window in milliseconds for rate limiting, requests are counted within this sliding window (default: `60000`) - `shutdownGracePeriodMs` - time in milliseconds to wait after sending shutdown request before forcing process termination, allows language server to complete cleanup operations (default: `100`) -- `timeoutMs` - maximum time in milliseconds to wait for language server initialization, prevents hanging on unresponsive servers (default: `600000` - 10 minutes) -- `workspace` - controls whether the language server initialization sends `workspace/symbol` requests to test workspace capabilities, disable for servers that don't support workspace operations or cause initialization failures (default: `true`) +- `timeoutMs` - maximum time in milliseconds to wait for language server initialization, prevents hanging on unresponsive servers (default: `600000`) #### Optional Language Server Project File Patterns @@ -177,13 +171,13 @@ To switch projects, restart the language server with the desired project name. Ask Claude to explain how the LSP tools work: - *Start the TypeScript language server with `typescript-sdk` project and check the server capabilities.* -- *Please explain how LSP tools help you understand and review source code.* +- *Please explain how the supported LSP tools help you understand and review source code.* To start performing a code review, ask Claude to: - *Start the TypeScript language server with `typescript-sdk` project and check the server capabilities.* - *Read the `/Users/username/github/mcp-lsp/.claude/templates/code-review.md` template prior code review.* -- *Perform a detailed review of project source code using the LSP tools and let me know your findings.* +- *Perform a detailed review of project source code, using the code review template and supported LSP tools.* > [!NOTE] > Language server start time varies by language and project size, typically few seconds for a project with thousands of files. Some language servers like `Kotlin` may take several minutes to initialize large projects. Increase `timeoutMs` value accordingly, if default timeout is reached. @@ -198,6 +192,9 @@ See the available [templates](.claude/templates) Claude can use for systematic d ## MCP Tools +> [!NOTE] +> See the [CHANGELOG](CHANGELOG.md) for version history and release details. + ### Server Management Tools 1. **`start_server`** diff --git a/package-lock.json b/package-lock.json index cf8026b..fb251e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@axivo/mcp-lsp", - "version": "1.0.5", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@axivo/mcp-lsp", - "version": "1.0.5", + "version": "1.1.0", "license": "BSD-3-Clause", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.0", diff --git a/package.json b/package.json index 21dad3c..b9beb9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@axivo/mcp-lsp", - "version": "1.0.5", + "version": "1.1.0", "type": "module", "description": "MCP server for Language Server Protocol integration", "keywords": [ diff --git a/src/server/client.ts b/src/server/client.ts index 6dab0d5..7e8fca0 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -11,7 +11,7 @@ import fg from 'fast-glob'; import find, { ProcessInfo } from 'find-process'; import gracefulFs from 'graceful-fs'; import { ChildProcess, exec, spawn } from 'node:child_process'; -import { dirname, isAbsolute, join } from 'node:path'; +import { isAbsolute, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; import pLimit from 'p-limit'; @@ -70,6 +70,7 @@ import { WorkspaceSymbolRequest } from 'vscode-languageserver-protocol'; import { Config, ProjectConfig } from './config.js'; +import { Logger } from './logger.js'; /** * Standardized response format for MCP tool execution @@ -113,6 +114,7 @@ export class Client { private config: Config; private connections: Map = new Map(); private initializedProjects: Set = new Set(); + private logger: Logger; private openedFiles: Map> = new Map(); private projectFiles: Map = new Map(); private projectId: Map = new Map(); @@ -122,22 +124,41 @@ export class Client { private serverStartTimes: Map = new Map(); private readonly execAsync = promisify(exec); private readonly readFileAsync = promisify(gracefulFs.readFile); - private readonly readFileSync = gracefulFs.readFileSync; private static readonly FallbackFilesLimit = 10; /** * Creates a new Client instance * - * @param {string} configPath - Path to the language server configuration file + * @param {Config} config - Validated configuration instance + * @param {Logger} logger - Logger instance for structured logging * @param {string} query - Default query */ - constructor(configPath: string, query: string) { - this.config = Config.load(configPath); + constructor(config: Config, logger: Logger, query: string) { + this.config = config; + this.logger = logger; this.query = query; process.on('SIGINT', () => this.shutdown()); process.on('SIGTERM', () => this.shutdown()); } + /** + * Gets package version from package.json + * + * Reads version from package.json using URL resolution relative to module. + * Returns error message string if reading fails rather than throwing. + * + * @returns {string} Package version string or error message + */ + static version(): string { + try { + const packagePath = fileURLToPath(new URL('../../package.json', import.meta.url)); + const packageJson = JSON.parse(gracefulFs.readFileSync(packagePath, 'utf8')); + return packageJson.version; + } catch (error) { + return `Failed to read package.json version. ${error}`; + } + } + /** * Checks project configuration validity for server operations * @@ -177,6 +198,8 @@ export class Client { const key = `ratelimit:${languageId}:${Math.floor(now / serverConfig.settings.rateLimitWindowMs)}`; const current = this.rateLimiter.get(key) ?? 0; if (current >= serverConfig.settings.rateLimitMaxRequests) { + const logMessage = `Rate limit exceeded: ${current}/${serverConfig.settings.rateLimitMaxRequests} requests`; + this.logger.log({ languageId, level: 'warning', message: logMessage }); return false; } this.rateLimiter.set(key, current + 1); @@ -210,22 +233,26 @@ export class Client { const serverConfig = this.config.getServerConfig(languageId); if (serverConfig.configuration && Object.keys(serverConfig.configuration).length) { connection.onRequest(ConfigurationRequest.method, (params: ConfigurationParams) => { + const logMessage = 'ConfigurationRequest received: returning server configuration'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); return [serverConfig.configuration]; }); } - if (serverConfig.settings.messageRequest === false) { - connection.onRequest(ShowMessageRequest.method, (params: ShowMessageParams) => { - return null; - }); - } - if (serverConfig.settings.registrationRequest === false) { - connection.onRequest(RegistrationRequest.method, (params: RegistrationParams) => { - return {}; - }); - connection.onRequest(UnregistrationRequest.method, (params: UnregistrationParams) => { - return {}; - }); - } + connection.onRequest(ShowMessageRequest.method, (params: ShowMessageParams) => { + const logMessage = 'ShowMessageRequest received and suppressed: returning null'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + return null; + }); + connection.onRequest(RegistrationRequest.method, (params: RegistrationParams) => { + const logMessage = 'RegistrationRequest received and suppressed: returning empty object'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + return {}; + }); + connection.onRequest(UnregistrationRequest.method, (params: UnregistrationParams) => { + const logMessage = 'UnregistrationRequest received and suppressed: returning empty object'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + return {}; + }); connection.listen(); return connection; } @@ -415,10 +442,7 @@ export class Client { }]; const initParams: InitializeParams = { capabilities: this.setClientCapabilities(languageId), - clientInfo: { - name: 'mcp-lsp-client', - version: this.version(), - }, + clientInfo: { name: 'mcp-lsp-client', version: Client.version() }, initializationOptions: serverConfig.configuration ?? {}, processId: process.pid, rootPath: projectConfig.path, @@ -442,9 +466,7 @@ export class Client { InitializeRequest.method, initParams, tokenSource.token - ).then(result => { - return result; - }), + ), timeout ]) as InitializeResult; serverConnection.capabilities = initializeResult.capabilities; @@ -454,19 +476,35 @@ export class Client { const projectFiles = this.projectFiles.get(project); try { if (projectFiles && projectFiles.length) { - if (serverConfig.settings.preloadFiles === true) { - await this.openFiles(languageId, project, projectFiles); - } - if (serverConfig.settings.workspace === true) { - const params = { query: this.query }; - await serverConnection.connection.sendRequest(WorkspaceSymbolRequest.method, params); + let openFiles = false; + const capabilities = this.getServerCapabilities(languageId); + if (capabilities && capabilities.workspaceSymbolProvider === true) { + try { + const params = { query: this.query }; + await serverConnection.connection.sendRequest(WorkspaceSymbolRequest.method, params); + const logMessage = 'WorkspaceSymbolRequest success: openFiles after initialization'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + } catch (error) { + openFiles = true; + const logMessage = 'WorkspaceSymbolRequest failure: openFiles before retry'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + await this.openFiles(languageId, project, projectFiles); + const params = { query: this.query }; + await serverConnection.connection.sendRequest(WorkspaceSymbolRequest.method, params); + } } serverConnection.initialized = true; - if (serverConfig.settings.preloadFiles === false) { + const logMessage = 'Server initialization completed: ready for requests'; + this.logger.log({ languageId, level: 'info', message: logMessage }); + if (!openFiles) { + const logMessage = 'Server initialization completed: openFiles required'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); await this.openFiles(languageId, project, projectFiles); } } } catch (error) { + const logMessage = `Server initialization failed: ${error}`; + this.logger.log({ languageId, level: 'error', message: logMessage }); serverConnection.initialized = false; } } finally { @@ -708,6 +746,9 @@ export class Client { /** * Gets the project name for a specific language ID * + * Retrieves the currently active project name associated with a language server. + * Returns undefined if no project is running for the specified language. + * * @param {string} languageId - Language identifier * @returns {string | undefined} Project name for the language, or undefined if not found */ @@ -718,9 +759,12 @@ export class Client { /** * Gets the cached project files for a specific project * - * @param {string} languageId - Language identifier - * @param {string} project - Project name - * @returns {Promise} Promise that resolves with array of file paths or null if not found + * Retrieves the list of discovered files from cache or performs file discovery + * if cache is empty. Returns null if server or project configuration is invalid. + * + * @param {string} languageId - Language identifier for server configuration lookup + * @param {string} project - Project name to retrieve files for + * @returns {Promise} Promise that resolves with array of absolute file paths, or null if configuration invalid */ async getProjectFiles(languageId: string, project: string): Promise { if (!this.config.hasServerConfig(languageId)) { @@ -741,14 +785,21 @@ export class Client { /** * Gets server capabilities for a specific language server - * - * @param {string} languageId - Language identifier - * @returns {ServerCapabilities | undefined} Server capabilities or undefined if not available + * + * Retrieves LSP capabilities from initialized server connection. + * Returns undefined if server is not running or not yet initialized. + * + * @param {string} languageId - Language identifier for server lookup + * @returns {ServerCapabilities | undefined} LSP server capabilities object, or undefined if server not available or not initialized */ getServerCapabilities(languageId: string): ServerCapabilities | undefined { const project = this.projectId.get(languageId); if (project) { const serverConnection = this.connections.get(project); + if (serverConnection?.capabilities) { + const logMessage = `Server capabilities retrieved: ${JSON.stringify(serverConnection.capabilities)}`; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + } return serverConnection?.capabilities; } return undefined; @@ -757,8 +808,11 @@ export class Client { /** * Gets server connection for status checking * - * @param {string} languageId - Language identifier - * @returns {ServerConnection | undefined} Server connection or undefined + * Retrieves the active server connection object including process and initialization state. + * Used primarily for status monitoring and diagnostic operations. + * + * @param {string} languageId - Language identifier for server lookup + * @returns {ServerConnection | undefined} Server connection with process and state information, or undefined if not running */ getServerConnection(languageId: string): ServerConnection | undefined { const project = this.projectId.get(languageId); @@ -771,7 +825,10 @@ export class Client { /** * Gets all configured servers * - * @returns {string[]} Array of servers + * Retrieves the complete list of language server identifiers from configuration. + * Includes both running and non-running servers. + * + * @returns {string[]} Array of language server identifiers from configuration */ getServers(): string[] { return this.config.getServers(); @@ -780,8 +837,11 @@ export class Client { /** * Gets the uptime of a specific language server in milliseconds * - * @param {string} languageId - Language identifier - * @returns {number} Uptime in milliseconds, or 0 if server not running + * Calculates elapsed time since server startup using cached start timestamp. + * Returns zero if server is not currently running. + * + * @param {string} languageId - Language identifier for server lookup + * @returns {number} Server uptime in milliseconds since startup, or 0 if server not running */ getServerUptime(languageId: string): number { const project = this.projectId.get(languageId); @@ -797,9 +857,12 @@ export class Client { /** * Checks if a language server is alive and can handle the specified project path * - * @param {string} languageId - Language identifier - * @param {string} path - Project path to validate against server configuration - * @returns {boolean} True if server is alive and can handle the project + * Validates that the server is running and that the specified path falls within + * the server's configured project path. Used for routing file requests to appropriate servers. + * + * @param {string} languageId - Language identifier for server lookup + * @param {string} path - Absolute file or project path to validate against server configuration + * @returns {boolean} True if server is running and path is within server's project scope, false otherwise */ isServerAlive(languageId: string, path: string): boolean { const project = this.projectId.get(languageId); @@ -814,8 +877,11 @@ export class Client { /** * Checks if a specific language server is currently running * - * @param {string} languageId - Language identifier - * @returns {boolean} True if server is running + * Verifies that a server process exists and has an active connection. + * Does not check initialization or readiness state. + * + * @param {string} languageId - Language identifier for server lookup + * @returns {boolean} True if server process is running with active connection, false otherwise */ isServerRunning(languageId: string): boolean { const project = this.projectId.get(languageId); @@ -1074,6 +1140,8 @@ export class Client { return this.response(`Language server '${languageId}' with '${runningProject}' project is already running.`); } try { + const logMessage = 'Server start initiated: spawning language server process'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); const timer = Date.now(); if (config.server.init?.length) { for (const command of config.server.init) { @@ -1144,11 +1212,20 @@ export class Client { return; } const languageId = this.getLanguageId(project); + if (languageId) { + const logMessage = 'Server shutdown started: sending LSP ShutdownRequest'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + } if (languageId) { const serverConfig = this.config.getServerConfig(languageId); try { await serverConnection.connection.sendRequest(ShutdownRequest.method, {}); - } catch (error) { } + const logMessage = 'LSP ShutdownRequest completed: waiting for grace period'; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + } catch (error) { + const logMessage = `LSP ShutdownRequest failed: ${error}`; + this.logger.log({ languageId, level: 'debug', message: logMessage }); + } await new Promise(resolve => setTimeout(resolve, serverConfig.settings.shutdownGracePeriodMs)); this.projectId.delete(languageId); } @@ -1172,25 +1249,9 @@ export class Client { this.openedFiles.delete(project); this.projectFiles.delete(project); this.serverStartTimes.delete(project); - } - - /** - * Gets package version from package.json - * - * Reads version from package.json relative to current module location. - * Returns error message string if reading fails rather than throwing. - * - * @returns {string} Package version string or error message - */ - version(): string { - try { - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - const packagePath = join(__dirname, '../../package.json'); - const packageJson = JSON.parse(this.readFileSync(packagePath, 'utf8')); - return packageJson.version; - } catch (error) { - return `Failed to read package.json version. ${error}`; + if (languageId) { + const logMessage = 'Server shutdown completed: process terminated and cleanup finished'; + this.logger.log({ languageId, level: 'info', message: logMessage }); } } } diff --git a/src/server/config.ts b/src/server/config.ts index b6116e5..99f78a4 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -67,15 +67,12 @@ export interface ServerConfig { init?: string[]; projects: ProjectConfig[]; settings?: { + loggingLevel?: 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency'; maxConcurrentFileReads?: number; - messageRequest?: boolean; - preloadFiles?: boolean; rateLimitMaxRequests?: number; rateLimitWindowMs?: number; - registrationRequest?: boolean; shutdownGracePeriodMs?: number; timeoutMs?: number; - workspace?: boolean; }; } @@ -110,15 +107,12 @@ export class Config { init: z.array(z.string()).optional(), projects: z.array(Config.ProjectConfigSchema).min(1), settings: z.object({ + loggingLevel: z.enum(['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency']).optional(), maxConcurrentFileReads: z.number().optional(), - messageRequest: z.boolean().optional(), - preloadFiles: z.boolean().optional(), rateLimitMaxRequests: z.number().optional(), rateLimitWindowMs: z.number().optional(), - registrationRequest: z.boolean().optional(), shutdownGracePeriodMs: z.number().optional(), timeoutMs: z.number().optional(), - workspace: z.boolean().optional() }).optional() }); private static readonly ConfigSchema = z.object({ @@ -142,7 +136,7 @@ export class Config { } /** - * Load and validate configuration from file + * Validates configuration from file * * Reads JSON configuration file, validates structure and content using Zod schema, * and returns validated Config instance. Provides detailed error messages for @@ -153,7 +147,7 @@ export class Config { * @returns {Config} Validated Config instance * @throws {Error} If file cannot be read or configuration is invalid */ - static load(configPath: string): Config { + static validate(configPath: string): Config { try { const configData = readFileSync(configPath, 'utf-8'); const parsedData = JSON.parse(configData); @@ -200,15 +194,12 @@ export class Config { * init?: string[], * projects: ProjectConfig[], * settings: { + * loggingLevel?: 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency', * maxConcurrentFileReads: number, - * messageRequest: boolean, - * preloadFiles: boolean, * rateLimitMaxRequests: number, * rateLimitWindowMs: number, - * registrationRequest: boolean, * shutdownGracePeriodMs: number, - * timeoutMs: number, - * workspace: boolean + * timeoutMs: number * } * }} Complete server configuration with applied defaults */ @@ -222,15 +213,12 @@ export class Config { init?: string[]; projects: ProjectConfig[]; settings: { + loggingLevel?: 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency'; maxConcurrentFileReads: number; - messageRequest: boolean; - preloadFiles: boolean; rateLimitMaxRequests: number; rateLimitWindowMs: number; - registrationRequest: boolean; shutdownGracePeriodMs: number; timeoutMs: number; - workspace: boolean; }; } { const serverConfig = this.config.servers[languageId]; @@ -244,14 +232,10 @@ export class Config { projects: [], settings: { maxConcurrentFileReads: 10, - messageRequest: true, - preloadFiles: true, rateLimitMaxRequests: 100, rateLimitWindowMs: 60000, - registrationRequest: true, shutdownGracePeriodMs: 100, - timeoutMs: 600000, - workspace: true + timeoutMs: 600000 } }; } @@ -265,15 +249,12 @@ export class Config { init: serverConfig.init, projects: serverConfig.projects, settings: { + loggingLevel: serverConfig.settings?.loggingLevel, maxConcurrentFileReads: serverConfig.settings?.maxConcurrentFileReads ?? 10, - messageRequest: serverConfig.settings?.messageRequest ?? true, - preloadFiles: serverConfig.settings?.preloadFiles ?? true, rateLimitMaxRequests: serverConfig.settings?.rateLimitMaxRequests ?? 100, rateLimitWindowMs: serverConfig.settings?.rateLimitWindowMs ?? 60000, - registrationRequest: serverConfig.settings?.registrationRequest ?? true, shutdownGracePeriodMs: serverConfig.settings?.shutdownGracePeriodMs ?? 100, - timeoutMs: serverConfig.settings?.timeoutMs ?? 600000, - workspace: serverConfig.settings?.workspace ?? true + timeoutMs: serverConfig.settings?.timeoutMs ?? 600000 } }; } diff --git a/src/server/logger.ts b/src/server/logger.ts new file mode 100644 index 0000000..485664a --- /dev/null +++ b/src/server/logger.ts @@ -0,0 +1,90 @@ +/** + * Logging utility for MCP server + * + * @module server/logger + * @author AXIVO + * @license BSD-3-Clause + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { LoggingLevel } from '@modelcontextprotocol/sdk/types.js'; +import { Config } from './config.js'; + +/** + * Log message parameters + * + * @interface LogMessage + * @property {string} languageId - Language server identifier + * @property {LoggingLevel} level - Log severity level + * @property {string} message - Log message content + */ +interface LogMessage { + languageId: string; + level: LoggingLevel; + message: string; +} + +/** + * Logger for MCP server with severity-based filtering + * + * Provides structured logging through MCP protocol with syslog hierarchy + * filtering based on configured logging levels per language server. + * + * @export + * @class Logger + */ +export class Logger { + private config: Config; + private server: Server; + + /** + * Creates a new Logger instance + * + * @param {Config} config - Configuration instance for reading logging levels + * @param {Server} server - MCP server instance for sending log messages + */ + constructor(config: Config, server: Server) { + this.config = config; + this.server = server; + } + + /** + * Sends structured logging message via MCP protocol + * + * Emits log messages with severity-based filtering using syslog hierarchy. + * Messages are only sent if their severity meets or exceeds the configured + * loggingLevel threshold for the language server. + * + * @param {LogMessage} args - Log message parameters + * @returns {Promise} Promise that resolves when message is sent + */ + async log(args: LogMessage): Promise { + const loggingLevel = this.getLoggingLevel(args.languageId); + const level: LoggingLevel[] = ['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency']; + const messageIndex = level.indexOf(args.level); + const configIndex = level.indexOf(loggingLevel); + if (messageIndex < configIndex) { + return; + } + await this.server.sendLoggingMessage({ + level: args.level, + data: args.message + }); + } + + /** + * Retrieves configured logging level for language server + * + * Reads loggingLevel setting from server configuration to determine + * which log messages should be emitted based on severity filtering. + * Defaults to 'info' if not configured. + * + * @private + * @param {string} languageId - Language server identifier + * @returns {LoggingLevel} Configured logging level or 'info' if not set + */ + private getLoggingLevel(languageId: string): LoggingLevel { + const serverConfig = this.config.getServerConfig(languageId); + return serverConfig.settings.loggingLevel ?? 'info'; + } +} diff --git a/src/server/mcp.ts b/src/server/mcp.ts index b3395d7..4fb2f82 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -73,6 +73,7 @@ import { import { z } from 'zod'; import { Client, Response } from './client.js'; import { Config } from './config.js'; +import { Logger } from './logger.js'; import { McpTool } from './tool.js'; /** @@ -133,8 +134,13 @@ interface GetDiagnostics extends FilePath { } * * @interface GetCompletions * @extends Position + * @property {number} [limit] - Maximum number of completions to return + * @property {number} [offset] - Pagination offset for completion listing */ -interface GetCompletions extends Position { } +interface GetCompletions extends Position { + limit?: number; + offset?: number; +} /** * Parameters for folding range identification requests @@ -621,6 +627,7 @@ export class McpServer { private client: Client; private config: Config; private limit: number; + private logger: Logger; private query: string; private server: Server; private tool: McpTool; @@ -636,14 +643,15 @@ export class McpServer { * @param {string} configPath - Path to the LSP configuration JSON file */ constructor(configPath: string) { - this.query = ''; - this.client = new Client(configPath, this.query); - this.config = Config.load(configPath); this.limit = 250; + this.query = ''; this.server = new Server( - { name: 'language-server-protocol', version: this.client.version() }, - { capabilities: { tools: {} } } + { name: 'mcp-lsp', version: Client.version() }, + { capabilities: { logging: {}, tools: {} } } ); + this.config = Config.validate(configPath); + this.logger = new Logger(this.config, this.server); + this.client = new Client(this.config, this.logger, this.query); this.tool = new McpTool(this.limit, this.query); this.toolHandler = new Map(); this.setupToolHandlers(); @@ -698,7 +706,9 @@ export class McpServer { */ private async getCallHierarchy(args: GetCallHierarchy): Promise { const error = this.validate(args, ['file_path', 'character', 'line']); - if (error) return error; + if (error) { + return error; + } const params: CallHierarchyPrepareParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -718,7 +728,9 @@ export class McpServer { */ private async getCodeActions(args: GetCodeActions): Promise { const error = this.validate(args, ['file_path', 'character', 'line']); - if (error) return error; + if (error) { + return error; + } const params: CodeActionParams = { context: { diagnostics: [] }, range: { @@ -742,7 +754,9 @@ export class McpServer { */ private async getCodeResolves(args: GetCodeResolves): Promise { const error = this.validate(args, ['file_path', 'item']); - if (error) return error; + if (error) { + return error; + } return await this.client.sendServerRequest(args.file_path, CodeActionResolveRequest.method, args.item); } @@ -757,7 +771,9 @@ export class McpServer { */ private async getColors(args: GetColors): Promise { const error = this.validate(args, ['file_path']); - if (error) return error; + if (error) { + return error; + } const params = { textDocument: { uri: `file://${args.file_path}` } }; @@ -776,12 +792,24 @@ export class McpServer { */ private async getCompletions(args: GetCompletions): Promise { const error = this.validate(args, ['file_path', 'character', 'line']); - if (error) return error; + if (error) { + return error; + } + const timer = Date.now(); const params: TextDocumentPositionParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } }; - return await this.client.sendServerRequest(args.file_path, CompletionRequest.method, params); + const fullResult = await this.client.sendServerRequest(args.file_path, CompletionRequest.method, params); + if (typeof fullResult === 'string' || !Array.isArray(fullResult)) { + return this.client.response(fullResult); + } + const description = `Showing completions for '${args.file_path}' file.`; + const elapsed = Date.now() - timer; + return this.paginatedResponse(fullResult, args, description, { + file_path: args.file_path, + time: `${elapsed}ms` + }); } /** @@ -796,7 +824,9 @@ export class McpServer { */ private async getDiagnostics(args: GetDiagnostics): Promise { const error = this.validate(args, ['file_path']); - if (error) return error; + if (error) { + return error; + } const params = { textDocument: { uri: `file://${args.file_path}` } }; @@ -815,7 +845,9 @@ export class McpServer { */ private async getFoldingRanges(args: GetFoldingRanges): Promise { const error = this.validate(args, ['file_path']); - if (error) return error; + if (error) { + return error; + } const params = { textDocument: { uri: `file://${args.file_path}` } }; @@ -834,7 +866,9 @@ export class McpServer { */ private async getFormat(args: GetFormat): Promise { const error = this.validate(args, ['file_path']); - if (error) return error; + if (error) { + return error; + } const params = { options: { tabSize: 2, insertSpaces: true }, textDocument: { uri: `file://${args.file_path}` } @@ -854,7 +888,9 @@ export class McpServer { */ private async getHighlights(args: GetHighlights): Promise { const error = this.validate(args, ['file_path', 'character', 'line']); - if (error) return error; + if (error) { + return error; + } const params: TextDocumentPositionParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -874,7 +910,9 @@ export class McpServer { */ private async getHover(args: GetHover): Promise { const error = this.validate(args, ['file_path', 'character', 'line']); - if (error) return error; + if (error) { + return error; + } const params: TextDocumentPositionParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -894,7 +932,9 @@ export class McpServer { */ private async getImplementations(args: GetImplementations): Promise { const error = this.validate(args, ['file_path', 'character', 'line']); - if (error) return error; + if (error) { + return error; + } const params: TextDocumentPositionParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -914,7 +954,9 @@ export class McpServer { */ private async getIncomingCalls(args: GetIncomingCalls): Promise { const error = this.validate(args, ['item']); - if (error) return error; + if (error) { + return error; + } const params: CallHierarchyIncomingCallsParams = { item: args.item }; @@ -934,7 +976,9 @@ export class McpServer { */ private async getInlayHint(args: GetInlayHint): Promise { const error = this.validate(args, ['file_path', 'item']); - if (error) return error; + if (error) { + return error; + } return await this.client.sendServerRequest(args.file_path, InlayHintResolveRequest.method, args.item); } @@ -950,7 +994,9 @@ export class McpServer { */ private async getInlayHints(args: GetInlayHints): Promise { const error = this.validate(args, ['end_character', 'end_line', 'file_path', 'start_character', 'start_line']); - if (error) return error; + if (error) { + return error; + } const params: InlayHintParams = { range: { start: { character: args.start_character, line: args.start_line }, @@ -973,7 +1019,9 @@ export class McpServer { */ private async getLinkedEditingRange(args: GetLinkedEditingRange): Promise { const error = this.validate(args, ['character', 'file_path', 'line']); - if (error) return error; + if (error) { + return error; + } const params: LinkedEditingRangeParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -993,7 +1041,9 @@ export class McpServer { */ private async getLinkResolves(args: GetLinkResolves): Promise { const error = this.validate(args, ['file_path', 'item']); - if (error) return error; + if (error) { + return error; + } return await this.client.sendServerRequest(args.file_path, DocumentLinkResolveRequest.method, args.item); } @@ -1009,7 +1059,9 @@ export class McpServer { */ private async getLinks(args: GetLinks): Promise { const error = this.validate(args, ['file_path']); - if (error) return error; + if (error) { + return error; + } const params = { textDocument: { uri: `file://${args.file_path}` } }; @@ -1028,7 +1080,9 @@ export class McpServer { */ private async getOutgoingCalls(args: GetOutgoingCalls): Promise { const error = this.validate(args, ['item']); - if (error) return error; + if (error) { + return error; + } const params: CallHierarchyOutgoingCallsParams = { item: args.item }; @@ -1048,7 +1102,9 @@ export class McpServer { */ private async getProjectFiles(args: GetProjectFiles): Promise { const error = this.validate(args, ['language_id', 'project']); - if (error) return error; + if (error) { + return error; + } if (args.project !== this.client.getProjectId(args.language_id)) { return `Language server '${args.language_id}' for project '${args.project}' is not running.`; } @@ -1078,7 +1134,9 @@ export class McpServer { */ private async getProjectSymbols(args: GetProjectSymbols): Promise { const error = this.validate(args, ['language_id', 'project', 'query']); - if (error) return error; + if (error) { + return error; + } const timer = Date.now(); if (args.project !== this.client.getProjectId(args.language_id)) { return `Language server '${args.language_id}' for project '${args.project}' is not running.`; @@ -1111,7 +1169,9 @@ export class McpServer { */ private async getRangeFormat(args: GetRangeFormat): Promise { const error = this.validate(args, ['end_character', 'end_line', 'file_path', 'start_character', 'start_line']); - if (error) return error; + if (error) { + return error; + } const params = { options: { tabSize: 2, insertSpaces: true }, range: { @@ -1135,7 +1195,9 @@ export class McpServer { */ private async getResolves(args: GetResolves): Promise { const error = this.validate(args, ['file_path', 'item']); - if (error) return error; + if (error) { + return error; + } const params = { ...args.item, uri: `file://${args.file_path}` @@ -1155,7 +1217,9 @@ export class McpServer { */ private async getSelectionRange(args: GetSelectionRange): Promise { const error = this.validate(args, ['character', 'file_path', 'line']); - if (error) return error; + if (error) { + return error; + } const params: SelectionRangeParams = { positions: [{ character: args.character, line: args.line }], textDocument: { uri: `file://${args.file_path}` } @@ -1175,7 +1239,9 @@ export class McpServer { */ private async getSemanticTokens(args: GetSemanticTokens): Promise { const error = this.validate(args, ['file_path']); - if (error) return error; + if (error) { + return error; + } const params: SemanticTokensParams = { textDocument: { uri: `file://${args.file_path}` } }; @@ -1195,7 +1261,9 @@ export class McpServer { */ private async getServerCapabilities(args: GetServerCapabilities, toolCapabilities?: ToolCapabilities[]): Promise { const error = this.validate(args, ['language_id']); - if (error) return error; + if (error) { + return error; + } if (!this.config.hasServerConfig(args.language_id)) { return `Language server '${args.language_id}' is not configured.`; } @@ -1226,7 +1294,9 @@ export class McpServer { */ private async getServerProjects(args: GetServerProjects): Promise { const error = this.validate(args, ['language_id']); - if (error) return error; + if (error) { + return error; + } if (!this.config.hasServerConfig(args.language_id)) { return `Language server '${args.language_id}' is not configured.`; } @@ -1318,7 +1388,9 @@ export class McpServer { */ private async getSignature(args: GetSignature): Promise { const error = this.validate(args, ['character', 'file_path', 'line']); - if (error) return error; + if (error) { + return error; + } const params: TextDocumentPositionParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -1338,7 +1410,9 @@ export class McpServer { */ private async getSubtypes(args: GetSubtypes): Promise { const error = this.validate(args, ['item']); - if (error) return error; + if (error) { + return error; + } const params: TypeHierarchySubtypesParams = { item: args.item }; @@ -1358,7 +1432,9 @@ export class McpServer { */ private async getSupertypes(args: GetSupertypes): Promise { const error = this.validate(args, ['item']); - if (error) return error; + if (error) { + return error; + } const params: TypeHierarchySupertypesParams = { item: args.item }; @@ -1378,7 +1454,9 @@ export class McpServer { */ private async getSymbolDefinitions(args: GetSymbolDefinitions): Promise { const error = this.validate(args, ['character', 'file_path', 'line']); - if (error) return error; + if (error) { + return error; + } const params: TextDocumentPositionParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -1398,7 +1476,9 @@ export class McpServer { */ private async getSymbolReferences(args: GetSymbolReferences): Promise { const error = this.validate(args, ['character', 'file_path', 'line']); - if (error) return error; + if (error) { + return error; + } const params: ReferenceParams = { context: { includeDeclaration: args.include_declaration ?? true }, position: { character: args.character, line: args.line }, @@ -1419,7 +1499,9 @@ export class McpServer { */ private async getSymbolRenames(args: GetSymbolRenames): Promise { const error = this.validate(args, ['character', 'file_path', 'line', 'new_name']); - if (error) return error; + if (error) { + return error; + } const params: RenameParams = { newName: args.new_name, position: { character: args.character, line: args.line }, @@ -1440,7 +1522,9 @@ export class McpServer { */ private async getSymbols(args: GetSymbols): Promise { const error = this.validate(args, ['file_path']); - if (error) return error; + if (error) { + return error; + } const timer = Date.now(); const params = { textDocument: { uri: `file://${args.file_path}` } @@ -1469,7 +1553,9 @@ export class McpServer { */ private async getTypeDefinitions(args: GetTypeDefinitions): Promise { const error = this.validate(args, ['character', 'file_path', 'line']); - if (error) return error; + if (error) { + return error; + } const params: TextDocumentPositionParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -1489,7 +1575,9 @@ export class McpServer { */ private async getTypeHierarchy(args: GetTypeHierarchy): Promise { const error = this.validate(args, ['character', 'file_path', 'line']); - if (error) return error; + if (error) { + return error; + } const params: TypeHierarchyPrepareParams = { position: { character: args.character, line: args.line }, textDocument: { uri: `file://${args.file_path}` } @@ -1566,7 +1654,9 @@ export class McpServer { */ private async restartServer(args: RestartServer): Promise { const error = this.validate(args, ['language_id', 'project']); - if (error) return error; + if (error) { + return error; + } return await this.client.restartServer(args.language_id, args.project); } @@ -1693,7 +1783,9 @@ export class McpServer { */ private async startServer(args: StartServer): Promise { const error = this.validate(args, ['language_id']); - if (error) return error; + if (error) { + return error; + } return await this.client.startServer(args.language_id, args.project); } @@ -1709,7 +1801,9 @@ export class McpServer { */ private async stopServer(args: StopServer): Promise { const error = this.validate(args, ['language_id']); - if (error) return error; + if (error) { + return error; + } if (!this.client.isServerRunning(args.language_id)) { return `Language server '${args.language_id}' is not running.`; } diff --git a/src/server/tool.ts b/src/server/tool.ts index 012dfd2..8e70a92 100644 --- a/src/server/tool.ts +++ b/src/server/tool.ts @@ -166,7 +166,9 @@ export class McpTool { properties: { character: { type: 'number', description: 'Character position (zero-based)' }, file_path: { type: 'string', description: 'Path to the project file' }, - line: { type: 'number', description: 'Line number (zero-based)' } + line: { type: 'number', description: 'Line number (zero-based)' }, + limit: { type: 'number', description: 'Pagination limit for number of completions to return', default: this.limit }, + offset: { type: 'number', description: 'Pagination offset for number of completions to skip', default: 0 } }, required: ['character', 'file_path', 'line'] }, @@ -636,7 +638,8 @@ export class McpTool { _meta: { usage: [ 'Field `supported` defines which tools can be used with language server', - 'Returns capabilities metadata including tool definitions and usage notes' + 'Returns capabilities metadata including tool definitions and usage notes', + 'Returns tools that can be used with language server' ] } };