Skip to content

Modify registerPrompt to accept undefined Args type#1516

Open
andershagbard wants to merge 3 commits intomodelcontextprotocol:mainfrom
andershagbard:patch-1
Open

Modify registerPrompt to accept undefined Args type#1516
andershagbard wants to merge 3 commits intomodelcontextprotocol:mainfrom
andershagbard:patch-1

Conversation

@andershagbard
Copy link
Copy Markdown

@andershagbard andershagbard commented Feb 10, 2026

Motivation and Context

When using registerPrompt without any argsSchema the callback will still be typed as the first argument is the input of the argsSchema, which is not the case.

This PR corrects that.

Code:

server.registerPrompt(
  'my_prompt',
  { title: 'My Prompt' },
  ({ authInfo }) => {

Current type:

McpServer.registerPrompt<ZodRawShapeCompat>(name: string, config: {
  ...
}, cb: (args: ShapeOutput<ZodRawShapeCompat>, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => ...

Correct type:

McpServer.registerPrompt<undefined>(name: string, config: {
 ...
}, cb: (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => ...

How Has This Been Tested?

Tested in a project using mcp-handler

Breaking Changes

Maybe someone has used the types incorrectly. Unsure if this counts as breaking, as it's a bugfix.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 10, 2026

🦋 Changeset detected

Latest commit: b32c18d

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

This PR includes changesets to release 4 packages
Name Type
@modelcontextprotocol/server Patch
@modelcontextprotocol/express Patch
@modelcontextprotocol/hono Patch
@modelcontextprotocol/node Patch

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 Feb 10, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@1516

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@1516

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@1516

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@1516

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@1516

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@1516

commit: b32c18d

@andershagbard andershagbard marked this pull request as ready for review February 10, 2026 16:39
@andershagbard andershagbard requested a review from a team as a code owner February 10, 2026 16:39
The Args generic now extends StandardSchemaWithJSON | undefined with a
default of undefined, matching registerTool. This lets PromptCallback
resolve to the (ctx) => result overload when argsSchema is omitted.

Co-authored-by: Anders Søgaard <andershagbard@gmail.com>
Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger left a comment

Choose a reason for hiding this comment

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

[deleted]

@km-anthropic
Copy link
Copy Markdown

@claude review

1 similar comment
@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

Comment on lines +28 to +34
function registerPrompt_withArgs() {
server.registerPrompt(
'with-args',
{ argsSchema: z.object({ code: z.string() }) },
(args: { code: string }, ctx: ServerContext) => result
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The registerPrompt_withArgs test only covers the positive case (callback correctly accepts (args, ctx)) but is missing a @ts-expect-error negative test verifying TypeScript rejects a no-args callback when argsSchema is provided. The registerPrompt_noArgs function correctly has both a positive and a negative test, but registerPrompt_withArgs only has the positive -- a regression in the Args extends StandardSchemaWithJSON branch of PromptCallback could go undetected.

Extended reasoning...

The new test file packages/server/test/server/mcp.types.test.ts was specifically added to validate the conditional type resolution of PromptCallback under both overloads of registerPrompt. The registerPrompt_noArgs function (lines 20-24) demonstrates the correct pattern: one positive test followed by one @ts-expect-error negative test. However, registerPrompt_withArgs (lines 27-34) only contains the positive case.

The missing negative test would verify that TypeScript rejects a no-args callback when argsSchema is present:
// @ts-expect-error -- callback must accept args when argsSchema is present
server.registerPrompt('with-args', { argsSchema: z.object({ code: z.string() }) }, (ctx: ServerContext) => result);

Without this test, a specific class of regression in PromptCallback could go undetected. The production code is correct today, and TypeScript structural typing means ServerContext (which has a required mcpReq property) is not directly assignable to { code: string }, providing incidental coverage. However, this structural incompatibility is a side-effect, not an intentional contract check.

The verifier refutation argues this is a test enhancement request rather than a blocking defect, which is fair. The primary fix (allowing undefined as the default Args type) is already validated by the existing tests, and the structural incompatibility between ServerContext and { code: string } means TypeScript would reject the mis-typed callback today even without an explicit negative test.

Nevertheless, the test file was designed to document and enforce the full type contract of both overloads symmetrically. The asymmetry between noArgs (2 tests: positive + negative) and withArgs (1 test: positive only) is a genuine gap. A deliberate regression that widens PromptCallback to accept either arity when Args extends StandardSchemaWithJSON would pass the existing positive test but would be caught by a negative test.

Step-by-step proof of the gap: (1) Suppose PromptCallback is changed to accept either arity as a union when Args extends StandardSchemaWithJSON. (2) The existing positive withArgs test still passes -- a callback with (args, ctx) is accepted. (3) There is no negative test to verify a no-arg-only callback (ctx: ServerContext) => result is rejected, so the regression goes undetected. (4) A @ts-expect-error negative test directly asserts this rejection, closing the gap.

@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

Comment on lines 1239 to 1245
enabled: boolean;
enable(): void;
disable(): void;
update<Args extends StandardSchemaWithJSON>(updates: {
update<Args extends StandardSchemaWithJSON | undefined = undefined>(updates: {
name?: string | null;
title?: string;
description?: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 The update() type change introduces a runtime mismatch: TypeScript now accepts a no-args callback on a prompt originally registered with an argsSchema, but at runtime the no-args callback silently receives the parsed schema data as its ctx parameter instead of the real ServerContext. To fix this properly, RegisteredPrompt must be made generic (carrying the original Args type) so that update() can enforce callback arity matches the stored schema.

Extended reasoning...

What the bug is

The PR changes update<Args extends StandardSchemaWithJSON | undefined = undefined>, giving Args a default of undefined. This means calling prompt.update({ callback: (ctx) => newResult }) on a prompt that was originally registered with an argsSchema is now accepted by TypeScript — it infers Args = undefined, making callback?: PromptCallback<undefined> which matches the no-args callback signature. Before the PR, update<Args extends StandardSchemaWithJSON> had no default and no | undefined, so TypeScript would reject that call, inadvertently blocking this misuse.

The specific code path that triggers it

Inside _createRegisteredPrompt's update closure (around line 727 of mcp.ts), there is a guard: if (updates.argsSchema !== undefined) { currentArgsSchema = updates.argsSchema; }. If the caller does not supply argsSchema in the update object, this guard does not fire — currentArgsSchema remains the original zod schema. Meanwhile, updates.callback IS supplied, so currentCallback is updated to the new no-args callback. Then createPromptHandler(name, currentArgsSchema, noArgsCallback) is called.

Why existing code does not prevent it

Inside createPromptHandler, the if (argsSchema) branch is entered because currentArgsSchema is still truthy. It casts callback to (args: unknown, ctx: ServerContext) => ... and calls typedCallback(parseResult.data, ctx). But typedCallback is actually the no-args (ctx) => newResult — it receives parseResult.data (the parsed args object, e.g. {code: "something"}) as its ctx parameter, and the real ServerContext is silently dropped as the second argument. No exception is thrown; the callback silently uses wrong data.

Impact

Any server that registers a with-args prompt and later calls .update({ callback: (ctx) => ... }) (intending to replace the callback while keeping the schema) will silently produce incorrect results. The callback's ctx will contain schema-parsed data instead of the MCP ServerContext, so operations like ctx.authInfo, ctx.sessionId, etc. will be undefined or behave unexpectedly — a silent correctness regression with no compile-time or runtime warning.

How to fix it

The root cause is that RegisteredPrompt is non-generic: it does not carry the original Args type, so update() cannot verify that the new callback arity matches the existing schema. The proper fix is to make RegisteredPrompt<Args> generic so the update method ties its callback type to the original Args. A simpler short-term mitigation: require that if callback is provided without a new argsSchema, the caller must explicitly pass argsSchema: undefined to signal intent to clear the schema, preventing the implicit mismatch.

Step-by-step proof

  1. Register: const prompt = server.registerPrompt('p', { argsSchema: z.object({ code: z.string() }) }, (args, ctx) => result)currentArgsSchema is set to the zod schema.
  2. Call: prompt.update({ callback: (ctx) => newResult }) — TypeScript infers Args = undefined (satisfies StandardSchemaWithJSON | undefined), accepts the call without error.
  3. In the update closure: updates.argsSchema is undefined — the guard if (updates.argsSchema !== undefined) does NOT fire — currentArgsSchema stays as the original zod schema.
  4. updates.callback is provided — currentCallback is updated to the no-args (ctx) => newResult.
  5. createPromptHandler(name, currentArgsSchema, noArgsCallback) is called with the original schema still set.
  6. In createPromptHandler: if (argsSchema) is true — enters the with-args branch — calls typedCallback(parseResult.data, ctx).
  7. The no-args callback (ctx) => newResult receives parseResult.data (e.g. {code: "something"}) as its ctx — the real ServerContext is silently discarded as the ignored second argument.

Comment on lines +37 to +42
describe('registerPrompt types', () => {
it('compiles', () => {
// The functions above are compile-time type assertions; this suite
// exists so vitest detects the file as containing tests.
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The new mcp.types.test.ts file covers only the registerPrompt half of the type fix, leaving RegisteredPrompt.update() without compile-time assertions. If the update fix were reverted, the test suite would not catch the regression.

Extended reasoning...

The PR makes two parallel type fixes: adding undefined = undefined to Args in both registerPrompt (line 918) and RegisteredPrompt.update (line 1242). Both fixes are present in the diff and production code is correct. However, mcp.types.test.ts only exercises registerPrompt via registerPrompt_noArgs and registerPrompt_withArgs, with zero compile-time assertions for the update() method.

The specific uncovered path is RegisteredPrompt.update typed as update<Args extends StandardSchemaWithJSON | undefined = undefined>. The test file verifies registerPrompt overloads but never calls .update() on a returned RegisteredPrompt handle.

If the update fix were reverted to update, no existing test would fail. The registerPrompt_noArgs and registerPrompt_withArgs functions call server.registerPrompt() directly and never exercise .update() on the returned handle. TypeScript would only surface the regression when a user called .update({ callback: (ctx) => result }) on a no-args prompt in real code.

This is a test coverage gap, not a production bug — the actual fix in mcp.ts is correct. The risk is a future refactor accidentally removing the undefined from update() without any test catching it, since the test file was specifically introduced to guard against this class of type regression.

To fix, add to mcp.types.test.ts: const prompt = server.registerPrompt('no-args', {}, (ctx: ServerContext) => result); prompt.update({ callback: (ctx: ServerContext) => result }); // should compile. And a @ts-expect-error line rejecting an args callback when no argsSchema.

Step-by-step proof: (1) Revert line 1242 of mcp.ts from update<Args extends StandardSchemaWithJSON | undefined = undefined> back to update. (2) Run the type check — all tests in mcp.types.test.ts still pass because none call .update(). (3) A user calling prompt.update({ callback: (ctx) => result }) in their own project would get a TS error, but the test suite stays green. (4) Adding prompt.update() assertions to the test file would immediately catch the regression in step 2.

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.

3 participants