Skip to content

Commit 5eb6d54

Browse files
feat: backwards-compatible createMessage overloads for SEP-1577
Introduce method overloading for createMessage to preserve backwards compatibility while supporting the new tools feature from SEP-1577. When called without tools, createMessage returns CreateMessageResult with single content (backwards compatible). When called with tools, it returns CreateMessageResultWithTools which allows array content. This allows existing code that doesn't use tools to continue working without any changes, while new code using tools gets the appropriate type that handles array content. Changes: - Add SamplingContentSchema for basic content types (no tool use) - Add CreateMessageResultWithToolsSchema for tool-enabled responses - Add CreateMessageRequestParamsBase/WithTools types for overloads - Add method overloads to Server.createMessage() - Update tests to use appropriate schemas - Simplify example that doesn't use tools
1 parent f67fc2f commit 5eb6d54

File tree

4 files changed

+110
-12
lines changed

4 files changed

+110
-12
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 using tools, response.content is guaranteed to be 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.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/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
});
@@ -2005,6 +2040,7 @@ export const ClientNotificationSchema = z.union([
20052040
export const ClientResultSchema = z.union([
20062041
EmptyResultSchema,
20072042
CreateMessageResultSchema,
2043+
CreateMessageResultWithToolsSchema,
20082044
ElicitResultSchema,
20092045
ListRootsResultSchema,
20102046
GetTaskResultSchema,
@@ -2274,11 +2310,26 @@ export type LoggingMessageNotification = Infer<typeof LoggingMessageNotification
22742310
export type ToolChoice = Infer<typeof ToolChoiceSchema>;
22752311
export type ModelHint = Infer<typeof ModelHintSchema>;
22762312
export type ModelPreferences = Infer<typeof ModelPreferencesSchema>;
2313+
export type SamplingContent = Infer<typeof SamplingContentSchema>;
22772314
export type SamplingMessageContentBlock = Infer<typeof SamplingMessageContentBlockSchema>;
22782315
export type SamplingMessage = Infer<typeof SamplingMessageSchema>;
22792316
export type CreateMessageRequestParams = Infer<typeof CreateMessageRequestParamsSchema>;
22802317
export type CreateMessageRequest = Infer<typeof CreateMessageRequestSchema>;
22812318
export type CreateMessageResult = Infer<typeof CreateMessageResultSchema>;
2319+
export type CreateMessageResultWithTools = Infer<typeof CreateMessageResultWithToolsSchema>;
2320+
2321+
/**
2322+
* CreateMessageRequestParams without tools - for backwards-compatible overload.
2323+
* Excludes tools/toolChoice to indicate they should not be provided.
2324+
*/
2325+
export type CreateMessageRequestParamsBase = Omit<CreateMessageRequestParams, 'tools' | 'toolChoice'>;
2326+
2327+
/**
2328+
* CreateMessageRequestParams with required tools - for tool-enabled overload.
2329+
*/
2330+
export interface CreateMessageRequestParamsWithTools extends CreateMessageRequestParams {
2331+
tools: Tool[];
2332+
}
22822333

22832334
/* Elicitation */
22842335
export type BooleanSchema = Infer<typeof BooleanSchemaSchema>;

0 commit comments

Comments
 (0)