#
tokens: 30100/50000 17/17 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── app
│   └── Server.hs
├── index.ts
├── LICENSE
├── lsp-mcp.cabal
├── package.json
├── README.md
├── src
│   ├── extensions
│   │   ├── haskell.ts
│   │   └── index.ts
│   ├── logging
│   │   └── index.ts
│   ├── lspClient.ts
│   ├── prompts
│   │   └── index.ts
│   ├── resources
│   │   └── index.ts
│   ├── tools
│   │   └── index.ts
│   └── types
│       └── index.ts
├── test
│   ├── prompts.test.js
│   ├── ts-project
│   │   ├── src
│   │   │   └── example.ts
│   │   └── tsconfig.json
│   └── typescript-lsp.test.js
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
dist-newstyle/
node_modules/
dist/
.mcp.json

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# LSP MCP Server

An MCP (Model Context Protocol) server for interacting with LSP (Language Server Protocol) interface.
This server acts as a bridge that allows LLMs to query LSP Hover and Completion providers.

## Overview

The MCP Server works by:
1. Starting an LSP client that connects to a LSP server
2. Exposing MCP tools that send requests to the LSP server
3. Returning the results in a format that LLMs can understand and use

This enables LLMs to utilize LSPs for more accurate code suggestions.


## Configuration:

```json
{
  "mcpServers": {
    "lsp-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "tritlo/lsp-mcp",
        "<language-id>",
        "<path-to-lsp>",
        "<lsp-args>"
      ]
    }
  }
}
```


## Features

### MCP Tools
- `get_info_on_location`: Get hover information at a specific location in a file
- `get_completions`: Get completion suggestions at a specific location in a file
- `get_code_actions`: Get code actions for a specific range in a file
- `open_document`: Open a file in the LSP server for analysis
- `close_document`: Close a file in the LSP server
- `get_diagnostics`: Get diagnostic messages (errors, warnings) for open files
- `start_lsp`: Start the LSP server with a specified root directory
- `restart_lsp_server`: Restart the LSP server without restarting the MCP server
- `set_log_level`: Change the server's logging verbosity level at runtime

### MCP Resources
- `lsp-diagnostics://` resources for accessing diagnostic messages with real-time updates via subscriptions
- `lsp-hover://` resources for retrieving hover information at specific file locations
- `lsp-completions://` resources for getting code completion suggestions at specific positions

### Additional Features
- Comprehensive logging system with multiple severity levels
- Colorized console output for better readability
- Runtime-configurable log level
- Detailed error handling and reporting
- Simple command-line interface

## Prerequisites

- Node.js (v16 or later)
- npm

For the demo server:
- GHC (8.10 or later)
- Cabal (3.0 or later)

## Installation

### Building the MCP Server

1. Clone this repository:
   ```
   git clone https://github.com/your-username/lsp-mcp.git
   cd lsp-mcp
   ```

2. Install dependencies:
   ```
   npm install
   ```

3. Build the MCP server:
   ```
   npm run build
   ```

## Testing

The project includes integration tests for the TypeScript LSP support. These tests verify that the LSP-MCP server correctly handles LSP operations like hover information, completions, diagnostics, and code actions.

### Running Tests

To run the TypeScript LSP tests:

```
npm test
```

or specifically:

```
npm run test:typescript
```

### Test Coverage

The tests verify the following functionality:
- Initializing the TypeScript LSP with a mock project
- Opening TypeScript files for analysis
- Getting hover information for functions and types
- Getting code completion suggestions
- Getting diagnostic error messages
- Getting code actions for errors

The test project is located in `test/ts-project/` and contains TypeScript files with intentional errors to test diagnostic feedback.

## Usage

Run the MCP server by providing the path to the LSP executable and any arguments to pass to the LSP server:

```
npx tritlo/lsp-mcp <language> /path/to/lsp [lsp-args...]
```

For example:
```
npx tritlo/lsp-mcp haskell /usr/bin/haskell-language-server-wrapper lsp
```

### Important: Starting the LSP Server

With version 0.2.0 and later, you must explicitly start the LSP server by calling the `start_lsp` tool before using any LSP functionality. This ensures proper initialization with the correct root directory, which is especially important when using tools like npx:

```json
{
  "tool": "start_lsp",
  "arguments": {
    "root_dir": "/path/to/your/project"
  }
}
```

### Logging

The server includes a comprehensive logging system with 8 severity levels:
- `debug`: Detailed information for debugging purposes
- `info`: General informational messages about system operation
- `notice`: Significant operational events
- `warning`: Potential issues that might need attention
- `error`: Error conditions that affect operation but don't halt the system
- `critical`: Critical conditions requiring immediate attention
- `alert`: System is in an unstable state
- `emergency`: System is unusable

By default, logs are sent to:
1. Console output with color-coding for better readability
2. MCP notifications to the client (via the `notifications/message` method)

#### Viewing Debug Logs

For detailed debugging, you can:

1. Use the `claude --mcp-debug` flag when running Claude to see all MCP traffic between Claude and the server:
   ```
   claude --mcp-debug
   ```

2. Change the log level at runtime using the `set_log_level` tool:
   ```json
   {
     "tool": "set_log_level",
     "arguments": {
       "level": "debug"
     }
   }
   ```

The default log level is `info`, which shows moderate operational detail while filtering out verbose debug messages.

## API

The server provides the following MCP tools:

### get_info_on_location

Gets hover information at a specific location in a file.

Parameters:
- `file_path`: Path to the file
- `language_id`: The programming language the file is written in (e.g., "haskell")
- `line`: Line number
- `column`: Column position

Example:
```json
{
  "tool": "get_info_on_location",
  "arguments": {
    "file_path": "/path/to/your/file",
    "language_id": "haskell",
    "line": 3,
    "column": 5
  }
}
```

### get_completions

Gets completion suggestions at a specific location in a file.

Parameters:
- `file_path`: Path to the file
- `language_id`: The programming language the file is written in (e.g., "haskell")
- `line`: Line number
- `column`: Column position

Example:
```json
{
  "tool": "get_completions",
  "arguments": {
    "file_path": "/path/to/your/file",
    "language_id": "haskell",
    "line": 3,
    "column": 10
  }
}
```

### get_code_actions

Gets code actions for a specific range in a file.

Parameters:
- `file_path`: Path to the file
- `language_id`: The programming language the file is written in (e.g., "haskell")
- `start_line`: Start line number
- `start_column`: Start column position
- `end_line`: End line number
- `end_column`: End column position

Example:
```json
{
  "tool": "get_code_actions",
  "arguments": {
    "file_path": "/path/to/your/file",
    "language_id": "haskell",
    "start_line": 3,
    "start_column": 5,
    "end_line": 3,
    "end_column": 10
  }
}
```

### start_lsp

Starts the LSP server with a specified root directory. This must be called before using any other LSP-related tools.

Parameters:
- `root_dir`: The root directory for the LSP server (absolute path recommended)

Example:
```json
{
  "tool": "start_lsp",
  "arguments": {
    "root_dir": "/path/to/your/project"
  }
}
```

### restart_lsp_server

Restarts the LSP server process without restarting the MCP server. This is useful for recovering from LSP server issues or for applying changes to the LSP server configuration.

Parameters:
- `root_dir`: (Optional) The root directory for the LSP server. If provided, the server will be initialized with this directory after restart.

Example without root_dir (uses previously set root directory):
```json
{
  "tool": "restart_lsp_server",
  "arguments": {}
}
```

Example with root_dir:
```json
{
  "tool": "restart_lsp_server",
  "arguments": {
    "root_dir": "/path/to/your/project"
  }
}
```

### open_document

Opens a file in the LSP server for analysis. This must be called before accessing diagnostics or performing other operations on the file.

Parameters:
- `file_path`: Path to the file to open
- `language_id`: The programming language the file is written in (e.g., "haskell")

Example:
```json
{
  "tool": "open_document",
  "arguments": {
    "file_path": "/path/to/your/file",
    "language_id": "haskell"
  }
}
```

### close_document

Closes a file in the LSP server when you're done working with it. This helps manage resources and cleanup.

Parameters:
- `file_path`: Path to the file to close

Example:
```json
{
  "tool": "close_document",
  "arguments": {
    "file_path": "/path/to/your/file"
  }
}
```

### get_diagnostics

Gets diagnostic messages (errors, warnings) for one or all open files.

Parameters:
- `file_path`: (Optional) Path to the file to get diagnostics for. If not provided, returns diagnostics for all open files.

Example for a specific file:
```json
{
  "tool": "get_diagnostics",
  "arguments": {
    "file_path": "/path/to/your/file"
  }
}
```

Example for all open files:
```json
{
  "tool": "get_diagnostics",
  "arguments": {}
}
```

### set_log_level

Sets the server's logging level to control verbosity of log messages.

Parameters:
- `level`: The logging level to set. One of: `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`.

Example:
```json
{
  "tool": "set_log_level",
  "arguments": {
    "level": "debug"
  }
}
```

## MCP Resources

In addition to tools, the server provides resources for accessing LSP features including diagnostics, hover information, and code completions:

### Diagnostic Resources

The server exposes diagnostic information via the `lsp-diagnostics://` resource scheme. These resources can be subscribed to for real-time updates when diagnostics change.

Resource URIs:
- `lsp-diagnostics://` - Diagnostics for all open files
- `lsp-diagnostics:///path/to/file` - Diagnostics for a specific file

Important: Files must be opened using the `open_document` tool before diagnostics can be accessed.

### Hover Information Resources

The server exposes hover information via the `lsp-hover://` resource scheme. This allows you to get information about code elements at specific positions in files.

Resource URI format:
```
lsp-hover:///path/to/file?line={line}&column={column}&language_id={language_id}
```

Parameters:
- `line`: Line number (1-based)
- `column`: Column position (1-based)
- `language_id`: The programming language (e.g., "haskell")

Example:
```
lsp-hover:///home/user/project/src/Main.hs?line=42&column=10&language_id=haskell
```

### Code Completion Resources

The server exposes code completion suggestions via the `lsp-completions://` resource scheme. This allows you to get completion candidates at specific positions in files.

Resource URI format:
```
lsp-completions:///path/to/file?line={line}&column={column}&language_id={language_id}
```

Parameters:
- `line`: Line number (1-based)
- `column`: Column position (1-based)
- `language_id`: The programming language (e.g., "haskell")

Example:
```
lsp-completions:///home/user/project/src/Main.hs?line=42&column=10&language_id=haskell
```

### Listing Available Resources

To discover available resources, use the MCP `resources/list` endpoint. The response will include all available resources for currently open files, including:
- Diagnostics resources for all open files
- Hover information templates for all open files
- Code completion templates for all open files

### Subscribing to Resource Updates

Diagnostic resources support subscriptions to receive real-time updates when diagnostics change (e.g., when files are modified and new errors or warnings appear). Subscribe to diagnostic resources using the MCP `resources/subscribe` endpoint.

Note: Hover and completion resources don't support subscriptions as they represent point-in-time queries.

### Working with Resources vs. Tools

You can choose between two approaches for accessing LSP features:

1. Tool-based approach: Use the `get_diagnostics`, `get_info_on_location`, and `get_completions` tools for a simple, direct way to fetch information.
2. Resource-based approach: Use the `lsp-diagnostics://`, `lsp-hover://`, and `lsp-completions://` resources for a more RESTful approach.

Both approaches provide the same data in the same format and enforce the same requirement that files must be opened first.


## Troubleshooting

- If the server fails to start, make sure the path to the LSP executable is correct
- Check the log file (if configured) for detailed error messages

## License

MIT License



## Extensions

The LSP-MCP server supports language-specific extensions that enhance its capabilities for different programming languages. Extensions can provide:

- Custom LSP-specific tools and functionality
- Language-specific resource handlers and templates
- Specialized prompts for language-related tasks
- Custom subscription handlers for real-time data

### Available Extensions

Currently, the following extensions are available:

- **Haskell**: Provides specialized prompts for Haskell development, including typed-hole exploration guidance

### Using Extensions

Extensions are loaded automatically when you specify a language ID when starting the server:

```
npx tritlo/lsp-mcp haskell /path/to/haskell-language-server-wrapper lsp
```

### Extension Namespacing

All extension-provided features are namespaced with the language ID. For example, the Haskell extension's typed-hole prompt is available as `haskell.typed-hole-use`.

### Creating New Extensions

To create a new extension:

1. Create a new TypeScript file in `src/extensions/` named after your language (e.g., `typescript.ts`)
2. Implement the Extension interface with any of these optional functions:
   - `getToolHandlers()`: Provide custom tool implementations
   - `getToolDefinitions()`: Define custom tools in the MCP API
   - `getResourceHandlers()`: Implement custom resource handlers
   - `getSubscriptionHandlers()`: Implement custom subscription handlers
   - `getUnsubscriptionHandlers()`: Implement custom unsubscription handlers
   - `getResourceTemplates()`: Define custom resource templates
   - `getPromptDefinitions()`: Define custom prompts for language tasks
   - `getPromptHandlers()`: Implement custom prompt handlers

3. Export your implementation functions

The extension system will automatically load your extension when the matching language ID is specified.

## Acknowledgments

- HLS team for the Language Server Protocol implementation
- Anthropic for the Model Context Protocol specification

```

--------------------------------------------------------------------------------
/test/ts-project/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist",
    "sourceMap": true
  },
  "include": ["src/**/*"]
}
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": ".",
    "moduleResolution": "NodeNext",
    "module": "NodeNext",
    "target": "ES2022",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["./index.ts", "./src/**/*.ts"]
}
```

--------------------------------------------------------------------------------
/test/ts-project/src/example.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * A simple function that adds two numbers
 */
export function add(a: number, b: number): number {
  return a + b;
}

/**
 * An interface representing a person
 */
export interface Person {
  name: string;
  age: number;
  email?: string;
}

/**
 * A class representing a greeter
 */
export class Greeter {
  private greeting: string;

  constructor(greeting: string) {
    this.greeting = greeting;
  }

  /**
   * Greets a person
   */
  greet(person: Person): string {
    return `${this.greeting}, ${person.name}!`;
  }
}

// This will generate a diagnostic error - missing return type
export function multiply(a: number, b: number) {
  return a * b;
}

// This will generate a diagnostic error - unused variable
const unused = "This variable is not used";

