Skip to content
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"lint:fix": "eslint src/ --fix && prettier --write .",
"check": "npm run fetch:spec-types && npm run typecheck && npm run lint",
"test": "npm run fetch:spec-types && jest",
"test:no-spec": "jest --testPathIgnorePatterns=/dist/ --testPathIgnorePatterns=spec\\.types\\.test\\.ts$",
"start": "npm run server",
"server": "tsx watch --clear-screen=false scripts/cli.ts server",
"client": "tsx scripts/cli.ts client"
Expand Down
9 changes: 3 additions & 6 deletions src/client/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,7 @@ test('should connect new client to old, supported server version', async () => {
const client = new Client(
{
name: 'new client',
version: '1.0',
protocolVersion: LATEST_PROTOCOL_VERSION
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Property doesn't exist.

version: '1.0'
},
{
capabilities: {
Expand Down Expand Up @@ -279,8 +278,7 @@ test('should negotiate version when client is old, and newer server supports its
const client = new Client(
{
name: 'old client',
version: '1.0',
protocolVersion: OLD_VERSION
version: '1.0'
},
{
capabilities: {
Expand Down Expand Up @@ -342,8 +340,7 @@ test("should throw when client is old, and server doesn't support its version",
const client = new Client(
{
name: 'old client',
version: '1.0',
protocolVersion: OLD_VERSION
version: '1.0'
},
{
capabilities: {
Expand Down
2 changes: 1 addition & 1 deletion src/examples/client/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ async function getPrompt(name: string, args: Record<string, unknown>): Promise<v
const promptResult = await client.request(promptRequest, GetPromptResultSchema);
console.log('Prompt template:');
promptResult.messages.forEach((msg, index) => {
console.log(` [${index + 1}] ${msg.role}: ${msg.content.text}`);
console.log(` [${index + 1}] ${msg.role}: ${msg.content.type === 'text' ? msg.content.text : JSON.stringify(msg.content)}`);
});
} catch (error) {
console.log(`Error getting prompt ${name}: ${error}`);
Expand Down
6 changes: 2 additions & 4 deletions src/examples/server/elicitationExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,7 @@ mcpServer.registerTool(
startTime: {
type: 'string',
title: 'Start Time',
description: 'Event start time (HH:MM)',
pattern: '^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pattern could be left here, but needs ts-ignore, as not supported by the spec. We could allow it on an optional basis and still match the spec if we wanted.

description: 'Event start time (HH:MM)'
},
duration: {
type: 'integer',
Expand Down Expand Up @@ -268,8 +267,7 @@ mcpServer.registerTool(
zipCode: {
type: 'string',
title: 'ZIP/Postal Code',
description: '5-digit ZIP code',
pattern: '^[0-9]{5}$'
description: '5-digit ZIP code'
},
phone: {
type: 'string',
Expand Down
4 changes: 2 additions & 2 deletions src/integration-tests/taskResumability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe('Transport resumability', () => {

// Create first client
const client1 = new Client({
id: clientId,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No such property.

title: clientId,
name: 'test-client',
version: '1.0.0'
});
Expand Down Expand Up @@ -223,7 +223,7 @@ describe('Transport resumability', () => {

// Create second client with same client ID
const client2 = new Client({
id: clientId,
title: clientId,
name: 'test-client',
version: '1.0.0'
});
Expand Down
2 changes: 2 additions & 0 deletions src/server/elicitation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
age: { type: 'integer', minimum: 0, maximum: 150 },
street: { type: 'string' },
city: { type: 'string' },
// @ts-expect-error - pattern is not a valid property by MCP spec, however it is making use of the Ajv validator
zipCode: { type: 'string', pattern: '^[0-9]{5}$' },
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Patterns not supported by the spec. Passing them will work, but the TS error needs ignoring.

newsletter: { type: 'boolean' },
notifications: { type: 'boolean' }
Expand Down Expand Up @@ -355,6 +356,7 @@ function testElicitationFlow(validatorProvider: typeof ajvProvider | typeof cfWo
requestedSchema: {
type: 'object',
properties: {
// @ts-expect-error - pattern is not a valid property by MCP spec, however it is making use of the Ajv validator
zipCode: { type: 'string', pattern: '^[0-9]{5}$' }
},
required: ['zipCode']
Expand Down
8 changes: 2 additions & 6 deletions src/server/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,9 +702,7 @@ test('should handle server cancelling a request', async () => {
version: '1.0'
},
{
capabilities: {
sampling: {}
}
capabilities: {}
}
);

Expand Down Expand Up @@ -763,9 +761,7 @@ test('should handle request timeout', async () => {
version: '1.0'
},
{
capabilities: {
sampling: {}
}
capabilities: {}
}
);

Expand Down
6 changes: 3 additions & 3 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,9 @@ export class Server<

protected assertRequestHandlerCapability(method: string): void {
switch (method) {
case 'sampling/createMessage':
if (!this._capabilities.sampling) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Servers do not have sampling capabilities, thus removed. Swapped with completions capabilities.

throw new Error(`Server does not support sampling (required for ${method})`);
case 'completion/complete':
if (!this._capabilities.completions) {
throw new Error(`Server does not support completions (required for ${method})`);
}
break;

Expand Down
67 changes: 58 additions & 9 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,7 +1407,12 @@ describe('tool()', () => {

expect(receivedRequestId).toBeDefined();
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
expect(result.content?.[0].text).toContain('Received request ID:');
expect(result.content).toEqual(expect.arrayContaining([
{
type: 'text',
text: expect.stringContaining('Received request ID:')
}
]));
});

/***
Expand Down Expand Up @@ -1781,7 +1786,12 @@ describe('resource()', () => {
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toBe('Updated content');
expect(result.contents).toEqual(expect.arrayContaining([
{
text: expect.stringContaining('Updated content'),
uri: 'test://resource'
}
]));

// Update happened before transport was connected, so no notifications should be expected
expect(notifications).toHaveLength(0);
Expand Down Expand Up @@ -1846,7 +1856,12 @@ describe('resource()', () => {
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toBe('Updated content');
expect(result.contents).toEqual(expect.arrayContaining([
{
text: expect.stringContaining('Updated content'),
uri: 'test://resource/123'
}
]));

// Update happened before transport was connected, so no notifications should be expected
expect(notifications).toHaveLength(0);
Expand Down Expand Up @@ -2195,7 +2210,12 @@ describe('resource()', () => {
ReadResourceResultSchema
);

expect(result.contents[0].text).toBe('Category: books, ID: 123');
expect(result.contents).toEqual(expect.arrayContaining([
{
text: expect.stringContaining('Category: books, ID: 123'),
uri: 'test://resource/books/123'
}
]));
});

/***
Expand Down Expand Up @@ -2556,7 +2576,12 @@ describe('resource()', () => {

expect(receivedRequestId).toBeDefined();
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
expect(result.contents[0].text).toContain('Received request ID:');
expect(result.contents).toEqual(expect.arrayContaining([
{
text: expect.stringContaining(`Received request ID:`),
uri: 'test://resource'
}
]));
});
});

Expand Down Expand Up @@ -2662,7 +2687,15 @@ describe('prompt()', () => {
);

expect(result.messages).toHaveLength(1);
expect(result.messages[0].content.text).toBe('Updated response');
expect(result.messages).toEqual(expect.arrayContaining([
{
role: 'assistant',
content: {
type: 'text',
text: 'Updated response'
}
}
]));

// Update happened before transport was connected, so no notifications should be expected
expect(notifications).toHaveLength(0);
Expand Down Expand Up @@ -2754,7 +2787,15 @@ describe('prompt()', () => {
);

expect(getResult.messages).toHaveLength(1);
expect(getResult.messages[0].content.text).toBe('Updated: test, value');
expect(getResult.messages).toEqual(expect.arrayContaining([
{
role: 'assistant',
content: {
type: 'text',
text: 'Updated: test, value'
}
}
]));

// Update happened before transport was connected, so no notifications should be expected
expect(notifications).toHaveLength(0);
Expand Down Expand Up @@ -3411,7 +3452,15 @@ describe('prompt()', () => {

expect(receivedRequestId).toBeDefined();
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
expect(result.messages[0].content.text).toContain('Received request ID:');
expect(result.messages).toEqual(expect.arrayContaining([
{
role: 'assistant',
content: {
type: 'text',
text: expect.stringContaining(`Received request ID:`)
}
}
]));
});

/***
Expand Down Expand Up @@ -3513,7 +3562,7 @@ describe('prompt()', () => {
})
}),
{
name: 'Template Name',
title: 'Template Name',
description: 'Template description',
mimeType: 'application/json'
},
Expand Down
13 changes: 9 additions & 4 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
CallToolResult,
McpError,
ErrorCode,
CompleteRequest,
CompleteResult,
PromptReference,
ResourceTemplateReference,
Expand All @@ -31,7 +30,11 @@ import {
ServerRequest,
ServerNotification,
ToolAnnotations,
LoggingMessageNotification
LoggingMessageNotification,
CompleteRequestPrompt,
CompleteRequestResourceTemplate,
assertCompleteRequestPrompt,
assertCompleteRequestResourceTemplate
} from '../types.js';
import { Completable, CompletableDef } from './completable.js';
import { UriTemplate, Variables } from '../shared/uriTemplate.js';
Expand Down Expand Up @@ -217,9 +220,11 @@ export class McpServer {
this.server.setRequestHandler(CompleteRequestSchema, async (request): Promise<CompleteResult> => {
switch (request.params.ref.type) {
case 'ref/prompt':
assertCompleteRequestPrompt(request);
return this.handlePromptCompletion(request, request.params.ref);
Copy link
Contributor Author

@KKonstantinov KKonstantinov Nov 7, 2025

Choose a reason for hiding this comment

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

When passthrough removed, assertions are needed to proceed with a narrowed type to alleviate type errors downstream.


case 'ref/resource':
assertCompleteRequestResourceTemplate(request);
return this.handleResourceCompletion(request, request.params.ref);

default:
Expand All @@ -230,7 +235,7 @@ export class McpServer {
this._completionHandlerInitialized = true;
}

private async handlePromptCompletion(request: CompleteRequest, ref: PromptReference): Promise<CompleteResult> {
private async handlePromptCompletion(request: CompleteRequestPrompt, ref: PromptReference): Promise<CompleteResult> {
const prompt = this._registeredPrompts[ref.name];
if (!prompt) {
throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} not found`);
Expand All @@ -254,7 +259,7 @@ export class McpServer {
return createCompletionResult(suggestions);
}

private async handleResourceCompletion(request: CompleteRequest, ref: ResourceTemplateReference): Promise<CompleteResult> {
private async handleResourceCompletion(request: CompleteRequestResourceTemplate, ref: ResourceTemplateReference): Promise<CompleteResult> {
const template = Object.values(this._registeredResourceTemplates).find(t => t.resourceTemplate.uriTemplate.toString() === ref.uri);

if (!template) {
Expand Down
7 changes: 6 additions & 1 deletion src/server/title.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,12 @@ describe('Title field backwards compatibility', () => {
// Test reading the resource
const readResult = await client.readResource({ uri: 'users://123/profile' });
expect(readResult.contents).toHaveLength(1);
expect(readResult.contents[0].text).toBe('Profile data for user 123');
expect(readResult.contents).toEqual(expect.arrayContaining([
{
text: expect.stringContaining('Profile data for user 123'),
uri: 'users://123/profile'
}
]));
});

it('should support serverInfo with title', async () => {
Expand Down
9 changes: 3 additions & 6 deletions src/shared/metadataUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,15 @@ import { BaseMetadata } from '../types.js';
* For other objects: title → name
* This implements the spec requirement: "if no title is provided, name should be used for display purposes"
*/
export function getDisplayName(metadata: BaseMetadata): string {
export function getDisplayName(metadata: BaseMetadata & { annotations?: { title?: string } }): string {
// First check for title (not undefined and not empty string)
if (metadata.title !== undefined && metadata.title !== '') {
return metadata.title;
}

// Then check for annotations.title (only present in Tool objects)
if ('annotations' in metadata) {
const metadataWithAnnotations = metadata as BaseMetadata & { annotations?: { title?: string } };
if (metadataWithAnnotations.annotations?.title) {
return metadataWithAnnotations.annotations.title;
}
if (metadata.annotations?.title) {
return metadata.annotations.title;
}

// Finally fall back to name
Expand Down
14 changes: 8 additions & 6 deletions src/shared/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,11 +666,13 @@ describe('mergeCapabilities', () => {

const additional: ClientCapabilities = {
experimental: {
feature: true
feature: {
featureFlag: true
}
},
elicitation: {},
roots: {
newProp: true
listChanged: true
}
};

Expand All @@ -680,10 +682,11 @@ describe('mergeCapabilities', () => {
elicitation: {},
roots: {
listChanged: true,
newProp: true
},
experimental: {
feature: true
feature: {
featureFlag: true
}
}
});
});
Expand All @@ -701,7 +704,7 @@ describe('mergeCapabilities', () => {
subscribe: true
},
prompts: {
newProp: true
listChanged: true
}
};

Expand All @@ -710,7 +713,6 @@ describe('mergeCapabilities', () => {
logging: {},
prompts: {
listChanged: true,
newProp: true
},
resources: {
subscribe: true
Expand Down
Loading
Loading