diff --git a/package.json b/package.json index 560c4bcb2..08abd562c 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,10 @@ "import": "./dist/esm/validation/cfworker-provider.js", "require": "./dist/cjs/validation/cfworker-provider.js" }, + "./experimental": { + "import": "./dist/esm/experimental/index.js", + "require": "./dist/cjs/experimental/index.js" + }, "./*": { "import": "./dist/esm/*", "require": "./dist/cjs/*" diff --git a/src/client/experimental.ts b/src/client/experimental.ts new file mode 100644 index 000000000..5faaa80b6 --- /dev/null +++ b/src/client/experimental.ts @@ -0,0 +1,49 @@ +/** + * Experimental client features. + * + * WARNING: These APIs are experimental and may change without notice. + * + * @module experimental + */ + +import { ElicitationCompleteNotificationSchema, type ElicitationCompleteNotification } from '../types.js'; +import type { AnyObjectSchema } from '../server/zod-compat.js'; + +/** + * Handler for URL elicitation completion notifications. + */ +export type ElicitationCompleteHandler = (notification: ElicitationCompleteNotification) => void; + +/** + * Interface for the client methods used by experimental features. + * @internal + */ +interface ClientLike { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setNotificationHandler(schema: T, handler: (notification: any) => void): void; +} + +/** + * Experimental client features for MCP. + * + * Access via `client.experimental`. + * + * WARNING: These APIs are experimental and may change without notice. + */ +export class ExperimentalClientFeatures { + constructor(private client: ClientLike) {} + + /** + * Sets a handler for URL elicitation completion notifications. + * + * When a server completes an out-of-band URL elicitation interaction, + * it sends a `notifications/elicitation/complete` notification. + * This handler allows the client to react programmatically. + * + * @experimental This API may change without notice. + * @param handler The handler function to call when a completion notification is received. + */ + setElicitationCompleteHandler(handler: ElicitationCompleteHandler): void { + this.client.setNotificationHandler(ElicitationCompleteNotificationSchema, handler); + } +} diff --git a/src/client/index.ts b/src/client/index.ts index 694ae4a1a..8a281cfc7 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,4 +1,5 @@ import { mergeCapabilities, Protocol, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js'; +import { ExperimentalClientFeatures } from './experimental.js'; import type { Transport } from '../shared/transport.js'; import { type CallToolRequest, @@ -196,6 +197,13 @@ export class Client< private _jsonSchemaValidator: jsonSchemaValidator; private _cachedToolOutputValidators: Map> = new Map(); + /** + * Experimental client features. + * + * WARNING: These APIs are experimental and may change without notice. + */ + public readonly experimental: ExperimentalClientFeatures; + /** * Initializes this client with the given name and version information. */ @@ -206,6 +214,7 @@ export class Client< super(options); this._capabilities = options?.capabilities ?? {}; this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator(); + this.experimental = new ExperimentalClientFeatures(this); } /** diff --git a/src/examples/client/elicitationUrlExample.ts b/src/examples/client/elicitationUrlExample.ts index b57927e3f..68775ff8b 100644 --- a/src/examples/client/elicitationUrlExample.ts +++ b/src/examples/client/elicitationUrlExample.ts @@ -17,12 +17,10 @@ import { ElicitRequest, ElicitResult, ResourceLink, - ElicitRequestURLParams, McpError, - ErrorCode, - UrlElicitationRequiredError, - ElicitationCompleteNotificationSchema + ErrorCode } from '../../types.js'; +import { ElicitRequestURLParams, UrlElicitationRequiredError } from '../../experimental/index.js'; import { getDisplayName } from '../../shared/metadataUtils.js'; import { OAuthClientMetadata } from '../../shared/auth.js'; import { exec } from 'node:child_process'; @@ -548,7 +546,7 @@ async function connect(url?: string): Promise { client.setRequestHandler(ElicitRequestSchema, elicitationRequestHandler); // Set up notification handler for elicitation completion - client.setNotificationHandler(ElicitationCompleteNotificationSchema, notification => { + client.experimental.setElicitationCompleteHandler(notification => { const { elicitationId } = notification.params; const pending = pendingURLElicitations.get(elicitationId); if (pending) { diff --git a/src/examples/server/elicitationUrlExample.ts b/src/examples/server/elicitationUrlExample.ts index 089c6f887..73ecf9ca0 100644 --- a/src/examples/server/elicitationUrlExample.ts +++ b/src/examples/server/elicitationUrlExample.ts @@ -14,7 +14,8 @@ import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js'; -import { CallToolResult, UrlElicitationRequiredError, ElicitRequestURLParams, ElicitResult, isInitializeRequest } from '../../types.js'; +import { CallToolResult, ElicitResult, isInitializeRequest } from '../../types.js'; +import { UrlElicitationRequiredError, ElicitRequestURLParams } from '../../experimental/index.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; import { setupAuthServer } from './demoInMemoryOAuthProvider.js'; import { OAuthMetadata } from '../../shared/auth.js'; @@ -54,7 +55,7 @@ const getServer = () => { // Create and track the elicitation const elicitationId = generateTrackedElicitation(sessionId, elicitationId => - mcpServer.server.createElicitationCompletionNotifier(elicitationId) + mcpServer.server.experimental.createElicitationCompleteNotifier(elicitationId) ); throw new UrlElicitationRequiredError([ { @@ -90,7 +91,7 @@ const getServer = () => { // Create and track the elicitation const elicitationId = generateTrackedElicitation(sessionId, elicitationId => - mcpServer.server.createElicitationCompletionNotifier(elicitationId) + mcpServer.server.experimental.createElicitationCompleteNotifier(elicitationId) ); // Simulate OAuth callback and token exchange after 5 seconds @@ -623,8 +624,9 @@ const mcpPostHandler = async (req: Request, res: Response) => { console.log(`Session initialized with ID: ${sessionId}`); transports[sessionId] = transport; sessionsNeedingElicitation[sessionId] = { - elicitationSender: params => server.server.elicitInput(params), - createCompletionNotifier: elicitationId => server.server.createElicitationCompletionNotifier(elicitationId) + elicitationSender: params => server.server.experimental.elicitUrl(params), + createCompletionNotifier: elicitationId => + server.server.experimental.createElicitationCompleteNotifier(elicitationId) }; } }); diff --git a/src/experimental/index.ts b/src/experimental/index.ts new file mode 100644 index 000000000..f55df6a55 --- /dev/null +++ b/src/experimental/index.ts @@ -0,0 +1,28 @@ +/** + * Experimental MCP features. + * + * APIs in this module may change without notice in future versions. + * Use at your own risk. + * + * @module experimental + */ + +// URL Elicitation - experimental feature introduced in MCP 2025-11-25 +export { + // Schemas + ElicitRequestURLParamsSchema, + ElicitationCompleteNotificationParamsSchema, + ElicitationCompleteNotificationSchema, + + // Types + type ElicitRequestURLParams, + type ElicitationCompleteNotificationParams, + type ElicitationCompleteNotification, + + // Error class + UrlElicitationRequiredError +} from '../types.js'; + +// Re-export experimental feature classes +export { ExperimentalServerFeatures } from '../server/experimental.js'; +export { ExperimentalClientFeatures, type ElicitationCompleteHandler } from '../client/experimental.js'; diff --git a/src/server/experimental.ts b/src/server/experimental.ts new file mode 100644 index 000000000..b9a66b0cb --- /dev/null +++ b/src/server/experimental.ts @@ -0,0 +1,59 @@ +/** + * Experimental server features. + * + * WARNING: These APIs are experimental and may change without notice. + * + * @module experimental + */ + +import type { NotificationOptions, RequestOptions } from '../shared/protocol.js'; +import type { ElicitRequestURLParams, ElicitResult } from '../types.js'; + +/** + * Interface for the server methods used by experimental features. + * @internal + */ +interface ServerLike { + elicitInput(params: ElicitRequestURLParams, options?: RequestOptions): Promise; + createElicitationCompletionNotifier(elicitationId: string, options?: NotificationOptions): () => Promise; +} + +/** + * Experimental server features for MCP. + * + * Access via `server.experimental`. + * + * WARNING: These APIs are experimental and may change without notice. + */ +export class ExperimentalServerFeatures { + constructor(private server: ServerLike) {} + + /** + * Creates a URL elicitation request. + * + * URL mode elicitation enables servers to direct users to external URLs + * for out-of-band interactions that must not pass through the MCP client. + * This is essential for auth flows, payment processing, and other sensitive operations. + * + * @experimental This API may change without notice. + * @param params The URL elicitation parameters including url, message, and elicitationId. + * @param options Optional request options. + * @returns The result of the elicitation request. + */ + async elicitUrl(params: ElicitRequestURLParams, options?: RequestOptions): Promise { + return this.server.elicitInput(params, options); + } + + /** + * Creates a reusable callback that, when invoked, will send a `notifications/elicitation/complete` + * notification for the specified elicitation ID. + * + * @experimental This API may change without notice. + * @param elicitationId The ID of the elicitation to mark as complete. + * @param options Optional notification options. + * @returns A function that emits the completion notification when awaited. + */ + createElicitationCompleteNotifier(elicitationId: string, options?: NotificationOptions): () => Promise { + return this.server.createElicitationCompletionNotifier(elicitationId, options); + } +} diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 86eaf6d9e..d5e441650 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -5,7 +5,6 @@ import type { Transport } from '../shared/transport.js'; import { CreateMessageRequestSchema, ElicitRequestSchema, - ElicitationCompleteNotificationSchema, ErrorCode, LATEST_PROTOCOL_VERSION, ListPromptsRequestSchema, @@ -18,6 +17,7 @@ import { SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS } from '../types.js'; +import { ElicitationCompleteNotificationSchema } from '../experimental/index.js'; import { Server } from './index.js'; import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from '../validation/types.js'; import type { AnyObjectSchema } from './zod-compat.js'; @@ -925,7 +925,7 @@ test('should forward notification options when using elicitation completion noti } ); - client.setNotificationHandler(ElicitationCompleteNotificationSchema, () => {}); + client.experimental.setElicitationCompleteHandler(() => {}); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -933,7 +933,7 @@ test('should forward notification options when using elicitation completion noti const notificationSpy = vi.spyOn(server, 'notification'); - const notifier = server.createElicitationCompletionNotifier('elicitation-789', { relatedRequestId: 42 }); + const notifier = server.experimental.createElicitationCompleteNotifier('elicitation-789', { relatedRequestId: 42 }); await notifier(); expect(notificationSpy).toHaveBeenCalledWith( @@ -973,7 +973,7 @@ test('should create notifier that emits elicitation completion notification', as ); const receivedIds: string[] = []; - client.setNotificationHandler(ElicitationCompleteNotificationSchema, notification => { + client.experimental.setElicitationCompleteHandler(notification => { receivedIds.push(notification.params.elicitationId); }); @@ -981,7 +981,7 @@ test('should create notifier that emits elicitation completion notification', as await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); - const notifier = server.createElicitationCompletionNotifier('elicitation-123'); + const notifier = server.experimental.createElicitationCompleteNotifier('elicitation-123'); await notifier(); await new Promise(resolve => setTimeout(resolve, 0)); @@ -1018,7 +1018,7 @@ test('should throw when creating notifier if client lacks URL elicitation suppor await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); - expect(() => server.createElicitationCompletionNotifier('elicitation-123')).toThrow( + expect(() => server.experimental.createElicitationCompleteNotifier('elicitation-123')).toThrow( 'Client does not support URL elicitation (required for notifications/elicitation/complete)' ); }); diff --git a/src/server/index.ts b/src/server/index.ts index 8ec838e51..40296d9ac 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,4 +1,5 @@ import { mergeCapabilities, Protocol, type NotificationOptions, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js'; +import { ExperimentalServerFeatures } from './experimental.js'; import { type ClientCapabilities, type CreateMessageRequest, @@ -117,6 +118,13 @@ export class Server< private _instructions?: string; private _jsonSchemaValidator: jsonSchemaValidator; + /** + * Experimental server features. + * + * WARNING: These APIs are experimental and may change without notice. + */ + public readonly experimental: ExperimentalServerFeatures; + /** * Callback for when initialization has fully completed (i.e., the client has sent an `initialized` notification). */ @@ -133,6 +141,7 @@ export class Server< this._capabilities = options?.capabilities ?? {}; this._instructions = options?.instructions; this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator(); + this.experimental = new ExperimentalServerFeatures(this); this.setRequestHandler(InitializeRequestSchema, request => this._oninitialize(request)); this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.()); diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 776d0a129..e3f27b6e1 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -15,9 +15,9 @@ import { type Notification, ReadResourceResultSchema, type TextContent, - UrlElicitationRequiredError, ErrorCode } from '../types.js'; +import { UrlElicitationRequiredError } from '../experimental/index.js'; import { completable } from './completable.js'; import { McpServer, ResourceTemplate } from './mcp.js'; import { zodTestMatrix, type ZodMatrixEntry } from '../shared/zodTestMatrix.js';