@gesslar/lpc-mcp 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "npm" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
@@ -0,0 +1,33 @@
1
+ name: Giddyup
2
+ permissions:
3
+ contents: read
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ branches: [main]
10
+
11
+ jobs:
12
+ cowyboysounds:
13
+ runs-on: ubuntu-latest
14
+
15
+ strategy:
16
+ matrix:
17
+ node-version: [20.x, 22.x]
18
+
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Setup Node.js ${{ matrix.node-version }}
24
+ uses: actions/setup-node@v4
25
+ with:
26
+ node-version: ${{ matrix.node-version }}
27
+ cache: "npm"
28
+
29
+ - name: Install dependencies
30
+ run: npm ci
31
+
32
+ - name: Run ESLint
33
+ run: npm run lint
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # LPC MCP Server
2
+
3
+ **Bring LPC development into 2025** with AI-powered code intelligence.
4
+
5
+ This MCP (Model Context Protocol) server wraps the [jlchmura/lpc-language-server](https://github.com/jlchmura/lpc-language-server) and exposes it to AI assistants, enabling natural language queries about your LPC codebase with real language server-powered understanding.
6
+
7
+ ## What This Enables
8
+
9
+ ✨ **AI assistants can now:**
10
+
11
+ - Understand your LPC code structure through the language server
12
+ - Get real documentation from hover information
13
+ - Jump to definitions and find references
14
+ - Answer natural language questions about your mudlib
15
+ - Trace inheritance chains and function calls
16
+ - Explain complex code patterns
17
+
18
+ All through conversation, powered by actual code intelligence instead of pattern matching.
19
+
20
+ ## Features
21
+
22
+ - 🔍 **`lpc_hover`**: Get documentation/hover information for symbols
23
+ - 🎯 **`lpc_definition`**: Jump to definition of symbols
24
+ - 🔗 **`lpc_references`**: Find all references to a symbol
25
+ - 🐛 **`lpc_diagnostics`**: Get real-time errors, warnings, and hints from the language server
26
+ - 📁 **Workspace-aware**: Reads your `lpc-config.json` for proper symbol resolution
27
+ - 🚀 **Fast**: Direct JSON-RPC communication with the language server
28
+
29
+ ## Prerequisites
30
+
31
+ ### 1. Install Node.js
32
+
33
+ Node.js 16+ required:
34
+
35
+ ```bash
36
+ node --version # Should be v16.0.0 or higher
37
+ ```
38
+
39
+ ### 2. Install the LPC Language Server Extension
40
+
41
+ The extension must be installed in VS Code (the server binary is bundled with it):
42
+
43
+ ```bash
44
+ code --install-extension jlchmura.lpc
45
+ ```
46
+
47
+ Verify installation:
48
+
49
+ ```bash
50
+ ls ~/.vscode/extensions/jlchmura.lpc-*/out/server/src/server.js
51
+ ```
52
+
53
+ ### 3. Install Dependencies
54
+
55
+ ```bash
56
+ cd /path/to/lpc-mcp
57
+ npm install
58
+ ```
59
+
60
+ ### 4. Create `lpc-config.json` in Your Mudlib
61
+
62
+ The language server needs this config file at your mudlib root to understand includes, simul_efuns, etc.
63
+
64
+ Example `/path/to/your/mudlib/lpc-config.json`:
65
+
66
+ ```json
67
+ {
68
+ "driver": {
69
+ "type": "fluffos"
70
+ },
71
+ "libFiles": {
72
+ "master": "adm/obj/master.c",
73
+ "simul_efun": "adm/obj/simul_efun.c",
74
+ "global_include": "include/global.h"
75
+ },
76
+ "libInclude": [
77
+ "include",
78
+ "include/driver",
79
+ "adm/include"
80
+ ],
81
+ "exclude": [
82
+ ".git/",
83
+ "tmp/"
84
+ ]
85
+ }
86
+ ```
87
+
88
+ ## Setup for Different AI Tools
89
+
90
+ ### Warp (Terminal)
91
+
92
+ Add to your Warp MCP configuration:
93
+
94
+ **Location**: Settings → AI → Model Context Protocol
95
+
96
+ ```json
97
+ {
98
+ "lpc": {
99
+ "command": "node",
100
+ "args": ["/absolute/path/to/lpc-mcp/index.js"],
101
+ "env": {
102
+ "LPC_WORKSPACE_ROOT": "/path/to/your/mudlib"
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ **Important**: Use absolute paths! Replace:
109
+
110
+ - `/absolute/path/to/lpc-mcp/index.js` with the actual path to this repo
111
+ - `/path/to/your/mudlib` with the directory containing your `lpc-config.json`
112
+
113
+ Restart Warp after adding the configuration.
114
+
115
+ ### Claude Desktop
116
+
117
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or equivalent:
118
+
119
+ ```json
120
+ {
121
+ "mcpServers": {
122
+ "lpc": {
123
+ "command": "node",
124
+ "args": ["/absolute/path/to/lpc-mcp/index.js"],
125
+ "env": {
126
+ "LPC_WORKSPACE_ROOT": "/path/to/your/mudlib"
127
+ }
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ Restart Claude Desktop after configuration.
134
+
135
+ ### Cline (VS Code Extension)
136
+
137
+ Add to your Cline MCP settings:
138
+
139
+ ```json
140
+ {
141
+ "mcpServers": {
142
+ "lpc": {
143
+ "command": "node",
144
+ "args": ["/absolute/path/to/lpc-mcp/index.js"],
145
+ "env": {
146
+ "LPC_WORKSPACE_ROOT": "/path/to/your/mudlib"
147
+ }
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### GitHub Copilot (VS Code)
154
+
155
+ **Prerequisites:**
156
+
157
+ - Install the [Copilot MCP extension](https://marketplace.visualstudio.com/items?itemName=automatalabs.copilot-mcp): `code --install-extension automatalabs.copilot-mcp`
158
+
159
+ **Configuration:**
160
+ Add to `~/Library/Application Support/Code/User/mcp.json` (macOS) or equivalent:
161
+
162
+ ```json
163
+ {
164
+ "servers": {
165
+ "lpc": {
166
+ "type": "node",
167
+ "command": "node",
168
+ "args": ["/absolute/path/to/lpc-mcp/index.js"],
169
+ "env": {
170
+ "LPC_WORKSPACE_ROOT": "/path/to/your/mudlib"
171
+ }
172
+ }
173
+ },
174
+ "inputs": []
175
+ }
176
+ ```
177
+
178
+ ### Other MCP-Compatible Tools
179
+
180
+ The configuration is the same for any MCP-compatible tool:
181
+
182
+ 1. Add the server to your MCP configuration
183
+ 2. Provide the Node.js command and path to `index.js`
184
+ 3. Set `LPC_WORKSPACE_ROOT` environment variable to your mudlib root
185
+
186
+ ## Usage Examples
187
+
188
+ Once configured, you can ask your AI assistant natural language questions:
189
+
190
+ **"What does the `query_short()` function do in room.c?"**
191
+ → AI uses `lpc_hover` to get documentation
192
+
193
+ **"Where is `STD_OBJECT` defined?"**
194
+ → AI uses `lpc_definition` to find the file
195
+
196
+ **"Find all places that call `set_room_size()`"**
197
+ → AI uses `lpc_references` to locate all callers
198
+
199
+ **"Explain how the maze generation algorithm works"**
200
+ → AI reads code and uses hover info to understand functions
201
+
202
+ **"What's the inheritance tree for rooms?"**
203
+ → AI traces `inherit` statements and jumps to definitions
204
+
205
+ **"Check if this LPC file has any syntax errors"**
206
+ → AI uses `lpc_diagnostics` to validate the code
207
+
208
+ **"Why won't this LPC code compile?"**
209
+ → AI checks diagnostics for errors like undeclared variables or type mismatches
210
+
211
+ ## Testing
212
+
213
+ To verify the server works:
214
+
215
+ ```bash
216
+ # Set workspace root for testing
217
+ export LPC_WORKSPACE_ROOT=/path/to/your/mudlib
218
+
219
+ # Start the server (it will wait for MCP protocol messages)
220
+ node index.js
221
+ ```
222
+
223
+ The server should output:
224
+
225
+ ```
226
+ Starting LPC Language Server...
227
+ Initializing LSP...
228
+ LPC Language Server started successfully
229
+ LPC MCP Server running on stdio
230
+ ```
231
+
232
+ ## Troubleshooting
233
+
234
+ ### Server won't start
235
+
236
+ **Check the LPC extension is installed:**
237
+
238
+ ```bash
239
+ code --list-extensions | grep jlchmura.lpc
240
+ ```
241
+
242
+ **Check logs** (for Warp):
243
+
244
+ ```bash
245
+ tail -f ~/.local/state/warp-terminal/mcp/*.log
246
+ ```
247
+
248
+ ### Language server not resolving symbols
249
+
250
+ **Verify workspace root:**
251
+
252
+ - Make sure `LPC_WORKSPACE_ROOT` points to the directory with `lpc-config.json`
253
+ - Use absolute paths, not relative
254
+
255
+ **Check your lpc-config.json:**
256
+
257
+ ```bash
258
+ cat $LPC_WORKSPACE_ROOT/lpc-config.json
259
+ ```
260
+
261
+ ### Extension version mismatch
262
+
263
+ If the extension path doesn't match, update line 40 in `index.js`:
264
+
265
+ ```javascript
266
+ const lspPath = path.join(
267
+ process.env.HOME,
268
+ ".vscode/extensions/jlchmura.lpc-VERSION/out/server/src/server.js"
269
+ );
270
+ ```
271
+
272
+ Find your version:
273
+
274
+ ```bash
275
+ ls ~/.vscode/extensions/ | grep jlchmura.lpc
276
+ ```
277
+
278
+ ## How It Works
279
+
280
+ ```
281
+ AI Assistant
282
+ ↓ (natural language)
283
+ MCP Protocol
284
+ ↓ (tool calls: lpc_hover, lpc_definition, lpc_references)
285
+ This Server
286
+ ↓ (JSON-RPC: textDocument/hover, etc.)
287
+ LPC Language Server
288
+ ↓ (parses LPC, reads lpc-config.json)
289
+ Your Mudlib
290
+ ```
291
+
292
+ 1. AI assistant sends MCP tool requests
293
+ 2. Server reads the file and sends `textDocument/didOpen` to LSP
294
+ 3. Server translates MCP → LSP JSON-RPC requests
295
+ 4. LSP analyzes code using your `lpc-config.json`
296
+ 5. Server returns LSP response as MCP result
297
+ 6. AI understands your code structure!
298
+
299
+ ## Roadmap
300
+
301
+ - [ ] Add completion support
302
+ - [x] Add diagnostics (errors/warnings)
303
+ - [ ] Support signature help
304
+ - [ ] Cache opened documents
305
+ - [ ] Support multiple workspace roots
306
+ - [ ] Add rename symbol support
307
+ - [ ] Add document symbols (outline)
308
+ - [ ] Add workspace symbol search
309
+
310
+ ## Contributing
311
+
312
+ PRs welcome! This is a proof-of-concept that can be extended with more LSP features.
313
+
314
+ ## Credits
315
+
316
+ - **John (jlchmura)** - The INCOMPARABLY SKILLED MASTER PROGRAMMER whose [LPC language server](https://github.com/jlchmura/lpc-language-server) rescued LPC development from 1995. Without his greatness, kindness, and all-around hunk demeanour, we would still be `grep`-ing through mudlibs like cavemen. This cos theMCP server is merely a humble wrapper around his genius. 🙇
317
+ - [Model Context Protocol](https://modelcontextprotocol.io/) - The protocol making this possible
318
+ - Built in an hour of inspired hacking in 2025
319
+
320
+ ## ~~License~~
321
+
322
+ Unlicense - Public Domain. Do whatever you want with this code.
323
+
324
+ ### Note
325
+
326
+ The LPC language server itself [(jlchmura/lpc-language-server)](https://github.com/jlchmura/lpc-language-server) is under its own license.
package/UNLICENSE.txt ADDED
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
@@ -0,0 +1,167 @@
1
+ import js from "@eslint/js"
2
+ import jsdoc from "eslint-plugin-jsdoc"
3
+ import stylistic from "@stylistic/eslint-plugin"
4
+ import globals from "globals"
5
+
6
+ export default [
7
+ js.configs.recommended,
8
+ jsdoc.configs['flat/recommended'], {
9
+ name: "gesslar/uglier/ignores",
10
+ ignores: [],
11
+ }, {
12
+ name: "gesslar/uglier/languageOptions",
13
+ languageOptions: {
14
+ ecmaVersion: "latest",
15
+ sourceType: "module",
16
+ globals: {
17
+ ...globals.node,
18
+ fetch: "readonly",
19
+ Headers: "readonly",
20
+ },
21
+ },
22
+ },
23
+ // Add override for webview files to include browser globals
24
+ {
25
+ name: "gesslar/uglier/webview-env",
26
+ files: ["src/webview/**/*.{js,mjs,cjs}"],
27
+ languageOptions: {
28
+ globals: {
29
+ ...globals.browser,
30
+ acquireVsCodeApi: "readonly"
31
+ }
32
+ }
33
+ },
34
+ // Add override for .cjs files to treat as CommonJS
35
+ {
36
+ name: "gesslar/uglier/cjs-override",
37
+ files: ["src/**/*.cjs"],
38
+ languageOptions: {
39
+ sourceType: "script",
40
+ ecmaVersion: 2021
41
+ },
42
+ },
43
+ // Add override for .mjs files to treat as ES modules
44
+ {
45
+ name: "gesslar/uglier/mjs-override",
46
+ files: ["src/**/*.mjs"],
47
+ languageOptions: {
48
+ sourceType: "module",
49
+ ecmaVersion: 2021
50
+ }
51
+ },
52
+ {
53
+ name: "gesslar/uglier/lints-js",
54
+ files: ["{work,src}/**/*.{mjs,cjs,js}"],
55
+ plugins: {
56
+ "@stylistic": stylistic,
57
+ },
58
+ rules: {
59
+ "@stylistic/arrow-parens": ["error", "as-needed"],
60
+ "@stylistic/arrow-spacing": ["error", { before: true, after: true }],
61
+ "@stylistic/brace-style": ["error", "1tbs", {allowSingleLine: false}],
62
+ "@stylistic/nonblock-statement-body-position": ["error", "below"],
63
+ "@stylistic/padding-line-between-statements": [
64
+ "error",
65
+ {blankLine: "always", prev: "if", next: "*"},
66
+ {blankLine: "always", prev: "*", next: "return"},
67
+ {blankLine: "always", prev: "while", next: "*"},
68
+ {blankLine: "always", prev: "for", next: "*"},
69
+ {blankLine: "always", prev: "switch", next: "*"},
70
+ {blankLine: "always", prev: "do", next: "*"},
71
+ // {blankLine: "always", prev: ["const", "let", "var"], next: "*"},
72
+ // {blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]},
73
+ {blankLine: "always", prev: "directive", next: "*" },
74
+ {blankLine: "any", prev: "directive", next: "directive" },
75
+ ],
76
+ "@stylistic/eol-last": ["error", "always"],
77
+ "@stylistic/indent": ["error", 2, {
78
+ SwitchCase: 1 // Indents `case` statements one level deeper than `switch`
79
+ }],
80
+ "@stylistic/key-spacing": ["error", { beforeColon: false, afterColon: true }],
81
+ "@stylistic/keyword-spacing": ["error", {
82
+ before: false,
83
+ after: true,
84
+ overrides: {
85
+ // Control statements
86
+ return: { before: true, after: true },
87
+ if: { after: false },
88
+ else: { before: true, after: true },
89
+ for: { after: false },
90
+ while: { before: true, after: false },
91
+ do: { after: true },
92
+ switch: { after: false },
93
+ case: { before: true, after: true },
94
+ throw: { before: true, after: false } ,
95
+
96
+ // Keywords
97
+ as: { before: true, after: true },
98
+ of: { before: true, after: true },
99
+ from: { before: true, after: true },
100
+ async: { before: true, after: true },
101
+ await: { before: true, after: false },
102
+ class: { before: true, after: true },
103
+ const: { before: true, after: true },
104
+ let: { before: true, after: true },
105
+ var: { before: true, after: true },
106
+
107
+ // Exception handling
108
+ catch: { before: true, after: true },
109
+ finally: { before: true, after: true },
110
+ }
111
+ }],
112
+ // Blocks
113
+ "@stylistic/space-before-blocks": ["error", "always"],
114
+ "@stylistic/max-len": ["warn", {
115
+ code: 80,
116
+ ignoreComments: true,
117
+ ignoreUrls: true,
118
+ ignoreStrings: true,
119
+ ignoreTemplateLiterals: true,
120
+ ignoreRegExpLiterals: true,
121
+ tabWidth: 2
122
+ }],
123
+ "@stylistic/no-tabs": "error",
124
+ "@stylistic/no-trailing-spaces": ["error"],
125
+ "@stylistic/object-curly-spacing": ["error", "never", {
126
+ objectsInObjects: false,
127
+ arraysInObjects: false
128
+ }],
129
+ "@stylistic/quotes": ["error", "double", {
130
+ avoidEscape: true,
131
+ allowTemplateLiterals: "always"
132
+ }],
133
+ "@stylistic/semi": ["error", "never"],
134
+ "@stylistic/space-before-function-paren": ["error", "never"],
135
+ "@stylistic/yield-star-spacing": ["error", { before: true, after: false }],
136
+ "constructor-super": "error",
137
+ "no-unexpected-multiline": "error",
138
+ "no-unused-vars": ["error", {
139
+ caughtErrors: "all",
140
+ caughtErrorsIgnorePattern: "^_+",
141
+ argsIgnorePattern: "^_+",
142
+ destructuredArrayIgnorePattern: "^_+",
143
+ varsIgnorePattern: "^_+"
144
+ }],
145
+ "no-useless-assignment": "error",
146
+ "prefer-const": "error",
147
+ "@stylistic/no-multiple-empty-lines": ["error", { max: 1 }],
148
+ "@stylistic/array-bracket-spacing": ["error", "never"],
149
+ }
150
+ },
151
+ {
152
+ name: "gesslar/uglier/lints-jsdoc",
153
+ files: ["{work,src}/**/*.{mjs,cjs,js}"],
154
+ plugins: {
155
+ jsdoc,
156
+ },
157
+ rules: {
158
+ "jsdoc/require-description": "error",
159
+ "jsdoc/tag-lines": ["error", "any", {"startLines":1}],
160
+ "jsdoc/require-jsdoc": ["error", { publicOnly: true }],
161
+ "jsdoc/check-tag-names": "error",
162
+ "jsdoc/check-types": "error",
163
+ "jsdoc/require-param-type": "error",
164
+ "jsdoc/require-returns-type": "error"
165
+ }
166
+ }
167
+ ]
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@gesslar/lpc-mcp",
3
+ "version": "0.1.2",
4
+ "description": "MCP server for LPC language server",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "lpc-mcp": "./src/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
13
+ "scripts": {
14
+ "start": "node src/index.js",
15
+ "types:build": "tsc -p tsconfig.types.json",
16
+ "lint": "eslint src/",
17
+ "lint:fix": "eslint src/ --fix",
18
+ "submit": "npm publish --access public",
19
+ "update": "npx npm-check-updates -u && npm install",
20
+ "pr": "gt submit --publish --restack --ai -m",
21
+ "patch": "npm version patch",
22
+ "minor": "npm version minor",
23
+ "major": "npm version major"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/gesslar/lpc-mcp.git"
28
+ },
29
+ "keywords": [],
30
+ "author": "gesslar",
31
+ "license": "Unlicense",
32
+ "homepage": "https://github.com/gesslar/lpc-mcp#readme",
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.20.2",
35
+ "vscode-jsonrpc": "^8.2.1"
36
+ },
37
+ "devDependencies": {
38
+ "@eslint/js": "^9.39.0",
39
+ "@stylistic/eslint-plugin": "^5.5.0",
40
+ "eslint-plugin-jsdoc": "^61.1.11",
41
+ "globals": "^16.5.0"
42
+ }
43
+ }
package/src/index.js ADDED
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {Server} from "@modelcontextprotocol/sdk/server/index.js"
4
+ import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js"
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js"
9
+ import {spawn} from "child_process"
10
+ import * as rpc from "vscode-jsonrpc/node.js"
11
+ import path from "path"
12
+ // ...existing code...
13
+ import {Transform} from "stream"
14
+
15
+ class LPCMCPServer {
16
+ constructor() {
17
+ this.server = new Server(
18
+ {
19
+ name: "lpc-mcp-server",
20
+ version: "0.1.0",
21
+ },
22
+ {
23
+ capabilities: {
24
+ tools: {},
25
+ },
26
+ }
27
+ )
28
+
29
+ this.lspProcess = null
30
+ this.lspConnection = null
31
+ this.diagnosticsCache = new Map() // Map of file URI -> diagnostics array
32
+ this.setupHandlers()
33
+ }
34
+
35
+ async startLSP() {
36
+ // Path to the LPC language server
37
+ const lspPath = path.join(
38
+ process.env.HOME,
39
+ ".vscode/extensions/jlchmura.lpc-1.1.42/out/server/src/server.js"
40
+ )
41
+
42
+ console.error("Starting LPC Language Server from:", lspPath)
43
+
44
+ // Spawn the LSP server
45
+ // Redirect stderr to /dev/null to prevent log messages from contaminating JSON-RPC
46
+ this.lspProcess = spawn("node", [lspPath, "--stdio"], {
47
+ stdio: ["pipe", "pipe", "ignore"],
48
+ })
49
+
50
+ this.lspProcess.on("error", err => {
51
+ console.error("LSP Process error:", err)
52
+ })
53
+
54
+ this.lspProcess.on("exit", code => {
55
+ console.error("LSP Process exited with code:", code)
56
+ })
57
+
58
+ // Create a transform stream to filter out non-JSON-RPC output
59
+ const cleanStream = new Transform({
60
+ transform(chunk, encoding, callback) {
61
+ const data = chunk.toString()
62
+ // Filter out lines that don't look like JSON-RPC
63
+ // JSON-RPC messages start with "Content-Length: "
64
+ const lines = data.split("\n")
65
+ const filtered = lines.filter(line => {
66
+ const trimmed = line.trim()
67
+
68
+ // Keep Content-Length headers and JSON lines
69
+ return trimmed.startsWith("Content-Length:") ||
70
+ trimmed.startsWith("{") ||
71
+ trimmed === ""
72
+ }).join("\n")
73
+
74
+ if(filtered) {
75
+ this.push(filtered)
76
+ }
77
+
78
+ callback()
79
+ }
80
+ })
81
+
82
+ // Pipe stdout through the cleaner
83
+ this.lspProcess.stdout.pipe(cleanStream)
84
+
85
+ // Create JSON-RPC connection
86
+ const reader = new rpc.StreamMessageReader(cleanStream)
87
+ const writer = new rpc.StreamMessageWriter(this.lspProcess.stdin)
88
+ this.lspConnection = rpc.createMessageConnection(reader, writer)
89
+
90
+ this.lspConnection.onError(error => {
91
+ console.error("LSP Connection error:", error)
92
+ })
93
+
94
+ this.lspConnection.onClose(() => {
95
+ console.error("LSP Connection closed")
96
+ })
97
+
98
+ // Listen for diagnostic notifications
99
+ this.lspConnection.onNotification("textDocument/publishDiagnostics", params => {
100
+ console.error(`Received diagnostics for ${params.uri}: ${params.diagnostics.length} issues`)
101
+ this.diagnosticsCache.set(params.uri, params.diagnostics)
102
+ })
103
+
104
+ // Start listening
105
+ this.lspConnection.listen()
106
+
107
+ console.error("Initializing LSP...")
108
+
109
+ // Get workspace root from environment variable
110
+ const workspaceRoot = process.env.LPC_WORKSPACE_ROOT || null
111
+ const rootUri = workspaceRoot ? `file://${workspaceRoot}` : null
112
+
113
+ console.error("Using workspace root:", rootUri)
114
+
115
+ // Initialize the LSP with timeout
116
+ try {
117
+ const initResult = await Promise.race([
118
+ this.lspConnection.sendRequest("initialize", {
119
+ processId: process.pid,
120
+ rootUri: rootUri,
121
+ capabilities: {
122
+ textDocument: {
123
+ hover: {dynamicRegistration: true},
124
+ definition: {dynamicRegistration: true},
125
+ references: {dynamicRegistration: true},
126
+ },
127
+ },
128
+ }),
129
+ new Promise((_, reject) =>
130
+ setTimeout(() => reject(new Error("LSP init timeout")), 5000)
131
+ ),
132
+ ])
133
+
134
+ console.error("LSP initialized:", JSON.stringify(initResult, null, 2))
135
+
136
+ await this.lspConnection.sendNotification("initialized", {})
137
+
138
+ console.error("LPC Language Server started successfully")
139
+ } catch(error) {
140
+ console.error("Failed to initialize LSP:", error)
141
+ throw error
142
+ }
143
+ }
144
+
145
+ setupHandlers() {
146
+ this.server.setRequestHandler(ListToolsRequestSchema, async() => ({
147
+ tools: [
148
+ {
149
+ name: "lpc_hover",
150
+ description:
151
+ "Get hover information (documentation) for a symbol at a specific position in an LPC file",
152
+ inputSchema: {
153
+ type: "object",
154
+ properties: {
155
+ file: {
156
+ type: "string",
157
+ description: "Absolute path to the LPC file",
158
+ },
159
+ line: {
160
+ type: "number",
161
+ description: "Line number (0-indexed)",
162
+ },
163
+ character: {
164
+ type: "number",
165
+ description: "Character position (0-indexed)",
166
+ },
167
+ },
168
+ required: ["file", "line", "character"],
169
+ },
170
+ },
171
+ {
172
+ name: "lpc_definition",
173
+ description:
174
+ "Go to definition of a symbol at a specific position in an LPC file",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ file: {
179
+ type: "string",
180
+ description: "Absolute path to the LPC file",
181
+ },
182
+ line: {
183
+ type: "number",
184
+ description: "Line number (0-indexed)",
185
+ },
186
+ character: {
187
+ type: "number",
188
+ description: "Character position (0-indexed)",
189
+ },
190
+ },
191
+ required: ["file", "line", "character"],
192
+ },
193
+ },
194
+ {
195
+ name: "lpc_references",
196
+ description:
197
+ "Find all references to a symbol at a specific position in an LPC file",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ file: {
202
+ type: "string",
203
+ description: "Absolute path to the LPC file",
204
+ },
205
+ line: {
206
+ type: "number",
207
+ description: "Line number (0-indexed)",
208
+ },
209
+ character: {
210
+ type: "number",
211
+ description: "Character position (0-indexed)",
212
+ },
213
+ },
214
+ required: ["file", "line", "character"],
215
+ },
216
+ },
217
+ {
218
+ name: "lpc_diagnostics",
219
+ description:
220
+ "Get diagnostics (errors, warnings, hints) for an LPC file. This reveals LPC language rules and syntax errors.",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {
224
+ file: {
225
+ type: "string",
226
+ description: "Absolute path to the LPC file",
227
+ },
228
+ },
229
+ required: ["file"],
230
+ },
231
+ },
232
+ ],
233
+ }))
234
+
235
+ this.server.setRequestHandler(CallToolRequestSchema, async request => {
236
+ const {name, arguments: args} = request.params
237
+
238
+ if(!this.lspConnection) {
239
+ throw new Error("LSP not initialized")
240
+ }
241
+
242
+ const uri = `file://${args.file}`
243
+
244
+ try {
245
+ // Read the file and send didOpen notification
246
+ const fs = await import("fs/promises")
247
+ const fileContent = await fs.readFile(args.file, "utf-8")
248
+
249
+ await this.lspConnection.sendNotification("textDocument/didOpen", {
250
+ textDocument: {
251
+ uri,
252
+ languageId: "lpc",
253
+ version: 1,
254
+ text: fileContent,
255
+ },
256
+ })
257
+
258
+ switch(name) {
259
+ case "lpc_hover": {
260
+ const result = await this.lspConnection.sendRequest(
261
+ "textDocument/hover",
262
+ {
263
+ textDocument: {uri},
264
+ position: {line: args.line, character: args.character},
265
+ }
266
+ )
267
+
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: JSON.stringify(result, null, 2),
273
+ },
274
+ ],
275
+ }
276
+ }
277
+
278
+ case "lpc_definition": {
279
+ const result = await this.lspConnection.sendRequest(
280
+ "textDocument/definition",
281
+ {
282
+ textDocument: {uri},
283
+ position: {line: args.line, character: args.character},
284
+ }
285
+ )
286
+
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: JSON.stringify(result, null, 2),
292
+ },
293
+ ],
294
+ }
295
+ }
296
+
297
+ case "lpc_references": {
298
+ const result = await this.lspConnection.sendRequest(
299
+ "textDocument/references",
300
+ {
301
+ textDocument: {uri},
302
+ position: {line: args.line, character: args.character},
303
+ context: {includeDeclaration: true},
304
+ }
305
+ )
306
+
307
+ return {
308
+ content: [
309
+ {
310
+ type: "text",
311
+ text: JSON.stringify(result, null, 2),
312
+ },
313
+ ],
314
+ }
315
+ }
316
+
317
+ case "lpc_diagnostics": {
318
+ // Just open the file to trigger diagnostics
319
+ // Wait a bit for diagnostics to come through
320
+ await new Promise(resolve => setTimeout(resolve, 500))
321
+
322
+ const diagnostics = this.diagnosticsCache.get(uri) || []
323
+
324
+ if(diagnostics.length === 0) {
325
+ return {
326
+ content: [
327
+ {
328
+ type: "text",
329
+ text: "No diagnostics found for this file. The code appears to be valid.",
330
+ },
331
+ ],
332
+ }
333
+ }
334
+
335
+ // Format diagnostics in a readable way
336
+ const formatted = diagnostics.map(d => {
337
+ const severity = ["Error", "Warning", "Information", "Hint"][d.severity - 1] || "Unknown"
338
+ const line = d.range.start.line + 1 // Convert to 1-indexed
339
+ const char = d.range.start.character + 1
340
+
341
+ return `[${severity}] Line ${line}:${char} - ${d.message}`
342
+ }).join("\n")
343
+
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: `Found ${diagnostics.length} diagnostic(s):\n\n${formatted}\n\nRaw diagnostics:\n${JSON.stringify(diagnostics, null, 2)}`,
349
+ },
350
+ ],
351
+ }
352
+ }
353
+
354
+ default:
355
+ throw new Error(`Unknown tool: ${name}`)
356
+ }
357
+ } catch(error) {
358
+ return {
359
+ content: [
360
+ {
361
+ type: "text",
362
+ text: `Error: ${error.message}`,
363
+ },
364
+ ],
365
+ isError: true,
366
+ }
367
+ }
368
+ })
369
+ }
370
+
371
+ async run() {
372
+ await this.startLSP()
373
+
374
+ const transport = new StdioServerTransport()
375
+ await this.server.connect(transport)
376
+
377
+ console.error("LPC MCP Server running on stdio")
378
+ }
379
+ }
380
+
381
+ const server = new LPCMCPServer()
382
+ server.run().catch(console.error)