Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/examples/server/toolWithSampleServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ mcpServer.registerTool(
maxTokens: 500
});

const contents = Array.isArray(response.content) ? response.content : [response.content];
// Since we're not passing tools param to createMessage, response.content is single content
return {
content: contents.map(content => ({
type: 'text',
text: content.type === 'text' ? content.text : 'Unable to generate summary'
}))
content: [
{
type: 'text',
text: response.content.type === 'text' ? response.content.text : 'Unable to generate summary'
}
]
};
}
);
Expand Down
64 changes: 64 additions & 0 deletions src/server/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down
36 changes: 35 additions & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}

Expand Down
20 changes: 19 additions & 1 deletion src/spec.types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ type FixSpecInitializeRequest<T> = T extends { params: infer P } ? Omit<T, 'para

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

// Targeted fix: CreateMessageResult in SDK uses single content for v1.x backwards compat.
// The full array-capable type is CreateMessageResultWithTools.
// This will be aligned with schema in v2.0.
// Narrows content from SamplingMessageContentBlock (includes tool types) to basic content types only.
type NarrowToBasicContent<C> = C extends { type: 'text' | 'image' | 'audio' } ? C : never;
type FixSpecCreateMessageResult<T> = T extends { content: infer C; role: infer R; model: infer M }
? {
_meta?: { [key: string]: unknown };
model: M;
role: R;
stopReason?: string;
content: C extends (infer U)[] ? NarrowToBasicContent<U> : NarrowToBasicContent<C>;
}
: T;

const sdkTypeChecks = {
RequestParams: (sdk: RemovePassthrough<SDKTypes.RequestParams>, spec: SpecTypes.RequestParams) => {
sdk = spec;
Expand Down Expand Up @@ -369,7 +384,10 @@ const sdkTypeChecks = {
sdk = spec;
spec = sdk;
},
CreateMessageResult: (sdk: RemovePassthrough<SDKTypes.CreateMessageResult>, spec: SpecTypes.CreateMessageResult) => {
CreateMessageResult: (
sdk: RemovePassthrough<SDKTypes.CreateMessageResult>,
spec: FixSpecCreateMessageResult<SpecTypes.CreateMessageResult>
) => {
sdk = spec;
spec = sdk;
},
Expand Down
19 changes: 15 additions & 4 deletions src/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SamplingMessageSchema,
CreateMessageRequestSchema,
CreateMessageResultSchema,
CreateMessageResultWithToolsSchema,
ClientCapabilitiesSchema
} from './types.js';

Expand Down Expand Up @@ -787,7 +788,7 @@ describe('Types', () => {
}
});

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

const parseResult = CreateMessageResultSchema.safeParse(result);
// Tool call results use CreateMessageResultWithToolsSchema
const parseResult = CreateMessageResultWithToolsSchema.safeParse(result);
expect(parseResult.success).toBe(true);
if (parseResult.success) {
expect(parseResult.data.stopReason).toBe('toolUse');
Expand All @@ -810,9 +812,13 @@ describe('Types', () => {
expect(content.type).toBe('tool_use');
}
}

// Basic CreateMessageResultSchema should NOT accept tool_use content
const basicResult = CreateMessageResultSchema.safeParse(result);
expect(basicResult.success).toBe(false);
});

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

const parseResult = CreateMessageResultSchema.safeParse(result);
// Array content uses CreateMessageResultWithToolsSchema
const parseResult = CreateMessageResultWithToolsSchema.safeParse(result);
expect(parseResult.success).toBe(true);
if (parseResult.success) {
expect(parseResult.data.stopReason).toBe('toolUse');
Expand All @@ -840,6 +847,10 @@ describe('Types', () => {
expect(content[1].type).toBe('tool_use');
}
}

// Basic CreateMessageResultSchema should NOT accept array content
const basicResult = CreateMessageResultSchema.safeParse(result);
expect(basicResult.success).toBe(false);
});

test('should validate all new stop reasons', () => {
Expand Down
55 changes: 53 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1495,6 +1495,12 @@ export const ToolResultContentSchema = z
})
.passthrough();

