diff --git a/packages/core/src/types/spec.types.ts b/packages/core/src/types/spec.types.ts index aa298e63c..b8fa1de20 100644 --- a/packages/core/src/types/spec.types.ts +++ b/packages/core/src/types/spec.types.ts @@ -379,6 +379,12 @@ export interface ClientCapabilities { }; }; }; + /** + * Present if the client supports declarative file inputs for tools and + * elicitation. When declared, servers MAY include `inputFiles` on {@link Tool} + * definitions and `requestedFiles` on {@link ElicitRequestFormParams}. + */ + fileInputs?: object; } /** @@ -1293,12 +1299,49 @@ export interface Tool extends BaseMetadata, Icons { */ annotations?: ToolAnnotations; + /** + * Declares which arguments in `inputSchema` are file inputs. Keys MUST match + * property names in `inputSchema.properties`, and the corresponding schema + * properties MUST be `{"type": "string", "format": "uri"}` or an array thereof. + * + * Servers MUST NOT include this field unless the client declared the + * `fileInputs` capability during initialization. + * + * Clients SHOULD render a native file picker for these arguments. Selected files + * are encoded as RFC 2397 data URIs: `data:;name=;base64,`, + * where the `name=` parameter (percent-encoded) carries the original filename. + */ + inputFiles?: { [argName: string]: FileInputDescriptor }; + /** * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. */ _meta?: { [key: string]: unknown }; } +/** + * Describes a single file input argument for a tool or elicitation form. + * Provides optional hints for client-side file picker filtering and validation. + * All fields are advisory; servers MUST still validate inputs independently. + * + * @category `tools/list` + */ +export interface FileInputDescriptor { + /** + * MIME type patterns that the server will accept for this input. + * Supports exact types (e.g., `"image/png"`) and wildcard subtypes + * (e.g., `"image/*"`). If omitted, any file type is accepted. + */ + accept?: string[]; + + /** + * Maximum file size in bytes (decoded size, per file). Servers SHOULD reject + * larger files with JSON-RPC `-32602` (Invalid Params) and the structured reason + * `"file_too_large"`. + */ + maxSize?: number; +} + /* Tasks */ /** @@ -2159,6 +2202,21 @@ export interface ElicitRequestFormParams extends TaskAugmentedRequestParams { }; required?: string[]; }; + + /** + * Declares which fields in `requestedSchema` are file inputs. Keys MUST match + * property names in `requestedSchema.properties`, and the corresponding schema + * properties MUST be a {@link StringSchema} with `format: "uri"` or a + * {@link StringArraySchema} whose `items` has `format: "uri"`. + * + * Servers MUST NOT include this field unless the client declared the + * `fileInputs` capability during initialization. + * + * Clients SHOULD render a native file picker for these fields. Selected files + * are encoded as RFC 2397 data URIs: `data:;name=;base64,`, + * where the `name=` parameter (percent-encoded) carries the original filename. + */ + requestedFiles?: { [fieldName: string]: FileInputDescriptor }; } /** @@ -2209,12 +2267,12 @@ export interface ElicitRequest extends JSONRPCRequest { } /** - * Restricted schema definitions that only allow primitive types - * without nested objects or arrays. + * Restricted schema definitions that only allow primitive types and + * flat arrays of strings (for multi-file inputs), without nested objects. * * @category `elicitation/create` */ -export type PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSchema | EnumSchema; +export type PrimitiveSchemaDefinition = StringSchema | NumberSchema | BooleanSchema | EnumSchema | StringArraySchema; /** * @category `elicitation/create` @@ -2433,6 +2491,22 @@ export interface LegacyTitledEnumSchema { // Union type for all enum schemas export type EnumSchema = SingleSelectEnumSchema | MultiSelectEnumSchema | LegacyTitledEnumSchema; +/** + * Schema for a flat array of strings. Intended primarily for multi-file + * inputs in elicitation forms, where each item is a data URI string with + * `format: "uri"`. Items MUST use {@link StringSchema}; nesting is not permitted. + * + * @category `elicitation/create` + */ +export interface StringArraySchema { + type: 'array'; + items: StringSchema; + title?: string; + description?: string; + minItems?: number; + maxItems?: number; +} + /** * The client's response to an elicitation request. * diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts index 6985154db..9fdc4d8fa 100644 --- a/packages/core/src/types/types.ts +++ b/packages/core/src/types/types.ts @@ -526,7 +526,13 @@ export const ClientCapabilitiesSchema = z.object({ /** * Present if the client supports task creation. */ - tasks: ClientTasksCapabilitySchema.optional() + tasks: ClientTasksCapabilitySchema.optional(), + /** + * Present if the client supports declarative file inputs for tools and + * elicitation. When declared, servers MAY include `inputFiles` on {@linkcode Tool} + * definitions and `requestedFiles` on form-mode elicitation parameters. + */ + fileInputs: AssertObjectSchema.optional() }); export const InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({ @@ -1374,6 +1380,26 @@ export const ToolExecutionSchema = z.object({ taskSupport: z.enum(['required', 'optional', 'forbidden']).optional() }); +/** + * Describes a single file input argument for a tool or elicitation form. + * Provides optional hints for client-side file picker filtering and validation. + * All fields are advisory; servers MUST still validate inputs independently. + */ +export const FileInputDescriptorSchema = z.object({ + /** + * MIME type patterns that the server will accept for this input. + * Supports exact types (e.g., `"image/png"`) and wildcard subtypes + * (e.g., `"image/*"`). If omitted, any file type is accepted. + */ + accept: z.array(z.string()).optional(), + /** + * Maximum file size in bytes (decoded size, per file). Servers SHOULD reject + * larger files with JSON-RPC `-32602` (Invalid Params) and the structured + * reason `"file_too_large"`. + */ + maxSize: z.number().int().optional() +}); + /** * Definition for a tool the client can call. */ @@ -1416,6 +1442,19 @@ export const ToolSchema = z.object({ * Execution-related properties for this tool. */ execution: ToolExecutionSchema.optional(), + /** + * Declares which arguments in `inputSchema` are file inputs. Keys MUST match + * property names in `inputSchema.properties`, and the corresponding schema + * properties MUST be `{"type": "string", "format": "uri"}` or an array thereof. + * + * Servers MUST NOT include this field unless the client declared the + * `fileInputs` capability during initialization. + * + * Clients SHOULD render a native file picker for these arguments. Selected files + * are encoded as RFC 2397 data URIs: `data:;name=;base64,`, + * where the `name=` parameter (percent-encoded) carries the original filename. + */ + inputFiles: z.record(z.string(), FileInputDescriptorSchema).optional(), /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) @@ -1970,10 +2009,30 @@ export const MultiSelectEnumSchemaSchema = z.union([UntitledMultiSelectEnumSchem */ export const EnumSchemaSchema = z.union([LegacyTitledEnumSchemaSchema, SingleSelectEnumSchemaSchema, MultiSelectEnumSchemaSchema]); +/** + * Schema for a flat array of strings. Intended primarily for multi-file + * inputs in elicitation forms, where each item is a data URI string with + * `format: "uri"`. Items MUST use {@linkcode StringSchema}; nesting is not permitted. + */ +export const StringArraySchemaSchema = z.object({ + type: z.literal('array'), + items: StringSchemaSchema, + title: z.string().optional(), + description: z.string().optional(), + minItems: z.number().int().optional(), + maxItems: z.number().int().optional() +}); + /** * Union of all primitive schema definitions. */ -export const PrimitiveSchemaDefinitionSchema = z.union([EnumSchemaSchema, BooleanSchemaSchema, StringSchemaSchema, NumberSchemaSchema]); +export const PrimitiveSchemaDefinitionSchema = z.union([ + EnumSchemaSchema, + BooleanSchemaSchema, + StringSchemaSchema, + NumberSchemaSchema, + StringArraySchemaSchema +]); /** * Parameters for an `elicitation/create` request for form-based elicitation. @@ -1997,7 +2056,21 @@ export const ElicitRequestFormParamsSchema = TaskAugmentedRequestParamsSchema.ex type: z.literal('object'), properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema), required: z.array(z.string()).optional() - }) + }), + /** + * Declares which fields in `requestedSchema` are file inputs. Keys MUST match + * property names in `requestedSchema.properties`, and the corresponding schema + * properties MUST be a {@linkcode StringSchema} with `format: "uri"` or a + * {@linkcode StringArraySchema} whose `items` has `format: "uri"`. + * + * Servers MUST NOT include this field unless the client declared the + * `fileInputs` capability during initialization. + * + * Clients SHOULD render a native file picker for these fields. Selected files + * are encoded as RFC 2397 data URIs: `data:;name=;base64,`, + * where the `name=` parameter (percent-encoded) carries the original filename. + */ + requestedFiles: z.record(z.string(), FileInputDescriptorSchema).optional() }); /** @@ -2516,6 +2589,7 @@ export type PromptListChangedNotification = Infer; export type ToolExecution = Infer; +export type FileInputDescriptor = Infer; export type Tool = Infer; export type ListToolsRequest = Infer; export type ListToolsResult = Infer; @@ -2570,6 +2644,7 @@ export type UntitledMultiSelectEnumSchema = Infer; export type SingleSelectEnumSchema = Infer; export type MultiSelectEnumSchema = Infer; +export type StringArraySchema = Infer; export type PrimitiveSchemaDefinition = Infer; export type ElicitRequestParams = Infer; diff --git a/packages/core/test/spec.types.test.ts b/packages/core/test/spec.types.test.ts index 8ec29b096..4f0de296d 100644 --- a/packages/core/test/spec.types.test.ts +++ b/packages/core/test/spec.types.test.ts @@ -523,6 +523,10 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, + StringArraySchema: (sdk: SDKTypes.StringArraySchema, spec: SpecTypes.StringArraySchema) => { + sdk = spec; + spec = sdk; + }, JSONRPCErrorResponse: (sdk: SDKTypes.JSONRPCErrorResponse, spec: SpecTypes.JSONRPCErrorResponse) => { sdk = spec; spec = sdk; @@ -629,6 +633,10 @@ const sdkTypeChecks = { sdk = spec; spec = sdk; }, + FileInputDescriptor: (sdk: SDKTypes.FileInputDescriptor, spec: SpecTypes.FileInputDescriptor) => { + sdk = spec; + spec = sdk; + }, TaskStatus: (sdk: SDKTypes.TaskStatus, spec: SpecTypes.TaskStatus) => { sdk = spec; spec = sdk; @@ -714,7 +722,7 @@ describe('Spec Types', () => { it('should define some expected types', () => { expect(specTypes).toContain('JSONRPCNotification'); expect(specTypes).toContain('ElicitResult'); - expect(specTypes).toHaveLength(145); + expect(specTypes).toHaveLength(147); }); it('should have up to date list of missing sdk types', () => { diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index 8798c496a..4232ec9d1 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -14,7 +14,10 @@ import { ToolChoiceSchema, ToolResultContentSchema, ToolSchema, - ToolUseContentSchema + ToolUseContentSchema, + FileInputDescriptorSchema, + StringArraySchemaSchema, + ElicitRequestFormParamsSchema } from '../src/types/types.js'; describe('Types', () => { @@ -983,4 +986,142 @@ describe('Types', () => { } }); }); + + describe('File inputs (SEP)', () => { + test('FileInputDescriptor: should validate full descriptor', () => { + const descriptor = { + accept: ['image/png', 'image/*', 'application/pdf'], + maxSize: 10485760 + }; + const result = FileInputDescriptorSchema.safeParse(descriptor); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.accept).toEqual(['image/png', 'image/*', 'application/pdf']); + expect(result.data.maxSize).toBe(10485760); + } + }); + + test('FileInputDescriptor: should validate empty descriptor (all fields optional)', () => { + const result = FileInputDescriptorSchema.safeParse({}); + expect(result.success).toBe(true); + }); + + test('FileInputDescriptor: should reject non-integer maxSize', () => { + const result = FileInputDescriptorSchema.safeParse({ maxSize: 1.5 }); + expect(result.success).toBe(false); + }); + + test('ClientCapabilities: should validate fileInputs capability', () => { + const capabilities = { + fileInputs: {} + }; + const result = ClientCapabilitiesSchema.safeParse(capabilities); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.fileInputs).toBeDefined(); + } + }); + + test('Tool: should validate inputFiles round-trip', () => { + const tool = { + name: 'analyze_image', + inputSchema: { + type: 'object', + properties: { + image: { type: 'string', format: 'uri' }, + caption: { type: 'string' } + }, + required: ['image'] + }, + inputFiles: { + image: { + accept: ['image/*'], + maxSize: 5242880 + } + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.inputFiles?.image?.accept).toEqual(['image/*']); + expect(result.data.inputFiles?.image?.maxSize).toBe(5242880); + } + }); + + test('Tool: should validate inputFiles with empty descriptor', () => { + const tool = { + name: 'upload', + inputSchema: { type: 'object' }, + inputFiles: { + attachment: {} + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('StringArraySchema: should validate multi-file schema', () => { + const schema = { + type: 'array', + items: { type: 'string', format: 'uri' }, + title: 'Attachments', + description: 'Upload up to 5 files', + minItems: 1, + maxItems: 5 + }; + const result = StringArraySchemaSchema.safeParse(schema); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('array'); + expect(result.data.items.type).toBe('string'); + expect(result.data.items.format).toBe('uri'); + expect(result.data.maxItems).toBe(5); + } + }); + + test('StringArraySchema: should reject wrong type literal', () => { + const result = StringArraySchemaSchema.safeParse({ + type: 'object', + items: { type: 'string' } + }); + expect(result.success).toBe(false); + }); + + test('StringArraySchema: should reject non-string items', () => { + const result = StringArraySchemaSchema.safeParse({ + type: 'array', + items: { type: 'number' } + }); + expect(result.success).toBe(false); + }); + + test('ElicitRequestFormParams: should validate requestedFiles', () => { + const params = { + message: 'Please upload your documents', + mode: 'form', + requestedSchema: { + type: 'object', + properties: { + document: { type: 'string', format: 'uri' }, + attachments: { + type: 'array', + items: { type: 'string', format: 'uri' }, + maxItems: 3 + } + }, + required: ['document'] + }, + requestedFiles: { + document: { accept: ['application/pdf'], maxSize: 1048576 }, + attachments: { accept: ['image/*'] } + } + }; + const result = ElicitRequestFormParamsSchema.safeParse(params); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.requestedFiles?.document?.accept).toEqual(['application/pdf']); + expect(result.data.requestedFiles?.attachments?.accept).toEqual(['image/*']); + } + }); + }); });