From d25c8724d5bda58b068065c6fc446bc62fbb4000 Mon Sep 17 00:00:00 2001 From: Katniss Date: Fri, 5 Jun 2026 22:29:39 -0700 Subject: [PATCH] fix(openapi): add shebang and chmod +x to gen-mcp generated index.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generateMcpServer() emits a package.json with a bin entry pointing to index.js, but the generated index.js starts with a comment line — no shebang. When the user runs 'npm i -g @generated/foo-mcp' and then invokes 'foo-mcp' on the shell, the OS tries to exec index.js as a native script. Without #!/usr/bin/env node it sees 'import' as the first token and fails with ENOEXEC on Linux or a cryptic syntax error on macOS, neither of which suggests the real problem. Two fixes: 1. Prepend '#!/usr/bin/env node' to the generated index template so the file is self-describing as a Node.js script. 2. chmod(dest, 0o755) the emitted index.js at write time (only that file, not package.json) so 'npm link' / global install symlinks can exec it. Uses .catch(() => {}) so the write step doesn't fail on Windows or read-only filesystems where the mode change is irrelevant. The bug affects every MCP server generated by gen-mcp when installed globally, which is the documented primary use case per the emitted README. --- packages/openapi/src/gen-mcp/index.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/openapi/src/gen-mcp/index.ts b/packages/openapi/src/gen-mcp/index.ts index fcfe3308..69014858 100644 --- a/packages/openapi/src/gen-mcp/index.ts +++ b/packages/openapi/src/gen-mcp/index.ts @@ -1,4 +1,4 @@ -import { mkdir, writeFile } from 'node:fs/promises'; +import { mkdir, writeFile, chmod } from 'node:fs/promises'; import { join } from 'node:path'; import type { ApiIR, Operation, Parameter } from '../core/types.js'; @@ -23,7 +23,15 @@ export async function generateMcpServer(ir: ApiIR, opts: GenerateMcpOptions): Pr await mkdir(opts.outDir, { recursive: true }); for (const f of files) { await mkdir(join(opts.outDir, dirOf(f.path)), { recursive: true }); - await writeFile(join(opts.outDir, f.path), f.contents, 'utf8'); + const dest = join(opts.outDir, f.path); + await writeFile(dest, f.contents, 'utf8'); + // Make the bin entry executable so `npm i -g` installs a runnable + // script. Without 0755, the OS refuses to exec it and the user sees + // a confusing EACCES or "import statement" shell error instead of a + // running MCP server. Only needed for the entry-point declared in bin. + if (f.path === 'index.js') { + await chmod(dest, 0o755).catch(() => {}); + } } return files; } @@ -35,7 +43,8 @@ function render(ir: ApiIR, opts: GenerateMcpOptions): GeneratedFile[] { const tools = ir.operations.map(toolDescriptor).join(',\n'); const handlers = ir.operations.map(toolHandler).join('\n\n'); - const index = `// AUTO-GENERATED by @profullstack/sh1pt-openapi/gen-mcp. + const index = `#!/usr/bin/env node +// AUTO-GENERATED by @profullstack/sh1pt-openapi/gen-mcp. // Source: ${ir.title} v${ir.version}. Do not edit by hand. // // Stdio MCP server: each upstream API operation is exposed as one tool.