From 2a95b1b546f3af2a51fa91f673513fdd2fedeff0 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 12 Aug 2025 22:01:35 +0300 Subject: [PATCH 1/2] add index.ts barrel files for @modelcontextprotocol/sdk/server, @modelcontextprotocol/sdk/client and @modelcontextprotocol/sdk/shared --- package.json | 4 + src/client/{index.test.ts => client.test.ts} | 2 +- src/client/client.ts | 520 ++++++++++++++++++ src/client/index.ts | 526 +------------------ src/server/auth/index.ts | 21 + src/server/index.ts | 391 +------------- src/server/{index.test.ts => server.test.ts} | 2 +- src/server/server.ts | 387 ++++++++++++++ src/shared/index.ts | 7 + 9 files changed, 957 insertions(+), 903 deletions(-) rename src/client/{index.test.ts => client.test.ts} (99%) create mode 100644 src/client/client.ts create mode 100644 src/server/auth/index.ts rename src/server/{index.test.ts => server.test.ts} (99%) create mode 100644 src/server/server.ts create mode 100644 src/shared/index.ts diff --git a/package.json b/package.json index 445134892..a403802c7 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,10 @@ "import": "./dist/esm/server/index.js", "require": "./dist/cjs/server/index.js" }, + "./shared": { + "import": "./dist/esm/shared/index.js", + "require": "./dist/cjs/shared/index.js" + }, "./*": { "import": "./dist/esm/*", "require": "./dist/cjs/*" diff --git a/src/client/index.test.ts b/src/client/client.test.ts similarity index 99% rename from src/client/index.test.ts rename to src/client/client.test.ts index abd0c34e4..e90955a51 100644 --- a/src/client/index.test.ts +++ b/src/client/client.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-constant-binary-expression */ /* eslint-disable @typescript-eslint/no-unused-expressions */ -import { Client } from "./index.js"; +import { Client } from "./client.js"; import { z } from "zod"; import { RequestSchema, diff --git a/src/client/client.ts b/src/client/client.ts new file mode 100644 index 000000000..f6717e287 --- /dev/null +++ b/src/client/client.ts @@ -0,0 +1,520 @@ +import { + mergeCapabilities, + Protocol, + ProtocolOptions, + RequestOptions, + } from "../shared/protocol.js"; + import { Transport } from "../shared/transport.js"; + import { + CallToolRequest, + CallToolResultSchema, + ClientCapabilities, + ClientNotification, + ClientRequest, + ClientResult, + CompatibilityCallToolResultSchema, + CompleteRequest, + CompleteResultSchema, + EmptyResultSchema, + GetPromptRequest, + GetPromptResultSchema, + Implementation, + InitializeResultSchema, + LATEST_PROTOCOL_VERSION, + ListPromptsRequest, + ListPromptsResultSchema, + ListResourcesRequest, + ListResourcesResultSchema, + ListResourceTemplatesRequest, + ListResourceTemplatesResultSchema, + ListToolsRequest, + ListToolsResultSchema, + LoggingLevel, + Notification, + ReadResourceRequest, + ReadResourceResultSchema, + Request, + Result, + ServerCapabilities, + SubscribeRequest, + SUPPORTED_PROTOCOL_VERSIONS, + UnsubscribeRequest, + Tool, + ErrorCode, + McpError, + } from "../types.js"; + import Ajv from "ajv"; + import type { ValidateFunction } from "ajv"; + + export type ClientOptions = ProtocolOptions & { + /** + * Capabilities to advertise as being supported by this client. + */ + capabilities?: ClientCapabilities; + }; + + /** + * An MCP client on top of a pluggable transport. + * + * The client will automatically begin the initialization flow with the server when connect() is called. + * + * To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters: + * + * ```typescript + * // Custom schemas + * const CustomRequestSchema = RequestSchema.extend({...}) + * const CustomNotificationSchema = NotificationSchema.extend({...}) + * const CustomResultSchema = ResultSchema.extend({...}) + * + * // Type aliases + * type CustomRequest = z.infer + * type CustomNotification = z.infer + * type CustomResult = z.infer + * + * // Create typed client + * const client = new Client({ + * name: "CustomClient", + * version: "1.0.0" + * }) + * ``` + */ + export class Client< + RequestT extends Request = Request, + NotificationT extends Notification = Notification, + ResultT extends Result = Result, + > extends Protocol< + ClientRequest | RequestT, + ClientNotification | NotificationT, + ClientResult | ResultT + > { + private _serverCapabilities?: ServerCapabilities; + private _serverVersion?: Implementation; + private _capabilities: ClientCapabilities; + private _instructions?: string; + private _cachedToolOutputValidators: Map = new Map(); + private _ajv: InstanceType; + + /** + * Initializes this client with the given name and version information. + */ + constructor( + private _clientInfo: Implementation, + options?: ClientOptions, + ) { + super(options); + this._capabilities = options?.capabilities ?? {}; + this._ajv = new Ajv(); + } + + /** + * Registers new capabilities. This can only be called before connecting to a transport. + * + * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization). + */ + public registerCapabilities(capabilities: ClientCapabilities): void { + if (this.transport) { + throw new Error( + "Cannot register capabilities after connecting to transport", + ); + } + + this._capabilities = mergeCapabilities(this._capabilities, capabilities); + } + + protected assertCapability( + capability: keyof ServerCapabilities, + method: string, + ): void { + if (!this._serverCapabilities?.[capability]) { + throw new Error( + `Server does not support ${capability} (required for ${method})`, + ); + } + } + + override async connect(transport: Transport, options?: RequestOptions): Promise { + await super.connect(transport); + // When transport sessionId is already set this means we are trying to reconnect. + // In this case we don't need to initialize again. + if (transport.sessionId !== undefined) { + return; + } + try { + const result = await this.request( + { + method: "initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: this._capabilities, + clientInfo: this._clientInfo, + }, + }, + InitializeResultSchema, + options + ); + + if (result === undefined) { + throw new Error(`Server sent invalid initialize result: ${result}`); + } + + if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { + throw new Error( + `Server's protocol version is not supported: ${result.protocolVersion}`, + ); + } + + this._serverCapabilities = result.capabilities; + this._serverVersion = result.serverInfo; + // HTTP transports must set the protocol version in each header after initialization. + if (transport.setProtocolVersion) { + transport.setProtocolVersion(result.protocolVersion); + } + + this._instructions = result.instructions; + + await this.notification({ + method: "notifications/initialized", + }); + } catch (error) { + // Disconnect if initialization fails. + void this.close(); + throw error; + } + } + + /** + * After initialization has completed, this will be populated with the server's reported capabilities. + */ + getServerCapabilities(): ServerCapabilities | undefined { + return this._serverCapabilities; + } + + /** + * After initialization has completed, this will be populated with information about the server's name and version. + */ + getServerVersion(): Implementation | undefined { + return this._serverVersion; + } + + /** + * After initialization has completed, this may be populated with information about the server's instructions. + */ + getInstructions(): string | undefined { + return this._instructions; + } + + protected assertCapabilityForMethod(method: RequestT["method"]): void { + switch (method as ClientRequest["method"]) { + case "logging/setLevel": + if (!this._serverCapabilities?.logging) { + throw new Error( + `Server does not support logging (required for ${method})`, + ); + } + break; + + case "prompts/get": + case "prompts/list": + if (!this._serverCapabilities?.prompts) { + throw new Error( + `Server does not support prompts (required for ${method})`, + ); + } + break; + + case "resources/list": + case "resources/templates/list": + case "resources/read": + case "resources/subscribe": + case "resources/unsubscribe": + if (!this._serverCapabilities?.resources) { + throw new Error( + `Server does not support resources (required for ${method})`, + ); + } + + if ( + method === "resources/subscribe" && + !this._serverCapabilities.resources.subscribe + ) { + throw new Error( + `Server does not support resource subscriptions (required for ${method})`, + ); + } + + break; + + case "tools/call": + case "tools/list": + if (!this._serverCapabilities?.tools) { + throw new Error( + `Server does not support tools (required for ${method})`, + ); + } + break; + + case "completion/complete": + if (!this._serverCapabilities?.completions) { + throw new Error( + `Server does not support completions (required for ${method})`, + ); + } + break; + + case "initialize": + // No specific capability required for initialize + break; + + case "ping": + // No specific capability required for ping + break; + } + } + + protected assertNotificationCapability( + method: NotificationT["method"], + ): void { + switch (method as ClientNotification["method"]) { + case "notifications/roots/list_changed": + if (!this._capabilities.roots?.listChanged) { + throw new Error( + `Client does not support roots list changed notifications (required for ${method})`, + ); + } + break; + + case "notifications/initialized": + // No specific capability required for initialized + break; + + case "notifications/cancelled": + // Cancellation notifications are always allowed + break; + + case "notifications/progress": + // Progress notifications are always allowed + break; + } + } + + protected assertRequestHandlerCapability(method: string): void { + switch (method) { + case "sampling/createMessage": + if (!this._capabilities.sampling) { + throw new Error( + `Client does not support sampling capability (required for ${method})`, + ); + } + break; + + case "elicitation/create": + if (!this._capabilities.elicitation) { + throw new Error( + `Client does not support elicitation capability (required for ${method})`, + ); + } + break; + + case "roots/list": + if (!this._capabilities.roots) { + throw new Error( + `Client does not support roots capability (required for ${method})`, + ); + } + break; + + case "ping": + // No specific capability required for ping + break; + } + } + + async ping(options?: RequestOptions) { + return this.request({ method: "ping" }, EmptyResultSchema, options); + } + + async complete(params: CompleteRequest["params"], options?: RequestOptions) { + return this.request( + { method: "completion/complete", params }, + CompleteResultSchema, + options, + ); + } + + async setLoggingLevel(level: LoggingLevel, options?: RequestOptions) { + return this.request( + { method: "logging/setLevel", params: { level } }, + EmptyResultSchema, + options, + ); + } + + async getPrompt( + params: GetPromptRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "prompts/get", params }, + GetPromptResultSchema, + options, + ); + } + + async listPrompts( + params?: ListPromptsRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "prompts/list", params }, + ListPromptsResultSchema, + options, + ); + } + + async listResources( + params?: ListResourcesRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "resources/list", params }, + ListResourcesResultSchema, + options, + ); + } + + async listResourceTemplates( + params?: ListResourceTemplatesRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "resources/templates/list", params }, + ListResourceTemplatesResultSchema, + options, + ); + } + + async readResource( + params: ReadResourceRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "resources/read", params }, + ReadResourceResultSchema, + options, + ); + } + + async subscribeResource( + params: SubscribeRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "resources/subscribe", params }, + EmptyResultSchema, + options, + ); + } + + async unsubscribeResource( + params: UnsubscribeRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "resources/unsubscribe", params }, + EmptyResultSchema, + options, + ); + } + + async callTool( + params: CallToolRequest["params"], + resultSchema: + | typeof CallToolResultSchema + | typeof CompatibilityCallToolResultSchema = CallToolResultSchema, + options?: RequestOptions, + ) { + const result = await this.request( + { method: "tools/call", params }, + resultSchema, + options, + ); + + // Check if the tool has an outputSchema + const validator = this.getToolOutputValidator(params.name); + if (validator) { + // If tool has outputSchema, it MUST return structuredContent (unless it's an error) + if (!result.structuredContent && !result.isError) { + throw new McpError( + ErrorCode.InvalidRequest, + `Tool ${params.name} has an output schema but did not return structured content` + ); + } + + // Only validate structured content if present (not when there's an error) + if (result.structuredContent) { + try { + // Validate the structured content (which is already an object) against the schema + const isValid = validator(result.structuredContent); + + if (!isValid) { + throw new McpError( + ErrorCode.InvalidParams, + `Structured content does not match the tool's output schema: ${this._ajv.errorsText(validator.errors)}` + ); + } + } catch (error) { + if (error instanceof McpError) { + throw error; + } + throw new McpError( + ErrorCode.InvalidParams, + `Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + } + + return result; + } + + private cacheToolOutputSchemas(tools: Tool[]) { + this._cachedToolOutputValidators.clear(); + + for (const tool of tools) { + // If the tool has an outputSchema, create and cache the Ajv validator + if (tool.outputSchema) { + try { + const validator = this._ajv.compile(tool.outputSchema); + this._cachedToolOutputValidators.set(tool.name, validator); + } catch { + // Ignore schema compilation errors + } + } + } + } + + private getToolOutputValidator(toolName: string): ValidateFunction | undefined { + return this._cachedToolOutputValidators.get(toolName); + } + + async listTools( + params?: ListToolsRequest["params"], + options?: RequestOptions, + ) { + const result = await this.request( + { method: "tools/list", params }, + ListToolsResultSchema, + options, + ); + + // Cache the tools and their output schemas for future validation + this.cacheToolOutputSchemas(result.tools); + + return result; + } + + async sendRootsListChanged() { + return this.notification({ method: "notifications/roots/list_changed" }); + } + } + \ No newline at end of file diff --git a/src/client/index.ts b/src/client/index.ts index 3e8d8ec80..449f67a0c 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,519 +1,7 @@ -import { - mergeCapabilities, - Protocol, - ProtocolOptions, - RequestOptions, -} from "../shared/protocol.js"; -import { Transport } from "../shared/transport.js"; -import { - CallToolRequest, - CallToolResultSchema, - ClientCapabilities, - ClientNotification, - ClientRequest, - ClientResult, - CompatibilityCallToolResultSchema, - CompleteRequest, - CompleteResultSchema, - EmptyResultSchema, - GetPromptRequest, - GetPromptResultSchema, - Implementation, - InitializeResultSchema, - LATEST_PROTOCOL_VERSION, - ListPromptsRequest, - ListPromptsResultSchema, - ListResourcesRequest, - ListResourcesResultSchema, - ListResourceTemplatesRequest, - ListResourceTemplatesResultSchema, - ListToolsRequest, - ListToolsResultSchema, - LoggingLevel, - Notification, - ReadResourceRequest, - ReadResourceResultSchema, - Request, - Result, - ServerCapabilities, - SubscribeRequest, - SUPPORTED_PROTOCOL_VERSIONS, - UnsubscribeRequest, - Tool, - ErrorCode, - McpError, -} from "../types.js"; -import Ajv from "ajv"; -import type { ValidateFunction } from "ajv"; - -export type ClientOptions = ProtocolOptions & { - /** - * Capabilities to advertise as being supported by this client. - */ - capabilities?: ClientCapabilities; -}; - -/** - * An MCP client on top of a pluggable transport. - * - * The client will automatically begin the initialization flow with the server when connect() is called. - * - * To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters: - * - * ```typescript - * // Custom schemas - * const CustomRequestSchema = RequestSchema.extend({...}) - * const CustomNotificationSchema = NotificationSchema.extend({...}) - * const CustomResultSchema = ResultSchema.extend({...}) - * - * // Type aliases - * type CustomRequest = z.infer - * type CustomNotification = z.infer - * type CustomResult = z.infer - * - * // Create typed client - * const client = new Client({ - * name: "CustomClient", - * version: "1.0.0" - * }) - * ``` - */ -export class Client< - RequestT extends Request = Request, - NotificationT extends Notification = Notification, - ResultT extends Result = Result, -> extends Protocol< - ClientRequest | RequestT, - ClientNotification | NotificationT, - ClientResult | ResultT -> { - private _serverCapabilities?: ServerCapabilities; - private _serverVersion?: Implementation; - private _capabilities: ClientCapabilities; - private _instructions?: string; - private _cachedToolOutputValidators: Map = new Map(); - private _ajv: InstanceType; - - /** - * Initializes this client with the given name and version information. - */ - constructor( - private _clientInfo: Implementation, - options?: ClientOptions, - ) { - super(options); - this._capabilities = options?.capabilities ?? {}; - this._ajv = new Ajv(); - } - - /** - * Registers new capabilities. This can only be called before connecting to a transport. - * - * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization). - */ - public registerCapabilities(capabilities: ClientCapabilities): void { - if (this.transport) { - throw new Error( - "Cannot register capabilities after connecting to transport", - ); - } - - this._capabilities = mergeCapabilities(this._capabilities, capabilities); - } - - protected assertCapability( - capability: keyof ServerCapabilities, - method: string, - ): void { - if (!this._serverCapabilities?.[capability]) { - throw new Error( - `Server does not support ${capability} (required for ${method})`, - ); - } - } - - override async connect(transport: Transport, options?: RequestOptions): Promise { - await super.connect(transport); - // When transport sessionId is already set this means we are trying to reconnect. - // In this case we don't need to initialize again. - if (transport.sessionId !== undefined) { - return; - } - try { - const result = await this.request( - { - method: "initialize", - params: { - protocolVersion: LATEST_PROTOCOL_VERSION, - capabilities: this._capabilities, - clientInfo: this._clientInfo, - }, - }, - InitializeResultSchema, - options - ); - - if (result === undefined) { - throw new Error(`Server sent invalid initialize result: ${result}`); - } - - if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { - throw new Error( - `Server's protocol version is not supported: ${result.protocolVersion}`, - ); - } - - this._serverCapabilities = result.capabilities; - this._serverVersion = result.serverInfo; - // HTTP transports must set the protocol version in each header after initialization. - if (transport.setProtocolVersion) { - transport.setProtocolVersion(result.protocolVersion); - } - - this._instructions = result.instructions; - - await this.notification({ - method: "notifications/initialized", - }); - } catch (error) { - // Disconnect if initialization fails. - void this.close(); - throw error; - } - } - - /** - * After initialization has completed, this will be populated with the server's reported capabilities. - */ - getServerCapabilities(): ServerCapabilities | undefined { - return this._serverCapabilities; - } - - /** - * After initialization has completed, this will be populated with information about the server's name and version. - */ - getServerVersion(): Implementation | undefined { - return this._serverVersion; - } - - /** - * After initialization has completed, this may be populated with information about the server's instructions. - */ - getInstructions(): string | undefined { - return this._instructions; - } - - protected assertCapabilityForMethod(method: RequestT["method"]): void { - switch (method as ClientRequest["method"]) { - case "logging/setLevel": - if (!this._serverCapabilities?.logging) { - throw new Error( - `Server does not support logging (required for ${method})`, - ); - } - break; - - case "prompts/get": - case "prompts/list": - if (!this._serverCapabilities?.prompts) { - throw new Error( - `Server does not support prompts (required for ${method})`, - ); - } - break; - - case "resources/list": - case "resources/templates/list": - case "resources/read": - case "resources/subscribe": - case "resources/unsubscribe": - if (!this._serverCapabilities?.resources) { - throw new Error( - `Server does not support resources (required for ${method})`, - ); - } - - if ( - method === "resources/subscribe" && - !this._serverCapabilities.resources.subscribe - ) { - throw new Error( - `Server does not support resource subscriptions (required for ${method})`, - ); - } - - break; - - case "tools/call": - case "tools/list": - if (!this._serverCapabilities?.tools) { - throw new Error( - `Server does not support tools (required for ${method})`, - ); - } - break; - - case "completion/complete": - if (!this._serverCapabilities?.completions) { - throw new Error( - `Server does not support completions (required for ${method})`, - ); - } - break; - - case "initialize": - // No specific capability required for initialize - break; - - case "ping": - // No specific capability required for ping - break; - } - } - - protected assertNotificationCapability( - method: NotificationT["method"], - ): void { - switch (method as ClientNotification["method"]) { - case "notifications/roots/list_changed": - if (!this._capabilities.roots?.listChanged) { - throw new Error( - `Client does not support roots list changed notifications (required for ${method})`, - ); - } - break; - - case "notifications/initialized": - // No specific capability required for initialized - break; - - case "notifications/cancelled": - // Cancellation notifications are always allowed - break; - - case "notifications/progress": - // Progress notifications are always allowed - break; - } - } - - protected assertRequestHandlerCapability(method: string): void { - switch (method) { - case "sampling/createMessage": - if (!this._capabilities.sampling) { - throw new Error( - `Client does not support sampling capability (required for ${method})`, - ); - } - break; - - case "elicitation/create": - if (!this._capabilities.elicitation) { - throw new Error( - `Client does not support elicitation capability (required for ${method})`, - ); - } - break; - - case "roots/list": - if (!this._capabilities.roots) { - throw new Error( - `Client does not support roots capability (required for ${method})`, - ); - } - break; - - case "ping": - // No specific capability required for ping - break; - } - } - - async ping(options?: RequestOptions) { - return this.request({ method: "ping" }, EmptyResultSchema, options); - } - - async complete(params: CompleteRequest["params"], options?: RequestOptions) { - return this.request( - { method: "completion/complete", params }, - CompleteResultSchema, - options, - ); - } - - async setLoggingLevel(level: LoggingLevel, options?: RequestOptions) { - return this.request( - { method: "logging/setLevel", params: { level } }, - EmptyResultSchema, - options, - ); - } - - async getPrompt( - params: GetPromptRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "prompts/get", params }, - GetPromptResultSchema, - options, - ); - } - - async listPrompts( - params?: ListPromptsRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "prompts/list", params }, - ListPromptsResultSchema, - options, - ); - } - - async listResources( - params?: ListResourcesRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "resources/list", params }, - ListResourcesResultSchema, - options, - ); - } - - async listResourceTemplates( - params?: ListResourceTemplatesRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "resources/templates/list", params }, - ListResourceTemplatesResultSchema, - options, - ); - } - - async readResource( - params: ReadResourceRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "resources/read", params }, - ReadResourceResultSchema, - options, - ); - } - - async subscribeResource( - params: SubscribeRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "resources/subscribe", params }, - EmptyResultSchema, - options, - ); - } - - async unsubscribeResource( - params: UnsubscribeRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "resources/unsubscribe", params }, - EmptyResultSchema, - options, - ); - } - - async callTool( - params: CallToolRequest["params"], - resultSchema: - | typeof CallToolResultSchema - | typeof CompatibilityCallToolResultSchema = CallToolResultSchema, - options?: RequestOptions, - ) { - const result = await this.request( - { method: "tools/call", params }, - resultSchema, - options, - ); - - // Check if the tool has an outputSchema - const validator = this.getToolOutputValidator(params.name); - if (validator) { - // If tool has outputSchema, it MUST return structuredContent (unless it's an error) - if (!result.structuredContent && !result.isError) { - throw new McpError( - ErrorCode.InvalidRequest, - `Tool ${params.name} has an output schema but did not return structured content` - ); - } - - // Only validate structured content if present (not when there's an error) - if (result.structuredContent) { - try { - // Validate the structured content (which is already an object) against the schema - const isValid = validator(result.structuredContent); - - if (!isValid) { - throw new McpError( - ErrorCode.InvalidParams, - `Structured content does not match the tool's output schema: ${this._ajv.errorsText(validator.errors)}` - ); - } - } catch (error) { - if (error instanceof McpError) { - throw error; - } - throw new McpError( - ErrorCode.InvalidParams, - `Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}` - ); - } - } - } - - return result; - } - - private cacheToolOutputSchemas(tools: Tool[]) { - this._cachedToolOutputValidators.clear(); - - for (const tool of tools) { - // If the tool has an outputSchema, create and cache the Ajv validator - if (tool.outputSchema) { - try { - const validator = this._ajv.compile(tool.outputSchema); - this._cachedToolOutputValidators.set(tool.name, validator); - } catch { - // Ignore schema compilation errors - } - } - } - } - - private getToolOutputValidator(toolName: string): ValidateFunction | undefined { - return this._cachedToolOutputValidators.get(toolName); - } - - async listTools( - params?: ListToolsRequest["params"], - options?: RequestOptions, - ) { - const result = await this.request( - { method: "tools/list", params }, - ListToolsResultSchema, - options, - ); - - // Cache the tools and their output schemas for future validation - this.cacheToolOutputSchemas(result.tools); - - return result; - } - - async sendRootsListChanged() { - return this.notification({ method: "notifications/roots/list_changed" }); - } -} +// Client exports +export * from './auth.js'; +export * from './client.js'; +export * from './sse.js'; +export * from './stdio.js'; +export * from './streamableHttp.js'; +export * from './websocket.js'; \ No newline at end of file diff --git a/src/server/auth/index.ts b/src/server/auth/index.ts new file mode 100644 index 000000000..e59cb12d8 --- /dev/null +++ b/src/server/auth/index.ts @@ -0,0 +1,21 @@ +// Auth exports - root auth level +export * from './clients.js'; +export * from './errors.js'; +export * from './provider.js'; +export * from './router.js'; +export * from './types.js'; + +// Auth exports - handlers +export * from './handlers/authorize.js'; +export * from './handlers/metadata.js'; +export * from './handlers/register.js'; +export * from './handlers/revoke.js'; +export * from './handlers/token.js'; + +// Auth exports - middleware +export * from './middleware/allowedMethods.js'; +export * from './middleware/bearerAuth.js'; +export * from './middleware/clientAuth.js'; + +// Auth exports - providers +export * from './providers/proxyProvider.js'; \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 10ae2fadc..125145b55 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,386 +1,13 @@ -import { - mergeCapabilities, - Protocol, - ProtocolOptions, - RequestOptions, -} from "../shared/protocol.js"; -import { - ClientCapabilities, - CreateMessageRequest, - CreateMessageResultSchema, - ElicitRequest, - ElicitResult, - ElicitResultSchema, - EmptyResultSchema, - Implementation, - InitializedNotificationSchema, - InitializeRequest, - InitializeRequestSchema, - InitializeResult, - LATEST_PROTOCOL_VERSION, - ListRootsRequest, - ListRootsResultSchema, - LoggingMessageNotification, - McpError, - ErrorCode, - Notification, - Request, - ResourceUpdatedNotification, - Result, - ServerCapabilities, - ServerNotification, - ServerRequest, - ServerResult, - SUPPORTED_PROTOCOL_VERSIONS, -} from "../types.js"; -import Ajv from "ajv"; +// Server exports +export * from './completable.js' +export * from './mcp.js'; +export * from './server.js'; +export * from './sse.js'; +export * from './stdio.js'; +export * from './streamableHttp.js'; -export type ServerOptions = ProtocolOptions & { - /** - * Capabilities to advertise as being supported by this server. - */ - capabilities?: ServerCapabilities; +// Auth exports +export * from './auth/index.js'; - /** - * Optional instructions describing how to use the server and its features. - */ - instructions?: string; -}; -/** - * An MCP server on top of a pluggable transport. - * - * This server will automatically respond to the initialization flow as initiated from the client. - * - * To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters: - * - * ```typescript - * // Custom schemas - * const CustomRequestSchema = RequestSchema.extend({...}) - * const CustomNotificationSchema = NotificationSchema.extend({...}) - * const CustomResultSchema = ResultSchema.extend({...}) - * - * // Type aliases - * type CustomRequest = z.infer - * type CustomNotification = z.infer - * type CustomResult = z.infer - * - * // Create typed server - * const server = new Server({ - * name: "CustomServer", - * version: "1.0.0" - * }) - * ``` - */ -export class Server< - RequestT extends Request = Request, - NotificationT extends Notification = Notification, - ResultT extends Result = Result, -> extends Protocol< - ServerRequest | RequestT, - ServerNotification | NotificationT, - ServerResult | ResultT -> { - private _clientCapabilities?: ClientCapabilities; - private _clientVersion?: Implementation; - private _capabilities: ServerCapabilities; - private _instructions?: string; - /** - * Callback for when initialization has fully completed (i.e., the client has sent an `initialized` notification). - */ - oninitialized?: () => void; - - /** - * Initializes this server with the given name and version information. - */ - constructor( - private _serverInfo: Implementation, - options?: ServerOptions, - ) { - super(options); - this._capabilities = options?.capabilities ?? {}; - this._instructions = options?.instructions; - - this.setRequestHandler(InitializeRequestSchema, (request) => - this._oninitialize(request), - ); - this.setNotificationHandler(InitializedNotificationSchema, () => - this.oninitialized?.(), - ); - } - - /** - * Registers new capabilities. This can only be called before connecting to a transport. - * - * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization). - */ - public registerCapabilities(capabilities: ServerCapabilities): void { - if (this.transport) { - throw new Error( - "Cannot register capabilities after connecting to transport", - ); - } - - this._capabilities = mergeCapabilities(this._capabilities, capabilities); - } - - protected assertCapabilityForMethod(method: RequestT["method"]): void { - switch (method as ServerRequest["method"]) { - case "sampling/createMessage": - if (!this._clientCapabilities?.sampling) { - throw new Error( - `Client does not support sampling (required for ${method})`, - ); - } - break; - - case "elicitation/create": - if (!this._clientCapabilities?.elicitation) { - throw new Error( - `Client does not support elicitation (required for ${method})`, - ); - } - break; - - case "roots/list": - if (!this._clientCapabilities?.roots) { - throw new Error( - `Client does not support listing roots (required for ${method})`, - ); - } - break; - - case "ping": - // No specific capability required for ping - break; - } - } - - protected assertNotificationCapability( - method: (ServerNotification | NotificationT)["method"], - ): void { - switch (method as ServerNotification["method"]) { - case "notifications/message": - if (!this._capabilities.logging) { - throw new Error( - `Server does not support logging (required for ${method})`, - ); - } - break; - - case "notifications/resources/updated": - case "notifications/resources/list_changed": - if (!this._capabilities.resources) { - throw new Error( - `Server does not support notifying about resources (required for ${method})`, - ); - } - break; - - case "notifications/tools/list_changed": - if (!this._capabilities.tools) { - throw new Error( - `Server does not support notifying of tool list changes (required for ${method})`, - ); - } - break; - - case "notifications/prompts/list_changed": - if (!this._capabilities.prompts) { - throw new Error( - `Server does not support notifying of prompt list changes (required for ${method})`, - ); - } - break; - - case "notifications/cancelled": - // Cancellation notifications are always allowed - break; - - case "notifications/progress": - // Progress notifications are always allowed - break; - } - } - - protected assertRequestHandlerCapability(method: string): void { - switch (method) { - case "sampling/createMessage": - if (!this._capabilities.sampling) { - throw new Error( - `Server does not support sampling (required for ${method})`, - ); - } - break; - - case "logging/setLevel": - if (!this._capabilities.logging) { - throw new Error( - `Server does not support logging (required for ${method})`, - ); - } - break; - - case "prompts/get": - case "prompts/list": - if (!this._capabilities.prompts) { - throw new Error( - `Server does not support prompts (required for ${method})`, - ); - } - break; - - case "resources/list": - case "resources/templates/list": - case "resources/read": - if (!this._capabilities.resources) { - throw new Error( - `Server does not support resources (required for ${method})`, - ); - } - break; - - case "tools/call": - case "tools/list": - if (!this._capabilities.tools) { - throw new Error( - `Server does not support tools (required for ${method})`, - ); - } - break; - - case "ping": - case "initialize": - // No specific capability required for these methods - break; - } - } - - private async _oninitialize( - request: InitializeRequest, - ): Promise { - const requestedVersion = request.params.protocolVersion; - - this._clientCapabilities = request.params.capabilities; - this._clientVersion = request.params.clientInfo; - - const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) - ? requestedVersion - : LATEST_PROTOCOL_VERSION; - - return { - protocolVersion, - capabilities: this.getCapabilities(), - serverInfo: this._serverInfo, - ...(this._instructions && { instructions: this._instructions }), - }; - } - - /** - * After initialization has completed, this will be populated with the client's reported capabilities. - */ - getClientCapabilities(): ClientCapabilities | undefined { - return this._clientCapabilities; - } - - /** - * After initialization has completed, this will be populated with information about the client's name and version. - */ - getClientVersion(): Implementation | undefined { - return this._clientVersion; - } - - private getCapabilities(): ServerCapabilities { - return this._capabilities; - } - - async ping() { - return this.request({ method: "ping" }, EmptyResultSchema); - } - - async createMessage( - params: CreateMessageRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "sampling/createMessage", params }, - CreateMessageResultSchema, - options, - ); - } - - async elicitInput( - params: ElicitRequest["params"], - options?: RequestOptions, - ): Promise { - const result = await this.request( - { method: "elicitation/create", params }, - ElicitResultSchema, - options, - ); - - // Validate the response content against the requested schema if action is "accept" - if (result.action === "accept" && result.content) { - try { - const ajv = new Ajv(); - - const validate = ajv.compile(params.requestedSchema); - const isValid = validate(result.content); - - if (!isValid) { - throw new McpError( - ErrorCode.InvalidParams, - `Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`, - ); - } - } catch (error) { - if (error instanceof McpError) { - throw error; - } - throw new McpError( - ErrorCode.InternalError, - `Error validating elicitation response: ${error}`, - ); - } - } - - return result; - } - - async listRoots( - params?: ListRootsRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { method: "roots/list", params }, - ListRootsResultSchema, - options, - ); - } - - async sendLoggingMessage(params: LoggingMessageNotification["params"]) { - return this.notification({ method: "notifications/message", params }); - } - - async sendResourceUpdated(params: ResourceUpdatedNotification["params"]) { - return this.notification({ - method: "notifications/resources/updated", - params, - }); - } - - async sendResourceListChanged() { - return this.notification({ - method: "notifications/resources/list_changed", - }); - } - - async sendToolListChanged() { - return this.notification({ method: "notifications/tools/list_changed" }); - } - - async sendPromptListChanged() { - return this.notification({ method: "notifications/prompts/list_changed" }); - } -} diff --git a/src/server/index.test.ts b/src/server/server.test.ts similarity index 99% rename from src/server/index.test.ts rename to src/server/server.test.ts index 46205d726..ae7aac505 100644 --- a/src/server/index.test.ts +++ b/src/server/server.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-constant-binary-expression */ /* eslint-disable @typescript-eslint/no-unused-expressions */ -import { Server } from "./index.js"; +import { Server } from "./server.js"; import { z } from "zod"; import { RequestSchema, diff --git a/src/server/server.ts b/src/server/server.ts new file mode 100644 index 000000000..2b1b8e619 --- /dev/null +++ b/src/server/server.ts @@ -0,0 +1,387 @@ +import { + mergeCapabilities, + Protocol, + ProtocolOptions, + RequestOptions, + } from "../shared/protocol.js"; + import { + ClientCapabilities, + CreateMessageRequest, + CreateMessageResultSchema, + ElicitRequest, + ElicitResult, + ElicitResultSchema, + EmptyResultSchema, + Implementation, + InitializedNotificationSchema, + InitializeRequest, + InitializeRequestSchema, + InitializeResult, + LATEST_PROTOCOL_VERSION, + ListRootsRequest, + ListRootsResultSchema, + LoggingMessageNotification, + McpError, + ErrorCode, + Notification, + Request, + ResourceUpdatedNotification, + Result, + ServerCapabilities, + ServerNotification, + ServerRequest, + ServerResult, + SUPPORTED_PROTOCOL_VERSIONS, + } from "../types.js"; + import Ajv from "ajv"; + + export type ServerOptions = ProtocolOptions & { + /** + * Capabilities to advertise as being supported by this server. + */ + capabilities?: ServerCapabilities; + + /** + * Optional instructions describing how to use the server and its features. + */ + instructions?: string; + }; + + /** + * An MCP server on top of a pluggable transport. + * + * This server will automatically respond to the initialization flow as initiated from the client. + * + * To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters: + * + * ```typescript + * // Custom schemas + * const CustomRequestSchema = RequestSchema.extend({...}) + * const CustomNotificationSchema = NotificationSchema.extend({...}) + * const CustomResultSchema = ResultSchema.extend({...}) + * + * // Type aliases + * type CustomRequest = z.infer + * type CustomNotification = z.infer + * type CustomResult = z.infer + * + * // Create typed server + * const server = new Server({ + * name: "CustomServer", + * version: "1.0.0" + * }) + * ``` + */ + export class Server< + RequestT extends Request = Request, + NotificationT extends Notification = Notification, + ResultT extends Result = Result, + > extends Protocol< + ServerRequest | RequestT, + ServerNotification | NotificationT, + ServerResult | ResultT + > { + private _clientCapabilities?: ClientCapabilities; + private _clientVersion?: Implementation; + private _capabilities: ServerCapabilities; + private _instructions?: string; + + /** + * Callback for when initialization has fully completed (i.e., the client has sent an `initialized` notification). + */ + oninitialized?: () => void; + + /** + * Initializes this server with the given name and version information. + */ + constructor( + private _serverInfo: Implementation, + options?: ServerOptions, + ) { + super(options); + this._capabilities = options?.capabilities ?? {}; + this._instructions = options?.instructions; + + this.setRequestHandler(InitializeRequestSchema, (request) => + this._oninitialize(request), + ); + this.setNotificationHandler(InitializedNotificationSchema, () => + this.oninitialized?.(), + ); + } + + /** + * Registers new capabilities. This can only be called before connecting to a transport. + * + * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization). + */ + public registerCapabilities(capabilities: ServerCapabilities): void { + if (this.transport) { + throw new Error( + "Cannot register capabilities after connecting to transport", + ); + } + + this._capabilities = mergeCapabilities(this._capabilities, capabilities); + } + + protected assertCapabilityForMethod(method: RequestT["method"]): void { + switch (method as ServerRequest["method"]) { + case "sampling/createMessage": + if (!this._clientCapabilities?.sampling) { + throw new Error( + `Client does not support sampling (required for ${method})`, + ); + } + break; + + case "elicitation/create": + if (!this._clientCapabilities?.elicitation) { + throw new Error( + `Client does not support elicitation (required for ${method})`, + ); + } + break; + + case "roots/list": + if (!this._clientCapabilities?.roots) { + throw new Error( + `Client does not support listing roots (required for ${method})`, + ); + } + break; + + case "ping": + // No specific capability required for ping + break; + } + } + + protected assertNotificationCapability( + method: (ServerNotification | NotificationT)["method"], + ): void { + switch (method as ServerNotification["method"]) { + case "notifications/message": + if (!this._capabilities.logging) { + throw new Error( + `Server does not support logging (required for ${method})`, + ); + } + break; + + case "notifications/resources/updated": + case "notifications/resources/list_changed": + if (!this._capabilities.resources) { + throw new Error( + `Server does not support notifying about resources (required for ${method})`, + ); + } + break; + + case "notifications/tools/list_changed": + if (!this._capabilities.tools) { + throw new Error( + `Server does not support notifying of tool list changes (required for ${method})`, + ); + } + break; + + case "notifications/prompts/list_changed": + if (!this._capabilities.prompts) { + throw new Error( + `Server does not support notifying of prompt list changes (required for ${method})`, + ); + } + break; + + case "notifications/cancelled": + // Cancellation notifications are always allowed + break; + + case "notifications/progress": + // Progress notifications are always allowed + break; + } + } + + protected assertRequestHandlerCapability(method: string): void { + switch (method) { + case "sampling/createMessage": + if (!this._capabilities.sampling) { + throw new Error( + `Server does not support sampling (required for ${method})`, + ); + } + break; + + case "logging/setLevel": + if (!this._capabilities.logging) { + throw new Error( + `Server does not support logging (required for ${method})`, + ); + } + break; + + case "prompts/get": + case "prompts/list": + if (!this._capabilities.prompts) { + throw new Error( + `Server does not support prompts (required for ${method})`, + ); + } + break; + + case "resources/list": + case "resources/templates/list": + case "resources/read": + if (!this._capabilities.resources) { + throw new Error( + `Server does not support resources (required for ${method})`, + ); + } + break; + + case "tools/call": + case "tools/list": + if (!this._capabilities.tools) { + throw new Error( + `Server does not support tools (required for ${method})`, + ); + } + break; + + case "ping": + case "initialize": + // No specific capability required for these methods + break; + } + } + + private async _oninitialize( + request: InitializeRequest, + ): Promise { + const requestedVersion = request.params.protocolVersion; + + this._clientCapabilities = request.params.capabilities; + this._clientVersion = request.params.clientInfo; + + const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) + ? requestedVersion + : LATEST_PROTOCOL_VERSION; + + return { + protocolVersion, + capabilities: this.getCapabilities(), + serverInfo: this._serverInfo, + ...(this._instructions && { instructions: this._instructions }), + }; + } + + /** + * After initialization has completed, this will be populated with the client's reported capabilities. + */ + getClientCapabilities(): ClientCapabilities | undefined { + return this._clientCapabilities; + } + + /** + * After initialization has completed, this will be populated with information about the client's name and version. + */ + getClientVersion(): Implementation | undefined { + return this._clientVersion; + } + + private getCapabilities(): ServerCapabilities { + return this._capabilities; + } + + async ping() { + return this.request({ method: "ping" }, EmptyResultSchema); + } + + async createMessage( + params: CreateMessageRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "sampling/createMessage", params }, + CreateMessageResultSchema, + options, + ); + } + + async elicitInput( + params: ElicitRequest["params"], + options?: RequestOptions, + ): Promise { + const result = await this.request( + { method: "elicitation/create", params }, + ElicitResultSchema, + options, + ); + + // Validate the response content against the requested schema if action is "accept" + if (result.action === "accept" && result.content) { + try { + const ajv = new Ajv(); + + const validate = ajv.compile(params.requestedSchema); + const isValid = validate(result.content); + + if (!isValid) { + throw new McpError( + ErrorCode.InvalidParams, + `Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`, + ); + } + } catch (error) { + if (error instanceof McpError) { + throw error; + } + throw new McpError( + ErrorCode.InternalError, + `Error validating elicitation response: ${error}`, + ); + } + } + + return result; + } + + async listRoots( + params?: ListRootsRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { method: "roots/list", params }, + ListRootsResultSchema, + options, + ); + } + + async sendLoggingMessage(params: LoggingMessageNotification["params"]) { + return this.notification({ method: "notifications/message", params }); + } + + async sendResourceUpdated(params: ResourceUpdatedNotification["params"]) { + return this.notification({ + method: "notifications/resources/updated", + params, + }); + } + + async sendResourceListChanged() { + return this.notification({ + method: "notifications/resources/list_changed", + }); + } + + async sendToolListChanged() { + return this.notification({ method: "notifications/tools/list_changed" }); + } + + async sendPromptListChanged() { + return this.notification({ method: "notifications/prompts/list_changed" }); + } + } + \ No newline at end of file diff --git a/src/shared/index.ts b/src/shared/index.ts new file mode 100644 index 000000000..b013b01ff --- /dev/null +++ b/src/shared/index.ts @@ -0,0 +1,7 @@ +export * from './auth-utils.js'; +export * from './auth.js'; +export * from './metadataUtils.js'; +export * from './protocol.js'; +export * from './stdio.js'; +export * from './transport.js'; +export * from './uriTemplate.js'; \ No newline at end of file From f4b0f454ce34af6b9b616bd02cc8f3d6b8730424 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 12 Aug 2025 22:07:23 +0300 Subject: [PATCH 2/2] add inMemory and main types.ts to shared exports --- src/shared/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shared/index.ts b/src/shared/index.ts index b013b01ff..cdd8070b7 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -4,4 +4,6 @@ export * from './metadataUtils.js'; export * from './protocol.js'; export * from './stdio.js'; export * from './transport.js'; -export * from './uriTemplate.js'; \ No newline at end of file +export * from './uriTemplate.js'; +export * from '../types.js'; +export * from '../inMemory.js'; \ No newline at end of file