// This will generate a diagnostic error - undefined variable
const result = undefinedVariable + 10;

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "lsp-mcp-server",
  "version": "0.2.0",
  "description": "MCP server for Language Server Protocol (LSP) integration, providing hover information, code completions, diagnostics, and code actions with resource-based access",
  "license": "MIT",
  "type": "module",
  "bin": {
    "lsp-mcp-server": "dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build",
    "watch": "tsc --watch",
    "test": "npm run test:typescript && npm run test:prompts",
    "test:typescript": "node test/typescript-lsp.test.js",
    "test:prompts": "node test/prompts.test.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0",
    "zod": "^3.22.4",
    "zod-to-json-schema": "^3.24.5"
  },
  "devDependencies": {
    "@types/node": "^22",
    "typescript": "^5.3.3",
    "typescript-language-server": "^4.3.4"
  }
}

```

--------------------------------------------------------------------------------
/src/extensions/haskell.ts:
--------------------------------------------------------------------------------

```typescript
// Haskell extension for LSP-MCP
import {
  ToolHandler,
  ResourceHandler,
  SubscriptionHandler,
  UnsubscriptionHandler,
  PromptHandler,
  Prompt,
  ToolInput
} from "../types/index.js";

// This extension provides no additional tools, resources, or prompts
// It's just a simple example of the extension structure

// Export tool handlers
export const getToolHandlers = (): Record<string, { schema: any, handler: ToolHandler }> => {
  return {};
};

// Export tool definitions
export const getToolDefinitions = (): Array<{
  name: string;
  description: string;
  inputSchema: ToolInput;
}> => {
  return [];
};

// Export resource handlers
export const getResourceHandlers = (): Record<string, ResourceHandler> => {
  return {};
};

// Export subscription handlers
export const getSubscriptionHandlers = (): Record<string, SubscriptionHandler> => {
  return {};
};

// Export unsubscription handlers
export const getUnsubscriptionHandlers = (): Record<string, UnsubscriptionHandler> => {
  return {};
};

// Export resource templates
export const getResourceTemplates = (): Array<{
  name: string;
  scheme: string;
  pattern: string;
  description: string;
  subscribe: boolean;
}> => {
  return [];
};

// Export prompt definitions
export const getPromptDefinitions = (): Prompt[] => {
  return [
    {
      name: "typed-hole-use",
      description: "Guide on using typed-holes in Haskell to explore type information and function possibilities"
    }
  ];
};

// Export prompt handlers
export const getPromptHandlers = (): Record<string, PromptHandler> => {
  return {
    "typed-hole-use": async (args?: Record<string,string>) => {
      return {
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: `
              Please use a typed-hole to synthesize replacement code for this expression.

              You do this by replacing the expression with a hole \`_mcp_typed_hole\`
              and calling the code action on the location of the hole.

              Make sure you call it on the hole, i.e. the line should be the actual line of the hole
              and the column should one

              Then, looking at the labels that the code-action returns,
              you can see the identifiers that can be used to fill in the hole.
              `
            }
          },
        ]
      };
    }
  };
};

```

--------------------------------------------------------------------------------
/src/prompts/index.ts:
--------------------------------------------------------------------------------

```typescript
// Prompts module for LSP MCP
import { Prompt, PromptHandler } from "../types/index.js";
import { debug, info } from "../logging/index.js";

// Enum for prompt names
enum PromptName {
  LSP_GUIDE = "lsp_guide",
  LANGUAGE_HELP = "language_help",
}

// Get prompt definitions for the server
export const getPromptDefinitions = (): Prompt[] => {
  return [
    {
      name: PromptName.LSP_GUIDE,
      description: "A guide on how to use the LSP (Language Server Protocol) functions available through this MCP server",
    }
  ];
};

// Define handlers for each prompt
export const getPromptHandlers = (): Record<string, PromptHandler> => {
  return {
    [PromptName.LSP_GUIDE]: async () => {
      debug(`Handling LSP guide prompt`);

      return {
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: "How do I use the LSP functions in this server?",
            },
          },
          {
            role: "assistant",
            content: {
              type: "text",
              text: `# LSP MCP Server Guide

This server provides access to Language Server Protocol (LSP) features through MCP tools. Here's how to use them:

## Getting Started

1. First, start the LSP server with a root directory:
   \`\`\`
   start_lsp(root_dir: "/path/to/your/project")
   \`\`\`

2. Open a file for analysis:
   \`\`\`
   open_document(file_path: "/path/to/your/project/src/file.ts", language_id: "typescript")
   \`\`\`

## Available Tools

- **get_info_on_location**: Get hover information (types, documentation) at a specific position
- **get_completions**: Get code completion suggestions at a cursor position
- **get_code_actions**: Get available code refactorings and quick fixes for a selection
- **get_diagnostics**: Get errors and warnings for open files
- **open_document**: Open a file for analysis
- **close_document**: Close a file when done
- **restart_lsp_server**: Restart the LSP server if needed
- **set_log_level**: Control the server's logging verbosity

## Workflow Example

1. Start LSP: \`start_lsp(root_dir: "/my/project")\`
2. Open file: \`open_document(file_path: "/my/project/src/app.ts", language_id: "typescript")\`
3. Get diagnostics: \`get_diagnostics(file_path: "/my/project/src/app.ts")\`
4. Get hover info: \`get_info_on_location(file_path: "/my/project/src/app.ts", line: 10, character: 15, language_id: "typescript")\`
5. Get completions: \`get_completions(file_path: "/my/project/src/app.ts", line: 12, character: 8, language_id: "typescript")\`
6. Close file when done: \`close_document(file_path: "/my/project/src/app.ts")\`

Remember that line and character positions are 1-based (first line is 1, first character is 1), but LSP internally uses 0-based positions.`,
            },
          },
        ],
      };
    },
  };
};

```

--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------

```typescript
// Type definitions

import { z } from "zod";
import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";

// LSP message handling
export interface LSPMessage {
  jsonrpc: string;
  id?: number | string;
  method?: string;
  params?: any;
  result?: any;
  error?: any;
}

// Define a type for diagnostic subscribers
export type DiagnosticUpdateCallback = (uri: string, diagnostics: any[]) => void;

// Define a type for subscription context
export interface SubscriptionContext {
  callback: DiagnosticUpdateCallback;
}

// Logging level type
export type LoggingLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';

// Tool input type
export const ToolInputSchema = ToolSchema.shape.inputSchema;
export type ToolInput = z.infer<typeof ToolInputSchema>;

// Tool handler types
export type ToolHandler = (args: any) => Promise<{ content: Array<{ type: string, text: string }>, isError?: boolean }>;

// Resource handler type
export type ResourceHandler = (uri: string) => Promise<{ contents: Array<{ type: string, text: string, uri: string }> }>;

// Subscription handler type
export type SubscriptionHandler = (uri: string) => Promise<{ ok: boolean, context?: SubscriptionContext, error?: string }>;

// Unsubscription handler type
export type UnsubscriptionHandler = (uri: string, context: any) => Promise<{ ok: boolean, error?: string }>;

// Prompt types
export interface Prompt {
  name: string;
  description: string;
  arguments?: Array<{
    name: string;
    description: string;
    required: boolean;
  }>;
}

export type PromptHandler = (args?: Record<string, string>) => Promise<{
  messages: Array<{
    role: string;
    content: {
      type: string;
      text: string;
    };
  }>;
}>;

// Schema definitions
export const GetInfoOnLocationArgsSchema = z.object({
  file_path: z.string().describe("Path to the file"),
  language_id: z.string().describe("The programming language the file is written in"),
  line: z.number().describe(`Line number`),
  column: z.number().describe(`Column position`),
});

export const GetCompletionsArgsSchema = z.object({
  file_path: z.string().describe(`Path to the file`),
  language_id: z.string().describe(`The programming language the file is written in`),
  line: z.number().describe(`Line number`),
  column: z.number().describe(`Column position`),
});

export const GetCodeActionsArgsSchema = z.object({
  file_path: z.string().describe(`Path to the file`),
  language_id: z.string().describe(`The programming language the file is written in`),
  start_line: z.number().describe(`Start line number`),
  start_column: z.number().describe(`Start column position`),
  end_line: z.number().describe(`End line number`),
  end_column: z.number().describe(`End column position`),
});

export const OpenDocumentArgsSchema = z.object({
  file_path: z.string().describe(`Path to the file to open`),
  language_id: z.string().describe(`The programming language the file is written in`),
});

export const CloseDocumentArgsSchema = z.object({
  file_path: z.string().describe(`Path to the file to close`),
});

export const GetDiagnosticsArgsSchema = z.object({
  file_path: z.string().optional().describe(`Path to the file to get diagnostics for. If not provided, returns diagnostics for all open files.`),
});

export const SetLogLevelArgsSchema = z.object({
  level: z.enum(['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'])
    .describe("The logging level to set")
});

export const RestartLSPServerArgsSchema = z.object({
  root_dir: z.string().optional().describe("The root directory for the LSP server. If not provided, the server will not be initialized automatically."),
});

export const StartLSPArgsSchema = z.object({
  root_dir: z.string().describe("The root directory for the LSP server"),
});

```

--------------------------------------------------------------------------------
/src/logging/index.ts:
--------------------------------------------------------------------------------

```typescript
import { LoggingLevel } from "../types/index.js";

// Store original console methods before we do anything else
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;

// Current log level - can be changed at runtime
// Initialize with default or from environment variable
let logLevel: LoggingLevel = (process.env.LOG_LEVEL as LoggingLevel) || 'info';

// Validate that the log level is valid, default to 'info' if not
if (!['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'].includes(logLevel)) {
  logLevel = 'info';
}

// Map of log levels and their priorities (higher number = higher priority)
const LOG_LEVEL_PRIORITY: Record<LoggingLevel, number> = {
  'debug': 0,
  'info': 1,
  'notice': 2,
  'warning': 3,
  'error': 4,
  'critical': 5,
  'alert': 6,
  'emergency': 7
};

// Check if message should be logged based on current level
const shouldLog = (level: LoggingLevel): boolean => {
  return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[logLevel];
};

// Reference to the server for sending notifications
let serverInstance: any = null;

// Set the server instance for notifications
export const setServer = (server: any): void => {
  serverInstance = server;
};

// Flag to prevent recursion in logging
let isLogging = false;

// Core logging function
export const log = (level: LoggingLevel, ...args: any[]): void => {
  if (!shouldLog(level)) return;

  const timestamp = new Date().toISOString();
  const message = args.map(arg =>
    typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
  ).join(' ');

  // Format for console output with color coding
  let consoleMethod = originalConsoleLog; // Use original methods to prevent recursion
  let consolePrefix = '';

  switch(level) {
    case 'debug':
      consolePrefix = '\x1b[90m[DEBUG]\x1b[0m'; // Gray
      consoleMethod = originalConsoleWarn || originalConsoleLog;
      break;
    case 'info':
      consolePrefix = '\x1b[36m[INFO]\x1b[0m'; // Cyan
      break;
    case 'notice':
      consolePrefix = '\x1b[32m[NOTICE]\x1b[0m'; // Green
      break;
    case 'warning':
      consolePrefix = '\x1b[33m[WARNING]\x1b[0m'; // Yellow
      consoleMethod = originalConsoleWarn || originalConsoleLog;
      break;
    case 'error':
      consolePrefix = '\x1b[31m[ERROR]\x1b[0m'; // Red
      consoleMethod = originalConsoleError;
      break;
    case 'critical':
      consolePrefix = '\x1b[41m\x1b[37m[CRITICAL]\x1b[0m'; // White on red
      consoleMethod = originalConsoleError;
      break;
    case 'alert':
      consolePrefix = '\x1b[45m\x1b[37m[ALERT]\x1b[0m'; // White on purple
      consoleMethod = originalConsoleError;
      break;
    case 'emergency':
      consolePrefix = '\x1b[41m\x1b[1m[EMERGENCY]\x1b[0m'; // Bold white on red
      consoleMethod = originalConsoleError;
      break;
  }

  consoleMethod(`${consolePrefix} ${message}`);

  // Send notification to MCP client if server is available and initialized
  if (serverInstance && typeof serverInstance.notification === 'function') {
    try {
      serverInstance.notification({
        method: "notifications/message",
        params: {
          level,
          logger: "lsp-mcp-server",
          data: message,
        },
      });
    } catch (error) {
      // Use original console methods to avoid recursion
      originalConsoleError("Error sending notification:", error);
    }
  }
};

// Create helper functions for each log level
export const debug = (...args: any[]): void => log('debug', ...args);
export const info = (...args: any[]): void => log('info', ...args);
export const notice = (...args: any[]): void => log('notice', ...args);
export const warning = (...args: any[]): void => log('warning', ...args);
export const logError = (...args: any[]): void => log('error', ...args);
export const critical = (...args: any[]): void => log('critical', ...args);
export const alert = (...args: any[]): void => log('alert', ...args);
export const emergency = (...args: any[]): void => log('emergency', ...args);

// Set log level function - defined after log function to avoid circular references
export const setLogLevel = (level: LoggingLevel): void => {
  const oldLevel = logLevel;
  logLevel = level;

  // Always log this message regardless of the new log level
  // Use notice level to ensure it's visible
  originalConsoleLog(`\x1b[32m[NOTICE]\x1b[0m Log level changed from ${oldLevel} to ${level}`);

  // Also log through standard channels
  log('notice', `Log level set to: ${level}`);
};

// Override console methods to use our logging system
console.log = function(...args) {
  if (isLogging) {
    // Use original method to prevent recursion
    originalConsoleLog(...args);
    return;
  }

  isLogging = true;
  info(...args);
  isLogging = false;
};

console.warn = function(...args) {
  if (isLogging) {
    // Use original method to prevent recursion
    originalConsoleWarn(...args);
    return;
  }

  isLogging = true;
  warning(...args);
  isLogging = false;
};

console.error = function(...args) {
  if (isLogging) {
    // Use original method to prevent recursion
    originalConsoleError(...args);
    return;
  }

  isLogging = true;
  logError(...args);
  isLogging = false;
};
```

--------------------------------------------------------------------------------
/src/extensions/index.ts:
--------------------------------------------------------------------------------

```typescript
// Extensions management system for LSP-MCP
import * as fs from "fs/promises";
import * as path from "path";
import { debug, info, warning, logError } from "../logging/index.js";
import {
  ToolHandler,
  ResourceHandler,
  SubscriptionHandler,
  UnsubscriptionHandler,
  PromptHandler,
  Prompt,
  ToolInput
} from "../types/index.js";

