Skip to content

Commit cf51343

Browse files
feat: backwards-compatible createMessage overloads for SEP-1577 (#1212)
1 parent 8204126 commit cf51343

File tree

6 files changed

+193
-13
lines changed

6 files changed

+193
-13
lines changed

src/examples/server/toolWithSampleServer.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ mcpServer.registerTool(
3333
maxTokens: 500
3434
});
3535

36-
const contents = Array.isArray(response.content) ? response.content : [response.content];
36+
// Since we're not passing tools param to createMessage, response.content is single content
3737
return {
38-
content: contents.map(content => ({
39-
type: 'text',
40-
text: content.type === 'text' ? content.text : 'Unable to generate summary'
41-
}))
38+
content: [
39+
{
40+
type: 'text',
41+
text: response.content.type === 'text' ? response.content.text : 'Unable to generate summary'
42+
}
43+
]
4244
};
4345
}
4446
);

src/server/index.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1924,6 +1924,70 @@ describe('createMessage validation', () => {
19241924
});
19251925
});
19261926

1927+
describe('createMessage backwards compatibility', () => {
1928+
test('createMessage without tools returns single content (backwards compat)', async () => {
1929+
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
1930+
1931+
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
1932+
1933+
// Mock client returns single text content
1934+
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
1935+
model: 'test-model',
1936+
role: 'assistant',
1937+
content: { type: 'text', text: 'Hello from LLM' }
1938+
}));
1939+
1940+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1941+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
1942+
1943+
// Call createMessage WITHOUT tools
1944+
const result = await server.createMessage({
1945+
messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
1946+
maxTokens: 100
1947+
});
1948+
1949+
// Backwards compat: result.content should be single (not array)
1950+
expect(result.model).toBe('test-model');
1951+
expect(Array.isArray(result.content)).toBe(false);
1952+
expect(result.content.type).toBe('text');
1953+
if (result.content.type === 'text') {
1954+
expect(result.content.text).toBe('Hello from LLM');
1955+
}
1956+
});
1957+
1958+
test('createMessage with tools accepts request and returns result', async () => {
1959+
const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
1960+
1961+
const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
1962+
1963+
// Mock client returns text content (tool_use schema validation is tested in types.test.ts)
1964+
client.setRequestHandler(CreateMessageRequestSchema, async () => ({
1965+
model: 'test-model',
1966+
role: 'assistant',
1967+
content: { type: 'text', text: 'I will use the weather tool' },
1968+
stopReason: 'endTurn'
1969+
}));
1970+
1971+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1972+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
1973+
1974+
// Call createMessage WITH tools - verifies the overload works
1975+
const result = await server.createMessage({
1976+
messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
1977+
maxTokens: 100,
1978+
tools: [{ name: 'get_weather', inputSchema: { type: 'object' } }]
1979+
});
1980+
1981+
// Verify result is returned correctly
1982+
expect(result.model).toBe('test-model');
1983+
expect(result.content.type).toBe('text');
1984+
// With tools param, result.content can be array (CreateMessageResultWithTools type)
1985+
// This would fail type-check if we used CreateMessageResult which doesn't allow arrays
1986+
const contentArray = Array.isArray(result.content) ? result.content : [result.content];
1987+
expect(contentArray.length).toBe(1);
1988+
});
1989+
});
1990+
19271991
test('should respect log level for transport with sessionId', async () => {
19281992
const server = new Server(
19291993
{

src/server/index.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { mergeCapabilities, Protocol, type NotificationOptions, type ProtocolOpt
22
import {
33
type ClientCapabilities,
44
type CreateMessageRequest,
5+
type CreateMessageResult,
56
CreateMessageResultSchema,
7+
type CreateMessageResultWithTools,
8+
CreateMessageResultWithToolsSchema,
9+
type CreateMessageRequestParamsBase,
10+
type CreateMessageRequestParamsWithTools,
611
type ElicitRequestFormParams,
712
type ElicitRequestURLParams,
813
type ElicitResult,
@@ -467,7 +472,32 @@ export class Server<
467472
return this.request({ method: 'ping' }, EmptyResultSchema);
468473
}
469474

470-
async createMessage(params: CreateMessageRequest['params'], options?: RequestOptions) {
475+
/**
476+
* Request LLM sampling from the client (without tools).
477+
* Returns single content block for backwards compatibility.
478+
*/
479+
async createMessage(params: CreateMessageRequestParamsBase, options?: RequestOptions): Promise<CreateMessageResult>;
480+
481+
/**
482+
* Request LLM sampling from the client with tool support.
483+
* Returns content that may be a single block or array (for parallel tool calls).
484+
*/
485+
async createMessage(params: CreateMessageRequestParamsWithTools, options?: RequestOptions): Promise<CreateMessageResultWithTools>;
486+
487+
/**
488+
* Request LLM sampling from the client.
489+
* When tools may or may not be present, returns the union type.
490+
*/
491+
async createMessage(
492+
params: CreateMessageRequest['params'],
493+
options?: RequestOptions
494+
): Promise<CreateMessageResult | CreateMessageResultWithTools>;
495+
496+
// Implementation
497+
async createMessage(
498+
params: CreateMessageRequest['params'],
499+
options?: RequestOptions
500+
): Promise<CreateMessageResult | CreateMessageResultWithTools> {
471501
// Capability check - only required when tools/toolChoice are provided
472502
if (params.tools || params.toolChoice) {
473503
if (!this._clientCapabilities?.sampling?.tools) {
@@ -510,6 +540,10 @@ export class Server<
510540
}
511541
}
512542

543+
// Use different schemas based on whether tools are provided
544+
if (params.tools) {
545+
return this.request({ method: 'sampling/createMessage', params }, CreateMessageResultWithToolsSchema, options);
546+
}
513547
return this.request({ method: 'sampling/createMessage', params }, CreateMessageResultSchema, options);
514548
}
515549

src/spec.types.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ type FixSpecInitializeRequest<T> = T extends { params: infer P } ? Omit<T, 'para
8080

8181
type FixSpecClientRequest<T> = T extends { params: infer P } ? Omit<T, 'params'> & { params: FixSpecInitializeRequestParams<P> } : T;
8282

83+
// Targeted fix: CreateMessageResult in SDK uses single content for v1.x backwards compat.
84+
// The full array-capable type is CreateMessageResultWithTools.
85+
// This will be aligned with schema in v2.0.
86+
// Narrows content from SamplingMessageContentBlock (includes tool types) to basic content types only.
87+
type NarrowToBasicContent<C> = C extends { type: 'text' | 'image' | 'audio' } ? C : never;
88+
type FixSpecCreateMessageResult<T> = T extends { content: infer C; role: infer R; model: infer M }
89+
? {
90+
_meta?: { [key: string]: unknown };
91+
model: M;
92+
role: R;
93+
stopReason?: string;
94+
content: C extends (infer U)[] ? NarrowToBasicContent<U> : NarrowToBasicContent<C>;
95+
}
96+
: T;
97+
8398
const sdkTypeChecks = {
8499
RequestParams: (sdk: RemovePassthrough<SDKTypes.RequestParams>, spec: SpecTypes.RequestParams) => {
85100
sdk = spec;
@@ -369,7 +384,10 @@ const sdkTypeChecks = {
369384
sdk = spec;
370385
spec = sdk;
371386
},
372-
CreateMessageResult: (sdk: RemovePassthrough<SDKTypes.CreateMessageResult>, spec: SpecTypes.CreateMessageResult) => {
387+
CreateMessageResult: (
388+
sdk: RemovePassthrough<SDKTypes.CreateMessageResult>,
389+
spec: FixSpecCreateMessageResult<SpecTypes.CreateMessageResult>
390+
) => {
373391
sdk = spec;
374392
spec = sdk;
375393
},

src/types.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
SamplingMessageSchema,
1414
CreateMessageRequestSchema,
1515
CreateMessageResultSchema,
16+
CreateMessageResultWithToolsSchema,
1617
ClientCapabilitiesSchema
1718
} from './types.js';
1819

@@ -787,7 +788,7 @@ describe('Types', () => {
787788
}
788789
});
789790

790-
test('should validate result with tool call', () => {
791+
test('should validate result with tool call (using WithTools schema)', () => {
791792
const result = {
792793
model: 'claude-3-5-sonnet-20241022',
793794
role: 'assistant',
@@ -800,7 +801,8 @@ describe('Types', () => {
800801
stopReason: 'toolUse'
801802
};
802803

803-
const parseResult = CreateMessageResultSchema.safeParse(result);
804+
// Tool call results use CreateMessageResultWithToolsSchema
805+
const parseResult = CreateMessageResultWithToolsSchema.safeParse(result);
804806
expect(parseResult.success).toBe(true);
805807
if (parseResult.success) {
806808
expect(parseResult.data.stopReason).toBe('toolUse');
@@ -810,9 +812,13 @@ describe('Types', () => {
810812
expect(content.type).toBe('tool_use');
811813
}
812814
}
815+
816+
// Basic CreateMessageResultSchema should NOT accept tool_use content
817+
const basicResult = CreateMessageResultSchema.safeParse(result);
818+
expect(basicResult.success).toBe(false);
813819
});
814820

815-
test('should validate result with array content', () => {
821+
test('should validate result with array content (using WithTools schema)', () => {
816822
const result = {
817823
model: 'claude-3-5-sonnet-20241022',
818824
role: 'assistant',
@@ -828,7 +834,8 @@ describe('Types', () => {
828834
stopReason: 'toolUse'
829835
};
830836

831-
const parseResult = CreateMessageResultSchema.safeParse(result);
837+
// Array content uses CreateMessageResultWithToolsSchema
838+
const parseResult = CreateMessageResultWithToolsSchema.safeParse(result);
832839
expect(parseResult.success).toBe(true);
833840
if (parseResult.success) {
834841
expect(parseResult.data.stopReason).toBe('toolUse');
@@ -840,6 +847,10 @@ describe('Types', () => {
840847
expect(content[1].type).toBe('tool_use');
841848
}
842849
}
850+
851+
// Basic CreateMessageResultSchema should NOT accept array content
852+
const basicResult = CreateMessageResultSchema.safeParse(result);
853+
expect(basicResult.success).toBe(false);
843854
});
844855

845856
test('should validate all new stop reasons', () => {

src/types.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,12 @@ export const ToolResultContentSchema = z
14951495
})
14961496
.passthrough();
14971497

1498+
/**
1499+
* Basic content types for sampling responses (without tool use).
1500+
* Used for backwards-compatible CreateMessageResult when tools are not used.
1501+
*/
1502+
export const SamplingContentSchema = z.discriminatedUnion('type', [TextContentSchema, ImageContentSchema, AudioContentSchema]);
1503+
14981504
/**
14991505
* Content block types allowed in sampling messages.
15001506
* This includes text, image, audio, tool use requests, and tool results.
@@ -1576,9 +1582,38 @@ export const CreateMessageRequestSchema = RequestSchema.extend({
15761582
});
15771583

15781584
/**
1579-
* The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it.
1585+
* The client's response to a sampling/create_message request from the server.
1586+
* This is the backwards-compatible version that returns single content (no arrays).
1587+
* Used when the request does not include tools.
15801588
*/
15811589
export const CreateMessageResultSchema = ResultSchema.extend({
1590+
/**
1591+
* The name of the model that generated the message.
1592+
*/
1593+
model: z.string(),
1594+
/**
1595+
* The reason why sampling stopped, if known.
1596+
*
1597+
* Standard values:
1598+
* - "endTurn": Natural end of the assistant's turn
1599+
* - "stopSequence": A stop sequence was encountered
1600+
* - "maxTokens": Maximum token limit was reached
1601+
*
1602+
* This field is an open string to allow for provider-specific stop reasons.
1603+
*/
1604+
stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens']).or(z.string())),
1605+
role: z.enum(['user', 'assistant']),
1606+
/**
1607+
* Response content. Single content block (text, image, or audio).
1608+
*/
1609+
content: SamplingContentSchema
1610+
});
1611+
1612+
/**
1613+
* The client's response to a sampling/create_message request when tools were provided.
1614+
* This version supports array content for tool use flows.
1615+
*/
1616+
export const CreateMessageResultWithToolsSchema = ResultSchema.extend({
15821617
/**
15831618
* The name of the model that generated the message.
15841619
*/
@@ -1597,7 +1632,7 @@ export const CreateMessageResultSchema = ResultSchema.extend({
15971632
stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens', 'toolUse']).or(z.string())),
15981633
role: z.enum(['user', 'assistant']),
15991634
/**
1600-
* Response content. May be ToolUseContent if stopReason is "toolUse".
1635+
* Response content. May be a single block or array. May include ToolUseContent if stopReason is "toolUse".
16011636
*/
16021637
content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)])
16031638
});
@@ -2010,6 +2045,7 @@ export const ClientNotificationSchema = z.union([
20102045
export const ClientResultSchema = z.union([
20112046
EmptyResultSchema,
20122047
CreateMessageResultSchema,
2048+
CreateMessageResultWithToolsSchema,
20132049
ElicitResultSchema,
20142050
ListRootsResultSchema,
20152051
GetTaskResultSchema,
@@ -2285,11 +2321,26 @@ export type LoggingMessageNotification = Infer<typeof LoggingMessageNotification
22852321
export type ToolChoice = Infer<typeof ToolChoiceSchema>;
22862322
export type ModelHint = Infer<typeof ModelHintSchema>;
22872323
export type ModelPreferences = Infer<typeof ModelPreferencesSchema>;
2324+
export type SamplingContent = Infer<typeof SamplingContentSchema>;
22882325
export type SamplingMessageContentBlock = Infer<typeof SamplingMessageContentBlockSchema>;
22892326
export type SamplingMessage = Infer<typeof SamplingMessageSchema>;
22902327
export type CreateMessageRequestParams = Infer<typeof CreateMessageRequestParamsSchema>;
22912328
export type CreateMessageRequest = Infer<typeof CreateMessageRequestSchema>;
22922329
export type CreateMessageResult = Infer<typeof CreateMessageResultSchema>;
2330+
export type CreateMessageResultWithTools = Infer<typeof CreateMessageResultWithToolsSchema>;
2331+
2332+
/**
2333+
* CreateMessageRequestParams without tools - for backwards-compatible overload.
2334+
* Excludes tools/toolChoice to indicate they should not be provided.
2335+
*/
2336+
export type CreateMessageRequestParamsBase = Omit<CreateMessageRequestParams, 'tools' | 'toolChoice'>;
2337+
2338+
/**
2339+
* CreateMessageRequestParams with required tools - for tool-enabled overload.
2340+
*/
2341+
export interface CreateMessageRequestParamsWithTools extends CreateMessageRequestParams {
2342+
tools: Tool[];
2343+
}
22932344

22942345
/* Elicitation */
22952346
export type BooleanSchema = Infer<typeof BooleanSchemaSchema>;

0 commit comments

Comments
 (0)