@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.
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +33 -0
- package/README.md +209 -0
- package/UNLICENSE.txt +24 -0
- package/eslint.config.js +167 -0
- package/index.js +306 -0
- package/package.json +43 -0
- package/scripts/search_docs.sh +24 -0
- package/src/index.js +318 -0
|
@@ -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>
|
package/eslint.config.js
ADDED
|
@@ -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)
|