// Type definitions for extension structure
interface Extension {
  getToolHandlers?: () => Record<string, { schema: any, handler: ToolHandler }>;
  getToolDefinitions?: () => Array<{
    name: string;
    description: string;
    inputSchema: ToolInput;
  }>;
  getResourceHandlers?: () => Record<string, ResourceHandler>;
  getSubscriptionHandlers?: () => Record<string, SubscriptionHandler>;
  getUnsubscriptionHandlers?: () => Record<string, UnsubscriptionHandler>;
  getResourceTemplates?: () => Array<{
    name: string;
    scheme: string;
    pattern: string;
    description: string;
    subscribe: boolean;
  }>;
  getPromptDefinitions?: () => Prompt[];
  getPromptHandlers?: () => Record<string, PromptHandler>;
}

// Track active extensions
const activeExtensions: Record<string, Extension> = {};

// Import an extension module by language ID
async function importExtension(languageId: string): Promise<Extension | null> {
  try {
    // Normalize language ID to use only alphanumeric characters and hyphens
    const safeLanguageId = languageId.replace(/[^a-zA-Z0-9-]/g, '');

    // Check if extension file exists
    const extensionPath = path.resolve(process.cwd(), 'dist', 'src', 'extensions', `${safeLanguageId}.js`);
    try {
      await fs.access(extensionPath);
    } catch (error) {
      info(`No extension found for language: ${languageId}`);
      return null;
    }

    // Import the extension module
    const extensionModule = await import(`./${safeLanguageId}.js`);
    return extensionModule as Extension;
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error importing extension for ${languageId}: ${errorMessage}`);
    return null;
  }
}

// Activate an extension by language ID
export async function activateExtension(languageId: string): Promise<{success: boolean}> {
  try {
    // Check if already active
    if (activeExtensions[languageId]) {
      info(`Extension for ${languageId} is already active`);
      return { success: true };
    }

    // Import the extension
    const extension = await importExtension(languageId);
    if (!extension) {
      info(`No extension found for language: ${languageId}`);
      return { success: false };
    }

    // Store the active extension
    activeExtensions[languageId] = extension;
    info(`Activated extension for language: ${languageId}`);
    return { success: true };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error activating extension for ${languageId}: ${errorMessage}`);
    return { success: false };
  }
}

// Deactivate an extension by language ID
export function deactivateExtension(languageId: string): {success: boolean} {
  try {
    // Check if active
    if (!activeExtensions[languageId]) {
      info(`No active extension found for language: ${languageId}`);
      return { success: false };
    }

    // Remove the extension
    delete activeExtensions[languageId];
    info(`Deactivated extension for language: ${languageId}`);

    return { success: true };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error deactivating extension for ${languageId}: ${errorMessage}`);
    return { success: false };
  }
}

// List all active extensions
export function listActiveExtensions(): string[] {
  return Object.keys(activeExtensions);
}

// Get all tool handlers from active extensions
export function getExtensionToolHandlers(): Record<string, { schema: any, handler: ToolHandler }> {
  const handlers: Record<string, { schema: any, handler: ToolHandler }> = {};

  for (const [languageId, extension] of Object.entries(activeExtensions)) {
    if (extension.getToolHandlers) {
      const extensionHandlers = extension.getToolHandlers();
      for (const [name, handler] of Object.entries(extensionHandlers)) {
        handlers[`${languageId}.${name}`] = handler;
      }
    }
  }

  return handlers;
}

// Get all tool definitions from active extensions
export function getExtensionToolDefinitions(): Array<{
  name: string;
  description: string;
  inputSchema: ToolInput;
}> {
  const definitions: Array<{
    name: string;
    description: string;
    inputSchema: ToolInput;
  }> = [];

  for (const [languageId, extension] of Object.entries(activeExtensions)) {
    if (extension.getToolDefinitions) {
      const extensionDefinitions = extension.getToolDefinitions();
      for (const def of extensionDefinitions) {
        definitions.push({
          name: `${languageId}.${def.name}`,
          description: def.description,
          inputSchema: def.inputSchema
        });
      }
    }
  }

  return definitions;
}

// Get all resource handlers from active extensions
export function getExtensionResourceHandlers(): Record<string, ResourceHandler> {
  const handlers: Record<string, ResourceHandler> = {};

  for (const [languageId, extension] of Object.entries(activeExtensions)) {
    if (extension.getResourceHandlers) {
      const extensionHandlers = extension.getResourceHandlers();
      for (const [scheme, handler] of Object.entries(extensionHandlers)) {
        handlers[`${languageId}.${scheme}`] = handler;
      }
    }
  }

  return handlers;
}

// Get all subscription handlers from active extensions
export function getExtensionSubscriptionHandlers(): Record<string, SubscriptionHandler> {
  const handlers: Record<string, SubscriptionHandler> = {};

  for (const [languageId, extension] of Object.entries(activeExtensions)) {
    if (extension.getSubscriptionHandlers) {
      const extensionHandlers = extension.getSubscriptionHandlers();
      for (const [scheme, handler] of Object.entries(extensionHandlers)) {
        handlers[`${languageId}.${scheme}`] = handler;
      }
    }
  }

  return handlers;
}

// Get all unsubscription handlers from active extensions
export function getExtensionUnsubscriptionHandlers(): Record<string, UnsubscriptionHandler> {
  const handlers: Record<string, UnsubscriptionHandler> = {};

  for (const [languageId, extension] of Object.entries(activeExtensions)) {
    if (extension.getUnsubscriptionHandlers) {
      const extensionHandlers = extension.getUnsubscriptionHandlers();
      for (const [scheme, handler] of Object.entries(extensionHandlers)) {
        handlers[`${languageId}.${scheme}`] = handler;
      }
    }
  }

  return handlers;
}

// Get all resource templates from active extensions
export function getExtensionResourceTemplates(): Array<{
  name: string;
  scheme: string;
  pattern: string;
  description: string;
  subscribe: boolean;
}> {
  const templates: Array<{
    name: string;
    scheme: string;
    pattern: string;
    description: string;
    subscribe: boolean;
  }> = [];

  for (const [languageId, extension] of Object.entries(activeExtensions)) {
    if (extension.getResourceTemplates) {
      const extensionTemplates = extension.getResourceTemplates();
      for (const template of extensionTemplates) {
        templates.push({
          name: `${languageId}.${template.name}`,
          scheme: `${languageId}.${template.scheme}`,
          pattern: template.pattern,
          description: template.description,
          subscribe: template.subscribe
        });
      }
    }
  }

  return templates;
}

// Get all prompt definitions from active extensions
export function getExtensionPromptDefinitions(): Prompt[] {
  const definitions: Prompt[] = [];

  for (const [languageId, extension] of Object.entries(activeExtensions)) {
    if (extension.getPromptDefinitions) {
      const extensionDefinitions = extension.getPromptDefinitions();
      for (const def of extensionDefinitions) {
        definitions.push({
          name: `${languageId}.${def.name}`,
          description: def.description,
          arguments: def.arguments
        });
      }
    }
  }

  return definitions;
}

// Get all prompt handlers from active extensions
export function getExtensionPromptHandlers(): Record<string, PromptHandler> {
  const handlers: Record<string, PromptHandler> = {};

  for (const [languageId, extension] of Object.entries(activeExtensions)) {
    if (extension.getPromptHandlers) {
      const extensionHandlers = extension.getPromptHandlers();
      for (const [name, handler] of Object.entries(extensionHandlers)) {
        handlers[`${languageId}.${name}`] = handler;
      }
    }
  }

  return handlers;
}

```

--------------------------------------------------------------------------------
/test/prompts.test.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node
// Prompts feature test for LSP MCP server

import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import path from 'path';
import fsSync from 'fs';
import assert from 'assert';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/sdk/shared/stdio.js';

// Get the current file's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Custom transport that works with an existing child process
class CustomStdioTransport {
  constructor(childProcess) {
    this.childProcess = childProcess;
    this.readBuffer = new ReadBuffer();
    this.onmessage = null;
    this.onerror = null;
    this.onclose = null;

    this._setupListeners();
  }

  _setupListeners() {
    // Set up stdout handler for responses
    this.childProcess.stdout.on('data', (data) => {
      this.readBuffer.append(data);
      this._processReadBuffer();
    });

    // Set up error handler
    this.childProcess.on('error', (error) => {
      if (this.onerror) this.onerror(error);
    });

    // Set up close handler
    this.childProcess.on('close', (code) => {
      if (this.onclose) this.onclose();
    });

    // Handle errors on streams
    this.childProcess.stdout.on('error', (error) => {
      if (this.onerror) this.onerror(error);
    });

    this.childProcess.stdin.on('error', (error) => {
      if (this.onerror) this.onerror(error);
    });
  }

  _processReadBuffer() {
    while (true) {
      try {
        const message = this.readBuffer.readMessage();
        if (message === null) {
          break;
        }
        if (this.onmessage) this.onmessage(message);
      } catch (error) {
        if (this.onerror) this.onerror(error);
      }
    }
  }

  async start() {
    // No need to start since we're using an existing process
    return Promise.resolve();
  }

  async close() {
    // Don't actually kill the process here - we'll handle that separately
    this.readBuffer.clear();
  }

  send(message) {
    return new Promise((resolve) => {
      if (!this.childProcess.stdin) {
        throw new Error('Not connected');
      }

      const json = serializeMessage(message);
      console.log('>>> SENDING:', json.toString().trim());

      if (this.childProcess.stdin.write(json)) {
        resolve();
      } else {
        this.childProcess.stdin.once('drain', resolve);
      }
    });
  }
}

// Path to our compiled server script and the typescript-language-server binary
const LSP_MCP_SERVER = path.join(__dirname, '..', 'dist', 'index.js');
const TS_SERVER_BIN = path.join(__dirname, '..', 'node_modules', '.bin', 'typescript-language-server');

// Check prerequisites
try {
  const stats = fsSync.statSync(TS_SERVER_BIN);
  if (!stats.isFile()) {
    console.error(`Error: The typescript-language-server at '${TS_SERVER_BIN}' is not a file`);
    process.exit(1);
  }
} catch (error) {
  console.error(`Error: Could not find typescript-language-server at '${TS_SERVER_BIN}'`);
  console.error('Make sure you have installed the typescript-language-server as a dev dependency');
  process.exit(1);
}

if (!fsSync.existsSync(LSP_MCP_SERVER)) {
  console.error(`ERROR: LSP MCP server not found at ${LSP_MCP_SERVER}`);
  console.error(`Make sure you've built the project with 'npm run build'`);
  process.exit(1);
}

class PromptsTester {
  constructor() {
    this.client = null;
    this.serverProcess = null;
    this.testResults = {
      passed: [],
      failed: []
    };
  }

  async start() {
    // Start the MCP server
    console.log(`Starting MCP server: node ${LSP_MCP_SERVER} typescript ${TS_SERVER_BIN} --stdio`);

    this.serverProcess = spawn('node', [LSP_MCP_SERVER, 'typescript', TS_SERVER_BIN, '--stdio'], {
      env: {
        ...process.env,
        DEBUG: 'true',
        LOG_LEVEL: 'debug'
      },
      stdio: ['pipe', 'pipe', 'pipe']
    });

    console.log(`MCP server started with PID: ${this.serverProcess.pid}`);

    // Set up stderr handler for logging
    this.serverProcess.stderr.on('data', (data) => {
      console.log(`SERVER STDERR: ${data.toString().trim()}`);
    });

    // Set up error handler
    this.serverProcess.on('error', (error) => {
      console.error(`SERVER ERROR: ${error.message}`);
    });

    // Create our custom transport with the existing server process
    const transport = new CustomStdioTransport(this.serverProcess);

    // Create the client with proper initialization
    this.client = new Client(
      // clientInfo
      {
        name: "prompts-test-client",
        version: "1.0.0"
      },
      // options
      {
        capabilities: {
          tools: true,
          resources: true,
          prompts: true,
          logging: true
        }
      }
    );

    // Connect client to the transport
    try {
      await this.client.connect(transport);
      console.log("Connected to MCP server successfully");
    } catch (error) {
      console.error("Failed to connect to MCP server:", error);
      throw error;
    }

    // Wait a bit to ensure everything is initialized
    await new Promise(resolve => setTimeout(resolve, 2000));

    return this;
  }

  stop() {
    if (this.serverProcess) {
      console.log("Sending SIGINT to MCP server");
      this.serverProcess.kill('SIGINT');
      this.serverProcess = null;
    }
  }

  // Helper method to run a test case and record result
  async runTest(name, func) {
    console.log(`\nTest: ${name}`);
    try {
      await func();
      console.log(`✅ Test passed: ${name}`);
      this.testResults.passed.push(name);
      return true;
    } catch (error) {
      console.error(`❌ Test failed: ${name}`);
      console.error(`Error: ${error.message}`);
      this.testResults.failed.push(name);
      return false;
    }
  }

  // Test listing the available prompts
  async testListPrompts() {
    console.log("Listing available prompts...");

    try {
      const response = await this.client.listPrompts();

      // Extract the prompts array
      let prompts = [];
      if (response && response.prompts && Array.isArray(response.prompts)) {
        prompts = response.prompts;
      } else if (Array.isArray(response)) {
        prompts = response;
      } else {
        console.log("Unexpected prompts response format:", response);
        prompts = []; // Ensure we have an array to work with
      }

      console.log(`Found ${prompts.length} prompts`);
      prompts.forEach(prompt => {
        if (prompt && prompt.name) {
          console.log(`- ${prompt.name}: ${prompt.description || 'No description'}`);
          
          if (prompt.arguments && prompt.arguments.length > 0) {
            console.log(`  Arguments:`);
            prompt.arguments.forEach(arg => {
              console.log(`  - ${arg.name}: ${arg.description} (${arg.required ? 'required' : 'optional'})`);
            });
          }
        }
      });

      // If we didn't get any prompts, we'll fail the test
      if (prompts.length === 0) {
        throw new Error("No prompts returned");
      }

      // Verify we have the expected prompts
      const requiredPrompts = ['lsp_guide'];

      const missingPrompts = requiredPrompts.filter(prompt =>
        !prompts.some(p => p.name === prompt)
      );

      if (missingPrompts.length > 0) {
        throw new Error(`Missing expected prompts: ${missingPrompts.join(', ')}`);
      }

      return prompts;
    } catch (error) {
      console.error(`Error listing prompts: ${error.message}`);
      throw error;
    }
  }

  // Test getting a prompt
  async testGetPrompt(name, args = {}) {
    console.log(`Getting prompt: ${name}`);
    
    try {
      const params = {
        name: name,
        arguments: args
      };

      const result = await this.client.getPrompt(params);
      console.log(`Prompt result:`, JSON.stringify(result, null, 2));

      // Basic validation
      assert(result && result.messages && Array.isArray(result.messages),
        'Expected messages array in the result');
      
      assert(result.messages.length > 0,
        'Expected at least one message in the result');
      
      // Check for user and assistant roles
      const hasUserMessage = result.messages.some(m => m.role === 'user');
      const hasAssistantMessage = result.messages.some(m => m.role === 'assistant');
      
      assert(hasUserMessage, 'Expected a user message in the result');
      assert(hasAssistantMessage, 'Expected an assistant message in the result');

      return result;
    } catch (error) {
      console.error(`Failed to get prompt ${name}:`, error);
      throw error;
    }
  }

  // Print a summary of the test results
  printResults() {
    console.log('\n=== Test Results ===');
    console.log(`Passed: ${this.testResults.passed.length}/${this.testResults.passed.length + this.testResults.failed.length}`);

    console.log('\nPassed Tests:');
    for (const test of this.testResults.passed) {
      console.log(`  ✅ ${test}`);
    }

    console.log('\nFailed Tests:');
    for (const test of this.testResults.failed) {
      console.log(`  ❌ ${test}`);
    }

    if (this.testResults.failed.length > 0) {
      console.log('\n❌ Some tests failed');
      return false;
    } else if (this.testResults.passed.length === 0) {
      console.log('\n❌ No tests passed');
      return false;
    } else {
      console.log('\n✅ All tests passed');
      return true;
    }
  }
}

// Run the tests
async function runTests() {
  console.log('=== LSP MCP Prompts Feature Tests ===');

  const tester = await new PromptsTester().start();

  try {
    // Test listing prompts
    await tester.runTest('List prompts', async () => {
      await tester.testListPrompts();
    });

    // Test getting the LSP guide prompt
    await tester.runTest('Get LSP guide prompt', async () => {
      await tester.testGetPrompt('lsp_guide');
    });

  } catch (error) {
    console.error('ERROR in tests:', error);
  } finally {
    // Print results
    const allPassed = tester.printResults();

    // Clean up
    console.log('\nShutting down tester...');
    tester.stop();

    // Exit with appropriate status code
    process.exit(allPassed ? 0 : 1);
  }
}

// Execute the tests
console.log('Starting LSP MCP Prompts tests');
runTests().catch(error => {
  console.error('Unhandled error:', error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
  ListResourcesRequestSchema,
  SubscribeRequestSchema,
  UnsubscribeRequestSchema,
  SetLevelRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as fsSync from "fs";

import { LSPClient } from "./src/lspClient.js";
import { debug, info, notice, warning, logError, critical, alert, emergency, setLogLevel, setServer } from "./src/logging/index.js";
import { getToolHandlers, getToolDefinitions } from "./src/tools/index.js";
import { getPromptHandlers, getPromptDefinitions } from "./src/prompts/index.js";
import {
  getResourceHandlers,
  getSubscriptionHandlers,
  getUnsubscriptionHandlers,
  getResourceTemplates,
  generateResourcesList
} from "./src/resources/index.js";
import {
  getExtensionToolHandlers,
  getExtensionToolDefinitions,
  getExtensionResourceHandlers,
  getExtensionSubscriptionHandlers,
  getExtensionUnsubscriptionHandlers,
  getExtensionResourceTemplates,
  getExtensionPromptDefinitions,
  getExtensionPromptHandlers
} from "./src/extensions/index.js";

import { activateExtension } from "./src/extensions/index.js";

// Get the language ID from the command line arguments
const languageId = process.argv[2];

// Add any language-specific extensions here
await activateExtension(languageId);

// Get LSP binary path and arguments from command line arguments
const lspServerPath = process.argv[3];
if (!lspServerPath) {
  console.error("Error: LSP server path is required as the first argument");
  console.error("Usage: node dist/index.js <language> <lsp-server-path> [lsp-server-args...]");
  process.exit(1);
}

// Get any additional arguments to pass to the LSP server
const lspServerArgs = process.argv.slice(4);

// Verify the LSP server binary exists
try {
  const stats = fsSync.statSync(lspServerPath);
  if (!stats.isFile()) {
    console.error(`Error: The specified path '${lspServerPath}' is not a file`);
    process.exit(1);
  }
} catch (error) {
  console.error(`Error: Could not access the LSP server at '${lspServerPath}'`);
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
}

// We'll create the LSP client but won't initialize it until start_lsp is called
let lspClient: LSPClient | null = null;
let rootDir = "."; // Default to current directory

// Set the LSP client function
const setLspClient = (client: LSPClient) => {
  lspClient = client;
};

// Set the root directory function
const setRootDir = (dir: string) => {
  rootDir = dir;
};

// Server setup
const server = new Server(
  {
    name: "lsp-mcp-server",
    version: "0.3.0",
    description: "MCP server for Language Server Protocol (LSP) integration, providing hover information, code completions, diagnostics, and code actions with resource-based access and extensibility"
  },
  {
    capabilities: {
      tools: {
        description: "A set of tools for interacting with the Language Server Protocol (LSP). These tools provide access to language-specific features like code completion, hover information, diagnostics, and code actions. Before using any LSP features, you must first call start_lsp with the project root directory, then open the files you wish to analyze."
      },
      resources: {
        description: "URI-based access to Language Server Protocol (LSP) features. These resources provide a way to access language-specific features like diagnostics, hover information, and completions through a URI pattern. Before using these resources, you must first call the start_lsp tool with the project root directory, then open the files you wish to analyze using the open_document tool. Additional resources may be available through language-specific extensions.",
        templates: getResourceTemplates()
      },
      prompts: {
        description: "Helpful prompts related to using the LSP MCP server. These prompts provide guidance on how to use the LSP features and tools available in this server. Additional prompts may be available through language-specific extensions."
      },
      logging: {
        description: "Logging capabilities for the LSP MCP server. Use the set_log_level tool to control logging verbosity. The server sends notifications about important events, errors, and diagnostic updates."
      }
    },
  },
);

// Set the server instance for logging and tools
setServer(server);

// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
  debug("Handling ListTools request");
  // Combine core tools and extension tools
  const coreTools = getToolDefinitions();
  const extensionTools = getExtensionToolDefinitions();
  return {
    tools: [...coreTools, ...extensionTools],
  };
});

// Get the combined tool handlers from core and extensions
const getToolsHandlers = () => {
  // Get core handlers, passing the server instance for notifications
  const coreHandlers = getToolHandlers(lspClient, lspServerPath, lspServerArgs, setLspClient, rootDir, setRootDir, server);
  // Get extension handlers
  const extensionHandlers = getExtensionToolHandlers();
  // Combine them (extensions take precedence in case of name conflicts)
  return { ...coreHandlers, ...extensionHandlers };
};

// Handle tool requests using the toolHandlers object
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const { name, arguments: args } = request.params;
    debug(`Handling CallTool request for tool: ${name}`);

    // Get the latest tool handlers and look up the handler for this tool
    const toolHandlers = getToolsHandlers();

    // Check if it's a direct handler or an extension handler
    const toolHandler = toolHandlers[name as keyof typeof toolHandlers];

    if (!toolHandler) {
      throw new Error(`Unknown tool: ${name}`);
    }

    // Validate the arguments against the schema
    const parsed = toolHandler.schema.safeParse(args);
    if (!parsed.success) {
      throw new Error(`Invalid arguments for ${name}: ${parsed.error}`);
    }

    // Call the handler with the validated arguments
    return await toolHandler.handler(parsed.data);

  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error handling tool request: ${errorMessage}`);
    return {
      content: [{ type: "text", text: `Error: ${errorMessage}` }],
      isError: true,
    };
  }
});

// Resource handler
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  try {
    const uri = request.params.uri;
    debug(`Handling ReadResource request for URI: ${uri}`);

    // Get the core and extension resource handlers
    const coreHandlers = getResourceHandlers(lspClient);
    const extensionHandlers = getExtensionResourceHandlers();

    // Combine them (extensions take precedence in case of conflicts)
    const resourceHandlers = { ...coreHandlers, ...extensionHandlers };

    // Find the appropriate handler for this URI scheme
    const handlerKey = Object.keys(resourceHandlers).find(key => uri.startsWith(key));
    if (handlerKey) {
      return await resourceHandlers[handlerKey](uri);
    }

    throw new Error(`Unknown resource URI: ${uri}`);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error handling resource request: ${errorMessage}`);
    return {
      contents: [{ type: "text", text: `Error: ${errorMessage}`, uri: request.params.uri }],
      isError: true,
    };
  }
});

