diff --git a/apps/gateway/src/lib/cf-validate-json-schema-object.ts b/apps/gateway/src/lib/cf-validate-json-schema.ts similarity index 74% rename from apps/gateway/src/lib/cf-validate-json-schema-object.ts rename to apps/gateway/src/lib/cf-validate-json-schema.ts index 87f71e94..5e89b402 100644 --- a/apps/gateway/src/lib/cf-validate-json-schema-object.ts +++ b/apps/gateway/src/lib/cf-validate-json-schema.ts @@ -1,10 +1,10 @@ import type { ContentfulStatusCode } from 'hono/utils/http-status' import { Validator } from '@agentic/json-schema' -import { HttpError } from '@agentic/platform-core' +import { assert, HttpError } from '@agentic/platform-core' import plur from 'plur' /** - * Validates `data` against the provided JSON schema object. + * Validates `data` against the provided JSON schema. * * This method uses a fork of `@cfworker/json-schema`. It does not use `ajv` * because `ajv` is not supported on CF workers due to its dynamic code @@ -14,9 +14,7 @@ import plur from 'plur' * not running on CF workers, consider using `validateJsonSchemaObject` from * `@agentic/platform-openapi-utils`. */ -export function cfValidateJsonSchemaObject< - T extends Record = Record ->({ +export function cfValidateJsonSchema({ schema, data, coerce = false, @@ -25,16 +23,29 @@ export function cfValidateJsonSchemaObject< errorStatusCode = 400 }: { schema: any - data: Record + data: unknown coerce?: boolean strictAdditionalProperties?: boolean errorMessage?: string errorStatusCode?: ContentfulStatusCode }): T { - // Special-case check for required fields to give better error messages. - if (schema.required && Array.isArray(schema.required)) { + assert(schema, 400, '`schema` is required') + const isSchemaObject = + typeof schema === 'object' && + !Array.isArray(schema) && + schema.type === 'object' + const isDataObject = typeof data === 'object' && !Array.isArray(data) + if (isSchemaObject && !isDataObject) { + throw new HttpError({ + statusCode: 400, + message: `${errorMessage ? errorMessage + ': ' : ''}Data must be an object according to its schema.` + }) + } + + // Special-case check for required fields to give better error messages + if (isSchemaObject && Array.isArray(schema.required)) { const missingRequiredFields: string[] = schema.required.filter( - (field: string) => (data as T)[field] === undefined + (field: string) => (data as Record)[field] === undefined ) if (missingRequiredFields.length > 0) { @@ -48,11 +59,12 @@ export function cfValidateJsonSchemaObject< // Special-case check for additional top-level fields to give better error // messages. if ( + isSchemaObject && schema.properties && (schema.additionalProperties === false || (schema.additionalProperties === undefined && strictAdditionalProperties)) ) { - const extraProperties = Object.keys(data).filter( + const extraProperties = Object.keys(data as Record).filter( (key) => !schema.properties[key] ) diff --git a/apps/gateway/src/lib/create-http-response-from-mcp-tool-call-response.ts b/apps/gateway/src/lib/create-http-response-from-mcp-tool-call-response.ts index 2715d254..eaa5d7fa 100644 --- a/apps/gateway/src/lib/create-http-response-from-mcp-tool-call-response.ts +++ b/apps/gateway/src/lib/create-http-response-from-mcp-tool-call-response.ts @@ -2,7 +2,7 @@ import type { AdminDeployment, Tool } from '@agentic/platform-types' import { assert } from '@agentic/platform-core' import type { GatewayHonoContext, McpToolCallResponse } from './types' -import { cfValidateJsonSchemaObject } from './cf-validate-json-schema-object' +import { cfValidateJsonSchema } from './cf-validate-json-schema' export async function createHttpResponseFromMcpToolCallResponse( _ctx: GatewayHonoContext, @@ -35,7 +35,7 @@ export async function createHttpResponseFromMcpToolCallResponse( ) // Validate tool response against the tool's output schema. - const toolCallResponseContent = cfValidateJsonSchemaObject({ + const toolCallResponseContent = cfValidateJsonSchema({ schema: tool.outputSchema, data: toolCallResponse.structuredContent as Record, coerce: false, diff --git a/apps/gateway/src/lib/get-tool-args-from-request.ts b/apps/gateway/src/lib/get-tool-args-from-request.ts index 2b561861..d95c3c29 100644 --- a/apps/gateway/src/lib/get-tool-args-from-request.ts +++ b/apps/gateway/src/lib/get-tool-args-from-request.ts @@ -2,7 +2,7 @@ import type { AdminDeployment, Tool } from '@agentic/platform-types' import { assert } from '@agentic/platform-core' import type { GatewayHonoContext } from './types' -import { cfValidateJsonSchemaObject } from './cf-validate-json-schema-object' +import { cfValidateJsonSchema } from './cf-validate-json-schema' export async function getToolArgsFromRequest( ctx: GatewayHonoContext, @@ -48,7 +48,7 @@ export async function getToolArgsFromRequest( } // Validate incoming request params against the tool's input schema. - const incomingRequestArgs = cfValidateJsonSchemaObject({ + const incomingRequestArgs = cfValidateJsonSchema>({ schema: tool.inputSchema, data: incomingRequestArgsRaw, errorMessage: `Invalid request parameters for tool "${tool.name}"`, diff --git a/packages/fixtures/valid/basic-mcp/src/index.ts b/packages/fixtures/valid/basic-mcp/src/index.ts index 6b4f1fee..2c344292 100644 --- a/packages/fixtures/valid/basic-mcp/src/index.ts +++ b/packages/fixtures/valid/basic-mcp/src/index.ts @@ -22,6 +22,18 @@ server.addTool({ } }) +server.addTool({ + name: 'add2', + description: 'TODO', + parameters: z.object({ + a: z.number(), + b: z.number() + }), + execute: async (args) => { + return String(args.a + args.b) + } +}) + await server.start({ transportType: 'httpStream', httpStream: { diff --git a/packages/openapi-utils/src/validate-json-schema-object.ts b/packages/openapi-utils/src/validate-json-schema-object.ts index b99c11d9..2b449378 100644 --- a/packages/openapi-utils/src/validate-json-schema-object.ts +++ b/packages/openapi-utils/src/validate-json-schema-object.ts @@ -1,4 +1,4 @@ -import { hashObject, HttpError } from '@agentic/platform-core' +import { assert, hashObject, HttpError } from '@agentic/platform-core' import { betterAjvErrors } from '@apideck/better-ajv-errors' import Ajv, { type ValidateFunction } from 'ajv' import addFormats from 'ajv-formats' @@ -19,7 +19,7 @@ const globalAjv = new Ajv({ addFormats(globalAjv) /** - * Validates `data` against the provided JSON schema object. + * Validates `data` against the provided JSON schema. * * This method uses `ajv` and is therefore not compatible with CF workers due * to its use of code generation and evaluation. @@ -29,9 +29,7 @@ addFormats(globalAjv) * * @see https://github.com/ajv-validator/ajv/issues/2318 */ -export function validateJsonSchemaObject< - T extends Record = Record ->({ +export function validateJsonSchema({ schema, data, ajv = globalAjv, @@ -42,10 +40,23 @@ export function validateJsonSchemaObject< ajv?: Ajv errorMessage?: string }): T { + assert(schema, 400, '`schema` is required') + const isSchemaObject = + typeof schema === 'object' && + !Array.isArray(schema) && + schema.type === 'object' + const isDataObject = typeof data === 'object' && !Array.isArray(data) + if (isSchemaObject && !isDataObject) { + throw new HttpError({ + statusCode: 400, + message: `${errorMessage ? errorMessage + ': ' : ''}Data must be an object according to its schema.` + }) + } + // Special-case check for required fields to give better error messages - if (Array.isArray(schema.required)) { + if (isSchemaObject && Array.isArray(schema.required)) { const missingRequiredFields: string[] = schema.required.filter( - (field: string) => (data as T)[field] === undefined + (field: string) => (data as Record)[field] === undefined ) if (missingRequiredFields.length > 0) {