SEP-1319: Decouple Request Payloads, Remove passthrough iteration, Typecheck fixes (#1086)

This commit is contained in:
Konstantin Konstantinov 2025-11-07 18:50:20 +02:00 • committed by GitHub
commit f59996ae66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 2840 additions and 951 deletions

View file

@ -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}$' },
newsletter: { type: 'boolean' },
notifications: { type: 'boolean' }
@ -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']

View file

@ -702,9 +702,7 @@ test('should handle server cancelling a request', async () => {
version: '1.0'
},
{
capabilities: {
sampling: {}
}
capabilities: {}
}
);
@ -763,9 +761,7 @@ test('should handle request timeout', async () => {
version: '1.0'
},
{
capabilities: {
sampling: {}
}
capabilities: {}
}
);

View file

@ -237,9 +237,9 @@ export class Server<
protected assertRequestHandlerCapability(method: string): void {
switch (method) {
case 'sampling/createMessage':
if (!this._capabilities.sampling) {
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;

View file

@ -1407,7 +1407,14 @@ 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:')
}
])
);
});
/***
@ -1781,7 +1788,14 @@ 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);
@ -1846,7 +1860,14 @@ 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);
@ -2195,7 +2216,14 @@ 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'
}
])
);
});
/***
@ -2556,7 +2584,14 @@ 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'
}
])
);
});
});
@ -2662,7 +2697,17 @@ 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);
@ -2754,7 +2799,17 @@ 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);
@ -3411,7 +3466,17 @@ 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:`)
}
}
])
);
});
/***
@ -3513,7 +3578,7 @@ describe('prompt()', () => {
})
}),
{
name: 'Template Name',
title: 'Template Name',
description: 'Template description',
mimeType: 'application/json'
},

View file

@ -8,7 +8,6 @@ import {
CallToolResult,
McpError,
ErrorCode,
CompleteRequest,
CompleteResult,
PromptReference,
ResourceTemplateReference,
@ -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';
@ -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);
case 'ref/resource':
assertCompleteRequestResourceTemplate(request);
return this.handleResourceCompletion(request, request.params.ref);
default:
@ -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`);
@ -254,7 +259,10 @@ 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) {

View file

@ -189,7 +189,14 @@ 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 () => {