// Resource subscription handler
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
  try {
    const { uri } = request.params;
    debug(`Handling SubscribeResource request for URI: ${uri}`);

    // Get the core and extension subscription handlers
    const coreHandlers = getSubscriptionHandlers(lspClient, server);
    const extensionHandlers = getExtensionSubscriptionHandlers();

    // Combine them (extensions take precedence in case of conflicts)
    const subscriptionHandlers = { ...coreHandlers, ...extensionHandlers };

    // Find the appropriate handler for this URI scheme
    const handlerKey = Object.keys(subscriptionHandlers).find(key => uri.startsWith(key));
    if (handlerKey) {
      return await subscriptionHandlers[handlerKey](uri);
    }

    throw new Error(`Unknown resource URI: ${uri}`);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error handling subscription request: ${errorMessage}`);
    return {
      ok: false,
      error: errorMessage
    };
  }
});

// Resource unsubscription handler
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
  try {
    const { uri, context } = request.params;
    debug(`Handling UnsubscribeResource request for URI: ${uri}`);

    // Get the core and extension unsubscription handlers
    const coreHandlers = getUnsubscriptionHandlers(lspClient);
    const extensionHandlers = getExtensionUnsubscriptionHandlers();

    // Combine them (extensions take precedence in case of conflicts)
    const unsubscriptionHandlers = { ...coreHandlers, ...extensionHandlers };

    // Find the appropriate handler for this URI scheme
    const handlerKey = Object.keys(unsubscriptionHandlers).find(key => uri.startsWith(key));
    if (handlerKey) {
      return await unsubscriptionHandlers[handlerKey](uri, context);
    }

    throw new Error(`Unknown resource URI: ${uri}`);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error handling unsubscription request: ${errorMessage}`);
    return {
      ok: false,
      error: errorMessage
    };
  }
});

// Handle log level changes from client
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
  try {
    const { level } = request.params;
    debug(`Received request to set log level to: ${level}`);

    // Set the log level
    setLogLevel(level);

    return {};
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error handling set level request: ${errorMessage}`);
    return {
      ok: false,
      error: errorMessage
    };
  }
});

// Resource listing handler
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  try {
    debug("Handling ListResource request");

    // Generate the core resources list
    const coreResources = generateResourcesList(lspClient);

    // Get extension resource templates
    const extensionTemplates = getExtensionResourceTemplates();

    // Combine core resources and extension templates
    const resources = [...coreResources, ...extensionTemplates];

    return { resources };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error handling list resources request: ${errorMessage}`);
    return {
      resources: [],
      isError: true,
      error: errorMessage
    };
  }
});

