Skip to content

fix(server): accept empty {} annotations in tool() overload#1980

Open
AP3X-Dev wants to merge 1 commit intomodelcontextprotocol:v1.xfrom
AP3X-Dev:fix/empty-annotations-typed-handler
Open

fix(server): accept empty {} annotations in tool() overload#1980
AP3X-Dev wants to merge 1 commit intomodelcontextprotocol:v1.xfrom
AP3X-Dev:fix/empty-annotations-typed-handler

Conversation

@AP3X-Dev
Copy link
Copy Markdown

Summary

server.tool(name, description, schema, {}, callback) registers successfully but throws typedHandler is not a function at first call.

The overload parser at src/server/mcp.ts:993 uses isZodRawShapeCompat() to disambiguate between a Zod raw shape and a ToolAnnotations object in the slot after the schema. isZodRawShapeCompat({}) returns true (intentionally — to accept tools with no parameters in the schema slot), so an empty annotations object passed after a populated schema is misclassified as a second schema, the annotations branch is skipped, and callback = rest[0] ends up as {} instead of the function.

At dispatch time, executeToolHandler does:

```ts
const typedHandler = handler;
return await Promise.resolve(typedHandler(args, extra));
```

…which throws TypeError: typedHandler is not a function.

Root cause

src/server/mcp.ts:1021 (pre-fix):

```ts
if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !isZodRawShapeCompat(rest[0])) {
annotations = rest.shift() as ToolAnnotations;
}
```

For empty {}, !isZodRawShapeCompat({}) is false, so annotations are not consumed. The handler slot then receives {}.

Fix

Once the schema slot has been filled above, an empty object in the next slot is unambiguously annotations (a populated raw shape would have at least one Zod value). The annotations check now accepts empty objects:

```ts
if (
rest.length > 1 &&
typeof rest[0] === 'object' &&
rest[0] !== null &&
(Object.keys(rest[0] as object).length === 0 || !isZodRawShapeCompat(rest[0]))
) {
annotations = rest.shift() as ToolAnnotations;
}
```

Non-empty objects retain their existing protective check (a populated Zod raw shape in the annotations slot stays an error rather than being silently consumed as annotations).

Test plan

  • New regression test in test/server/mcp.test.ts covering tool(name, desc, schema, {}, cb) — registers, lists, and calls successfully (passes against both Zod v3 and Zod v4 fixtures).
  • Full test/server/mcp.test.ts suite passes (234/234).
  • npm run typecheck passes.
  • npx prettier --check passes on touched files.
  • npx eslint src/server/mcp.ts passes.

Real-world impact

Any tool registered with server.tool(name, desc, schema, {}, cb) is unreachable. Encountered this in a downstream MCP server where 6 write-tool registrations all used {} as a placeholder for "no annotations to declare yet" — every write call surfaced as typedHandler is not a function while reads (which used populated annotations) worked.

Affects v1.x line. Not present on main — the 2.0 rewrite uses a different dispatch path.

server.tool(name, desc, schema, {}, cb) was misclassifying the empty
annotations object as a second schema. isZodRawShapeCompat({}) returns
true to support no-arg-tool schemas, so the annotations branch was
skipped and the callback position fell through to {}. Calls then threw
"typedHandler is not a function" at dispatch.

The annotations-position parser now accepts {} after a schema has
already been consumed, since an empty raw shape can't appear in the
annotations slot ambiguously once the schema slot is filled.
@AP3X-Dev AP3X-Dev requested a review from a team as a code owner April 29, 2026 20:04
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 29, 2026

🦋 Changeset detected

Latest commit: 88f46af

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 29, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@1980

commit: 88f46af

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