Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .changeset/dispatcher-extraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
'@modelcontextprotocol/core': major
---
Extract Dispatcher from Protocol. Protocol composes `protected readonly dispatcher`; setRequestHandler/_onrequest delegate. The protected `_wrapHandler` override hook is replaced by `dispatcher.use(middleware)`.
7 changes: 0 additions & 7 deletions .changeset/wraphandler-hook.md

This file was deleted.

5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,11 @@ When a request arrives from the remote side:
1. **Transport** receives message, calls `transport.onmessage()`
2. **`Protocol.connect()`** routes to `_onrequest()`, `_onresponse()`, or `_onnotification()`
3. **`Protocol._onrequest()`**:
- Looks up handler in `_requestHandlers` map (keyed by method name)
- Checks `dispatcher.canHandle(method)`; sends a `MethodNotFound` error and returns early if no handler (or fallback) is registered
- Creates `BaseContext` with `signal`, `sessionId`, `sendNotification`, `sendRequest`, etc.
- Calls `buildContext()` to let subclasses enrich the context (e.g., Server adds HTTP request info)
- Invokes handler, sends JSON-RPC response back via transport
- Calls `dispatcher.dispatch()` which looks up the handler (keyed by method name), runs the middleware chain, invokes the handler, and wraps the result as a JSON-RPC response
- Sends the response back via transport
4. **Handler** was registered via `setRequestHandler('method', handler)`

### Handler Registration
Expand Down
138 changes: 66 additions & 72 deletions packages/client/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
CompleteRequest,
GetPromptRequest,
Implementation,
JSONRPCRequest,
JsonSchemaType,
JsonSchemaValidator,
jsonSchemaValidator,
Expand All @@ -19,12 +18,12 @@ import type {
ListToolsRequest,
LoggingLevel,
MessageExtraInfo,
Middleware,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
RequestMethod,
RequestOptions,
Result,
ServerCapabilities,
SubscribeRequest,
Tool,
Expand Down Expand Up @@ -229,6 +228,8 @@ export class Client extends Protocol<ClientContext> {
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator();
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;

this.dispatcher.use(this._validationMiddleware);

// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
this._pendingListChangedConfig = options.listChanged;
Expand Down Expand Up @@ -283,93 +284,86 @@ export class Client extends Protocol<ClientContext> {

/**
* Enforces client-side validation for `elicitation/create` and `sampling/createMessage`
* regardless of how the handler was registered.
* regardless of how the handler was registered. Installed as a {@linkcode Dispatcher}
* middleware so it applies to both the legacy `_onrequest` path and the 2026-06
* dispatch path.
*/
protected override _wrapHandler(
method: string,
handler: (request: JSONRPCRequest, ctx: ClientContext) => Promise<Result>
): (request: JSONRPCRequest, ctx: ClientContext) => Promise<Result> {
if (method === 'elicitation/create') {
return async (request, ctx) => {
const validatedRequest = parseSchema(ElicitRequestSchema, request);
if (!validatedRequest.success) {
// Type guard: if success is false, error is guaranteed to exist
const errorMessage =
validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid elicitation request: ${errorMessage}`);
}
private readonly _validationMiddleware: Middleware<ClientContext> = async (request, _ctx, next) => {
if (request.method === 'elicitation/create') {
const validatedRequest = parseSchema(ElicitRequestSchema, request);
if (!validatedRequest.success) {
const errorMessage =
validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid elicitation request: ${errorMessage}`);
}

const { params } = validatedRequest.data;
params.mode = params.mode ?? 'form';
const { supportsFormMode, supportsUrlMode } = getSupportedElicitationModes(this._capabilities.elicitation);
const { params } = validatedRequest.data;
params.mode = params.mode ?? 'form';
const { supportsFormMode, supportsUrlMode } = getSupportedElicitationModes(this._capabilities.elicitation);

if (params.mode === 'form' && !supportsFormMode) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Client does not support form-mode elicitation requests');
}
if (params.mode === 'form' && !supportsFormMode) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Client does not support form-mode elicitation requests');
}

if (params.mode === 'url' && !supportsUrlMode) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Client does not support URL-mode elicitation requests');
}
if (params.mode === 'url' && !supportsUrlMode) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Client does not support URL-mode elicitation requests');
}

const result = await handler(request, ctx);
const result = await next();

const validationResult = parseSchema(ElicitResultSchema, result);
if (!validationResult.success) {
// Type guard: if success is false, error is guaranteed to exist
const errorMessage =
validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid elicitation result: ${errorMessage}`);
}
const validationResult = parseSchema(ElicitResultSchema, result);
if (!validationResult.success) {
const errorMessage =
validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid elicitation result: ${errorMessage}`);
}

const validatedResult = validationResult.data;
const requestedSchema = params.mode === 'form' ? (params.requestedSchema as JsonSchemaType) : undefined;

if (
params.mode === 'form' &&
validatedResult.action === 'accept' &&
validatedResult.content &&
requestedSchema &&
this._capabilities.elicitation?.form?.applyDefaults
) {
try {
applyElicitationDefaults(requestedSchema, validatedResult.content);
} catch {
// gracefully ignore errors in default application
}
const validatedResult = validationResult.data;
const requestedSchema = params.mode === 'form' ? (params.requestedSchema as JsonSchemaType) : undefined;

if (
params.mode === 'form' &&
validatedResult.action === 'accept' &&
validatedResult.content &&
requestedSchema &&
this._capabilities.elicitation?.form?.applyDefaults
) {
try {
applyElicitationDefaults(requestedSchema, validatedResult.content);
} catch {
// gracefully ignore errors in default application
}
}

return validatedResult;
};
return validatedResult;
}

if (method === 'sampling/createMessage') {
return async (request, ctx) => {
const validatedRequest = parseSchema(CreateMessageRequestSchema, request);
if (!validatedRequest.success) {
const errorMessage =
validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid sampling request: ${errorMessage}`);
}
if (request.method === 'sampling/createMessage') {
const validatedRequest = parseSchema(CreateMessageRequestSchema, request);
if (!validatedRequest.success) {
const errorMessage =
validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid sampling request: ${errorMessage}`);
}

const { params } = validatedRequest.data;
const { params } = validatedRequest.data;

const result = await handler(request, ctx);
const result = await next();

const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid sampling result: ${errorMessage}`);
}
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const validationResult = parseSchema(resultSchema, result);
if (!validationResult.success) {
const errorMessage =
validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid sampling result: ${errorMessage}`);
}

return validationResult.data;
};
return validationResult.data;
}

return handler;
}
return next();
};

protected assertCapability(capability: keyof ServerCapabilities, method: string): void {
if (!this._serverCapabilities?.[capability]) {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@ export { checkResourceAllowed, resourceUrlFromServerUrl } from '../../shared/aut
// Metadata utilities
export { getDisplayName } from '../../shared/metadataUtils.js';

// Dispatcher types (handler registry; consumed by Protocol)
export type { RequestHandlerSchemas } from '../../shared/dispatcher.js';

// Protocol types (NOT the Protocol class itself or mergeCapabilities)
export type {
BaseContext,
ClientContext,
NotificationOptions,
ProgressCallback,
ProtocolOptions,
RequestHandlerSchemas,
RequestOptions,
ServerContext
} from '../../shared/protocol.js';
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './auth/errors.js';
export * from './errors/sdkErrors.js';
export * from './shared/auth.js';
export * from './shared/authUtils.js';
export * from './shared/dispatcher.js';
export * from './shared/metadataUtils.js';
export * from './shared/protocol.js';
export * from './shared/stdio.js';
Expand Down
Loading
Loading