// Prompt listing handler
server.setRequestHandler(ListPromptsRequestSchema, async () => {
  try {
    debug("Handling ListPrompts request");
    // Combine core and extension prompts
    const corePrompts = getPromptDefinitions();
    const extensionPrompts = getExtensionPromptDefinitions();
    return {
      prompts: [...corePrompts, ...extensionPrompts],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error handling list prompts request: ${errorMessage}`);
    return {
      prompts: [],
      isError: true,
      error: errorMessage
    };
  }
});

// Get prompt handler
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  try {
    const { name, arguments: args } = request.params;
    debug(`Handling GetPrompt request for prompt: ${name}`);

    // Get the core and extension prompt handlers
    const coreHandlers = getPromptHandlers();
    const extensionHandlers = getExtensionPromptHandlers();

    // Combine them (extensions take precedence in case of conflicts)
    const promptHandlers = { ...coreHandlers, ...extensionHandlers };

    const promptHandler = promptHandlers[name];

    if (!promptHandler) {
      throw new Error(`Unknown prompt: ${name}`);
    }

    // Call the handler with the arguments
    return await promptHandler(args);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logError(`Error handling get prompt request: ${errorMessage}`);
    throw new Error(`Error handling get prompt request: ${errorMessage}`);
  }
});

// Clean up on process exit
process.on('exit', async () => {
  info("Shutting down MCP server...");
  try {
    // Only attempt shutdown if lspClient exists and is initialized
    if (lspClient) {
      await lspClient.shutdown();
    }
  } catch (error) {
    warning("Error during shutdown:", error);
  }
});

// Log uncaught exceptions
process.on('uncaughtException', (error) => {
  const errorMessage = error instanceof Error ? error.message : String(error);

  // Don't exit for "Not connected" errors during startup
  if (errorMessage === 'Not connected') {
    warning(`Uncaught exception (non-fatal): ${errorMessage}`, error);
    return;
  }

  critical(`Uncaught exception: ${errorMessage}`, error);
  // Exit with status code 1 to indicate error
  process.exit(1);
});

// Start server
async function runServer() {
  notice(`Starting LSP MCP Server`);

  const transport = new StdioServerTransport();
  await server.connect(transport);
  notice("LSP MCP Server running on stdio");
  info("Using LSP server:", lspServerPath);
  if (lspServerArgs.length > 0) {
    info("With arguments:", lspServerArgs.join(' '));
  }

  // Create LSP client instance but don't start the process or initialize yet
  // Both will happen when start_lsp is called
  lspClient = new LSPClient(lspServerPath, lspServerArgs);
  info("LSP client created. Use the start_lsp tool to start and initialize with a root directory.");
}

runServer().catch((error) => {
  emergency("Fatal error running server:", error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------

```typescript
import * as fs from "fs/promises";
import * as path from "path";
import { DiagnosticUpdateCallback, ResourceHandler, SubscriptionContext, SubscriptionHandler, UnsubscriptionHandler } from "../types/index.js";
import { LSPClient } from "../lspClient.js";
import { createFileUri, checkLspClientInitialized } from "../tools/index.js";
import { debug, logError } from "../logging/index.js";

// Helper function to parse a URI path
export const parseUriPath = (uri: URL): string => {
  // Ensure we handle paths correctly - URL parsing can remove the leading slash
  let decodedPath = decodeURIComponent(uri.pathname);
  // Normalize path to ensure it starts with a slash
  return path.posix.normalize(decodedPath.startsWith('/') ? decodedPath : '/' + decodedPath);
};

// Helper function to parse location parameters
export const parseLocationParams = (uri: URL): { filePath: string, line: number, character: number, languageId: string } => {
  // Get the file path
  const filePath = parseUriPath(uri);

  // Get the query parameters
  const lineParam = uri.searchParams.get('line');
  const columnParam = uri.searchParams.get('column');
  const languageId = uri.searchParams.get('language_id');

  if (!languageId) {
    throw new Error("language_id parameter is required");
  }

  if (!filePath || !lineParam || !columnParam) {
    throw new Error("Required parameters: file_path, line, column");
  }

  // Parse line and column as numbers
  const line = parseInt(lineParam, 10);
  const character = parseInt(columnParam, 10);

  if (isNaN(line) || isNaN(character)) {
    throw new Error("Line and column must be valid numbers");
  }

  return { filePath, line, character, languageId };
};

// Get resource handlers
export const getResourceHandlers = (lspClient: LSPClient | null): Record<string, ResourceHandler> => {
  return {
    // Handler for lsp-diagnostics://
    'lsp-diagnostics://': async (uri: string) => {
      checkLspClientInitialized(lspClient);

      try {
        // Parse the URI to handle query parameters correctly
        const diagnosticsUri = new URL(uri);
        
        // Get the file path from the pathname
        let filePath = parseUriPath(diagnosticsUri);
        
        // Remove query parameters from the file path if needed
        const questionMarkIndex = filePath.indexOf('?');
        if (questionMarkIndex !== -1) {
          filePath = filePath.substring(0, questionMarkIndex);
        }

        let diagnosticsContent: string;

        if (filePath && filePath !== '/') {
          // For a specific file
          debug(`Getting diagnostics for file: ${filePath}`);
          const fileUri = createFileUri(filePath);

          // Verify the file is open
          if (!lspClient!.isDocumentOpen(fileUri)) {
            throw new Error(`File ${filePath} is not open. Please open the file with open_document before requesting diagnostics.`);
          }

          const diagnostics = lspClient!.getDiagnostics(fileUri);
          diagnosticsContent = JSON.stringify({ [fileUri]: diagnostics }, null, 2);
        } else {
          // For all files
          debug("Getting diagnostics for all files");
          const allDiagnostics = lspClient!.getAllDiagnostics();

          // Convert Map to object for JSON serialization
          const diagnosticsObject: Record<string, any[]> = {};
          allDiagnostics.forEach((value: any[], key: string) => {
            // Only include diagnostics for open files
            if (lspClient!.isDocumentOpen(key)) {
              diagnosticsObject[key] = value;
            }
          });

          diagnosticsContent = JSON.stringify(diagnosticsObject, null, 2);
        }

        return {
          contents: [{ type: "text", text: diagnosticsContent, uri }],
        };
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        logError(`Error parsing diagnostics URI or getting diagnostics: ${errorMessage}`);
        throw new Error(`Error processing diagnostics request: ${errorMessage}`);
      }
    },

    // Handler for lsp-hover://
    'lsp-hover://': async (uri: string) => {
      checkLspClientInitialized(lspClient);

      try {
        // Extract parameters from URI
        // Format: lsp-hover://{file_path}?line={line}&character={character}&language_id={language_id}
        const hoverUri = new URL(uri);
        const { filePath, line, character, languageId } = parseLocationParams(hoverUri);

        debug(`Getting hover info for ${filePath} at line ${line}, character ${character}`);

        // Read the file content
        const fileContent = await fs.readFile(filePath, 'utf-8');

        // Create a file URI
        const fileUri = createFileUri(filePath);

        // Open the document in the LSP server (won't reopen if already open)
        await lspClient!.openDocument(fileUri, fileContent, languageId);

        // Get information at the location (LSP is 0-based)
        const hoverText = await lspClient!.getInfoOnLocation(fileUri, {
          line: line - 1,
          character: character - 1
        });

        debug(`Got hover information: ${hoverText.slice(0, 100)}${hoverText.length > 100 ? '...' : ''}`);

        return {
          contents: [{ type: "text", text: hoverText, uri }],
        };
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        logError(`Error parsing hover URI or getting hover information: ${errorMessage}`);
        throw new Error(`Error processing hover request: ${errorMessage}`);
      }
    },

    // Handler for lsp-completions://
    'lsp-completions://': async (uri: string) => {
      checkLspClientInitialized(lspClient);

      try {
        // Extract parameters from URI
        // Format: lsp-completions://{file_path}?line={line}&character={character}&language_id={language_id}
        const completionsUri = new URL(uri);
        const { filePath, line, character, languageId } = parseLocationParams(completionsUri);

        debug(`Getting completions for ${filePath} at line ${line}, character ${character}`);

        // Read the file content
        const fileContent = await fs.readFile(filePath, 'utf-8');

        // Create a file URI
        const fileUri = createFileUri(filePath);

        // Open the document in the LSP server (won't reopen if already open)
        await lspClient!.openDocument(fileUri, fileContent, languageId);

        // Get completions at the location (LSP is 0-based)
        const completions = await lspClient!.getCompletion(fileUri, {
          line: line - 1,
          character: character - 1
        });

        debug(`Got ${completions.length} completions`);

        return {
          contents: [{ type: "text", text: JSON.stringify(completions, null, 2), uri }],
        };
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        logError(`Error parsing completions URI or getting completions: ${errorMessage}`);
        throw new Error(`Error processing completions request: ${errorMessage}`);
      }
    }
  };
};

// Get subscription handlers
export const getSubscriptionHandlers = (lspClient: LSPClient | null, server: any): Record<string, SubscriptionHandler> => {
  return {
    // Handler for lsp-diagnostics://
    'lsp-diagnostics://': async (uri: string) => {
      checkLspClientInitialized(lspClient);

      // Extract the file path parameter from the URI
      const filePath = uri.slice(18);

      if (filePath) {
        // Subscribe to a specific file
        const fileUri = createFileUri(filePath);

        // Verify the file is open
        if (!lspClient!.isDocumentOpen(fileUri)) {
          throw new Error(`File ${filePath} is not open. Please open the file with open_document before subscribing to diagnostics.`);
        }

        debug(`Subscribing to diagnostics for file: ${filePath}`);

        // Set up the subscription callback
        const callback: DiagnosticUpdateCallback = (diagUri: string, diagnostics: any[]) => {
          if (diagUri === fileUri) {
            // Send resource update to clients
            server.notification({
              method: "notifications/resources/update",
              params: {
                uri,
                content: [{ type: "text", text: JSON.stringify({ [diagUri]: diagnostics }, null, 2) }]
              }
            });
          }
        };

        // Store the callback in the subscription context for later use with unsubscribe
        const subscriptionContext: SubscriptionContext = { callback };

        // Subscribe to diagnostics
        lspClient!.subscribeToDiagnostics(callback);

        return {
          ok: true,
          context: subscriptionContext
        };
      } else {
        // Subscribe to all files
        debug("Subscribing to diagnostics for all files");

        // Set up the subscription callback for all files
        const callback: DiagnosticUpdateCallback = (diagUri: string, diagnostics: any[]) => {
          // Only send updates for open files
          if (lspClient!.isDocumentOpen(diagUri)) {
            // Get all open documents' diagnostics
            const allDiagnostics = lspClient!.getAllDiagnostics();

            // Convert Map to object for JSON serialization
            const diagnosticsObject: Record<string, any[]> = {};
            allDiagnostics.forEach((diagValue: any[], diagKey: string) => {
              // Only include diagnostics for open files
              if (lspClient!.isDocumentOpen(diagKey)) {
                diagnosticsObject[diagKey] = diagValue;
              }
            });

            // Send resource update to clients
            server.notification({
              method: "notifications/resources/update",
              params: {
                uri,
                content: [{ type: "text", text: JSON.stringify(diagnosticsObject, null, 2) }]
              }
            });
          }
        };

        // Store the callback in the subscription context for later use with unsubscribe
        const subscriptionContext: SubscriptionContext = { callback };

        // Subscribe to diagnostics
        lspClient!.subscribeToDiagnostics(callback);

        return {
          ok: true,
          context: subscriptionContext
        };
      }
    }
  };
};

// Get unsubscription handlers
export const getUnsubscriptionHandlers = (lspClient: LSPClient | null): Record<string, UnsubscriptionHandler> => {
  return {
    // Handler for lsp-diagnostics://
    'lsp-diagnostics://': async (uri: string, context: any) => {
      checkLspClientInitialized(lspClient);

      if (context && (context as SubscriptionContext).callback) {
        // Unsubscribe the callback
        lspClient!.unsubscribeFromDiagnostics((context as SubscriptionContext).callback);
        debug(`Unsubscribed from diagnostics for URI: ${uri}`);

        return { ok: true };
      }

      throw new Error(`Invalid subscription context for URI: ${uri}`);
    }
  };
};

// Get resource definitions for the server
export const getResourceTemplates = () => {
  return [
    {
      name: "lsp-diagnostics",
      scheme: "lsp-diagnostics",
      pattern: "lsp-diagnostics://{file_path}",
      description: "Get diagnostic messages (errors, warnings) for a specific file or all files. Use this resource to identify problems in code files such as syntax errors, type mismatches, or other issues detected by the language server. When used without a file_path, returns diagnostics for all open files. Supports live updates through subscriptions.",
      subscribe: true,
    },
    {
      name: "lsp-hover",
      scheme: "lsp-hover",
      pattern: "lsp-hover://{file_path}?line={line}&column={column}&language_id={language_id}",
      description: "Get hover information for a specific location in a file. Use this resource to retrieve type information, documentation, and other contextual details about symbols in your code. Particularly useful for understanding variable types, function signatures, and module documentation at a specific cursor position.",
      subscribe: false,
    },
    {
      name: "lsp-completions",
      scheme: "lsp-completions",
      pattern: "lsp-completions://{file_path}?line={line}&column={column}&language_id={language_id}",
      description: "Get completion suggestions for a specific location in a file. Use this resource to obtain code completion options based on the current context, including variable names, function calls, object properties, and more. Helpful for code assistance and auto-completion features at a specific cursor position.",
      subscribe: false,
    }
  ];
};

// Generate resources list from open documents
export const generateResourcesList = (lspClient: LSPClient | null) => {
  const resources: Array<{
    uri: string;
    name: string;
    description: string;
    subscribe: boolean;
    template?: boolean;
  }> = [];

  // Check if LSP client is initialized
  if (!lspClient) {
    return resources; // Return empty list if LSP is not initialized
  }

  // Add the "all diagnostics" resource
  resources.push({
    uri: "lsp-diagnostics://",
    name: "All diagnostics",
    description: "Diagnostics for all open files",
    subscribe: true,
  });

  // For each open document, add resources
  lspClient.getOpenDocuments().forEach((uri: string) => {
    if (uri.startsWith('file://')) {
      const filePath = uri.slice(7); // Remove 'file://' prefix
      const fileName = path.basename(filePath);

      // Add diagnostics resource
      resources.push({
        uri: `lsp-diagnostics://${filePath}`,
        name: `Diagnostics for ${fileName}`,
        description: `LSP diagnostics for ${filePath}`,
        subscribe: true,
      });

      // Add hover resource template
      // We don't add specific hover resources since they require line/column coordinates
      // which are not known until the client requests them
      resources.push({
        uri: `lsp-hover://${filePath}?line={line}&column={column}&language_id={language_id}`,
        name: `Hover for ${fileName}`,
        description: `LSP hover information template for ${fileName}`,
        subscribe: false,
        template: true,
      });

      // Add completions resource template
      resources.push({
        uri: `lsp-completions://${filePath}?line={line}&column={column}&language_id={language_id}`,
        name: `Completions for ${fileName}`,
        description: `LSP code completion suggestions template for ${fileName}`,
        subscribe: false,
        template: true,
      });
    }
  });

  return resources;
};
```

--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------

```typescript
import * as fs from "fs/promises";
import * as path from "path";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import {
  GetInfoOnLocationArgsSchema,
  GetCompletionsArgsSchema,
  GetCodeActionsArgsSchema,
  OpenDocumentArgsSchema,
  CloseDocumentArgsSchema,
  GetDiagnosticsArgsSchema,
  SetLogLevelArgsSchema,
  RestartLSPServerArgsSchema,
  StartLSPArgsSchema,
  ToolInput,
  ToolHandler
} from "../types/index.js";
import { LSPClient } from "../lspClient.js";
import { debug, info, logError, notice, warning, setLogLevel } from "../logging/index.js";
import { activateExtension, deactivateExtension, listActiveExtensions } from "../extensions/index.js";

// Create a file URI from a file path
export const createFileUri = (filePath: string): string => {
  return `file://${path.resolve(filePath)}`;
};

// Check if LSP client is initialized
export const checkLspClientInitialized = (lspClient: LSPClient | null): void => {
  if (!lspClient) {
    throw new Error("LSP server not started. Call start_lsp first with a root directory.");
  }
};

// Define handlers for each tool
export const getToolHandlers = (lspClient: LSPClient | null, lspServerPath: string, lspServerArgs: string[], setLspClient: (client: LSPClient) => void, rootDir: string, setRootDir: (dir: string) => void, server?: any) => {
  return {
    "get_info_on_location": {
      schema: GetInfoOnLocationArgsSchema,
      handler: async (args: any) => {
        debug(`Getting info on location in file: ${args.file_path} (${args.line}:${args.column})`);

        checkLspClientInitialized(lspClient);

        // Read the file content
        const fileContent = await fs.readFile(args.file_path, 'utf-8');

        // Create a file URI
        const fileUri = createFileUri(args.file_path);

        // Open the document in the LSP server (won't reopen if already open)
        await lspClient!.openDocument(fileUri, fileContent, args.language_id);

        // Get information at the location
        const text = await lspClient!.getInfoOnLocation(fileUri, {
          line: args.line - 1, // LSP is 0-based
          character: args.column - 1
        });

        debug(`Returned info on location: ${text.slice(0, 100)}${text.length > 100 ? '...' : ''}`);

        return {
          content: [{ type: "text", text }],
        };
      }
    },

    "get_completions": {
      schema: GetCompletionsArgsSchema,
      handler: async (args: any) => {
        debug(`Getting completions in file: ${args.file_path} (${args.line}:${args.column})`);

        checkLspClientInitialized(lspClient);

        // Read the file content
        const fileContent = await fs.readFile(args.file_path, 'utf-8');

        // Create a file URI
        const fileUri = createFileUri(args.file_path);

        // Open the document in the LSP server (won't reopen if already open)
        await lspClient!.openDocument(fileUri, fileContent, args.language_id);

        // Get completions at the location
        const completions = await lspClient!.getCompletion(fileUri, {
          line: args.line - 1, // LSP is 0-based
          character: args.column - 1
        });

        debug(`Returned ${completions.length} completions`);

        return {
          content: [{ type: "text", text: JSON.stringify(completions, null, 2) }],
        };
      }
    },

    "get_code_actions": {
      schema: GetCodeActionsArgsSchema,
      handler: async (args: any) => {
        debug(`Getting code actions in file: ${args.file_path} (${args.start_line}:${args.start_column} to ${args.end_line}:${args.end_column})`);

        checkLspClientInitialized(lspClient);

        // Read the file content
        const fileContent = await fs.readFile(args.file_path, 'utf-8');

        // Create a file URI
        const fileUri = createFileUri(args.file_path);

        // Open the document in the LSP server (won't reopen if already open)
        await lspClient!.openDocument(fileUri, fileContent, args.language_id);

        // Get code actions for the range
        const codeActions = await lspClient!.getCodeActions(fileUri, {
          start: {
            line: args.start_line - 1, // LSP is 0-based
            character: args.start_column - 1
          },
          end: {
            line: args.end_line - 1,
            character: args.end_column - 1
          }
        });

        debug(`Returned ${codeActions.length} code actions`);

        return {
          content: [{ type: "text", text: JSON.stringify(codeActions, null, 2) }],
        };
      }
    },

    "restart_lsp_server": {
      schema: RestartLSPServerArgsSchema,
      handler: async (args: any) => {
        checkLspClientInitialized(lspClient);

        // Get the root directory from args or use the stored one
        const restartRootDir = args.root_dir || rootDir;

        info(`Restarting LSP server${args.root_dir ? ` with root directory: ${args.root_dir}` : ''}...`);

        try {
          // If root_dir is provided, update the stored rootDir
          if (args.root_dir) {
            setRootDir(args.root_dir);
          }

          // Restart with the root directory
          await lspClient!.restart(restartRootDir);

          return {
            content: [{
              type: "text",
              text: args.root_dir
                ? `LSP server successfully restarted and initialized with root directory: ${args.root_dir}`
                : "LSP server successfully restarted"
            }],
          };
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          logError(`Error restarting LSP server: ${errorMessage}`);
          throw new Error(`Failed to restart LSP server: ${errorMessage}`);
        }
      }
    },

    "start_lsp": {
      schema: StartLSPArgsSchema,
      handler: async (args: any) => {
        const startRootDir = args.root_dir || rootDir;
        info(`Starting LSP server with root directory: ${startRootDir}`);

        try {
          setRootDir(startRootDir);

          // Create LSP client if it doesn't exist
          if (!lspClient) {
            const newClient = new LSPClient(lspServerPath, lspServerArgs);
            setLspClient(newClient);
          }

          // Initialize with the specified root directory
          await lspClient!.initialize(startRootDir);

          return {
            content: [{ type: "text", text: `LSP server successfully started with root directory: ${rootDir}` }],
          };
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          logError(`Error starting LSP server: ${errorMessage}`);
          throw new Error(`Failed to start LSP server: ${errorMessage}`);
        }
      }
    },

    "open_document": {
      schema: OpenDocumentArgsSchema,
      handler: async (args: any) => {
        debug(`Opening document: ${args.file_path}`);

        checkLspClientInitialized(lspClient);

        try {
          // Read the file content
          const fileContent = await fs.readFile(args.file_path, 'utf-8');

          // Create a file URI
          const fileUri = createFileUri(args.file_path);

          // Open the document in the LSP server
          await lspClient!.openDocument(fileUri, fileContent, args.language_id);

          return {
            content: [{ type: "text", text: `File successfully opened: ${args.file_path}` }],
          };
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          logError(`Error opening document: ${errorMessage}`);
          throw new Error(`Failed to open document: ${errorMessage}`);
        }
      }
    },

    "close_document": {
      schema: CloseDocumentArgsSchema,
      handler: async (args: any) => {
        debug(`Closing document: ${args.file_path}`);

        checkLspClientInitialized(lspClient);

        try {
          // Create a file URI
          const fileUri = createFileUri(args.file_path);

          // Use the closeDocument method
          await lspClient!.closeDocument(fileUri);

          return {
            content: [{ type: "text", text: `File successfully closed: ${args.file_path}` }],
          };
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          logError(`Error closing document: ${errorMessage}`);
          throw new Error(`Failed to close document: ${errorMessage}`);
        }
      }
    },

    "get_diagnostics": {
      schema: GetDiagnosticsArgsSchema,
      handler: async (args: any) => {
        checkLspClientInitialized(lspClient);

        try {
          // Get diagnostics for a specific file or all files
          if (args.file_path) {
            // For a specific file
            debug(`Getting diagnostics for file: ${args.file_path}`);
            const fileUri = createFileUri(args.file_path);

            // Verify the file is open
            if (!lspClient!.isDocumentOpen(fileUri)) {
              throw new Error(`File ${args.file_path} is not open. Please open the file with open_document before requesting diagnostics.`);
            }

            const diagnostics = lspClient!.getDiagnostics(fileUri);

            return {
              content: [{
                type: "text",
                text: JSON.stringify({ [fileUri]: diagnostics }, null, 2)
              }],
            };
          } else {
            // For all files
            debug("Getting diagnostics for all files");
            const allDiagnostics = lspClient!.getAllDiagnostics();

            // Convert Map to object for JSON serialization
            const diagnosticsObject: Record<string, any[]> = {};
            allDiagnostics.forEach((value: any[], key: string) => {
              // Only include diagnostics for open files
              if (lspClient!.isDocumentOpen(key)) {
                diagnosticsObject[key] = value;
              }
            });

            return {
              content: [{
                type: "text",
                text: JSON.stringify(diagnosticsObject, null, 2)
              }],
            };
          }
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);
          logError(`Error getting diagnostics: ${errorMessage}`);
          throw new Error(`Failed to get diagnostics: ${errorMessage}`);
        }
      }
    },

    "set_log_level": {
      schema: SetLogLevelArgsSchema,
      handler: async (args: any) => {
        // Set the log level
        const { level } = args;
        setLogLevel(level);

        return {
          content: [{ type: "text", text: `Log level set to: ${level}` }],
        };
      }
    },
  };
};

// Get tool definitions for the server
export const getToolDefinitions = () => {
  return [
    {
      name: "get_info_on_location",
      description: "Get information on a specific location in a file via LSP hover. Use this tool to retrieve detailed type information, documentation, and other contextual details about symbols in your code. Particularly useful for understanding variable types, function signatures, and module documentation at a specific location in the code. Use this whenever you need to get a better idea on what a particular function is doing in that context. Requires the file to be opened first.",
      inputSchema: zodToJsonSchema(GetInfoOnLocationArgsSchema) as ToolInput,
    },
    {
      name: "get_completions",
      description: "Get completion suggestions at a specific location in a file. Use this tool to retrieve code completion options based on the current context, including variable names, function calls, object properties, and more. Helpful for code assistance and auto-completion at a particular location. Use this when determining which functions you have available in a given package, for example when changing libraries. Requires the file to be opened first.",
      inputSchema: zodToJsonSchema(GetCompletionsArgsSchema) as ToolInput,
    },
    {
      name: "get_code_actions",
      description: "Get code actions for a specific range in a file. Use this tool to obtain available refactorings, quick fixes, and other code modifications that can be applied to a selected code range. Examples include adding imports, fixing errors, or implementing interfaces. Requires the file to be opened first.",
      inputSchema: zodToJsonSchema(GetCodeActionsArgsSchema) as ToolInput,
    },
    {
      name: "restart_lsp_server",
      description: "Restart the LSP server process. Use this tool to reset the LSP server if it becomes unresponsive, has stale data, or when you need to apply configuration changes. Can optionally reinitialize with a new root directory. Useful for troubleshooting language server issues or when switching projects.",
      inputSchema: zodToJsonSchema(RestartLSPServerArgsSchema) as ToolInput,
    },
    {
      name: "start_lsp",
      description: "Start the LSP server with a specified root directory. IMPORTANT: This tool must be called before using any other LSP functionality. The root directory should point to the project's base folder, which typically contains configuration files like tsconfig.json, package.json, or other language-specific project files. All file paths in other tool calls will be resolved relative to this root.",
      inputSchema: zodToJsonSchema(StartLSPArgsSchema) as ToolInput,
    },
    {
      name: "open_document",
      description: "Open a file in the LSP server for analysis. Use this tool before performing operations like getting diagnostics, hover information, or completions for a file. The file remains open for continued analysis until explicitly closed. The language_id parameter tells the server which language service to use (e.g., 'typescript', 'javascript', 'haskell').",
      inputSchema: zodToJsonSchema(OpenDocumentArgsSchema) as ToolInput,
    },
    {
      name: "close_document",
      description: "Close a file in the LSP server. Use this tool when you're done with a file to free up resources and reduce memory usage. It's good practice to close files that are no longer being actively analyzed, especially in long-running sessions or when working with large codebases.",
      inputSchema: zodToJsonSchema(CloseDocumentArgsSchema) as ToolInput,
    },
    {
      name: "get_diagnostics",
      description: "Get diagnostic messages (errors, warnings) for files. Use this tool to identify problems in code files such as syntax errors, type mismatches, or other issues detected by the language server. When used without a file_path, returns diagnostics for all open files. Requires files to be opened first.",
      inputSchema: zodToJsonSchema(GetDiagnosticsArgsSchema) as ToolInput,
    },
    {
      name: "set_log_level",
      description: "Set the server logging level. Use this tool to control the verbosity of logs generated by the LSP MCP server. Available levels from least to most verbose: emergency, alert, critical, error, warning, notice, info, debug. Increasing verbosity can help troubleshoot issues but may generate large amounts of output.",
      inputSchema: zodToJsonSchema(SetLogLevelArgsSchema) as ToolInput,
    },
  ];
};

```

--------------------------------------------------------------------------------
/test/typescript-lsp.test.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node
// TypeScript LSP integration test for MCP using the official SDK

import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import path from 'path';
import fs from 'fs/promises';
import fsSync from 'fs';
import assert from 'assert';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/sdk/shared/stdio.js';

// Get the current file's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Custom transport that works with an existing child process
class CustomStdioTransport {
  constructor(childProcess) {
    this.childProcess = childProcess;
    this.readBuffer = new ReadBuffer();
    this.onmessage = null;
    this.onerror = null;
    this.onclose = null;

    this._setupListeners();
  }

  _setupListeners() {
    // Set up stdout handler for responses
    this.childProcess.stdout.on('data', (data) => {
      this.readBuffer.append(data);
      this._processReadBuffer();
    });

    // Set up error handler
    this.childProcess.on('error', (error) => {
      if (this.onerror) this.onerror(error);
    });

    // Set up close handler
    this.childProcess.on('close', (code) => {
      if (this.onclose) this.onclose();
    });

    // Handle errors on streams
    this.childProcess.stdout.on('error', (error) => {
      if (this.onerror) this.onerror(error);
    });

    this.childProcess.stdin.on('error', (error) => {
      if (this.onerror) this.onerror(error);
    });
  }

  _processReadBuffer() {
    while (true) {
      try {
        const message = this.readBuffer.readMessage();
        if (message === null) {
          break;
        }
        if (this.onmessage) this.onmessage(message);
      } catch (error) {
        if (this.onerror) this.onerror(error);
      }
    }
  }

  async start() {
    // No need to start since we're using an existing process
    return Promise.resolve();
  }

  async close() {
    // Don't actually kill the process here - we'll handle that separately
    this.readBuffer.clear();
  }

  send(message) {
    return new Promise((resolve) => {
      if (!this.childProcess.stdin) {
        throw new Error('Not connected');
      }

      const json = serializeMessage(message);
      console.log('>>> SENDING:', json.toString().trim());

      if (this.childProcess.stdin.write(json)) {
        resolve();
      } else {
        this.childProcess.stdin.once('drain', resolve);
      }
    });
  }
}

// Path to the TypeScript project for testing
const TS_PROJECT_PATH = path.join(__dirname, 'ts-project');
const EXAMPLE_TS_FILE = path.join(TS_PROJECT_PATH, 'src', 'example.ts');

// Path to our compiled server script and the typescript-language-server binary
const LSP_MCP_SERVER = path.join(__dirname, '..', 'dist', 'index.js');
const TS_SERVER_BIN = path.join(__dirname, '..', 'node_modules', '.bin', 'typescript-language-server');

// Check prerequisites
try {
  const stats = fsSync.statSync(TS_SERVER_BIN);
  if (!stats.isFile()) {
    console.error(`Error: The typescript-language-server at '${TS_SERVER_BIN}' is not a file`);
    process.exit(1);
  }
} catch (error) {
  console.error(`Error: Could not find typescript-language-server at '${TS_SERVER_BIN}'`);
  console.error('Make sure you have installed the typescript-language-server as a dev dependency');
  process.exit(1);
}

if (!fsSync.existsSync(LSP_MCP_SERVER)) {
  console.error(`ERROR: LSP MCP server not found at ${LSP_MCP_SERVER}`);
  console.error(`Make sure you've built the project with 'npm run build'`);
  process.exit(1);
}

class TypeScriptLspTester {
  constructor() {
    this.client = null;
    this.serverProcess = null;
    this.testResults = {
      passed: [],
      failed: []
    };
  }

  async start() {
    // Start the MCP server
    console.log(`Starting MCP server: node ${LSP_MCP_SERVER} typescript ${TS_SERVER_BIN} --stdio`);

    this.serverProcess = spawn('node', [LSP_MCP_SERVER, 'typescript', TS_SERVER_BIN, '--stdio'], {
      env: {
        ...process.env,
        DEBUG: 'true',
        LOG_LEVEL: 'debug'
      },
      stdio: ['pipe', 'pipe', 'pipe']
    });

    console.log(`MCP server started with PID: ${this.serverProcess.pid}`);

    // Set up stderr handler for logging
    this.serverProcess.stderr.on('data', (data) => {
      console.log(`SERVER STDERR: ${data.toString().trim()}`);
    });

    // Set up error handler
    this.serverProcess.on('error', (error) => {
      console.error(`SERVER ERROR: ${error.message}`);
    });

    // Create our custom transport with the existing server process
    const transport = new CustomStdioTransport(this.serverProcess);

    // Create the client with proper initialization
    this.client = new Client(
      // clientInfo
      {
        name: "typescript-lsp-test-client",
        version: "1.0.0"
      },
      // options
      {
        capabilities: {
          tools: true,
          resources: true,
          logging: true
        }
      }
    );

    // Connect client to the transport
    try {
      await this.client.connect(transport);
      console.log("Connected to MCP server successfully");
    } catch (error) {
      console.error("Failed to connect to MCP server:", error);
      throw error;
    }

    // Wait a bit to ensure everything is initialized
    await new Promise(resolve => setTimeout(resolve, 2000));

    return this;
  }

  stop() {
    if (this.serverProcess) {
      console.log("Sending SIGINT to MCP server");
      this.serverProcess.kill('SIGINT');
      this.serverProcess = null;
    }
  }

  // Helper method to run a test case and record result
  async runTest(name, func) {
    console.log(`\nTest: ${name}`);
    try {
      await func();
      console.log(`✅ Test passed: ${name}`);
      this.testResults.passed.push(name);
      return true;
    } catch (error) {
      console.error(`❌ Test failed: ${name}`);
      console.error(`Error: ${error.message}`);
      this.testResults.failed.push(name);
      return false;
    }
  }

  // Execute a tool and verify the result
  async executeTool(toolName, args, validateFn = null) {
    console.log(`Executing tool: ${toolName}`);

    try {
      // The callTool method expects a name and arguments parameter
      const params = {
        name: toolName,
        arguments: args
      };

      const result = await this.client.callTool(params);
      console.log(`Tool result:`, result);

      // If a validation function is provided, run it
      if (validateFn) {
        validateFn(result);
      }

      return result;
    } catch (error) {
      console.error(`Failed to execute tool ${toolName}:`, error);
      throw error;
    }
  }

  // Test listing the available tools
  async testListTools() {
    console.log("Listing available tools...");

    try {
      const response = await this.client.listTools();

      // Depending on the response format, extract the tools array
      let tools = [];
      if (response && response.tools && Array.isArray(response.tools)) {
        tools = response.tools;
      } else if (Array.isArray(response)) {
        tools = response;
      } else {
        console.log("Unexpected tools response format:", response);
        tools = []; // Ensure we have an array to work with
      }

      console.log(`Found ${tools.length} tools`);
      tools.forEach(tool => {
        if (tool && tool.name) {
          console.log(`- ${tool.name}: ${tool.description || 'No description'}`);
        }
      });

      // If we didn't get any tools, we'll run the other tests anyway
      if (tools.length === 0) {
        console.log("WARNING: No tools returned but continuing with tests");
        return tools;
      }

      // Verify we have the expected tools
      const requiredTools = ['get_info_on_location', 'get_completions', 'get_code_actions',
                           'restart_lsp_server', 'start_lsp', 'open_document',
                           'close_document', 'get_diagnostics'];

      const missingTools = requiredTools.filter(tool =>
        !tools.some(t => t.name === tool)
      );

      if (missingTools.length > 0) {
        console.warn(`WARNING: Missing some expected tools: ${missingTools.join(', ')}`);
      }

      return tools;
    } catch (error) {
      // Just log the error but don't fail the test - we'll continue with the rest
      console.warn(`WARNING: Error listing tools: ${error.message}`);
      return [];
    }
  }

  // Test listing resources
  async testListResources() {
    console.log("Listing available resources...");

    try {
      // Using the listResources method which is the correct SDK method
      const response = await this.client.listResources();

      // Extract the resources array
      let resources = [];
      if (response && response.resources && Array.isArray(response.resources)) {
        resources = response.resources;
      } else if (Array.isArray(response)) {
        resources = response;
      } else {
        console.log("Unexpected resources response format:", response);
        resources = []; // Ensure we have an array to work with
      }

      console.log(`Found ${resources.length} resources`);
      resources.forEach(resource => {
        if (resource && resource.name) {
          console.log(`- ${resource.name}: ${resource.description || 'No description'}`);
        }
      });

      // If we didn't get any resources, we'll run the other tests anyway
      if (resources.length === 0) {
        console.log("WARNING: No resources returned but continuing with tests");
        return resources;
      }

      return resources;
    } catch (error) {
      // Just log the error but don't fail the test - we'll continue with the rest
      console.warn(`WARNING: Error listing resources: ${error.message}`);
      return [];
    }
  }

  // Execute a resource request and verify the result
  async accessResource(params, validateFn = null) {
    console.log(`Accessing resource: ${params.uri}`);

    try {
      // Use readResource to access a resource with the params object directly
      const result = await this.client.readResource(params);
      console.log(`Resource result:`, result);

      // If a validation function is provided, run it
      if (validateFn) {
        validateFn(result);
      }

      return result;
    } catch (error) {
      console.error(`Failed to access resource ${params.uri}:`, error);
      throw error;
    }
  }

  // Print a summary of the test results
  printResults() {
    console.log('\n=== Test Results ===');
    console.log(`Passed: ${this.testResults.passed.length}/${this.testResults.passed.length + this.testResults.failed.length}`);

    console.log('\nPassed Tests:');
    for (const test of this.testResults.passed) {
      console.log(`  ✅ ${test}`);
    }

    console.log('\nFailed Tests:');
    for (const test of this.testResults.failed) {
      console.log(`  ❌ ${test}`);
    }

    if (this.testResults.failed.length > 0) {
      console.log('\n❌ Some tests failed');
      return false;
    } else if (this.testResults.passed.length === 0) {
      console.log('\n❌ No tests passed');
      return false;
    } else {
      console.log('\n✅ All tests passed');
      return true;
    }
  }
}

// Run the tests
async function runTests() {
  console.log('=== TypeScript LSP MCP Integration Tests ===');

  const tester = await new TypeScriptLspTester().start();

  try {
    // Make sure the example file exists
    await fs.access(EXAMPLE_TS_FILE);
    const fileContent = await fs.readFile(EXAMPLE_TS_FILE, 'utf8');
    console.log(`Example file ${EXAMPLE_TS_FILE} exists and is ${fileContent.length} bytes`);

    // Test listing tools
    await tester.runTest('List tools', async () => {
      await tester.testListTools();
    });

    // Test starting the TypeScript LSP
    await tester.runTest('Start LSP', async () => {
      await tester.executeTool('start_lsp', {
        root_dir: TS_PROJECT_PATH
      }, (result) => {
        assert(result.content && result.content.length > 0,
              'Expected content in the result');
      });
    });

    // Wait for LSP to fully initialize
    console.log('\nWaiting for LSP to fully initialize...');
    await new Promise(resolve => setTimeout(resolve, 3000));

    // Test opening document
    await tester.runTest('Open document', async () => {
      await tester.executeTool('open_document', {
        file_path: EXAMPLE_TS_FILE,
        language_id: 'typescript'
      }, (result) => {
        assert(result.content && result.content.length > 0,
              'Expected content in the result');
      });
    });

    // Test getting hover information
    await tester.runTest('Hover information', async () => {
      await tester.executeTool('get_info_on_location', {
        file_path: EXAMPLE_TS_FILE,
        language_id: 'typescript',
        line: 4,
        column: 15
      }, (result) => {
        assert(result.content && result.content.length > 0,
              'Expected content in the result');
        // In a real test, we would verify the content contains actual hover info
      });
    });

    // Test getting completions
    await tester.runTest('Completions', async () => {
      await tester.executeTool('get_completions', {
        file_path: EXAMPLE_TS_FILE,
        language_id: 'typescript',
        line: 5,
        column: 10
      }, (result) => {
        assert(result.content && result.content.length > 0,
              'Expected content in the result');
        // In a real test, we would verify the content contains actual completions
      });
    });

    // Test getting diagnostics
    await tester.runTest('Diagnostics', async () => {
      await tester.executeTool('get_diagnostics', {
        file_path: EXAMPLE_TS_FILE
      }, (result) => {
        assert(result.content && result.content.length > 0,
              'Expected content in the result');
        // In a real test, we would verify the content contains actual diagnostics
      });
    });

    // Test getting code actions
    await tester.runTest('Code actions', async () => {
      await tester.executeTool('get_code_actions', {
        file_path: EXAMPLE_TS_FILE,
        language_id: 'typescript',
        start_line: 40,
        start_column: 1,
        end_line: 40,
        end_column: 20
      }, (result) => {
        assert(result.content && result.content.length > 0,
              'Expected content in the result');
        // In a real test, we would verify the content contains actual code actions
      });
    });

    // Test closing document
    await tester.runTest('Close document', async () => {
      await tester.executeTool('close_document', {
        file_path: EXAMPLE_TS_FILE
      }, (result) => {
        assert(result.content && result.content.length > 0,
              'Expected content in the result');
      });
    });

    // Test restarting LSP server
    await tester.runTest('Restart LSP server', async () => {
      await tester.executeTool('restart_lsp_server', {}, (result) => {
        assert(result.content && result.content.length > 0,
              'Expected content in the result');
      });
    });
    
    // Test listing resources
    await tester.runTest('List resources', async () => {
      const resources = await tester.testListResources();
      assert(Array.isArray(resources), 'Expected resources to be an array');
    });
    
    // Test accessing diagnostics resource
    await tester.runTest('Access diagnostics resource', async () => {
      // First make sure document is open again
      await tester.executeTool('open_document', {
        file_path: EXAMPLE_TS_FILE,
        language_id: 'typescript'
      });
      
      // Then try to access diagnostics resource using proper URI format
      const diagnosticsUri = `lsp-diagnostics://${EXAMPLE_TS_FILE}?language_id=typescript`;
      await tester.accessResource({
        uri: diagnosticsUri
      }, (result) => {
        assert(result && result.contents && result.contents.length > 0, 
              'Expected contents in the diagnostics result');
      });
    });
    
    // Test accessing hover resource
    await tester.runTest('Access hover resource', async () => {
      // Use proper URI format for hover resource
      const hoverUri = `lsp-hover://${EXAMPLE_TS_FILE}?line=4&column=15&language_id=typescript`;
      await tester.accessResource({
        uri: hoverUri
      }, (result) => {
        assert(result && result.contents && result.contents.length > 0,
              'Expected contents in the hover result');
      });
    });
    
    // Test accessing completion resource
    await tester.runTest('Access completion resource', async () => {
      // Use proper URI format for completion resource
      const completionUri = `lsp-completions://${EXAMPLE_TS_FILE}?line=5&column=10&language_id=typescript`;
      await tester.accessResource({
        uri: completionUri
      }, (result) => {
        assert(result && result.contents && result.contents.length > 0,
              'Expected contents in the completion result');
      });
    });

  } catch (error) {
    console.error('ERROR in tests:', error);
  } finally {
    // Print results
    const allPassed = tester.printResults();

    // Clean up
    console.log('\nShutting down tester...');
    tester.stop();

    // Exit with appropriate status code
    process.exit(allPassed ? 0 : 1);
  }
}

// Execute the tests
console.log('Starting TypeScript LSP MCP integration tests');
runTests().catch(error => {
  console.error('Unhandled error:', error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/src/lspClient.ts:
--------------------------------------------------------------------------------

```typescript
import { spawn } from "child_process";
import path from "path";
import { LSPMessage, DiagnosticUpdateCallback, LoggingLevel } from "./types/index.js";
import { debug, info, notice, warning, log, logError } from "./logging/index.js";

export class LSPClient {
  private process: any;
  private buffer: string = "";
  private messageQueue: LSPMessage[] = [];
  private nextId: number = 1;
  private responsePromises: Map<string | number, { resolve: Function; reject: Function }> = new Map();
  private initialized: boolean = false;
  private serverCapabilities: any = null;
  private lspServerPath: string;
  private lspServerArgs: string[];
  private openedDocuments: Set<string> = new Set();
  private documentVersions: Map<string, number> = new Map();
  private processingQueue: boolean = false;
  private documentDiagnostics: Map<string, any[]> = new Map();
  private diagnosticSubscribers: Set<DiagnosticUpdateCallback> = new Set();

  constructor(lspServerPath: string, lspServerArgs: string[] = []) {
    this.lspServerPath = lspServerPath;
    this.lspServerArgs = lspServerArgs;
    // Don't start the process automatically - it will be started when needed
  }

  private startProcess(): void {
    info(`Starting LSP client with binary: ${this.lspServerPath}`);
    info(`Using LSP server arguments: ${this.lspServerArgs.join(' ')}`);
    this.process = spawn(this.lspServerPath, this.lspServerArgs, {
      stdio: ["pipe", "pipe", "pipe"]
    });

    // Set up event listeners
    this.process.stdout.on("data", (data: Buffer) => this.handleData(data));
    this.process.stderr.on("data", (data: Buffer) => {
      debug(`LSP Server Message: ${data.toString()}`);
    });

    this.process.on("close", (code: number) => {
      notice(`LSP server process exited with code ${code}`);
    });
  }

  private handleData(data: Buffer): void {
    // Append new data to buffer
    this.buffer += data.toString();

    // Implement a safety limit to prevent excessive buffer growth
    const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB limit
    if (this.buffer.length > MAX_BUFFER_SIZE) {
      logError(`Buffer size exceeded ${MAX_BUFFER_SIZE} bytes, clearing buffer to prevent memory issues`);
      this.buffer = this.buffer.substring(this.buffer.length - MAX_BUFFER_SIZE);
    }

    // Process complete messages
    while (true) {
      // Look for the standard LSP header format - this captures the entire header including the \r\n\r\n
      const headerMatch = this.buffer.match(/^Content-Length: (\d+)\r\n\r\n/);
      if (!headerMatch) break;

      const contentLength = parseInt(headerMatch[1], 10);
      const headerEnd = headerMatch[0].length;

      // Prevent processing unreasonably large messages
      if (contentLength > MAX_BUFFER_SIZE) {
        logError(`Received message with content length ${contentLength} exceeds maximum size, skipping`);
        this.buffer = this.buffer.substring(headerEnd + contentLength);
        continue;
      }

      // Check if we have the complete message (excluding the header)
      if (this.buffer.length < headerEnd + contentLength) break; // Message not complete yet

      // Extract the message content - using exact content length without including the header
      let content = this.buffer.substring(headerEnd, headerEnd + contentLength);
      // Make the parsing more robust by ensuring content ends with a closing brace
      if (content[content.length - 1] !== '}') {
        debug("Content doesn't end with '}', adjusting...");
        const lastBraceIndex = content.lastIndexOf('}');
        if (lastBraceIndex !== -1) {
          const actualContentLength = lastBraceIndex + 1;
          debug(`Adjusted content length from ${contentLength} to ${actualContentLength}`);
          content = content.substring(0, actualContentLength);
          // Update buffer position based on actual content length
          this.buffer = this.buffer.substring(headerEnd + actualContentLength);
        } else {
          debug("No closing brace found, using original content length");
          // No closing brace found, use original approach
          this.buffer = this.buffer.substring(headerEnd + contentLength);
        }
      } else {
        debug("Content ends with '}', no adjustment needed");
        // Content looks good, remove precisely this processed message from buffer
        this.buffer = this.buffer.substring(headerEnd + contentLength);
      }


      // Parse the message and add to queue
      try {
        const message = JSON.parse(content) as LSPMessage;
        this.messageQueue.push(message);
        this.processMessageQueue();
      } catch (error) {
        logError("Failed to parse LSP message:", error);
      }
    }
  }

  private async processMessageQueue(): Promise<void> {
    // If already processing, return to avoid concurrent processing
    if (this.processingQueue) return;

    this.processingQueue = true;

    try {
      while (this.messageQueue.length > 0) {
        const message = this.messageQueue.shift()!;
        await this.handleMessage(message);
      }
    } finally {
      this.processingQueue = false;
    }
  }

  private async handleMessage(message: LSPMessage): Promise<void> {
    // Log the message with appropriate level
    try {
      const direction = 'RECEIVED';
      const messageStr = JSON.stringify(message, null, 2);
      // Use method to determine log level if available, otherwise use debug
      const method = message.method || '';
      const logLevel = this.getLSPMethodLogLevel(method);
      log(logLevel, `LSP ${direction} (${method}): ${messageStr}`);
    } catch (error) {
      warning("Error logging LSP message:", error);
    }

    // Handle response messages
    if ('id' in message && (message.result !== undefined || message.error !== undefined)) {
      const promise = this.responsePromises.get(message.id!);
      if (promise) {
        if (message.error) {
          promise.reject(message.error);
        } else {
          promise.resolve(message.result);
        }
        this.responsePromises.delete(message.id!);
      }
    }

    // Store server capabilities from initialize response
    if ('id' in message && message.result?.capabilities) {
      this.serverCapabilities = message.result.capabilities;
    }

    // Handle notification messages
    if ('method' in message && message.id === undefined) {
      // Handle diagnostic notifications
      if (message.method === 'textDocument/publishDiagnostics' && message.params) {
        const { uri, diagnostics } = message.params;

        if (uri && Array.isArray(diagnostics)) {
          const severity = diagnostics.length > 0 ?
            Math.min(...diagnostics.map(d => d.severity || 4)) : 4;

          // Map LSP severity to our log levels
          const severityToLevel: Record<number, string> = {
            1: 'error',      // Error
            2: 'warning',    // Warning
            3: 'info',       // Information
            4: 'debug'       // Hint
          };

          const level = severityToLevel[severity] || 'debug';

          log(level as any, `Received ${diagnostics.length} diagnostics for ${uri}`);

          // Store diagnostics, replacing any previous ones for this URI
          this.documentDiagnostics.set(uri, diagnostics);

          // Notify all subscribers about this update
          this.notifyDiagnosticUpdate(uri, diagnostics);
        }
      }
    }
  }

  private getLSPMethodLogLevel(method: string): LoggingLevel {
    // Define appropriate log levels for different LSP methods
    if (method.startsWith('textDocument/did')) {
      return 'debug'; // Document changes are usually debug level
    }

    if (method.includes('diagnostic') || method.includes('publishDiagnostics')) {
      return 'info'; // Diagnostics depend on their severity, but base level is info
    }

    if (method === 'initialize' || method === 'initialized' ||
        method === 'shutdown' || method === 'exit') {
      return 'notice'; // Important lifecycle events are notice level
    }

    // Default to debug level for most LSP operations
    return 'debug';
  }

  private sendRequest<T>(method: string, params?: any): Promise<T> {
    // Check if the process is started
    if (!this.process) {
      return Promise.reject(new Error("LSP process not started. Please call start_lsp first."));
    }

    const id = this.nextId++;
    const request: LSPMessage = {
      jsonrpc: "2.0",
      id,
      method,
      params
    };

    // Log the request with appropriate level
    try {
      const direction = 'SENT';
      const requestStr = JSON.stringify(request, null, 2);
      const logLevel = this.getLSPMethodLogLevel(method);
      log(logLevel as any, `LSP ${direction} (${method}): ${requestStr}`);
    } catch (error) {
      warning("Error logging LSP request:", error);
    }

    const promise = new Promise<T>((resolve, reject) => {
      // Set timeout for request
      const timeoutId = setTimeout(() => {
        if (this.responsePromises.has(id)) {
          this.responsePromises.delete(id);
          reject(new Error(`Timeout waiting for response to ${method} request`));
        }
      }, 10000); // 10 second timeout

      // Store promise with cleanup for timeout
      this.responsePromises.set(id, {
        resolve: (result: T) => {
          clearTimeout(timeoutId);
          resolve(result);
        },
        reject: (error: any) => {
          clearTimeout(timeoutId);
          reject(error);
        }
      });
    });

    const content = JSON.stringify(request);
    // Content-Length header should only include the length of the JSON content
    const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
    this.process.stdin.write(header + content);

    return promise;
  }

  private sendNotification(method: string, params?: any): void {
    // Check if the process is started
    if (!this.process) {
      console.error("LSP process not started. Please call start_lsp first.");
      return;
    }

    const notification: LSPMessage = {
      jsonrpc: "2.0",
      method,
      params
    };

    // Log the notification with appropriate level
    try {
      const direction = 'SENT';
      const notificationStr = JSON.stringify(notification, null, 2);
      const logLevel = this.getLSPMethodLogLevel(method);
      log(logLevel as any, `LSP ${direction} (${method}): ${notificationStr}`);
    } catch (error) {
      warning("Error logging LSP notification:", error);
    }

    const content = JSON.stringify(notification);
    // Content-Length header should only include the length of the JSON content
    const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
    this.process.stdin.write(header + content);
  }

  async initialize(rootDirectory: string = "."): Promise<void> {
    if (this.initialized) return;

    try {
      // Start the process if it hasn't been started yet
      if (!this.process) {
        this.startProcess();
      }

      info("Initializing LSP connection...");
      await this.sendRequest("initialize", {
        processId: process.pid,
        clientInfo: {
          name: "lsp-mcp-server"
        },
        rootUri: "file://" + path.resolve(rootDirectory),
        capabilities: {
          textDocument: {
            hover: {
              contentFormat: ["markdown", "plaintext"]
            },
            completion: {
              completionItem: {
                snippetSupport: false
              }
            },
            codeAction: {
              dynamicRegistration: true
            },
            diagnostic: {
              dynamicRegistration: false
            },
            publishDiagnostics: {
              relatedInformation: true,
              versionSupport: false,
              tagSupport: {},
              codeDescriptionSupport: true,
              dataSupport: true
            }
          }
        }
      });

      this.sendNotification("initialized", {});
      this.initialized = true;
      notice("LSP connection initialized successfully");
    } catch (error) {
      logError("Failed to initialize LSP connection:", error);
      throw error;
    }
  }

  async openDocument(uri: string, text: string, languageId: string): Promise<void> {
    // Check if initialized, but don't auto-initialize
    if (!this.initialized) {
      throw new Error("LSP client not initialized. Please call start_lsp first.");
    }

    // If document is already open, update it instead of reopening
    if (this.openedDocuments.has(uri)) {
      // Get current version and increment
      const currentVersion = this.documentVersions.get(uri) || 1;
      const newVersion = currentVersion + 1;

      debug(`Document already open, updating content: ${uri} (version ${newVersion})`);
      this.sendNotification("textDocument/didChange", {
        textDocument: {
          uri,
          version: newVersion
        },
        contentChanges: [
          {
            text // Full document update
          }
        ]
      });

      // Update version
      this.documentVersions.set(uri, newVersion);
      return;
    }

    debug(`Opening document: ${uri}`);
    this.sendNotification("textDocument/didOpen", {
      textDocument: {
        uri,
        languageId,
        version: 1,
        text
      }
    });

    // Mark document as open and initialize version
    this.openedDocuments.add(uri);
    this.documentVersions.set(uri, 1);
  }

  // Check if a document is open
  isDocumentOpen(uri: string): boolean {
    return this.openedDocuments.has(uri);
  }

  // Get a list of all open documents
  getOpenDocuments(): string[] {
    return Array.from(this.openedDocuments);
  }

  // Close a document
  async closeDocument(uri: string): Promise<void> {
    // Check if initialized
    if (!this.initialized) {
      throw new Error("LSP client not initialized. Please call start_lsp first.");
    }

    // Only close if document is open
    if (this.openedDocuments.has(uri)) {
      debug(`Closing document: ${uri}`);
      this.sendNotification("textDocument/didClose", {
        textDocument: { uri }
      });

      // Remove from tracking
      this.openedDocuments.delete(uri);
      this.documentVersions.delete(uri);
    } else {
      debug(`Document not open: ${uri}`);
    }
  }

  // Get diagnostics for a file
  getDiagnostics(uri: string): any[] {
    return this.documentDiagnostics.get(uri) || [];
  }

  // Get all diagnostics
  getAllDiagnostics(): Map<string, any[]> {
    return new Map(this.documentDiagnostics);
  }

  // Subscribe to diagnostic updates
  subscribeToDiagnostics(callback: DiagnosticUpdateCallback): void {
    this.diagnosticSubscribers.add(callback);

    // Send initial diagnostics for all open documents
    this.documentDiagnostics.forEach((diagnostics, uri) => {
      callback(uri, diagnostics);
    });
  }

  // Unsubscribe from diagnostic updates
  unsubscribeFromDiagnostics(callback: DiagnosticUpdateCallback): void {
    this.diagnosticSubscribers.delete(callback);
  }

  // Notify all subscribers about diagnostic updates
  private notifyDiagnosticUpdate(uri: string, diagnostics: any[]): void {
    this.diagnosticSubscribers.forEach(callback => {
      try {
        callback(uri, diagnostics);
      } catch (error) {
        warning("Error in diagnostic subscriber callback:", error);
      }
    });
  }

  // Clear all diagnostic subscribers
  clearDiagnosticSubscribers(): void {
    this.diagnosticSubscribers.clear();
  }

  async getInfoOnLocation(uri: string, position: { line: number, character: number }): Promise<string> {
    // Check if initialized, but don't auto-initialize
    if (!this.initialized) {
      throw new Error("LSP client not initialized. Please call start_lsp first.");
    }

    debug(`Getting info on location: ${uri} (${position.line}:${position.character})`);

    try {
      // Use hover request to get information at the position
      const response = await this.sendRequest<any>("textDocument/hover", {
        textDocument: { uri },
        position
      });

      if (response?.contents) {
        if (typeof response.contents === 'string') {
          return response.contents;
        } else if (response.contents.value) {
          return response.contents.value;
        } else if (Array.isArray(response.contents)) {
          return response.contents.map((item: any) =>
            typeof item === 'string' ? item : item.value || ''
          ).join('\n');
        }
      }
    } catch (error) {
      warning(`Error getting hover information: ${error instanceof Error ? error.message : String(error)}`);
    }

    return '';
  }

  async getCompletion(uri: string, position: { line: number, character: number }): Promise<any[]> {
    // Check if initialized, but don't auto-initialize
    if (!this.initialized) {
      throw new Error("LSP client not initialized. Please call start_lsp first.");
    }

    debug(`Getting completions at location: ${uri} (${position.line}:${position.character})`);

    try {
      const response = await this.sendRequest<any>("textDocument/completion", {
        textDocument: { uri },
        position
      });

      if (Array.isArray(response)) {
        return response;
      } else if (response?.items && Array.isArray(response.items)) {
        return response.items;
      }
    } catch (error) {
      warning(`Error getting completions: ${error instanceof Error ? error.message : String(error)}`);
    }

    return [];
  }

  async getCodeActions(uri: string, range: { start: { line: number, character: number }, end: { line: number, character: number } }): Promise<any[]> {
    // Check if initialized, but don't auto-initialize
    if (!this.initialized) {
      throw new Error("LSP client not initialized. Please call start_lsp first.");
    }

    debug(`Getting code actions for range: ${uri} (${range.start.line}:${range.start.character} to ${range.end.line}:${range.end.character})`);

    try {
      const response = await this.sendRequest<any>("textDocument/codeAction", {
        textDocument: { uri },
        range,
        context: {
          diagnostics: []
        }
      });

      if (Array.isArray(response)) {
        return response;
      }
    } catch (error) {
      warning(`Error getting code actions: ${error instanceof Error ? error.message : String(error)}`);
    }

    return [];
  }

  async shutdown(): Promise<void> {
    if (!this.initialized) return;

    try {
      info("Shutting down LSP connection...");

      // Clear all diagnostic subscribers
      this.clearDiagnosticSubscribers();

      // Close all open documents before shutting down
      for (const uri of this.openedDocuments) {
        try {
          this.sendNotification("textDocument/didClose", {
            textDocument: { uri }
          });
        } catch (error) {
          warning(`Error closing document ${uri}:`, error);
        }
      }

      await this.sendRequest("shutdown");
      this.sendNotification("exit");
      this.initialized = false;
      this.openedDocuments.clear();
      notice("LSP connection shut down successfully");
    } catch (error) {
      logError("Error shutting down LSP connection:", error);
    }
  }

  async restart(rootDirectory?: string): Promise<void> {
    info("Restarting LSP server...");

    // If initialized, try to shut down cleanly first
    if (this.initialized) {
      try {
        await this.shutdown();
      } catch (error) {
        warning("Error shutting down LSP server during restart:", error);
      }
    }

    // Kill the process if it's still running
    if (this.process && !this.process.killed) {
      try {
        this.process.kill();
        notice("Killed existing LSP process");
      } catch (error) {
        logError("Error killing LSP process:", error);
      }
    }

    // Reset state
    this.buffer = "";
    this.messageQueue = [];
    this.nextId = 1;
    this.responsePromises.clear();
    this.initialized = false;
    this.serverCapabilities = null;
    this.openedDocuments.clear();
    this.documentVersions.clear();
    this.processingQueue = false;
    this.documentDiagnostics.clear();
    this.clearDiagnosticSubscribers();

    // Start a new process
    this.startProcess();

    // Initialize with the provided root directory or use the stored one
    if (rootDirectory) {
      await this.initialize(rootDirectory);
      notice(`LSP server restarted and initialized with root directory: ${rootDirectory}`);
    } else {
      info("LSP server restarted but not initialized. Call start_lsp to initialize.");
    }
  }
}

```