@gesslar/fluffos-mcp 0.1.3 → 0.2.1

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,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run lint:*)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ name: Quality
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+ branches: [main]
9
+ schedule:
10
+ - cron: "20 14 * * 1"
11
+
12
+ jobs:
13
+ Quality:
14
+ permissions:
15
+ contents: read
16
+ uses: gesslar/Maint/.github/workflows/Quality.yaml@main
17
+ secrets: inherit
18
+ with:
19
+ package_manager: "auto"
20
+ perform_linting: "${{ vars.PERFORM_LINTING || 'yes' }}"
21
+ perform_testing: "${{ vars.PERFORM_TESTING || 'no' }}"
@@ -0,0 +1,16 @@
1
+ name: Release
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+ branches: [main]
7
+
8
+ jobs:
9
+ Release:
10
+ uses: gesslar/Maint/.github/workflows/Release.yaml@main
11
+ secrets: inherit
12
+ with:
13
+ package_manager: "auto"
14
+ quality_check: "Quality"
15
+ permissions:
16
+ contents: write
package/eslint.config.js CHANGED
@@ -1,167 +1,11 @@
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"
1
+ import uglify from "@gesslar/uglier"
5
2
 
6
3
  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
- }
4
+ ...uglify({
5
+ with: [
6
+ "lints-js", // default files: ["**/*.{js,mjs,cjs}"]
7
+ "lints-jsdoc", // default files: ["**/*.{js,mjs,cjs}"]
8
+ "node", // default files: ["**/*.{js,mjs,cjs}"]
9
+ ]
10
+ })
167
11
  ]
package/package.json CHANGED
@@ -1,22 +1,16 @@
1
1
  {
2
2
  "name": "@gesslar/fluffos-mcp",
3
- "version": "0.1.3",
4
3
  "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"
4
+ "author": {
5
+ "name": "gesslar",
6
+ "url": "https://gesslar.dev"
9
7
  },
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"
8
+ "version": "0.2.1",
9
+ "license": "Unlicense",
10
+ "homepage": "https://github.com/gesslar/fluffos-mcp#readme",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/gesslar/fluffos-mcp.git"
20
14
  },
21
15
  "keywords": [
22
16
  "fluffos",
@@ -29,19 +23,32 @@
29
23
  "white",
30
24
  "cheddar"
31
25
  ],
32
- "author": "gesslar",
33
- "license": "Unlicense",
34
- "repository": {
35
- "type": "git",
36
- "url": "https://github.com/gesslar/fluffos-mcp.git"
26
+ "engines": {
27
+ "node": ">=22"
28
+ },
29
+ "main": "src/index.js",
30
+ "type": "module",
31
+ "bin": {
32
+ "fluffos-mcp": "./src/index.js"
37
33
  },
38
34
  "dependencies": {
39
- "@modelcontextprotocol/sdk": "^1.20.2"
35
+ "@gesslar/toolkit": "^3.23.0",
36
+ "@modelcontextprotocol/sdk": "^1.25.2",
37
+ "zod": "^4.3.5"
40
38
  },
41
39
  "devDependencies": {
42
- "@eslint/js": "^9.39.0",
43
- "@stylistic/eslint-plugin": "^5.5.0",
44
- "eslint-plugin-jsdoc": "^61.1.11",
45
- "globals": "^16.5.0"
40
+ "@gesslar/uglier": "^1.1.0",
41
+ "eslint": "^9.39.2"
42
+ },
43
+ "scripts": {
44
+ "start": "node src/index.js",
45
+ "lint": "eslint src/",
46
+ "lint:fix": "eslint src/ --fix",
47
+ "submit": "pnpm publish --access public --//registry.npmjs.org/:_authToken=\"${NPM_ACCESS_TOKEN}\"",
48
+ "update": "pnpm self-update && pnpx npm-check-updates -u && pnpm install",
49
+ "pr": "gt submit -p --ai",
50
+ "patch": "pnpm version patch",
51
+ "minor": "pnpm version minor",
52
+ "major": "pnpm version major"
46
53
  }
47
- }
54
+ }
File without changes
package/src/index.js CHANGED
@@ -1,18 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import {Server} from "@modelcontextprotocol/sdk/server/index.js"
3
+ import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"
4
4
  import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js"
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- } from "@modelcontextprotocol/sdk/types.js"
5
+ import * as z from "zod/v4"
9
6
  import {spawn} from "child_process"