/**
* Basic content types for sampling responses (without tool use).
* Used for backwards-compatible CreateMessageResult when tools are not used.
*/
export const SamplingContentSchema = z.discriminatedUnion('type', [TextContentSchema, ImageContentSchema, AudioContentSchema]);

/**
* Content block types allowed in sampling messages.
* This includes text, image, audio, tool use requests, and tool results.
Expand Down Expand Up @@ -1576,9 +1582,38 @@ export const CreateMessageRequestSchema = RequestSchema.extend({
});

/**
* 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.
* The client's response to a sampling/create_message request from the server.
* This is the backwards-compatible version that returns single content (no arrays).
* Used when the request does not include tools.
*/
export const CreateMessageResultSchema = ResultSchema.extend({
/**
* The name of the model that generated the message.
*/
model: z.string(),
/**
* The reason why sampling stopped, if known.
*
* Standard values:
* - "endTurn": Natural end of the assistant's turn
* - "stopSequence": A stop sequence was encountered
* - "maxTokens": Maximum token limit was reached
*
* This field is an open string to allow for provider-specific stop reasons.
*/
stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens']).or(z.string())),
role: z.enum(['user', 'assistant']),
/**
* Response content. Single content block (text, image, or audio).
*/
content: SamplingContentSchema
});

/**
* The client's response to a sampling/create_message request when tools were provided.
* This version supports array content for tool use flows.
*/
export const CreateMessageResultWithToolsSchema = ResultSchema.extend({
/**
* The name of the model that generated the message.
*/
Expand All @@ -1597,7 +1632,7 @@ export const CreateMessageResultSchema = ResultSchema.extend({
stopReason: z.optional(z.enum(['endTurn', 'stopSequence', 'maxTokens', 'toolUse']).or(z.string())),
role: z.enum(['user', 'assistant']),
/**
* Response content. May be ToolUseContent if stopReason is "toolUse".
* Response content. May be a single block or array. May include ToolUseContent if stopReason is "toolUse".
*/
content: z.union([SamplingMessageContentBlockSchema, z.array(SamplingMessageContentBlockSchema)])
});
Expand Down Expand Up @@ -2010,6 +2045,7 @@ export const ClientNotificationSchema = z.union([
export const ClientResultSchema = z.union([
EmptyResultSchema,
CreateMessageResultSchema,
CreateMessageResultWithToolsSchema,
ElicitResultSchema,
ListRootsResultSchema,
GetTaskResultSchema,
Expand Down Expand Up @@ -2285,11 +2321,26 @@ export type LoggingMessageNotification = Infer<typeof LoggingMessageNotification
export type ToolChoice = Infer<typeof ToolChoiceSchema>;
export type ModelHint = Infer<typeof ModelHintSchema>;
export type ModelPreferences = Infer<typeof ModelPreferencesSchema>;
export type SamplingContent = Infer<typeof SamplingContentSchema>;
export type SamplingMessageContentBlock = Infer<typeof SamplingMessageContentBlockSchema>;
export type SamplingMessage = Infer<typeof SamplingMessageSchema>;
export type CreateMessageRequestParams = Infer<typeof CreateMessageRequestParamsSchema>;
export type CreateMessageRequest = Infer<typeof CreateMessageRequestSchema>;
export type CreateMessageResult = Infer<typeof CreateMessageResultSchema>;
export type CreateMessageResultWithTools = Infer<typeof CreateMessageResultWithToolsSchema>;

/**
* CreateMessageRequestParams without tools - for backwards-compatible overload.
* Excludes tools/toolChoice to indicate they should not be provided.
*/
export type CreateMessageRequestParamsBase = Omit<CreateMessageRequestParams, 'tools' | 'toolChoice'>;

/**
* CreateMessageRequestParams with required tools - for tool-enabled overload.
*/
export interface CreateMessageRequestParamsWithTools extends CreateMessageRequestParams {
tools: Tool[];
}

/* Elicitation */
export type BooleanSchema = Infer<typeof BooleanSchemaSchema>;
Expand Down
Loading