Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/openapi/src/gen-mcp/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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(() => {});
}
Comment on lines +32 to +34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The .catch(() => {}) swallows every possible chmod error, not only the platform-specific ones that are expected (e.g. ENOSYS on Windows, EPERM on a read-only volume). If chmod fails for an unexpected reason after a successful writeFile — for instance a race with another process renaming the file — the generator returns silently, the file is left non-executable, and the caller has no idea. Filtering to known "not applicable" codes preserves the intent while letting real errors bubble.

Suggested change
if (f.path === 'index.js') {
await chmod(dest, 0o755).catch(() => {});
}
if (f.path === 'index.js') {
await chmod(dest, 0o755).catch((e: NodeJS.ErrnoException) => {
// chmod is a no-op on Windows (ENOSYS) and may be refused on
// read-only volumes (EPERM/ENOTSUP). Swallow only those cases
// so that unexpected errors (e.g. the file was removed) still
// surface to the caller.
if (!['ENOSYS', 'EPERM', 'ENOTSUP'].includes(e.code ?? '')) throw e;
});
}

}
return files;
}
Expand All @@ -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.
Expand Down
Loading