Repository navigation
Conversation
An npm package can declare several executables in package.json "bin". `npx <identifier>@<version>` only works when the package has one bin or one named after the package; otherwise npx exits with "could not determine executable to run", and server.json had no way to say which bin starts the MCP server. Add an optional `executable` field to Package: - openapi.yaml and the generated draft schema: string, 1-255 chars, pattern ^[A-Za-z0-9_][A-Za-z0-9._-]*$ so the value can never be read as a path or a flag by a package runner. - Semantic validation: reject a malformed value, and reject the field on non-npm packages until other registry types define its meaning. - npm registry validation: the selected executable must be an entry of "bin" in the published version (object form, or the string shorthand that names the unscoped package). - Docs: CHANGELOG draft entry, a generic-server-json example, and a package-types section showing `npx -y --package=<id>@<ver> -- <exe>`. Refs modelcontextprotocol#1629 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FelixFangX
left a comment
There was a problem hiding this comment.
Checked this against lakeboard-converter, which ships three bins: lakeboard-converter and mindmap-converter (both the CLI) and lakeboard-converter-mcp (the server). One bin is named after the package, so npx doesn't fail, but it starts the CLI. Its server.json works around that with a positional mcp in packageArguments. With npm 11.21.0, npx -y lakeboard-converter@0.4.1 mcp and npx -y --package=lakeboard-converter@0.4.1 -- lakeboard-converter-mcp return the same initialize result, so executable covers this case too and makes the subcommand unnecessary. go test ./internal/validators/... ./pkg/... passes on 822b374.
Possible follow-up: mcp-publisher init (cmd/publisher/commands/init.go, the npm branch of the package switch) doesn't look at bin, so it can't suggest executable for multi-bin packages.
|
|
||
| ### Packages with multiple executables | ||
|
|
||
| If your package declares more than one executable in `package.json` `bin` and none of them is named after the package, `npx <package>` fails with "could not determine executable to run". Set `executable` to the `bin` entry that starts your MCP server: |
There was a problem hiding this comment.
This is also useful when a bin is named after the package but isn't the server, e.g. a CLI with a separate <name>-mcp bin. Those packages currently need a subcommand in packageArguments. Maybe something like: "If your package declares more than one executable in bin, set executable to the one that starts your MCP server. It's required when none of them is named after the package, since npx <package> then fails with ..."
|
|
||
| ### npm package with multiple executables | ||
|
|
||
| When an npm package declares several executables in its `package.json` `bin` field, `npx <package>` only works if one of them is named after the package. Use `executable` to name the one that starts the MCP server. The registry checks that it is an entry of `bin` in the published version, and clients run it with `npx -y --package=<identifier>@<version> -- <executable>`, followed by any `packageArguments`. |
There was a problem hiding this comment.
Worth saying where runtimeArguments go when executable is set, e.g. npx <runtimeArguments> --package=<identifier>@<version> -- <executable> <packageArguments>. npx -y --prefer-online --package=lakeboard-converter@0.4.1 -- lakeboard-converter-mcp works with npm 11.21.0.
The form is also npx-specific. pnpm dlx --package=lakeboard-converter@0.4.1 -- lakeboard-converter-mcp works, but bunx 1.4.2 rejects every variant I tried (--package=x -- bin gives Script not found "lakeboard-converter-mcp", -p x bin tries to eval the spec). Clients that map runtimeHint to another runner would need their own translation, so it may help to say that.
Summary
Adds an optional
executablefield toPackageso aserver.jsonentry can say which of an npm package'sbinexecutables starts the MCP server. Without it,npx -y <identifier>@<version>fails withcould not determine executable to runwhenever a package ships several bins and none is named after the package. Refs #1629. The design and the data behind it are also posted on the issue.Problem
npx picks a bin in one of two cases: the package has exactly one bin, or one bin matches the unscoped package name. Otherwise it errors. I scanned the latest version of every server in the registry and checked each npm version manifest against that rule:
binat allA live example is
io.github.hellocoop/admin-mcp→@hellocoop/admin-mcp@1.5.7, with binsmcpandhello-mcp-http:12 of the 137 work around this through
runtimeArguments(e.g.["-y", "-p", "@circulara/plugin", "circulara-mcp"]). That spec is unpinned and depends on how each client orders arguments around<identifier>@<version>.Changes
docs/reference/api/openapi.yaml: newPackage.executableproperty: string, 1 to 255 chars, pattern^[A-Za-z0-9_][A-Za-z0-9._-]*$. The description says npm-only and gives the client command.docs/reference/server-json/draft/server.schema.json: regenerated withmake generate-schema.pkg/model/types.go:Executablefield with matching huma tags, so/v0/publishrejects a malformed value with 422 before validation runs.internal/validators/validators.go,constants.go: semantic checkvalidatePackageExecutable. It rejects the field on non-npm packages (package-executable-unsupported) and rejects values that fail the pattern or length (package-executable-invalid). This also covers$schemaversions that predate the field.internal/validators/registries/npm.go:NPMPackageResponsenow decodesbin. Whenexecutableis set, it must be a key ofbinin the published version. The string shorthand counts as one bin named after the unscoped package. The existing version-manifest GET is reused, so there is no extra request. The error lists the available bins.generic-server-json.mdexample (the example count intools/validate-examplesgoes 17 to 18), and a "Packages with multiple executables" section inpackage-types.mdx.Client behavior
With
executableset, a client runs:Checked locally with npm 12.0.2:
npx -y --package=typescript@5.6.2 -- tsc --versionprintsVersion 5.6.2, andnpx -y typescript@5.6.2 --versionfails withcould not determine executable to run. Clients that don't know the field ignore it and behave as today.Testing
New tests:
TestValidate_PackageExecutable(validators): valid names, plus rejection of-c,../bin/server,bin/server,mcp server,mcp;rm,.mcp, a 256-char name, and the field on pypi and oci. It also asserts the issue path ispackages[0].executable.TestValidateNPM_Executable(registries, httptest mock): selected bin present; bin absent (error listssumlyzer, sumlyzer-mcp-server); string shorthand on a scoped package; nobin; malformedbin; no executable means no bin check.TestValidateNPM_RealPackages: two live cases on@hellocoop/admin-mcp@1.5.7.mcppasses, andadmin-mcpfails with(available: hello-mcp-http, mcp).TestPublishEndpoint_PackageExecutable(handlers, Postgres): the field is stored and echoed on publish,--call=evilgets 422 from the pattern, and pypi plusexecutablegets 422.TestValidateEndpoint:/v0/validatereportspackages[0].executableas a semantic issue for pypi.Before the implementation (tests only, with the
validators.go/npm.gochanges reverted), 16 subtests fail across those five tests. After:The DB-backed packages ran against a local PostgreSQL 16 (conda-forge build, since Docker isn't available on this machine) on
localhost:5432with themcpregistrycredentials fromtestutil.go. I did not runtests/integration/run.sh, which needs Docker.I also checked the draft schema directly with a JSON Schema validator. It accepts
sumlyzer-mcp-serverand_x.y-2, and rejects-c,../x,a/b,a b,.x, the empty string and a 256-char value. All 349 bin names from the 137 affected packages match the pattern.Notes for reviewers
executablebecause it reads the same for other registry types.binwould match npm and cargo vocabulary. Renaming is mechanical.uvx --from <pkg>==<ver> <script>) and cargo (crates.io returnsbin_namesper version) in follow-ups.Packagehas noadditionalProperties: falsein 2025-12-11, so publishers can send the field before the next schema release. The Go semantic check enforces the same rules either way. If you'd rather gate it on the next schema version, I can add that.bin. A publish-time warning for "npx cannot resolve a bin" could be a follow-up.AI assistance: this PR was written with Claude Code (design, implementation, tests and the registry scan). I reviewed the changes and ran the tests and commands shown above.
🤖 Generated with Claude Code