feat: backwards-compatible createMessage overloads for SEP-1577 (#1212)

This commit is contained in:
Felix Weinberger 2025-12-02 13:18:22 +00:00 • committed by GitHub
commit cf51343cc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 193 additions and 13 deletions

View file

@ -1924,6 +1924,70 @@ describe('createMessage validation', () => {
});
});
describe('createMessage backwards compatibility', () => {
test('createMessage without tools returns single content (backwards compat)', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
// Mock client returns single text content
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'Hello from LLM' }
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
// Call createMessage WITHOUT tools
const result = await server.createMessage({
messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
maxTokens: 100
});
// Backwards compat: result.content should be single (not array)
expect(result.model).toBe('test-model');
expect(Array.isArray(result.content)).toBe(false);
expect(result.content.type).toBe('text');
if (result.content.type === 'text') {
expect(result.content.text).toBe('Hello from LLM');
}
});
test('createMessage with tools accepts request and returns result', async () => {
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
// Mock client returns text content (tool_use schema validation is tested in types.test.ts)
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
model: 'test-model',
role: 'assistant',
content: { type: 'text', text: 'I will use the weather tool' },
stopReason: 'endTurn'
}));
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
// Call createMessage WITH tools - verifies the overload works
const result = await server.createMessage({
messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
maxTokens: 100,
tools: [{ name: 'get_weather', inputSchema: { type: 'object' } }]
});
// Verify result is returned correctly
expect(result.model).toBe('test-model');
expect(result.content.type).toBe('text');
// With tools param, result.content can be array (CreateMessageResultWithTools type)
// This would fail type-check if we used CreateMessageResult which doesn't allow arrays
const contentArray = Array.isArray(result.content) ? result.content : [result.content];
expect(contentArray.length).toBe(1);
});
});
test('should respect log level for transport with sessionId', async () => {
const server = new Server(
{

View file

@ -2,7 +2,12 @@ import { mergeCapabilities, Protocol, type NotificationOptions, type ProtocolOpt
import {
type ClientCapabilities,
type CreateMessageRequest,
type CreateMessageResult,
CreateMessageResultSchema,
type CreateMessageResultWithTools,
CreateMessageResultWithToolsSchema,
type CreateMessageRequestParamsBase,
type CreateMessageRequestParamsWithTools,
type ElicitRequestFormParams,
type ElicitRequestURLParams,
type ElicitResult,
@ -467,7 +472,32 @@ export class Server<
return this.request({ method: 'ping' }, EmptyResultSchema);
}
async createMessage(params: CreateMessageRequest['params'], options?: RequestOptions) {
/**
* Request LLM sampling from the client (without tools).
* Returns single content block for backwards compatibility.
*/
async createMessage(params: CreateMessageRequestParamsBase, options?: RequestOptions): Promise<CreateMessageResult>;
/**
* Request LLM sampling from the client with tool support.
* Returns content that may be a single block or array (for parallel tool calls).
*/
async createMessage(params: CreateMessageRequestParamsWithTools, options?: RequestOptions): Promise<CreateMessageResultWithTools>;
/**
* Request LLM sampling from the client.
* When tools may or may not be present, returns the union type.
*/
async createMessage(
params: CreateMessageRequest['params'],
options?: RequestOptions
): Promise<CreateMessageResult | CreateMessageResultWithTools>;
// Implementation
async createMessage(
params: CreateMessageRequest['params'],
options?: RequestOptions
): Promise<CreateMessageResult | CreateMessageResultWithTools> {
// Capability check - only required when tools/toolChoice are provided
if (params.tools || params.toolChoice) {
if (!this._clientCapabilities?.sampling?.tools) {
@ -510,6 +540,10 @@ export class Server<
}
}
// Use different schemas based on whether tools are provided
if (params.tools) {
return this.request({ method: 'sampling/createMessage', params }, CreateMessageResultWithToolsSchema, options);
}
return this.request({ method: 'sampling/createMessage', params }, CreateMessageResultSchema, options);
}