10
- import path from "path"
11
- import fs from "fs"
7
+ import {FileObject, DirectoryObject} from "@gesslar/toolkit"
12
8
 
13
9
  class FluffOSMCPServer {
14
10
  constructor() {
15
- this.server = new Server(
11
+ this.server = new McpServer(
16
12
  {
17
13
  name: "fluffos-mcp-server",
18
14
  version: "0.1.0",
@@ -28,7 +24,9 @@ class FluffOSMCPServer {
28
24
  this.configFile = process.env.MUD_RUNTIME_CONFIG_FILE
29
25
  this.docsDir = process.env.FLUFFOS_DOCS_DIR
30
26
  this.mudlibDir = null
27
+ }
31
28
 
29
+ async initialize() {
32
30
  if(!this.binDir) {
33
31
  console.error("Error: FLUFFOS_BIN_DIR environment variable not set")
34
32
  process.exit(1)
@@ -40,28 +38,29 @@ class FluffOSMCPServer {
40
38
  }
41
39
 
42
40
  // Parse mudlib directory from config file
43
- this.mudlibDir = this.parseMudlibDir()
41
+ this.mudlibDir = await this.parseMudlibDir()
44
42
 
45
43
  console.error(`FluffOS bin directory: ${this.binDir}`)
46
44
  console.error(`FluffOS config file: ${this.configFile}`)
47
45
  console.error(`Mudlib directory: ${this.mudlibDir || "(not found in config)"}`)
48
46
 
49
- if(this.docsDir) {
47
+ if(this.docsDir)
50
48
  console.error(`FluffOS docs directory: ${this.docsDir}`)
51
- } else {
49
+ else
52
50
  console.error(`FluffOS docs directory: not set (doc lookup disabled)`)
53
- }
54
51
 
55
- this.setupHandlers()
52
+ this.setupTools()
56
53
  }
57
54
 
58
- parseMudlibDir() {
55
+ async parseMudlibDir() {
59
56
  try {
60
- const configContent = fs.readFileSync(this.configFile, "utf8")
57
+ const configFile = new FileObject(this.configFile)
58
+ const configContent = await configFile.read()
61
59
  const match = configContent.match(/^mudlib directory\s*:\s*(.+)$/m)
62
- if(match) {
60
+
61
+ if(match)
63
62
  return match[1].trim()
64
- }
63
+
65
64
  } catch(err) {
66
65
  console.error(`Warning: Could not parse mudlib directory from config: ${err.message}`)
67
66
  }
@@ -73,7 +72,7 @@ class FluffOSMCPServer {
73
72
  // If we have a mudlib directory and the file path is absolute and starts with mudlib dir,
74
73
  // convert it to a relative path
75
74
  if(this.mudlibDir &&
76
- path.isAbsolute(lpcFile) &&
75
+ lpcFile.startsWith("/") &&
77
76
  lpcFile.startsWith(this.mudlibDir)
78
77
  ) {
79
78
  // Remove mudlib directory prefix and leading slash
@@ -84,109 +83,57 @@ class FluffOSMCPServer {
84
83
  return lpcFile
85
84
  }
86
85
 
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
- },
86
+ setupTools() {
87
+ // Register validate tool
88
+ this.server.registerTool("fluffos_validate", {
89
+ description: "Validate an LPC file using the FluffOS driver's symbol tool. " +
90
+ "Compiles the file and reports success or failure with any " +
91
+ "compilation errors. Fast and lightweight check for code validity.",
92
+ inputSchema: z.object({
93
+ file: z.string().describe("Absolute path to the LPC file to validate"),
94
+ }),
95
+ }, async({file}) => {
96
+ try {
97
+ const result = await this.runSymbol(file)
98
+
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: result,
118
104
  },
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
- },
105
+ ],
106
+ }
107
+ } catch(error) {
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: `Error: ${error.message}`,
133
113
  },
134
- required: ["query"],
135
- },
136
- }] : []),
137
- ],
138
- }))
139
-
140
- this.server.setRequestHandler(CallToolRequestSchema, async request => {
141
- const {name, arguments: args} = request.params
114
+ ],
115
+ isError: true,
116
+ }
117
+ }
118
+ })
142
119
 
