Skip to content

Commit 77e3106

Browse files
authored
Merge branch 'main' into feat-client-url
2 parents 801be7d + 29cb080 commit 77e3106

File tree

9 files changed

+1271
-235
lines changed

9 files changed

+1271
-235
lines changed

src/client/index.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,54 @@ import {
3636
SUPPORTED_PROTOCOL_VERSIONS,
3737
type SubscribeRequest,
3838
type Tool,
39-
type UnsubscribeRequest
39+
type UnsubscribeRequest,
40+
ElicitResultSchema,
41+
ElicitRequestSchema
4042
} from '../types.js';
4143
import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js';
4244
import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from '../validation/types.js';
45+
import { ZodLiteral, ZodObject, z } from 'zod';
46+
import type { RequestHandlerExtra } from '../shared/protocol.js';
47+
48+
/**
49+
* Elicitation default application helper. Applies defaults to the data based on the schema.
50+
*
51+
* @param schema - The schema to apply defaults to.
52+
* @param data - The data to apply defaults to.
53+
*/
54+
function applyElicitationDefaults(schema: JsonSchemaType | undefined, data: unknown): void {
55+
if (!schema || data === null || typeof data !== 'object') return;
56+
57+
// Handle object properties
58+
if (schema.type === 'object' && schema.properties && typeof schema.properties === 'object') {
59+
const obj = data as Record<string, unknown>;
60+
const props = schema.properties as Record<string, JsonSchemaType & { default?: unknown }>;
61+
for (const key of Object.keys(props)) {
62+
const propSchema = props[key];
63+
// If missing or explicitly undefined, apply default if present
64+
if (obj[key] === undefined && Object.prototype.hasOwnProperty.call(propSchema, 'default')) {
65+
obj[key] = propSchema.default;
66+
}
67+
// Recurse into existing nested objects/arrays
68+
if (obj[key] !== undefined) {
69+
applyElicitationDefaults(propSchema, obj[key]);
70+
}
71+
}
72+
}
73+
74+
if (Array.isArray(schema.anyOf)) {
75+
for (const sub of schema.anyOf) {
76+
applyElicitationDefaults(sub, data);
77+
}
78+
}
79+
80+
// Combine schemas
81+
if (Array.isArray(schema.oneOf)) {
82+
for (const sub of schema.oneOf) {
83+
applyElicitationDefaults(sub, data);
84+
}
85+
}
86+
}
4387

4488
export type ClientOptions = ProtocolOptions & {
4589
/**
@@ -141,6 +185,64 @@ export class Client<
141185
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
142186
}
143187

188+
/**
189+
* Override request handler registration to enforce client-side validation for elicitation.
190+
*/
191+
public override setRequestHandler<
192+
T extends ZodObject<{
193+
method: ZodLiteral<string>;
194+
}>
195+
>(
196+
requestSchema: T,
197+
handler: (
198+
request: z.infer<T>,
199+
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
200+
) => ClientResult | ResultT | Promise<ClientResult | ResultT>
201+
): void {
202+
const method = requestSchema.shape.method.value;
203+
if (method === 'elicitation/create') {
204+
const wrappedHandler = async (
205+
request: z.infer<T>,
206+
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
207+
): Promise<ClientResult | ResultT> => {
208+
const validatedRequest = ElicitRequestSchema.safeParse(request);
209+
if (!validatedRequest.success) {
210+
throw new McpError(ErrorCode.InvalidParams, `Invalid elicitation request: ${validatedRequest.error.message}`);
211+
}
212+
213+
const result = await Promise.resolve(handler(request, extra));
214+
215+
const validationResult = ElicitResultSchema.safeParse(result);
216+
if (!validationResult.success) {
217+
throw new McpError(ErrorCode.InvalidParams, `Invalid elicitation result: ${validationResult.error.message}`);
218+
}
219+
220+
const validatedResult = validationResult.data;
221+
222+
if (
223+
this._capabilities.elicitation?.applyDefaults &&
224+
validatedResult.action === 'accept' &&
225+
validatedResult.content &&
226+
validatedRequest.data.params.requestedSchema
227+
) {
228+
try {
229+
applyElicitationDefaults(validatedRequest.data.params.requestedSchema, validatedResult.content);
230+
} catch {
231+
// gracefully ignore errors in default application
232+
}
233+
}
234+
235+
return validatedResult;
236+
};
237+
238+
// Install the wrapped handler
239+
return super.setRequestHandler(requestSchema, wrappedHandler as unknown as typeof handler);
240+
}
241+
242+
// Non-elicitation handlers use default behavior
243+
return super.setRequestHandler(requestSchema, handler);
244+
}
245+
144246
protected assertCapability(capability: keyof ServerCapabilities, method: string): void {
145247
if (!this._serverCapabilities?.[capability]) {
146248
throw new Error(`Server does not support ${capability} (required for ${method})`);

0 commit comments

Comments
 (0)