Skip to content

fix: resolve symlinked bin path before entry-point comparison - #31

Open
autojack-bot[bot] wants to merge 1 commit into
mainfrom
fix/npx-bin-symlink-silent-exit
Open

fix: resolve symlinked bin path before entry-point comparison#31
autojack-bot[bot] wants to merge 1 commit into
mainfrom
fix/npx-bin-symlink-silent-exit

Conversation

@autojack-bot

@autojack-bot autojack-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Fixes the MCP server exiting cleanly with no output whenever it's launched via npx or an installed mcp-pirsch bin — reported by AutoHub as Server mcp-pirsch process exited unexpectedly (code: 0, signal: null).

Root cause

npm/npx install this package's bin as a symlink: node_modules/.bin/mcp-pirsch -> ../@verygoodplugins/mcp-pirsch/dist/index.js. The entry-point guard in src/index.ts was:

const isDirectExecution =
  typeof process.argv[1] === 'string' && import.meta.url === new URL(process.argv[1], 'file://').href;

When Node executes the file through that symlink, process.argv[1] is the symlink path, but ESM's import.meta.url is already resolved to the real target path. The two never match, so isDirectExecution was always false under npx/bin invocation, main() was never called, and the process fell off the end of the module with nothing pending — a clean exit(0) with zero output (no error, no "Pirsch MCP server running" log). Running node dist/index.js directly worked fine (no symlink involved), which is why this went unnoticed — the credential guard's process.exit(1) path was also ruled out since the observed exit code is 0, not 1.

Fix

Resolve process.argv[1] with realpathSync before comparing it to import.meta.url (which is already realpath-resolved by Node's ESM loader):

function isDirectExecution(): boolean {
  if (typeof process.argv[1] !== 'string') return false;
  try {
    return pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url;
  } catch {
    return false;
  }
}

Testing

  • Added src/index.spawn.test.ts: spawns the built dist/index.js (a) directly and (b) through a freshly created symlink (mirroring npm's bin shim), and asserts the server logs Pirsch MCP server running and stays alive in both cases. This test fails on main (symlink case hangs with zero stderr output) and passes on this branch.
  • npm run lint, npm run typecheck, npm run build, npm test all green locally (55/55 tests passing).
  • Manually reproduced pre-fix by symlinking dist/index.js and invoking via the symlink — confirmed silent exit 0 with no stdout/stderr before the fix, and correct startup log after.

Test plan

  • npm run lint
  • npm run typecheck
  • npm run build
  • npm test (55 passed, including the new regression test)
  • Once merged/released, confirm AutoHub's on-demand npx -y @verygoodplugins/mcp-pirsch@latest load succeeds against the published fix

npx/npm install this package's `mcp-pirsch` bin as a symlink to
dist/index.js. When invoked through that symlink, process.argv[1] is
the symlink path while import.meta.url is already resolved to the
real target, so the isDirectExecution comparison always evaluated
false. main() never ran and the process exited 0 with zero output —
surfacing in AutoHub as "Server mcp-pirsch process exited unexpectedly
(code: 0, signal: null)" on every npx-based load. Running
node dist/index.js directly worked fine, which masked the bug.

Resolve process.argv[1] with realpathSync before comparing against
import.meta.url so symlinked bin invocations are detected correctly.
Added a regression test that spawns the built entry point through a
symlink (matching npm's bin shim) and asserts the server starts.
@jack-arturo

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b0115c4a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/index.ts
// so process.argv[1] is the symlink path while import.meta.url is already resolved to
// the real target. Resolve the symlink before comparing or this always evaluates false
// under npx, main() never runs, and the process exits cleanly with no output.
return pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept preserved symlink mains in the entry guard

When users run the bin with NODE_OPTIONS=--preserve-symlinks-main (node --help describes this as preserving symbolic links when resolving the main module), Node keeps import.meta.url at the symlink path; this code always realpaths argv[1], so node node_modules/.bin/mcp-pirsch evaluates false again and exits 0 without starting. The guard should accept either the direct argv[1] file URL or the realpath URL so the symlink fix does not regress environments using that Node option.

Useful? React with 👍 / 👎.

Comment thread src/index.spawn.test.ts
Comment on lines +61 to +62
it('starts the server when invoked directly (dist/index.js exists)', () => {
expect(existsSync(distEntry)).toBe(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid requiring dist before the test script builds it

In a clean checkout, dist/ is gitignored and package.json's npm test script only runs vitest run, so this new test fails before exercising the fix whenever tests are run without a preceding npm run build (the README documents npm test as the standalone test command). Either have the test setup build the entry or run the source through the TS runner so the suite is not dependent on an untracked build artifact.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant