Skip to content
Open
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
13,508 changes: 6,816 additions & 6,692 deletions package-lock.json

Large diffs are not rendered by default.

34 changes: 32 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@
"import": "./dist/esm/server/index.js",
"require": "./dist/cjs/server/index.js"
},
"./validation": {
"import": "./dist/esm/validation/index.js",
"require": "./dist/cjs/validation/index.js"
},
"./validation/ajv": {
"import": "./dist/esm/validation/ajv-provider.js",
"require": "./dist/cjs/validation/ajv-provider.js"
},
"./validation/cfworker": {
"import": "./dist/esm/validation/cfworker-provider.js",
"require": "./dist/cjs/validation/cfworker-provider.js"
},
"./*": {
"import": "./dist/esm/*",
"require": "./dist/cjs/*"
Expand Down Expand Up @@ -63,7 +75,6 @@
"client": "tsx src/cli.ts client"
},
"dependencies": {
"ajv": "^6.12.6",
"content-type": "^1.0.5",
"cors": "^2.8.5",
"cross-spawn": "^7.0.5",
Expand All @@ -76,7 +87,24 @@
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
},
"peerDependencies": {
"@cfworker/json-schema": "^4.1.1",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
},
"ajv-formats": {
"optional": true
},
"@cfworker/json-schema": {
"optional": true
}
},
"devDependencies": {
"@cfworker/json-schema": "^4.1.1",
"@eslint/js": "^9.8.0",
"@jest-mock/express": "^3.0.0",
"@types/content-type": "^1.1.8",
Expand All @@ -89,10 +117,12 @@
"@types/node": "^22.0.2",
"@types/supertest": "^6.0.2",
"@types/ws": "^8.5.12",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"eslint": "^9.8.0",
"eslint-config-prettier": "^10.1.8",
"prettier": "3.6.2",
"jest": "^29.7.0",
"prettier": "3.6.2",
"supertest": "^7.0.0",
"ts-jest": "^29.2.4",
"tsx": "^4.16.5",
Expand Down
122 changes: 78 additions & 44 deletions src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,82 @@
import { mergeCapabilities, Protocol, ProtocolOptions, RequestOptions } from '../shared/protocol.js';
import { Transport } from '../shared/transport.js';
import { mergeCapabilities, Protocol, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js';
import type { Transport } from '../shared/transport.js';
import {
CallToolRequest,
type CallToolRequest,
CallToolResultSchema,
ClientCapabilities,
ClientNotification,
ClientRequest,
ClientResult,
CompatibilityCallToolResultSchema,
CompleteRequest,
type ClientCapabilities,
type ClientNotification,
type ClientRequest,
type ClientResult,
type CompatibilityCallToolResultSchema,
type CompleteRequest,
CompleteResultSchema,
EmptyResultSchema,
GetPromptRequest,
ErrorCode,
type GetPromptRequest,
GetPromptResultSchema,
Implementation,
type Implementation,
InitializeResultSchema,
LATEST_PROTOCOL_VERSION,
ListPromptsRequest,
type ListPromptsRequest,
ListPromptsResultSchema,
ListResourcesRequest,
type ListResourcesRequest,
ListResourcesResultSchema,
ListResourceTemplatesRequest,
type ListResourceTemplatesRequest,
ListResourceTemplatesResultSchema,
ListToolsRequest,
type ListToolsRequest,
ListToolsResultSchema,
LoggingLevel,
Notification,
ReadResourceRequest,
type LoggingLevel,
McpError,
type Notification,
type ReadResourceRequest,
ReadResourceResultSchema,
Request,
Result,
ServerCapabilities,
SubscribeRequest,
type Request,
type Result,
type ServerCapabilities,
SUPPORTED_PROTOCOL_VERSIONS,
UnsubscribeRequest,
Tool,
ErrorCode,
McpError
type SubscribeRequest,
type Tool,
type UnsubscribeRequest
} from '../types.js';
import Ajv from 'ajv';
import type { ValidateFunction } from 'ajv';
import { AjvJsonSchemaValidator } from '../validation/ajv-provider.js';
import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from '../validation/types.js';

export type ClientOptions = ProtocolOptions & {
/**
* Capabilities to advertise as being supported by this client.
*/
capabilities?: ClientCapabilities;

/**
* JSON Schema validator for tool output validation.
*
* The validator is used to validate structured content returned by tools
* against their declared output schemas.
*
* @default AjvJsonSchemaValidator
*
* @example
* ```typescript
* // ajv
* const client = new Client(
* { name: 'my-client', version: '1.0.0' },
* {
* capabilities: {},
* jsonSchemaValidator: new AjvJsonSchemaValidator()
* }
* );
*
* // @cfworker/json-schema
* const client = new Client(
* { name: 'my-client', version: '1.0.0' },
* {
* capabilities: {},
* jsonSchemaValidator: new CfWorkerJsonSchemaValidator()
* }
* );
* ```
*/
jsonSchemaValidator?: jsonSchemaValidator;
};

/**
Expand Down Expand Up @@ -82,8 +113,8 @@ export class Client<
private _serverVersion?: Implementation;
private _capabilities: ClientCapabilities;
private _instructions?: string;
private _cachedToolOutputValidators: Map<string, ValidateFunction> = new Map();
private _ajv: InstanceType<typeof Ajv>;
private _jsonSchemaValidator: jsonSchemaValidator;
private _cachedToolOutputValidators: Map<string, JsonSchemaValidator<unknown>> = new Map();

/**
* Initializes this client with the given name and version information.
Expand All @@ -94,7 +125,7 @@ export class Client<
) {
super(options);
this._capabilities = options?.capabilities ?? {};
this._ajv = new Ajv();
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
}

/**
Expand Down Expand Up @@ -347,13 +378,13 @@ export class Client<
// 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);
// Validate the structured content against the schema
const validationResult = validator(result.structuredContent);

if (!isValid) {
if (!validationResult.valid) {
throw new McpError(
ErrorCode.InvalidParams,
`Structured content does not match the tool's output schema: ${this._ajv.errorsText(validator.errors)}`
`Structured content does not match the tool's output schema: ${validationResult.errorMessage}`
);
}
} catch (error) {
Expand All @@ -371,23 +402,26 @@ export class Client<
return result;
}

private cacheToolOutputSchemas(tools: Tool[]) {
/**
* Cache validators for tool output schemas.
* Called after listTools() to pre-compile validators for better performance.
*/
private cacheToolOutputSchemas(tools: Tool[]): void {
this._cachedToolOutputValidators.clear();

for (const tool of tools) {
// If the tool has an outputSchema, create and cache the Ajv validator
// If the tool has an outputSchema, create and cache the validator
if (tool.outputSchema) {
try {
const validator = this._ajv.compile(tool.outputSchema);
this._cachedToolOutputValidators.set(tool.name, validator);
} catch {
// Ignore schema compilation errors
}
const toolValidator = this._jsonSchemaValidator.getValidator(tool.outputSchema as JsonSchemaType);
this._cachedToolOutputValidators.set(tool.name, toolValidator);
}
}
}

private getToolOutputValidator(toolName: string): ValidateFunction | undefined {
/**
* Get cached validator for a tool
*/
private getToolOutputValidator(toolName: string): JsonSchemaValidator<unknown> | undefined {
return this._cachedToolOutputValidators.get(toolName);
}

Expand Down
4 changes: 2 additions & 2 deletions src/examples/client/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
ReadResourceResultSchema
} from '../../types.js';
import { getDisplayName } from '../../shared/metadataUtils.js';
import Ajv from 'ajv';
import { Ajv } from 'ajv';

// Create readline interface for user input
const readline = createInterface({
Expand Down Expand Up @@ -377,7 +377,7 @@ async function connect(url?: string): Promise<void> {
if (!isValid) {
console.log('❌ Validation errors:');
validate.errors?.forEach(error => {
console.log(` - ${error.dataPath || 'root'}: ${error.message}`);
console.log(` - ${error.instancePath || 'root'}: ${error.message}`);
});

if (attempts < maxAttempts) {
Expand Down
Loading