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
3 changes: 3 additions & 0 deletions packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export { checkResourceAllowed, resourceUrlFromServerUrl } from '../../shared/aut
// Metadata utilities
export { getDisplayName } from '../../shared/metadataUtils.js';

// Dispatcher types referenced by public `use()` method (NOT the Dispatcher class itself)
export type { DispatchMiddleware } from '../../shared/dispatcher.js';

// Protocol types (NOT the Protocol class itself or mergeCapabilities)
export type {
BaseContext,
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export * from './auth/errors.js';
export * from './errors/sdkErrors.js';
export * from './shared/auth.js';
export * from './shared/authUtils.js';
export * from './shared/context.js';
export * from './shared/dispatcher.js';
export * from './shared/metadataUtils.js';
export * from './shared/protocol.js';
Expand Down
289 changes: 283 additions & 6 deletions packages/core/src/shared/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,238 @@
import type { AuthInfo, JSONRPCMessage, Notification, Request, RequestId, Result } from '../types/index.js';
import type {
AuthInfo,
ClientCapabilities,
CreateMessageRequest,
CreateMessageResult,
CreateMessageResultWithTools,
ElicitRequestFormParams,
ElicitRequestURLParams,
ElicitResult,
LoggingLevel,
Notification,
Progress,
Request,
RequestId,
RequestMeta,
RequestMethod,
Result,
ResultTypeMap,
ServerCapabilities
} from '../types/index.js';
import type { StandardSchemaV1 } from '../util/standardSchema.js';
import type { NotificationOptions, RequestOptions } from './protocol.js';
import type { TransportSendOptions } from './transport.js';

/**
* Callback for progress notifications.
*/
export type ProgressCallback = (progress: Progress) => void;

/**
* Additional initialization options.
*/
export type ProtocolOptions = {
/**
* Protocol versions supported. First version is preferred (sent by client,
* used as fallback by server). Passed to transport during `connect()`.
*
* @default {@linkcode SUPPORTED_PROTOCOL_VERSIONS}
*/
supportedProtocolVersions?: string[];

/**
* Whether to restrict emitted requests to only those that the remote side has indicated that they can handle, through their advertised capabilities.
*
* Note that this DOES NOT affect checking of _local_ side capabilities, as it is considered a logic error to mis-specify those.
*
* Currently this defaults to `false`, for backwards compatibility with SDK versions that did not advertise capabilities correctly. In future, this will default to `true`.
*/
enforceStrictCapabilities?: boolean;
/**
* An array of notification method names that should be automatically debounced.
* Any notifications with a method in this list will be coalesced if they
* occur in the same tick of the event loop.
* e.g., `['notifications/tools/list_changed']`
*/
debouncedNotificationMethods?: string[];
};

/**
* The default request timeout, in milliseconds.
*/
export const DEFAULT_REQUEST_TIMEOUT_MSEC = 60_000;

/**
* Options that can be given per request.
*/
export type RequestOptions = {
/**
* If set, requests progress notifications from the remote end (if supported). When progress notifications are received, this callback will be invoked.
*/
onprogress?: ProgressCallback;

/**
* Can be used to cancel an in-flight request. This will cause an `AbortError` to be raised from `request()`.
*/
signal?: AbortSignal;

/**
* A timeout (in milliseconds) for this request. If exceeded, an {@linkcode SdkError} with code {@linkcode SdkErrorCode.RequestTimeout} will be raised from `request()`.
*
* If not specified, {@linkcode DEFAULT_REQUEST_TIMEOUT_MSEC} will be used as the timeout.
*/
timeout?: number;

/**
* If `true`, receiving a progress notification will reset the request timeout.
* This is useful for long-running operations that send periodic progress updates.
* Default: `false`
*/
resetTimeoutOnProgress?: boolean;

/**
* Maximum total time (in milliseconds) to wait for a response.
* If exceeded, an {@linkcode SdkError} with code {@linkcode SdkErrorCode.RequestTimeout} will be raised, regardless of progress notifications.
* If not specified, there is no maximum total timeout.
*/
maxTotalTimeout?: number;
} & TransportSendOptions;

/**
* Options that can be given per notification.
*/
export type NotificationOptions = {
/**
* May be used to indicate to the transport which incoming request to associate this outgoing notification with.
*/
relatedRequestId?: RequestId;
};

/**
* Base context provided to all request handlers.
*/
export type BaseContext = {
/**
* The session ID from the transport, if available.
*/
sessionId?: string;

/**
* Information about the MCP request being handled.
*/
mcpReq: {
/**
* The JSON-RPC ID of the request being handled.
*/
id: RequestId;

/**
* The method name of the request (e.g., 'tools/call', 'ping').
*/
method: string;

/**
* Metadata from the original request.
*/
_meta?: RequestMeta;

/**
* An abort signal used to communicate if the request was cancelled from the sender's side.
*/
signal: AbortSignal;

/**
* Sends a request that relates to the current request being handled.
*
* This is used by certain transports to correctly associate related messages.
*
* For spec methods the result type is inferred from the method name.
* For custom (non-spec) methods, pass a result schema as the second argument.
*/
send: {
<M extends RequestMethod>(
request: { method: M; params?: Record<string, unknown> },
options?: RequestOptions
): Promise<ResultTypeMap[M]>;
<T extends StandardSchemaV1>(
request: Request,
resultSchema: T,
options?: RequestOptions
): Promise<StandardSchemaV1.InferOutput<T>>;
};

/**
* Sends a notification that relates to the current request being handled.
*
* This is used by certain transports to correctly associate related messages.
*/
notify: (notification: Notification) => Promise<void>;
};

/**
* HTTP transport information, only available when using an HTTP-based transport.
*/
http?: {
/**
* Information about a validated access token, provided to request handlers.
*/
authInfo?: AuthInfo;
};

/**
* Extension slot. Adapters and middleware populate keys here; handlers cast to the
* extension's declared type to read them. Core never reads or writes this field.
*/
ext?: Record<string, unknown>;
};

/**
* Context provided to server-side request handlers, extending {@linkcode BaseContext} with server-specific fields.
*/
export type ServerContext = BaseContext & {
mcpReq: {
/**
* Send a log message notification to the client.
* Respects the client's log level filter set via logging/setLevel.
*/
log: (level: LoggingLevel, data: unknown, logger?: string) => Promise<void>;

/**
* Send an elicitation request to the client, requesting user input.
*/
elicitInput: (params: ElicitRequestFormParams | ElicitRequestURLParams, options?: RequestOptions) => Promise<ElicitResult>;

/**
* Request LLM sampling from the client.
*/
requestSampling: (
params: CreateMessageRequest['params'],
options?: RequestOptions
) => Promise<CreateMessageResult | CreateMessageResultWithTools>;
};

http?: {
/**
* The original HTTP request.
*/
req?: globalThis.Request;

/**
* Closes the SSE stream for this request, triggering client reconnection.
* Only available when using a StreamableHTTPServerTransport with eventStore configured.
*/
closeSSE?: () => void;

/**
* Closes the standalone GET SSE stream, triggering client reconnection.
* Only available when using a StreamableHTTPServerTransport with eventStore configured.
*/
closeStandaloneSSE?: () => void;
};
};

/**
* Context provided to client-side request handlers.
*/
export type ClientContext = BaseContext;

/**
* Per-request environment a transport adapter passes to {@linkcode Dispatcher.dispatch}.
Expand All @@ -17,6 +249,13 @@ export type RequestEnv = {
*/
send?: (request: Request, options?: RequestOptions) => Promise<Result>;

/**
* Sends a notification back to the peer, related to the request being dispatched.
* When supplied, `ctx.mcpReq.notify` calls this; when undefined, the dispatcher
* yields the notification inline.
*/
notify?: (notification: Notification) => Promise<void>;

/** Validated auth token info for HTTP transports. */
authInfo?: AuthInfo;

Expand All @@ -29,6 +268,12 @@ export type RequestEnv = {
/** Transport session identifier (legacy `Mcp-Session-Id`). */
sessionId?: string;

/**
* The originating request id, when the dispatch is on behalf of an inbound request.
* Adapters propagate this so wrapped `send`/`notify` carry `relatedRequestId`.
*/
relatedRequestId?: RequestId;

/** Extension slot. Adapters and middleware populate keys here; copied onto `BaseContext.ext`. */
ext?: Record<string, unknown>;
};
Expand All @@ -48,10 +293,42 @@ export interface Outbound {
notification(notification: Notification, options?: NotificationOptions): Promise<void>;
/** Close the underlying connection. */
close(): Promise<void>;
/** Clear a registered progress callback by its message id. Optional; pipe-channels expose this. */
removeProgressHandler?(messageId: number): void;
/** Inform the channel which protocol version was negotiated (for header echoing etc.). Optional. */
setProtocolVersion?(version: string): void;
/** Write a raw JSON-RPC message on the same stream as a prior request. Optional; pipe-only. */
sendRaw?(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise<void>;
}

/**
* Schema bundle accepted by `setRequestHandler`'s 3-arg form.
*
* `params` is required and validates the inbound `request.params`. `result` is optional;
* when supplied it types the handler's return value (no runtime validation is performed
* on the result).
*/
export interface RequestHandlerSchemas<
P extends StandardSchemaV1 = StandardSchemaV1,
R extends StandardSchemaV1 | undefined = StandardSchemaV1 | undefined
> {
params: P;
result?: R;
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

export function mergeCapabilities(base: ServerCapabilities, additional: Partial<ServerCapabilities>): ServerCapabilities;
export function mergeCapabilities(base: ClientCapabilities, additional: Partial<ClientCapabilities>): ClientCapabilities;
export function mergeCapabilities<T extends ServerCapabilities | ClientCapabilities>(base: T, additional: Partial<T>): T {
const result: T = { ...base };
for (const key in additional) {
const k = key as keyof T;
const addValue = additional[k];
if (addValue === undefined) continue;
const baseValue = result[k];
result[k] =
isPlainObject(baseValue) && isPlainObject(addValue)
? ({ ...(baseValue as Record<string, unknown>), ...(addValue as Record<string, unknown>) } as T[typeof k])
: (addValue as T[typeof k]);
}
return result;
}
15 changes: 8 additions & 7 deletions packages/core/src/shared/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import type {
import { getNotificationSchema, getRequestSchema, getResultSchema, ProtocolError, ProtocolErrorCode } from '../types/index.js';
import type { StandardSchemaV1 } from '../util/standardSchema.js';
import { validateStandardSchema } from '../util/standardSchema.js';
import type { RequestEnv } from './context.js';
import type { BaseContext, RequestOptions } from './protocol.js';
import type { BaseContext, RequestEnv, RequestOptions } from './context.js';

/**
* One yielded item from {@linkcode Dispatcher.dispatch}. A dispatch yields zero or more
Expand Down Expand Up @@ -184,11 +183,13 @@ export class Dispatcher<ContextT extends BaseContext = BaseContext> {
}
return parsed.data;
}) as BaseContext['mcpReq']['send'],
notify: async (n: Notification) => {
if (done) return;
queue.push({ jsonrpc: '2.0', method: n.method, params: n.params } as JSONRPCNotification);
wake?.();
}
notify:
env.notify ??
(async (n: Notification) => {
if (done) return;
queue.push({ jsonrpc: '2.0', method: n.method, params: n.params } as JSONRPCNotification);
wake?.();
})
},
http: env.authInfo || env.httpReq ? { authInfo: env.authInfo } : undefined,
ext: env.ext
Expand Down
Loading
Loading