120
+ // Register disassemble tool
121
+ this.server.registerTool("fluffos_disassemble", {
122
+ description: "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.",
123
+ inputSchema: z.object({
124
+ file: z.string().describe("Absolute path to the LPC file to disassemble"),
125
+ }),
126
+ }, async({file}) => {
143
127
  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
- }
128
+ const result = await this.runLpcc(file)
187
129
 
188
- default:
189
- throw new Error(`Unknown tool: ${name}`)
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: result,
135
+ },
136
+ ],
190
137
  }
191
138
  } catch(error) {
192
139
  return {
@@ -200,14 +147,49 @@ class FluffOSMCPServer {
200
147
  }
201
148
  }
202
149
  })
150
+
151
+ // Register doc lookup tool (conditional)
152
+ if(this.docsDir) {
153
+ this.server.registerTool("fluffos_doc_lookup", {
154
+ description: "Search FluffOS documentation for information about efuns, applies, concepts, etc. Searches markdown documentation files.",
155
+ inputSchema: z.object({
156
+ query: z.string().describe("Term to search for in documentation (e.g., 'call_out', 'mapping', 'socket')"),
157
+ }),
158
+ }, async({query}) => {
159
+ try {
160
+ const result = await this.searchDocs(query)
161
+
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: result,
167
+ },
168
+ ],
169
+ }
170
+ } catch(error) {
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: `Error: ${error.message}`,
176
+ },
177
+ ],
178
+ isError: true,
179
+ }
180
+ }
181
+ })
182
+ }
203
183
  }
204
184
 
205
185
  async runSymbol(lpcFile) {
206
186
  return new Promise((resolve, reject) => {
207
187
  const normalizedPath = this.normalizePath(lpcFile)
208
- const symbolPath = path.join(this.binDir, "symbol")
188
+ const binDir = new DirectoryObject(this.binDir)
189
+ const symbolPath = binDir.getFile("symbol").path
190
+ const configFile = new FileObject(this.configFile)
209
191
  const proc = spawn(symbolPath, [this.configFile, normalizedPath], {
210
- cwd: path.dirname(this.configFile),
192
+ cwd: configFile.parentPath,
211
193
  })
212
194
 
213
195
  let stdout = ""
@@ -224,11 +206,11 @@ class FluffOSMCPServer {
224
206
  proc.on("close", code => {
225
207
  const output = (stdout + stderr).trim()
226
208
 
227
- if(code === 0) {
209
+ if(code === 0)
228
210
  resolve(`✓ File validated successfully\n\n${output}`)
229
- } else {
211
+ else
230
212
  resolve(`✗ Validation failed (exit code: ${code})\n\n${output}`)
231
- }
213
+
232
214
  })
233
215
 
234
216
  proc.on("error", err => {
@@ -240,9 +222,11 @@ class FluffOSMCPServer {
240
222
  async runLpcc(lpcFile) {
241
223
  return new Promise((resolve, reject) => {
242
224
  const normalizedPath = this.normalizePath(lpcFile)
243
- const lpccPath = path.join(this.binDir, "lpcc")
225
+ const binDir = new DirectoryObject(this.binDir)
226
+ const lpccPath = binDir.getFile("lpcc").path
227
+ const configFile = new FileObject(this.configFile)
244
228
  const proc = spawn(lpccPath, [this.configFile, normalizedPath], {
245
- cwd: path.dirname(this.configFile),
229
+ cwd: configFile.parentPath,
246
230
  })
247
231
 
248
232
  let stdout = ""
@@ -274,7 +258,9 @@ class FluffOSMCPServer {
274
258
 
275
259
  async searchDocs(query) {
276
260
  return new Promise((resolve, reject) => {
277
- const scriptPath = path.join(path.dirname(new URL(import.meta.url).pathname), "scripts", "search_docs.sh")
261
+ const moduleFile = new FileObject(new URL(import.meta.url).pathname)
262
+ const scriptsDir = moduleFile.parent.getDirectory("scripts")
263
+ const scriptPath = scriptsDir.getFile("search_docs.sh").path
278
264
  const proc = spawn(scriptPath, [this.docsDir, query])
279
265
 
280
266
  let stdout = ""
@@ -307,6 +293,8 @@ class FluffOSMCPServer {
307
293
  }
308
294
 
309
295
  async run() {
296
+ await this.initialize()
297
+
310
298
  const transport = new StdioServerTransport()
311
299
  await this.server.connect(transport)
312
300
 
@@ -1,33 +0,0 @@
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/index.js DELETED
@@ -1,306 +0,0 @@
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);