@gesslar/fluffos-mcp 0.1.0

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,209 @@
1
+ # FluffOS MCP Server
2
+
3
+ **Real driver validation for LPC development** - An MCP server that wraps FluffOS CLI tools to provide actual driver-level validation and debugging.
4
+
5
+ This MCP server exposes FluffOS's powerful CLI utilities (`symbol` and `lpcc`) to AI assistants, enabling them to validate LPC code against the actual driver and examine compiled bytecode.
6
+
7
+ ## What This Enables
8
+
9
+ **AI assistants can now:**
10
+
11
+ - Validate LPC files using the actual FluffOS driver (not just syntax checking)
12
+ - Catch runtime compilation issues that static analysis misses
13
+ - Examine compiled bytecode to debug performance or behavior issues
14
+ - Understand how LPC code actually compiles
15
+
16
+ ## Tools
17
+
18
+ - **`fluffos_validate`**: Validate an LPC file using FluffOS's `symbol` tool
19
+ - **`fluffos_disassemble`**: Disassemble LPC to bytecode using `lpcc`
20
+ - **`fluffos_doc_lookup`**: Search FluffOS documentation for efuns, applies, concepts, etc.
21
+
22
+ ## Prerequisites
23
+
24
+ ### 1. FluffOS Installation
25
+
26
+ You need FluffOS installed with the CLI tools available. The following binaries should exist:
27
+
28
+ - `symbol` - For validating LPC files
29
+ - `lpcc` - For disassembling to bytecode
30
+
31
+ ### 2. Node.js
32
+
33
+ Node.js 16+ required:
34
+
35
+ ```bash
36
+ node --version # Should be v16.0.0 or higher
37
+ ```
38
+
39
+ ### 3. Install Dependencies
40
+
41
+ ```bash
42
+ cd /path/to/fluffos-mcp
43
+ npm install
44
+ ```
45
+
46
+ ## Configuration
47
+
48
+ The server requires these environment variables:
49
+
50
+ - `FLUFFOS_BIN_DIR` - Directory containing FluffOS binaries (`symbol`, `lpcc`)
51
+ - `MUD_RUNTIME_CONFIG_FILE` - Path to your FluffOS config file (e.g., `/mud/lib/etc/config.test`)
52
+ - `FLUFFOS_DOCS_DIR` - (Optional) Directory containing FluffOS documentation for doc lookup
53
+
54
+ ## Setup for Different AI Tools
55
+
56
+ ### Warp (Terminal)
57
+
58
+ Add to your Warp MCP configuration:
59
+
60
+ **Location**: Settings → AI → Model Context Protocol
61
+
62
+ ```json
63
+ {
64
+ "fluffos": {
65
+ "command": "node",
66
+ "args": ["/absolute/path/to/fluffos-mcp/index.js"],
67
+ "env": {
68
+ "FLUFFOS_BIN_DIR": "/path/to/fluffos/bin",
69
+ "MUD_RUNTIME_CONFIG_FILE": "/mud/lib/etc/config.test",
70
+ "FLUFFOS_DOCS_DIR": "/path/to/fluffos/docs"
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ **Important**: Use absolute paths!
77
+
78
+ Restart Warp after adding the configuration.
79
+
80
+ ### Claude Desktop
81
+
82
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or equivalent:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "fluffos": {
88
+ "command": "node",
89
+ "args": ["/absolute/path/to/fluffos-mcp/index.js"],
90
+ "env": {
91
+ "FLUFFOS_BIN_DIR": "/path/to/fluffos/bin",
92
+ "MUD_RUNTIME_CONFIG_FILE": "/mud/lib/etc/config.test"
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ Restart Claude Desktop after configuration.
100
+
101
+ ## Usage Examples
102
+
103
+ Once configured, you can ask your AI assistant:
104
+
105
+ **"Validate this LPC file with the actual driver"**
106
+ → AI uses `fluffos_validate` to run `symbol`
107
+
108
+ **"Show me the bytecode for this function"**
109
+ → AI uses `fluffos_disassemble` to run `lpcc`
110
+
111
+ **"Why is this code slow?"**
112
+ → AI examines the disassembly to identify inefficient patterns
113
+
114
+ **"What's the syntax for call_out?"**
115
+ → AI uses `fluffos_doc_lookup` to search documentation
116
+
117
+ **"How do I use mappings?"**
118
+ → AI searches docs for mapping-related documentation
119
+
120
+ ## How It Works
121
+
122
+ ```text
123
+ AI Assistant
124
+ ↓ (natural language)
125
+ MCP Protocol
126
+ ↓ (tool calls: fluffos_validate, fluffos_disassemble)
127
+ This Server
128
+ ↓ (spawns: symbol, lpcc)
129
+ FluffOS CLI Tools
130
+ ↓ (validates/compiles with actual driver)
131
+ Your LPC Code
132
+ ```
133
+
134
+ 1. AI assistant sends MCP tool requests
135
+ 2. Server spawns appropriate FluffOS CLI tool
136
+ 3. CLI tool validates/disassembles using the driver
137
+ 4. Server returns results to AI
138
+ 5. AI understands your code at the driver level and can reference FluffOS documentation to explain how functions work!
139
+
140
+ ## Implementation Details
141
+
142
+ ### Architecture
143
+
144
+ The server is built using the [Model Context Protocol SDK](https://github.com/modelcontextprotocol/sdk) and follows a class-based architecture:
145
+
146
+ - **FluffOSMCPServer class**: Main server implementation
147
+ - **MCP SDK Server**: Handles protocol communication via stdio
148
+ - **Child process spawning**: Executes FluffOS CLI tools
149
+ - **Path normalization**: Converts absolute paths to mudlib-relative paths
150
+
151
+ ### Path Handling
152
+
153
+ The server intelligently handles file paths:
154
+
155
+ 1. Parses `mudlib directory` from your FluffOS config file
156
+ 2. Normalizes absolute paths to mudlib-relative paths
157
+ 3. Passes normalized paths to FluffOS tools (which expect relative paths)
158
+
159
+ Example: `/mud/ox/lib/std/object.c` → `std/object.c`
160
+
161
+ ### Tool Implementation
162
+
163
+ **`fluffos_validate`**:
164
+
165
+ - Spawns `symbol <config> <file>` from the config directory
166
+ - Captures stdout/stderr
167
+ - Returns success/failure with compilation errors
168
+ - Exit code 0 = validation passed
169
+
170
+ **`fluffos_disassemble`**:
171
+
172
+ - Spawns `lpcc <config> <file>` from the config directory
173
+ - Returns complete bytecode disassembly
174
+ - Includes function tables, strings, and instruction-level detail
175
+
176
+ **`fluffos_doc_lookup`** (optional):
177
+
178
+ - Runs `scripts/search_docs.sh` helper script
179
+ - Uses `grep` to search markdown files
180
+ - Only available if `FLUFFOS_DOCS_DIR` is set
181
+
182
+ ### Error Handling
183
+
184
+ - Validates required environment variables on startup
185
+ - Returns structured error responses via MCP
186
+ - Gracefully handles missing config or tool execution failures
187
+ - Non-zero exit codes are reported but don't crash the server
188
+
189
+ ## Complementary Tools
190
+
191
+ This server works great alongside:
192
+
193
+ - **[lpc-mcp](https://github.com/gesslar/lpc-mcp)** - Language server integration for code intelligence
194
+ - **VS Code with jlchmura's LPC extension** - IDE support
195
+
196
+ Use them together for the complete LPC development experience!
197
+
198
+ ## Contributing
199
+
200
+ PRs welcome! This is a simple wrapper that can be extended with more FluffOS tools.
201
+
202
+ ## Credits
203
+
204
+ - **FluffOS Team** - For the amazing driver and CLI tools
205
+ - [Model Context Protocol](https://modelcontextprotocol.io/) - Making this integration possible
206
+
207
+ ## License
208
+
209
+ Unlicense - Public Domain. Do whatever you want with this code.
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/index.js ADDED
@@ -0,0 +1,306 @@
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 path from "path";
11
+ import fs from "fs";
12
+
13
+ class FluffOSMCPServer {
14
+ constructor() {
15
+ this.server = new Server(
16
+ {
17
+ name: "fluffos-mcp-server",
18
+ version: "0.1.0",
19
+ },
20
+ {
21
+ capabilities: {
22
+ tools: {},
23
+ },
24
+ }
25
+ );
26
+
27
+ this.binDir = process.env.FLUFFOS_BIN_DIR;
28
+ this.configFile = process.env.MUD_RUNTIME_CONFIG_FILE;
29
+ this.docsDir = process.env.FLUFFOS_DOCS_DIR;
30
+
31
+ if (!this.binDir) {
32
+ console.error("Error: FLUFFOS_BIN_DIR environment variable not set");
33
+ process.exit(1);
34
+ }
35
+
36
+ if (!this.configFile) {
37
+ console.error("Error: MUD_RUNTIME_CONFIG_FILE environment variable not set");
38
+ process.exit(1);
39
+ }
40
+
41
+ // Parse mudlib directory from config file
42
+ this.mudlibDir = this.parseMudlibDir();
43
+
44
+ console.error(`FluffOS bin directory: ${this.binDir}`);
45
+ console.error(`FluffOS config file: ${this.configFile}`);
46
+ console.error(`Mudlib directory: ${this.mudlibDir || "(not found in config)"}`);
47
+
48
+ if (this.docsDir) {
49
+ console.error(`FluffOS docs directory: ${this.docsDir}`);
50
+ } else {
51
+ console.error(`FluffOS docs directory: not set (doc lookup disabled)`);
52
+ }
53
+
54
+ this.setupHandlers();
55
+ }
56
+
57
+ parseMudlibDir() {
58
+ try {
59
+ const configContent = fs.readFileSync(this.configFile, "utf8");
60
+ const match = configContent.match(/^mudlib directory\s*:\s*(.+)$/m);
61
+ if (match) {
62
+ return match[1].trim();
63
+ }
64
+ } catch (err) {
65
+ console.error(`Warning: Could not parse mudlib directory from config: ${err.message}`);
66
+ }
67
+ return null;
68
+ }
69
+
70
+ normalizePath(lpcFile) {
71
+ // If we have a mudlib directory and the file path is absolute and starts with mudlib dir,
72
+ // convert it to a relative path
73
+ if (this.mudlibDir && path.isAbsolute(lpcFile) && lpcFile.startsWith(this.mudlibDir)) {
74
+ // Remove mudlib directory prefix and leading slash
75
+ return lpcFile.substring(this.mudlibDir.length).replace(/^\/+/, "");
76
+ }
77
+ // Otherwise return as-is (already relative or not under mudlib)
78
+ return lpcFile;
79
+ }
80
+
81
+ setupHandlers() {
82
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
83
+ tools: [
84
+ {
85
+ name: "fluffos_validate",
86
+ description:
87
+ "Validate an LPC file using the FluffOS driver's symbol tool. Compiles the file and reports success or failure with any compilation errors. Fast and lightweight check for code validity.",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ file: {
92
+ type: "string",
93
+ description: "Absolute path to the LPC file to validate",
94
+ },
95
+ },
96
+ required: ["file"],
97
+ },
98
+ },
99
+ {
100
+ name: "fluffos_disassemble",
101
+ description:
102
+ "Disassemble an LPC file to show compiled bytecode using lpcc. Returns detailed bytecode, function tables, strings, and disassembly. Useful for debugging and understanding how code compiles.",
103
+ inputSchema: {
104
+ type: "object",
105
+ properties: {
106
+ file: {
107
+ type: "string",
108
+ description: "Absolute path to the LPC file to disassemble",
109
+ },
110
+ },
111
+ required: ["file"],
112
+ },
113
+ },
114
+ ...(this.docsDir ? [{
115
+ name: "fluffos_doc_lookup",
116
+ description:
117
+ "Search FluffOS documentation for information about efuns, applies, concepts, etc. Searches markdown documentation files.",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ query: {
122
+ type: "string",
123
+ description: "Term to search for in documentation (e.g., 'call_out', 'mapping', 'socket')",
124
+ },
125
+ },
126
+ required: ["query"],
127
+ },
128
+ }] : []),
129
+ ],
130
+ }));
131
+
132
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
133
+ const { name, arguments: args } = request.params;
134
+
135
+ try {
136
+ switch (name) {
137
+ case "fluffos_validate": {
138
+ const result = await this.runSymbol(args.file);
139
+ return {
140
+ content: [
141
+ {
142
+ type: "text",
143
+ text: result,
144
+ },
145
+ ],
146
+ };
147
+ }
148
+
149
+ case "fluffos_disassemble": {
150
+ const result = await this.runLpcc(args.file);
151
+ return {
152
+ content: [
153
+ {
154
+ type: "text",
155
+ text: result,
156
+ },
157
+ ],
158
+ };
159
+ }
160
+
161
+ case "fluffos_doc_lookup": {
162
+ if (!this.docsDir) {
163
+ throw new Error("Documentation lookup is not available (FLUFFOS_DOCS_DIR not set)");
164
+ }
165
+ const result = await this.searchDocs(args.query);
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: result,
171
+ },
172
+ ],
173
+ };
174
+ }
175
+
176
+ default:
177
+ throw new Error(`Unknown tool: ${name}`);
178
+ }
179
+ } catch (error) {
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: `Error: ${error.message}`,
185
+ },
186
+ ],
187
+ isError: true,
188
+ };
189
+ }
190
+ });
191
+ }
192
+
193
+ async runSymbol(lpcFile) {
194
+ return new Promise((resolve, reject) => {
195
+ const normalizedPath = this.normalizePath(lpcFile);
196
+ const symbolPath = path.join(this.binDir, "symbol");
197
+ const proc = spawn(symbolPath, [this.configFile, normalizedPath], {
198
+ cwd: path.dirname(this.configFile),
199
+ });
200
+
201
+ let stdout = "";
202
+ let stderr = "";
203
+
204
+ proc.stdout.on("data", (data) => {
205
+ stdout += data.toString();
206
+ });
207
+
208
+ proc.stderr.on("data", (data) => {
209
+ stderr += data.toString();
210
+ });
211
+
212
+ proc.on("close", (code) => {
213
+ const output = (stdout + stderr).trim();
214
+
215
+ if (code === 0) {
216
+ resolve(`✓ File validated successfully\n\n${output}`);
217
+ } else {
218
+ resolve(`✗ Validation failed (exit code: ${code})\n\n${output}`);
219
+ }
220
+ });
221
+
222
+ proc.on("error", (err) => {
223
+ reject(new Error(`Failed to run symbol: ${err.message}`));
224
+ });
225
+ });
226
+ }
227
+
228
+ async runLpcc(lpcFile) {
229
+ return new Promise((resolve, reject) => {
230
+ const normalizedPath = this.normalizePath(lpcFile);
231
+ const lpccPath = path.join(this.binDir, "lpcc");
232
+ const proc = spawn(lpccPath, [this.configFile, normalizedPath], {
233
+ cwd: path.dirname(this.configFile),
234
+ });
235
+
236
+ let stdout = "";
237
+ let stderr = "";
238
+
239
+ proc.stdout.on("data", (data) => {
240
+ stdout += data.toString();
241
+ });
242
+
243
+ proc.stderr.on("data", (data) => {
244
+ stderr += data.toString();
245
+ });
246
+
247
+ proc.on("close", (code) => {
248
+ const output = (stdout + stderr).trim();
249
+
250
+ if (code === 0) {
251
+ resolve(output);
252
+ } else {
253
+ resolve(`Error (exit code: ${code}):\n\n${output}`);
254
+ }
255
+ });
256
+
257
+ proc.on("error", (err) => {
258
+ reject(new Error(`Failed to run lpcc: ${err.message}`));
259
+ });
260
+ });
261
+ }
262
+
263
+ async searchDocs(query) {
264
+ return new Promise((resolve, reject) => {
265
+ const scriptPath = path.join(path.dirname(new URL(import.meta.url).pathname), "scripts", "search_docs.sh");
266
+ const proc = spawn(scriptPath, [this.docsDir, query]);
267
+
268
+ let stdout = "";
269
+ let stderr = "";
270
+
271
+ proc.stdout.on("data", (data) => {
272
+ stdout += data.toString();
273
+ });
274
+
275
+ proc.stderr.on("data", (data) => {
276
+ stderr += data.toString();
277
+ });
278
+
279
+ proc.on("close", (code) => {
280
+ if (code === 0) {
281
+ if (stdout.trim()) {
282
+ resolve(`Found documentation for "${query}":\n\n${stdout}`);
283
+ } else {
284
+ resolve(`No documentation found for "${query}".`);
285
+ }
286
+ } else {
287
+ resolve(`Error searching documentation:\n${stderr || stdout}`);
288
+ }
289
+ });
290
+
291
+ proc.on("error", (err) => {
292
+ reject(new Error(`Failed to search docs: ${err.message}`));
293
+ });
294
+ });
295
+ }
296
+
297
+ async run() {
298
+ const transport = new StdioServerTransport();
299
+ await this.server.connect(transport);
300
+
301
+ console.error("FluffOS MCP Server running on stdio");
302
+ }
303
+ }
304
+
305
+ const server = new FluffOSMCPServer();
306
+ server.run().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@gesslar/fluffos-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for FluffOS driver tools - validate and disassemble LPC code",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "fluffos-mcp": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "lint": "eslint src/",
13
+ "lint:fix": "eslint src/ --fix",
14
+ "submit": "npm publish --access public",
15
+ "update": "npx npm-check-updates -u && npm install",
16
+ "pr": "gt submit --publish --restack --ai -m",
17
+ "patch": "npm version patch",
18
+ "minor": "npm version minor",
19
+ "major": "npm version major"
20
+ },
21
+ "keywords": [
22
+ "fluffos",
23
+ "lpc",
24
+ "mud",
25
+ "mcp",
26
+ "model-context-protocol"
27
+ ],
28
+ "author": "gesslar",
29
+ "license": "Unlicense",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/gesslar/fluffos-mcp.git"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.20.2"
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
+ }
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+ # search_docs.sh - Search FluffOS documentation
3
+ # Usage: search_docs.sh <docs_dir> <query>
4
+
5
+ DOCS_DIR="$1"
6
+ QUERY="$2"
7
+
8
+ if [ -z "$DOCS_DIR" ] || [ -z "$QUERY" ]; then
9
+ echo "Usage: $0 <docs_dir> <query>"
10
+ exit 1
11
+ fi
12
+
13
+ if [ ! -d "$DOCS_DIR" ]; then
14
+ echo "Error: Documentation directory does not exist: $DOCS_DIR"
15
+ exit 1
16
+ fi
17
+
18
+ # Search for the query in markdown files, showing filename and context
19
+ # Use ripgrep if available, otherwise fall back to grep
20
+ if command -v rg &> /dev/null; then
21
+ rg --type md -i -C 3 --heading --color never "$QUERY" "$DOCS_DIR"
22
+ else
23
+ grep -r -i --include="*.md" -H -C 3 "$QUERY" "$DOCS_DIR"
24
+ fi
package/src/index.js ADDED
@@ -0,0 +1,318 @@
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 path from "path"
11
+ import fs from "fs"
12
+
13
+ class FluffOSMCPServer {
14
+ constructor() {
15
+ this.server = new Server(
16
+ {
17
+ name: "fluffos-mcp-server",
18
+ version: "0.1.0",
19
+ },
20
+ {
21
+ capabilities: {
22
+ tools: {},
23
+ },
24
+ }
25
+ )
26
+
27
+ this.binDir = process.env.FLUFFOS_BIN_DIR
28
+ this.configFile = process.env.MUD_RUNTIME_CONFIG_FILE
29
+ this.docsDir = process.env.FLUFFOS_DOCS_DIR
30
+ this.mudlibDir = null
31
+
32
+ if(!this.binDir) {
33
+ console.error("Error: FLUFFOS_BIN_DIR environment variable not set")
34
+ process.exit(1)
35
+ }
36
+
37
+ if(!this.configFile) {
38
+ console.error("Error: MUD_RUNTIME_CONFIG_FILE environment variable not set")
39
+ process.exit(1)
40
+ }
41
+
42
+ // Parse mudlib directory from config file
43
+ this.mudlibDir = this.parseMudlibDir()
44
+
45
+ console.error(`FluffOS bin directory: ${this.binDir}`)
46
+ console.error(`FluffOS config file: ${this.configFile}`)
47
+ console.error(`Mudlib directory: ${this.mudlibDir || "(not found in config)"}`)
48
+
49
+ if(this.docsDir) {
50
+ console.error(`FluffOS docs directory: ${this.docsDir}`)
51
+ } else {
52
+ console.error(`FluffOS docs directory: not set (doc lookup disabled)`)
53
+ }
54
+
55
+ this.setupHandlers()
56
+ }
57
+
58
+ parseMudlibDir() {
59
+ try {
60
+ const configContent = fs.readFileSync(this.configFile, "utf8")
61
+ const match = configContent.match(/^mudlib directory\s*:\s*(.+)$/m)
62
+ if(match) {
63
+ return match[1].trim()
64
+ }
65
+ } catch(err) {
66
+ console.error(`Warning: Could not parse mudlib directory from config: ${err.message}`)
67
+ }
68
+
69
+ return null
70
+ }
71
+
72
+ normalizePath(lpcFile) {
73
+ // If we have a mudlib directory and the file path is absolute and starts with mudlib dir,
74
+ // convert it to a relative path
75
+ if(this.mudlibDir &&
76
+ path.isAbsolute(lpcFile) &&
77
+ lpcFile.startsWith(this.mudlibDir)
78
+ ) {
79
+ // Remove mudlib directory prefix and leading slash
80
+ return lpcFile.substring(this.mudlibDir.length).replace(/^\/+/, "")
81
+ }
82
+
83
+ // Otherwise return as-is (already relative or not under mudlib)
84
+ return lpcFile
85
+ }
86
+
87
+ setupHandlers() {
88
+ this.server.setRequestHandler(ListToolsRequestSchema, async() => ({
89
+ tools: [
90
+ {
91
+ name: "fluffos_validate",
92
+ description:
93
+ "Validate an LPC file using the FluffOS driver's symbol tool. " +
94
+ "Compiles the file and reports success or failure with any " +
95
+ "compilation errors. Fast and lightweight check for code validity.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ file: {
100
+ type: "string",
101
+ description: "Absolute path to the LPC file to validate",
102
+ },
103
+ },
104
+ required: ["file"],
105
+ },
106
+ },
107
+ {
108
+ name: "fluffos_disassemble",
109
+ description:
110
+ "Disassemble an LPC file to show compiled bytecode using lpcc. Returns detailed bytecode, function tables, strings, and disassembly. Useful for debugging and understanding how code compiles.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ file: {
115
+ type: "string",
116
+ description: "Absolute path to the LPC file to disassemble",
117
+ },
118
+ },
119
+ required: ["file"],
120
+ },
121
+ },
122
+ ...(this.docsDir ? [{
123
+ name: "fluffos_doc_lookup",
124
+ description:
125
+ "Search FluffOS documentation for information about efuns, applies, concepts, etc. Searches markdown documentation files.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ query: {
130
+ type: "string",
131
+ description: "Term to search for in documentation (e.g., 'call_out', 'mapping', 'socket')",
132
+ },
133
+ },
134
+ required: ["query"],
135
+ },
136
+ }] : []),
137
+ ],
138
+ }))
139
+
140
+ this.server.setRequestHandler(CallToolRequestSchema, async request => {
141
+ const {name, arguments: args} = request.params
142
+
143
+ try {
144
+ switch(name) {
145
+ case "fluffos_validate": {
146
+ const result = await this.runSymbol(args.file)
147
+
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: result,
153
+ },
154
+ ],
155
+ }
156
+ }
157
+
158
+ case "fluffos_disassemble": {
159
+ const result = await this.runLpcc(args.file)
160
+
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: result,
166
+ },
167
+ ],
168
+ }
169
+ }
170
+
171
+ case "fluffos_doc_lookup": {
172
+ if(!this.docsDir) {
173
+ throw new Error("Documentation lookup is not available (FLUFFOS_DOCS_DIR not set)")
174
+ }
175
+
176
+ const result = await this.searchDocs(args.query)
177
+
178
+ return {
179
+ content: [
180
+ {
181
+ type: "text",
182
+ text: result,
183
+ },
184
+ ],
185
+ }
186
+ }
187
+
188
+ default:
189
+ throw new Error(`Unknown tool: ${name}`)
190
+ }
191
+ } catch(error) {
192
+ return {
193
+ content: [
194
+ {
195
+ type: "text",
196
+ text: `Error: ${error.message}`,
197
+ },
198
+ ],
199
+ isError: true,
200
+ }
201
+ }
202
+ })
203
+ }
204
+
205
+ async runSymbol(lpcFile) {
206
+ return new Promise((resolve, reject) => {
207
+ const normalizedPath = this.normalizePath(lpcFile)
208
+ const symbolPath = path.join(this.binDir, "symbol")
209
+ const proc = spawn(symbolPath, [this.configFile, normalizedPath], {
210
+ cwd: path.dirname(this.configFile),
211
+ })
212
+
213
+ let stdout = ""
214
+ let stderr = ""
215
+
216
+ proc.stdout.on("data", data => {
217
+ stdout += data.toString()
218
+ })
219
+
220
+ proc.stderr.on("data", data => {
221
+ stderr += data.toString()
222
+ })
223
+
224
+ proc.on("close", code => {
225
+ const output = (stdout + stderr).trim()
226
+
227
+ if(code === 0) {
228
+ resolve(`✓ File validated successfully\n\n${output}`)
229
+ } else {
230
+ resolve(`✗ Validation failed (exit code: ${code})\n\n${output}`)
231
+ }
232
+ })
233
+
234
+ proc.on("error", err => {
235
+ reject(new Error(`Failed to run symbol: ${err.message}`))
236
+ })
237
+ })
238
+ }
239
+
240
+ async runLpcc(lpcFile) {
241
+ return new Promise((resolve, reject) => {
242
+ const normalizedPath = this.normalizePath(lpcFile)
243
+ const lpccPath = path.join(this.binDir, "lpcc")
244
+ const proc = spawn(lpccPath, [this.configFile, normalizedPath], {
245
+ cwd: path.dirname(this.configFile),
246
+ })
247
+
248
+ let stdout = ""
249
+ let stderr = ""
250
+
251
+ proc.stdout.on("data", data => {
252
+ stdout += data.toString()
253
+ })
254
+
255
+ proc.stderr.on("data", data => {
256
+ stderr += data.toString()
257
+ })
258
+
259
+ proc.on("close", code => {
260
+ const output = (stdout + stderr).trim()
261
+
262
+ if(code === 0) {
263
+ resolve(output)
264
+ } else {
265
+ resolve(`Error (exit code: ${code}):\n\n${output}`)
266
+ }
267
+ })
268
+
269
+ proc.on("error", err => {
270
+ reject(new Error(`Failed to run lpcc: ${err.message}`))
271
+ })
272
+ })
273
+ }
274
+
275
+ async searchDocs(query) {
276
+ return new Promise((resolve, reject) => {
277
+ const scriptPath = path.join(path.dirname(new URL(import.meta.url).pathname), "scripts", "search_docs.sh")
278
+ const proc = spawn(scriptPath, [this.docsDir, query])
279
+
280
+ let stdout = ""
281
+ let stderr = ""
282
+
283
+ proc.stdout.on("data", data => {
284
+ stdout += data.toString()
285
+ })
286
+
287
+ proc.stderr.on("data", data => {
288
+ stderr += data.toString()
289
+ })
290
+
291
+ proc.on("close", code => {
292
+ if(code === 0) {
293
+ if(stdout.trim()) {
294
+ resolve(`Found documentation for "${query}":\n\n${stdout}`)
295
+ } else {
296
+ resolve(`No documentation found for "${query}".`)
297
+ }
298
+ } else {
299
+ resolve(`Error searching documentation:\n${stderr || stdout}`)
300
+ }
301
+ })
302
+
303
+ proc.on("error", err => {
304
+ reject(new Error(`Failed to search docs: ${err.message}`))
305
+ })
306
+ })
307
+ }
308
+
309
+ async run() {
310
+ const transport = new StdioServerTransport()
311
+ await this.server.connect(transport)
312
+
313
+ console.error("FluffOS MCP Server running on stdio")
314
+ }
315
+ }
316
+
317
+ const server = new FluffOSMCPServer()
318
+ server.run().catch(console.error)