Skip to content

fix(config): require a command on a local MCP server entry - #819

Merged
Ishaan Gangwani (ishaan1124) merged 4 commits into
synthetic-sciences:mainfrom
aniruddhaadak80:fix/mcp-command-empty-array
Sep 29, 2026
Merged

Ishaan Gangwani (ishaan1124) merged 4 commits into
synthetic-sciences:mainfrom
aniruddhaadak80:fix/mcp-command-empty-array

Conversation

@aniruddhaadak80

@aniruddhaadak80 ANIRUDDHA ADAK (aniruddhaadak80) commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

What

Config.McpLocal declared the command as z.string().array(), which accepts an empty list:

export const McpLocal = z.object({
  type: z.literal("local").describe("Type of MCP server connection"),
  command: z.string().array().describe("Command and arguments to run the MCP server"),
  ...
})

The connection is then started from src/mcp/index.ts:918:

if (mcp.type === "local") {
  const [cmd, ...args] = mcp.command
  ...
  const sandbox = Sandbox.wrapArgv({ file: cmd, args, ... })

With command: [], cmd is undefined, so the launch is attempted with file: undefined — a sandbox-wrapped spawn of nothing.

Why it matters

An MCP entry with an empty command is a configuration mistake with no valid reading: there is no executable to run. It should be rejected where the mistake is, not converted into a spawn attempt that fails somewhere else in the MCP layer.

McpRemote validates its url in the schema. A local server's command is the equivalent required field, and it is the only part of the entry with nothing to fall back on.

Verification

test/config/mcp-command-schema.test.ts is new. Two of five fail before the fix:

Expected: false
Received: true
(fail) Config.McpLocal > rejects an empty command, which has no executable to run

Expected: false
Received: true
(fail) Config.McpLocal > reports why the command was rejected

After:

(pass) accepts a command with an executable and arguments
(pass) rejects an empty command, which has no executable to run
(pass) rejects a non-array command
(pass) rejects a command whose entries are not all strings
(pass) reports why the command was rejected
 5 pass
 0 fail

The three pre-existing rejection cases are pinned alongside the new one so the constraint is known to be the length, not a change in how the element type is handled. The last test asserts the issue path mentions command, so a silent skip is not a way to pass it.

The change

       command: z.string().array().describe("Command and arguments to run the MCP server"),
+      command: z
+        .string()
+        .array()
+        .min(1, 'A local MCP server needs a command to run, for example ["npx", "-y", "my-mcp-server"]')
+        .describe("Command and arguments to run the MCP server"),

The message names the problem and shows a working example, matching the style of the other schema messages in this file.

On the surrounding suites

test/config/config.test.ts, test/config/mcp-secrets.test.ts and test/mcp/ were run before and after: 125 pass / 13 fail on main, 124/14 with the change. The one extra failure is a resumed exact flow restarts its callback listener without creating a replacement, which passes 3/3 in isolation with the change applied and is pre-existing order-dependent flakiness: repeated runs of the whole test/mcp/ suite give 55 pass / 10 fail and then 56/9 — identically with and without the change.

bun run typecheck clean; touched files are Prettier-clean (checked on LF-normalized copies — this checkout has core.autocrlf=true, which makes Prettier flag every file in the repo).

Fixes #818

@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

ANIRUDDHA ADAK (@aniruddhaadak80) is attempting to deploy a commit to the InkVell Team on Vercel.

A member of the Team first needs to authorize it.

z.string().array() accepts an empty list, and the connection is then
launched from `const [cmd, ...args] = mcp.command`, so an entry with no
command started a server with no executable. Reject it at the entry with
an example instead.
The `minItems: 1` added to a local MCP server's `command` reaches the
OpenAPI contract, and the compatibility job fails when the committed
specification is not what the generator produces. `./tooling/repo/generate.ts`
regenerates it; only `tooling/sdk/openapi.json` changes, by that one line.
…e config

Putting .min(1) on McpLocal.command made one incomplete entry a config
parse error, so the whole file stopped loading (CLI exit, config-error
page) where before only that connector failed; and [""] still passed.
Revert the schema and SDK change and check at launch instead: an empty
command, or one whose program is blank, marks that server failed with a
message naming it, without spawning anything. The CLI's `mcp add` prompt
also no longer accepts a blank command or splits one into empty words.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ishaan1124

Copy link
Copy Markdown
Member

I pushed one commit (bb7387b) on top of yours. .min(1) on the config schema made one incomplete MCP entry stop the whole config from loading, where main only failed that connector. I reverted the schema change; the empty command (including [""]) is now caught where the server is launched, so that entry is marked failed with a clear message and the rest of the config loads. openscience mcp add also rejects a blank command now. CI is green.

@ishaan1124
Ishaan Gangwani (ishaan1124) merged commit f0ff327 into synthetic-sciences:main Sep 29, 2026
8 of 9 checks passed
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.

A local MCP server entry may declare an empty command and is then launched with no executable

2 participants