Problem
Every argument-taking tool of the file-system MCP server fails at call time. Only healthcheck (no arguments) succeeds.
Verified against the published npm packages neo.mjs@13.0.0 and neo.mjs@13.1.0, by starting ai/mcp/server/file-system/mcp-server.mjs and connecting a real MCP client over stdio (@modelcontextprotocol/sdk):
tools/list -> healthcheck, read_file, write_file, list_directory, check_syntax, run_playwright_test
healthcheck isError=false {"status":"healthy"}
read_file isError=true Error executing read_file: The "paths[0]" argument must be of type string. Received undefined
list_directory isError=true (same)
write_file isError=true (same)
check_syntax isError=true (same)
run_playwright_test isError=true (same)Stack trace from the server process:
BaseServer.mjs:341 dispatch
ToolService.mjs:74 callTool -> tool.handler(...handlerArgs)
FileSystemService.mjs:54 checkSyntax
FileSystemService.mjs:16 ensureSandboxed -> TypeError (ERR_INVALID_ARG_TYPE)Root cause
Same defect class as #8332, #9545 and #14542.
ToolService dispatch branches on x-pass-as-object (ai/mcp/ToolService.mjs):
if (tool.passAsObject) {
return tool.handler(validatedArgs);
}
const handlerArgs = tool.argNames.map(name => validatedArgs[name]);
return tool.handler(...handlerArgs); All FileSystemService handlers destructure a single object:
static async readFile({absolutePath}) { ... }
static async writeFile({absolutePath, content}) { ... }
static async listDirectory({absolutePath}) { ... }
static async checkSyntax({absolutePath}) { ... }
static async runPlaywrightTest({absolutePath}) { ... }…but ai/mcp/server/file-system/openapi.yaml declares x-pass-as-object on none of its operations, so the positional branch is taken and the handlers receive a bare string. Destructuring it yields absolutePath === undefined, and path.resolve(undefined) throws.
Declaration counts per server (13.0.0):
| server |
operations |
x-pass-as-object |
| github-workflow |
23 |
16 |
| gitlab-workflow |
13 |
12 |
| knowledge-base |
9 |
8 |
| memory-core |
29 |
28 |
| neural-link |
37 |
32 |
| file-system |
6 |
0 |
Suggested fix
Add x-pass-as-object: true to the five argument-taking operations in ai/mcp/server/file-system/openapi.yaml:
/read_file:
post:
operationId: read_file
x-pass-as-object: true(same for write_file, list_directory, check_syntax, run_playwright_test)
Locally patching just check_syntax this way makes the tool return Syntax OK through the same MCP client, so the one-line annotation is sufficient.
Why it went unnoticed
No test exercises these tools through the real ToolService dispatch. FileSystemPolicy.spec.mjs substitutes callTool with a mock, and the FileSystemService unit specs call the service methods directly with an object. A dispatch-level test for one file-system tool would have caught this, and would guard the other servers too.
Related: PR #10873 review already flagged that x-pass-as-object lacks a discoverable definition in the docs surface, which is likely why the annotation keeps getting missed.
Environment
- neo.mjs 13.0.0 and 13.1.0 (npm)
- Node.js 22
- @modelcontextprotocol/sdk 1.27.x
Problem
Every argument-taking tool of the file-system MCP server fails at call time. Only
healthcheck(no arguments) succeeds.Verified against the published npm packages
neo.mjs@13.0.0andneo.mjs@13.1.0, by startingai/mcp/server/file-system/mcp-server.mjsand connecting a real MCP client over stdio (@modelcontextprotocol/sdk):tools/list -> healthcheck, read_file, write_file, list_directory, check_syntax, run_playwright_test healthcheck isError=false {"status":"healthy"} read_file isError=true Error executing read_file: The "paths[0]" argument must be of type string. Received undefined list_directory isError=true (same) write_file isError=true (same) check_syntax isError=true (same) run_playwright_test isError=true (same)Stack trace from the server process:
BaseServer.mjs:341 dispatch ToolService.mjs:74 callTool -> tool.handler(...handlerArgs) FileSystemService.mjs:54 checkSyntax FileSystemService.mjs:16 ensureSandboxed -> TypeError (ERR_INVALID_ARG_TYPE)Root cause
Same defect class as #8332, #9545 and #14542.
ToolServicedispatch branches onx-pass-as-object(ai/mcp/ToolService.mjs):if (tool.passAsObject) { return tool.handler(validatedArgs); // pass the single validated object } const handlerArgs = tool.argNames.map(name => validatedArgs[name]); return tool.handler(...handlerArgs); // else: spread positionallyAll
FileSystemServicehandlers destructure a single object:static async readFile({absolutePath}) { ... } static async writeFile({absolutePath, content}) { ... } static async listDirectory({absolutePath}) { ... } static async checkSyntax({absolutePath}) { ... } static async runPlaywrightTest({absolutePath}) { ... }…but
ai/mcp/server/file-system/openapi.yamldeclaresx-pass-as-objecton none of its operations, so the positional branch is taken and the handlers receive a bare string. Destructuring it yieldsabsolutePath === undefined, andpath.resolve(undefined)throws.Declaration counts per server (13.0.0):
Suggested fix
Add
x-pass-as-object: trueto the five argument-taking operations inai/mcp/server/file-system/openapi.yaml:/read_file: post: operationId: read_file x-pass-as-object: true(same for
write_file,list_directory,check_syntax,run_playwright_test)Locally patching just
check_syntaxthis way makes the tool returnSyntax OKthrough the same MCP client, so the one-line annotation is sufficient.Why it went unnoticed
No test exercises these tools through the real
ToolServicedispatch.FileSystemPolicy.spec.mjssubstitutescallToolwith a mock, and theFileSystemServiceunit specs call the service methods directly with an object. A dispatch-level test for one file-system tool would have caught this, and would guard the other servers too.Related: PR #10873 review already flagged that
x-pass-as-objectlacks a discoverable definition in the docs surface, which is likely why the annotation keeps getting missed.Environment