From afd6ee5c8fc58f761086d87d5a3f1c5364f9f16f Mon Sep 17 00:00:00 2001 From: Sharvil Saxena Date: Mon, 21 Sep 2026 13:39:34 +0000 Subject: [PATCH 1/4] build: run typecheck on the three test workspace packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test/helpers`, `test/conformance` and `test/integration` each define `check` as `npm run typecheck && npm run lint`, but none of them defines a `typecheck` script. `pnpm -r typecheck` skips a package that lacks the script rather than failing, so `pnpm check:all` — the CI gate in `.github/workflows/main.yml` — silently typechecked 52 of 53 workspace projects and these three were never covered. Running `npm run check` in any of them fails outright with `Missing script: "typecheck"`, so the per-package entry point was broken too. Add the script, matching the `tsgo -p tsconfig.json --noEmit` form every `packages/*` project already uses, and the `@typescript/native-preview` devDependency that provides `tsgo`. Each package already has a `tsconfig.json`, so nothing else was needed to make them checkable. Enabling the check surfaces 35 pre-existing type errors that the gap had been hiding; they are fixed in the following commit so this one stays reviewable. Signed-off-by: Sharvil Saxena --- pnpm-lock.yaml | 9 +++++++++ test/conformance/package.json | 2 ++ test/helpers/package.json | 4 +++- test/integration/package.json | 2 ++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c663ad7086..cb32c9289c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1972,6 +1972,9 @@ importers: '@modelcontextprotocol/vitest-config': specifier: workspace:^ version: link:../../common/vitest-config + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20260327.2 cors: specifier: catalog:runtimeServerOnly version: 2.8.6 @@ -2083,6 +2086,9 @@ importers: '@modelcontextprotocol/vitest-config': specifier: workspace:^ version: link:../../common/vitest-config + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20260327.2 vitest: specifier: catalog:devTools version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -2122,6 +2128,9 @@ importers: '@modelcontextprotocol/vitest-config': specifier: workspace:^ version: link:../../common/vitest-config + '@typescript/native-preview': + specifier: catalog:devTools + version: 7.0.0-dev.20260327.2 '@valibot/to-json-schema': specifier: catalog:devTools version: 1.6.0(valibot@1.3.1(typescript@5.9.3)) diff --git a/test/conformance/package.json b/test/conformance/package.json index f51e84d9ea..293180e6e3 100644 --- a/test/conformance/package.json +++ b/test/conformance/package.json @@ -22,6 +22,7 @@ "mcp" ], "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", "check": "npm run typecheck && npm run lint", @@ -51,6 +52,7 @@ "cors": "catalog:runtimeServerOnly", "express": "catalog:runtimeServerOnly", "tsx": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", "zod": "catalog:runtimeShared" } } diff --git a/test/helpers/package.json b/test/helpers/package.json index c359fbe6c2..60e0c6cae6 100644 --- a/test/helpers/package.json +++ b/test/helpers/package.json @@ -22,6 +22,7 @@ "mcp" ], "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", "lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .", "lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .", "check": "npm run typecheck && npm run lint" @@ -32,6 +33,7 @@ "vitest": "catalog:devTools", "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", - "@modelcontextprotocol/eslint-config": "workspace:^" + "@modelcontextprotocol/eslint-config": "workspace:^", + "@typescript/native-preview": "catalog:devTools" } } diff --git a/test/integration/package.json b/test/integration/package.json index ed75fb1b0c..df35ac13a5 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -22,6 +22,7 @@ "mcp" ], "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit", "lint": "eslint test/ && prettier --ignore-path ../../.prettierignore --check .", "lint:fix": "eslint test/ --fix && prettier --ignore-path ../../.prettierignore --write .", "check": "npm run typecheck && npm run lint", @@ -42,6 +43,7 @@ "@modelcontextprotocol/tsconfig": "workspace:^", "@modelcontextprotocol/vitest-config": "workspace:^", "@valibot/to-json-schema": "catalog:devTools", + "@typescript/native-preview": "catalog:devTools", "arktype": "catalog:devTools", "supertest": "catalog:devTools", "valibot": "catalog:devTools", From 8ee9aa7cadd7eeedd030fadf65bb1084a59cdc98 Mon Sep 17 00:00:00 2001 From: Sharvil Saxena Date: Mon, 21 Sep 2026 13:39:44 +0000 Subject: [PATCH 2/4] test: fix the type errors hidden by the missing typecheck scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enabling `typecheck` on `test/conformance` and `test/integration` surfaces 35 type errors that CI had never seen. Each is a fault in the test code, not in the SDK, and none changes what a test asserts: - `noUncheckedIndexedAccess` violations: `tools[0].x` and `registered[2].x` now use `?.`/`!`, matching how `packages/*/test` already writes these. - `everythingServer.ts` casts the `x-mcp-header` property to `Record`, as `mcpParamValidation.test.ts` and `scopeChallengeModern.test.ts` do — the annotation is not in the `JSONSchema` property type. - `dualEraStdio.test.ts` wrapped `onmessage`/`send` with a second parameter. `StdioClientTransport` declares neither, so the extra arguments were inert; the wrappers now match the real signatures. - `mcp.test.ts` called `registerResource`/`registerPrompt` without the required `config` argument, and built a flat `ServerContext` that no longer matches the type. - elicitation content values are `string | number | boolean | string[]`, so `server.test.ts` omits an absent `username` instead of passing `undefined`, and `elicitation.test.ts` annotates the handler return as `ElicitResult` so each branch widens to the shared type. - `client.test.ts`'s `roots/list` handler returns `{ roots: [] }`; the assertion is that registration throws, so the value is never produced. `pnpm -r typecheck` and `pnpm check:all` now pass across the whole workspace, and `test/integration` stays at 371/371. Signed-off-by: Sharvil Saxena --- test/conformance/src/everythingServer.ts | 5 +++- test/integration/test/client/client.test.ts | 10 ++++--- test/integration/test/server.test.ts | 10 +++++-- .../test/server/cloudflareWorkers.test.ts | 2 +- .../test/server/declaredCapabilities.test.ts | 4 +-- .../test/server/dualEraStdio.test.ts | 8 +++--- .../test/server/elicitation.test.ts | 14 +++++----- test/integration/test/server/mcp.test.ts | 27 ++++++++++--------- test/integration/test/standardSchema.test.ts | 18 ++++++------- 9 files changed, 57 insertions(+), 41 deletions(-) diff --git a/test/conformance/src/everythingServer.ts b/test/conformance/src/everythingServer.ts index 25926ba2a7..6ede22b2bd 100644 --- a/test/conformance/src/everythingServer.ts +++ b/test/conformance/src/everythingServer.ts @@ -200,7 +200,10 @@ function createMcpServer() { inputSchema: fromJsonSchema<{ region?: string; level?: number }>({ type: 'object', properties: { - region: { type: 'string', description: 'mirrored into Mcp-Param-Region', 'x-mcp-header': 'Region' }, + region: { type: 'string', description: 'mirrored into Mcp-Param-Region', 'x-mcp-header': 'Region' } as Record< + string, + unknown + >, level: { type: 'integer', description: 'non-mirrored argument' } } }) diff --git a/test/integration/test/client/client.test.ts b/test/integration/test/client/client.test.ts index 92140b531f..c3e58b8573 100644 --- a/test/integration/test/client/client.test.ts +++ b/test/integration/test/client/client.test.ts @@ -73,7 +73,9 @@ test('should initialize with matching protocol version', async () => { * Test: Initialize with Supported Older Protocol Version */ test('should initialize with supported older protocol version', async () => { - const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; + // Index 1 is always present: SUPPORTED_PROTOCOL_VERSIONS is a fixed 5-element + // list, but `noUncheckedIndexedAccess` cannot see that through the index. + const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]!; const clientTransport: Transport = { start: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), @@ -277,7 +279,9 @@ test('should reject unsupported protocol version', async () => { * Test: Connect New Client to Old Supported Server Version */ test('should connect new client to old, supported server version', async () => { - const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; + // Index 1 is always present: SUPPORTED_PROTOCOL_VERSIONS is a fixed 5-element + // list, but `noUncheckedIndexedAccess` cannot see that through the index. + const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]!; const server = new Server( { name: 'test server', @@ -725,7 +729,7 @@ test('should only allow setRequestHandler for declared capabilities', () => { // This should throw because roots listing is not a declared capability expect(() => { - client.setRequestHandler('roots/list', () => ({})); + client.setRequestHandler('roots/list', () => ({ roots: [] })); }).toThrow('Client does not support roots capability'); }); diff --git a/test/integration/test/server.test.ts b/test/integration/test/server.test.ts index 51cde29161..61c8c9b3f8 100644 --- a/test/integration/test/server.test.ts +++ b/test/integration/test/server.test.ts @@ -352,8 +352,11 @@ test('should respect client elicitation capabilities', async () => { client.setRequestHandler('elicitation/create', params => ({ action: 'accept', + // Omit `username` rather than sending an explicit `undefined`: elicitation + // content values are string | number | boolean | string[], so an absent key + // is the only way to express "not provided". content: { - username: params.params.message.includes('username') ? 'test-user' : undefined, + ...(params.params.message.includes('username') ? { username: 'test-user' } : {}), confirmed: true } })); @@ -436,8 +439,11 @@ test('should use elicitInput with mode: "form" by default for backwards compatib client.setRequestHandler('elicitation/create', params => ({ action: 'accept', + // Omit `username` rather than sending an explicit `undefined`: elicitation + // content values are string | number | boolean | string[], so an absent key + // is the only way to express "not provided". content: { - username: params.params.message.includes('username') ? 'test-user' : undefined, + ...(params.params.message.includes('username') ? { username: 'test-user' } : {}), confirmed: true } })); diff --git a/test/integration/test/server/cloudflareWorkers.test.ts b/test/integration/test/server/cloudflareWorkers.test.ts index 02add64201..45d0117510 100644 --- a/test/integration/test/server/cloudflareWorkers.test.ts +++ b/test/integration/test/server/cloudflareWorkers.test.ts @@ -36,7 +36,7 @@ const SERVER_VERSION_NONCE = randomUUID(); */ const WRANGLER_BIN = (() => { const pkgPath = createRequire(import.meta.url).resolve('wrangler/package.json'); - const bin = (JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { bin: Record }).bin.wrangler; + const bin = (JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { bin: Record }).bin.wrangler!; return path.resolve(path.dirname(pkgPath), bin); })(); diff --git a/test/integration/test/server/declaredCapabilities.test.ts b/test/integration/test/server/declaredCapabilities.test.ts index 60e214d129..dcab27f4a6 100644 --- a/test/integration/test/server/declaredCapabilities.test.ts +++ b/test/integration/test/server/declaredCapabilities.test.ts @@ -140,12 +140,12 @@ describe('deterministic tools/list ordering (draft spec)', () => { const client = await connect(mcpServer); // Disable a tool in the middle: relative order of the remaining tools is unchanged. - registered[2].disable(); + registered[2]!.disable(); const whileDisabled = await client.listTools(); expect(whileDisabled.tools.map(t => t.name)).toEqual(['zeta', 'alpha', 'omega', 'beta']); // Re-enable it: the original insertion order is restored, not appended at the end. - registered[2].enable(); + registered[2]!.enable(); const afterReenable = await client.listTools(); expect(afterReenable.tools.map(t => t.name)).toEqual(names); }); diff --git a/test/integration/test/server/dualEraStdio.test.ts b/test/integration/test/server/dualEraStdio.test.ts index 9cef3b8f7b..04dd468d3e 100644 --- a/test/integration/test/server/dualEraStdio.test.ts +++ b/test/integration/test/server/dualEraStdio.test.ts @@ -53,9 +53,9 @@ function spawnFixtureTransport(): StdioClientTransport { function recordInbound(transport: StdioClientTransport): JSONRPCMessage[] { const inbound: JSONRPCMessage[] = []; const original = transport.onmessage; - transport.onmessage = (message, extra) => { + transport.onmessage = message => { inbound.push(message); - original?.(message, extra); + original?.(message); }; return inbound; } @@ -64,9 +64,9 @@ function recordInbound(transport: StdioClientTransport): JSONRPCMessage[] { function recordOutbound(transport: StdioClientTransport): JSONRPCMessage[] { const outbound: JSONRPCMessage[] = []; const originalSend = transport.send.bind(transport); - transport.send = async (message, options) => { + transport.send = async message => { outbound.push(message); - return originalSend(message, options); + return originalSend(message); }; return outbound; } diff --git a/test/integration/test/server/elicitation.test.ts b/test/integration/test/server/elicitation.test.ts index 13fb77e944..11d9f1d0a5 100644 --- a/test/integration/test/server/elicitation.test.ts +++ b/test/integration/test/server/elicitation.test.ts @@ -8,7 +8,7 @@ */ import { Client } from '@modelcontextprotocol/client'; -import type { ElicitRequestFormParams } from '@modelcontextprotocol/core-internal'; +import type { ElicitRequestFormParams, ElicitResult } from '@modelcontextprotocol/core-internal'; import { InMemoryTransport } from '@modelcontextprotocol/core-internal'; import { AjvJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/ajv'; import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core-internal/validators/cfWorker'; @@ -338,7 +338,9 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo test(`${validatorName}: should handle multiple sequential elicitation requests`, async () => { let requestCount = 0; - client.setRequestHandler('elicitation/create', request => { + // Annotated so each branch widens to the shared result type instead of + // inferring a union whose members carry `age?: undefined` etc. + client.setRequestHandler('elicitation/create', (request): ElicitResult => { requestCount++; if (request.params.message.includes('name')) { return { action: 'accept', content: { name: 'Alice' } }; @@ -996,12 +998,12 @@ describe('declared-dialect requestedSchema (default validator)', () => { const requestedSchema = { $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - properties: { name: { type: 'string', minLength: 1 } }, + type: 'object' as const, + properties: { name: { type: 'string' as const, minLength: 1 } }, required: ['name'] - } as const; + }; - let content: Record = { name: 'John' }; + let content: { [key: string]: string | number | boolean | string[] } = { name: 'John' }; client.setRequestHandler('elicitation/create', () => ({ action: 'accept', content })); await expect(server.elicitInput({ mode: 'form', message: 'name?', requestedSchema })).resolves.toMatchObject({ diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 4b9a3865f0..a382780f1c 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -1,5 +1,5 @@ import { Client } from '@modelcontextprotocol/client'; -import type { Notification, TextContent } from '@modelcontextprotocol/core-internal'; +import type { Notification, ServerContext, TextContent } from '@modelcontextprotocol/core-internal'; import { getDisplayName, InMemoryTransport, @@ -411,16 +411,15 @@ describe('Zod v4', () => { expect(template.listCallback).toBe(list); const abortController = new AbortController(); + // The callback only reads what the fixture provides, so a partial + // context is cast rather than fully constructed. const result = await template.listCallback?.({ - signal: abortController.signal, - requestId: 'not-implemented', - sendRequest: () => { - throw new Error('Not implemented'); - }, - sendNotification: () => { - throw new Error('Not implemented'); + mcpReq: { + id: 'not-implemented', + method: 'resources/list', + signal: abortController.signal } - }); + } as unknown as ServerContext); expect(result?.resources).toHaveLength(1); expect(list).toHaveBeenCalled(); }); @@ -598,11 +597,13 @@ describe('Zod v4', () => { name: z.string(), value: z.number() }), - callback: async ({ name, value }) => ({ + // `update()` is not generic over the new schema, so its `callback` + // receives `args: unknown`; narrow it at the boundary. + callback: async args => ({ content: [ { type: 'text', - text: `Updated: ${name}, ${value}` + text: `Updated: ${(args as { name: string }).name}, ${(args as { value: number }).value}` } ] }) @@ -850,7 +851,7 @@ describe('Zod v4', () => { version: '1.0' }); - mcpServer.registerResource('test://resource', 'Test Resource', async () => ({ + mcpServer.registerResource('test-resource', 'test://resource', {}, async () => ({ contents: [{ uri: 'test://resource', text: 'Test' }] })); @@ -871,7 +872,7 @@ describe('Zod v4', () => { version: '1.0' }); - mcpServer.registerPrompt('test-prompt', async () => ({ + mcpServer.registerPrompt('test-prompt', {}, async () => ({ messages: [{ role: 'assistant', content: { type: 'text', text: 'Test' } }] })); diff --git a/test/integration/test/standardSchema.test.ts b/test/integration/test/standardSchema.test.ts index b9ab0284f3..cc82977a9e 100644 --- a/test/integration/test/standardSchema.test.ts +++ b/test/integration/test/standardSchema.test.ts @@ -57,8 +57,8 @@ describe('Standard Schema Support', () => { const result = await client.request({ method: 'tools/list' }); expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe('greet'); - expect(result.tools[0].inputSchema).toMatchObject({ + expect(result.tools[0]?.name).toBe('greet'); + expect(result.tools[0]?.inputSchema).toMatchObject({ $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object', properties: { @@ -67,7 +67,7 @@ describe('Standard Schema Support', () => { } }); // Check required array contains both fields (order may vary by library) - expect(result.tools[0].inputSchema.required).toEqual(expect.arrayContaining(['name', 'age'])); + expect(result.tools[0]?.inputSchema.required).toEqual(expect.arrayContaining(['name', 'age'])); }); test('should register tool with ArkType input and output schemas', async () => { @@ -91,7 +91,7 @@ describe('Standard Schema Support', () => { const result = await client.request({ method: 'tools/list' }); - expect(result.tools[0].outputSchema).toMatchObject({ + expect(result.tools[0]?.outputSchema).toMatchObject({ $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object', properties: { @@ -99,7 +99,7 @@ describe('Standard Schema Support', () => { operation: { type: 'string' } } }); - expect(result.tools[0].outputSchema!.required).toEqual(expect.arrayContaining(['result', 'operation'])); + expect(result.tools[0]?.outputSchema!.required).toEqual(expect.arrayContaining(['result', 'operation'])); }); }); @@ -212,8 +212,8 @@ describe('Standard Schema Support', () => { const result = await client.request({ method: 'tools/list' }); expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe('greet'); - expect(result.tools[0].inputSchema).toMatchObject({ + expect(result.tools[0]?.name).toBe('greet'); + expect(result.tools[0]?.inputSchema).toMatchObject({ type: 'object', properties: { name: { type: 'string' }, @@ -239,7 +239,7 @@ describe('Standard Schema Support', () => { const result = await client.request({ method: 'tools/list' }); - expect(result.tools[0].inputSchema.properties).toMatchObject({ + expect(result.tools[0]?.inputSchema.properties).toMatchObject({ city: { type: 'string', description: 'The city name' }, country: { type: 'string', description: 'The country code' } }); @@ -396,7 +396,7 @@ describe('Standard Schema Support', () => { await connectClientAndServer(); const listed = await client.request({ method: 'tools/list' }); - expect(listed.tools[0].inputSchema).toMatchObject({ + expect(listed.tools[0]?.inputSchema).toMatchObject({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] From 0efee44638a569b5e80503027e29ce020fedcf67 Mon Sep 17 00:00:00 2001 From: Sharvil Saxena Date: Mon, 21 Sep 2026 13:39:51 +0000 Subject: [PATCH 3/4] fix(server): type the context-only registerPrompt callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `registerPrompt` is given a config with no `argsSchema`, `createPromptHandler` takes its `else` branch and invokes the callback as `callback(ctx)` — the server context is the only argument. Neither public overload describes that: both constrain `Args` to a schema type, so the argument-less form fell through to the deprecated raw-shape signature and typed the first parameter as the arguments record. A callback that reads `ctx.mcpReq` therefore failed to compile against behaviour that works at runtime, and there was no annotation that fixed it, since an explicit `PromptCallback` also failed to match either overload. Add an `argsSchema?: undefined` overload typing the callback as `PromptCallback`, and widen the implementation signature to accept it. Callbacks that declare no parameters already compiled and are unaffected, as is every schema-bearing form. This was invisible because the only caller in the repo lives in `test/integration`, which was not typechecked until the preceding commits. Signed-off-by: Sharvil Saxena --- .../register-prompt-context-only-overload.md | 5 +++ packages/server/src/server/mcp.ts | 15 +++++++- .../test/server/registerPromptNoArgs.test.ts | 38 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .changeset/register-prompt-context-only-overload.md create mode 100644 packages/server/test/server/registerPromptNoArgs.test.ts diff --git a/.changeset/register-prompt-context-only-overload.md b/.changeset/register-prompt-context-only-overload.md new file mode 100644 index 0000000000..40d0eb38ab --- /dev/null +++ b/.changeset/register-prompt-context-only-overload.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +`McpServer.registerPrompt()` now type-checks the no-`argsSchema` form. When `config` carries no `argsSchema`, the prompt callback is invoked with the server context as its only argument, but both existing overloads constrained `Args` to a schema type, so the argument-less form resolved to the deprecated raw-shape signature and typed the parameter as the arguments record: reading `ctx.mcpReq` was a type error even though it works at runtime. A dedicated `argsSchema?: undefined` overload now types that callback as `PromptCallback`. Callbacks that take no parameters, and every schema-bearing form, are unchanged. diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 70a5539bfb..c6ac2d391b 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1116,6 +1116,19 @@ export class McpServer { * ); * ``` */ + registerPrompt( + name: string, + config: { + title?: string; + description?: string; + argsSchema?: undefined; + icons?: Icon[]; + /** Determines whether this prompt retrieval needs an OAuth scope challenge. */ + scopeChallenge?: ScopeChallengeHandler; + _meta?: Record; + }, + cb: PromptCallback + ): RegisteredPrompt; registerPrompt( name: string, config: { @@ -1152,7 +1165,7 @@ export class McpServer { scopeChallenge?: ScopeChallengeHandler; _meta?: Record; }, - cb: PromptCallback | LegacyPromptCallback + cb: PromptCallback | PromptCallback | LegacyPromptCallback ): RegisteredPrompt { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); diff --git a/packages/server/test/server/registerPromptNoArgs.test.ts b/packages/server/test/server/registerPromptNoArgs.test.ts new file mode 100644 index 0000000000..1efcdb4a1d --- /dev/null +++ b/packages/server/test/server/registerPromptNoArgs.test.ts @@ -0,0 +1,38 @@ +/** + * Type-surface pin for the no-`argsSchema` prompt registration form. + * + * With no `argsSchema`, `createPromptHandler` invokes the callback as + * `callback(ctx)` — the context is the ONLY argument (see `mcp.ts`, + * the `else` branch of `createPromptHandler`). Both generic overloads + * constrain `Args` to a schema type, so before the dedicated overload + * existed the argument-less form resolved to the deprecated raw-shape + * signature and typed `ctx` as the arguments record: reading + * `ctx.mcpReq` was a type error even though it works at runtime. + */ +import type { ServerContext } from '@modelcontextprotocol/core-internal'; +import { describe, expect, expectTypeOf, test } from 'vitest'; + +import { McpServer } from '../../src/server/mcp'; + +describe('registerPrompt without argsSchema', () => { + test('types the callback parameter as the server context', () => { + const server = new McpServer({ name: 'test server', version: '1.0' }); + + server.registerPrompt('ctx-only', {}, async ctx => { + expectTypeOf(ctx).toEqualTypeOf(); + return { messages: [{ role: 'assistant' as const, content: { type: 'text' as const, text: String(ctx.mcpReq.id) } }] }; + }); + + expect(server.server).toBeDefined(); + }); + + test('still accepts a callback that ignores the context', () => { + const server = new McpServer({ name: 'test server', version: '1.0' }); + + server.registerPrompt('no-args', { description: 'takes nothing' }, async () => ({ + messages: [{ role: 'assistant' as const, content: { type: 'text' as const, text: 'ok' } }] + })); + + expect(server.server).toBeDefined(); + }); +}); From 5a74dc90422abeb32e3bc87e08ed5bd8dfb49baf Mon Sep 17 00:00:00 2001 From: Sharvil Saxena Date: Mon, 21 Sep 2026 20:14:08 +0000 Subject: [PATCH 4/4] build: map the _shims subpath in test/conformance's tsconfig Enabling `typecheck` on `test/conformance` passes against a tree that has already been built, but fails from a clean checkout: ../../packages/client/src/client/auth.ts(1,34): error TS2307: Cannot find module '@modelcontextprotocol/client/_shims' or its corresponding type declarations. (and the same for four more files across `client` and `server`). `test/conformance/tsconfig.json` redirects `@modelcontextprotocol/client` and `@modelcontextprotocol/server` to their `src/index.ts` so the conformance suite typechecks against sources rather than build output. Those sources import the `/_shims` subpath, which is only resolvable through the package's `exports` map, and every target there is under `dist/`. With no `paths` entry for the subpath, resolution falls through to `exports` and needs a build to exist. CI runs `pnpm run check:all` before `pnpm run build:all`, so `dist/` is absent and the typecheck fails. It passed locally only because a previous build had left `dist/` behind. Map the subpath to `src/shimsNode.ts`, which is what every other project that redirects these packages to source already does: `test/e2e`, `test/integration`, `examples/*` and all four `packages/middleware/*` tsconfigs. `test/conformance` was the only one missing it. Verified by removing every `packages/*/dist` directory to reproduce a clean checkout: the five errors above appear before this change and none after. Signed-off-by: Sharvil Saxena --- test/conformance/tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/conformance/tsconfig.json b/test/conformance/tsconfig.json index b424eb35ec..a7617960f8 100644 --- a/test/conformance/tsconfig.json +++ b/test/conformance/tsconfig.json @@ -13,7 +13,9 @@ "./node_modules/@modelcontextprotocol/core-internal/src/exports/public/index.ts" ], "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], + "@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], + "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"],