11import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
22import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js" ;
3- import { CallToolRequestSchema , CallToolResult , ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" ;
3+ import { CallToolRequestSchema , CallToolResult , ElicitRequestFormParams , ListToolsRequestSchema , PrimitiveSchemaDefinition } from "@modelcontextprotocol/sdk/types.js" ;
44import { promises as fs } from "fs" ;
55import { createServer , IncomingMessage , ServerResponse } from "http" ;
66import os from "os" ;
@@ -17,6 +17,7 @@ import { ToolWindowManager } from "../managers/toolWindowManager";
1717import { logInvocation } from "./agentInvocationLogger" ;
1818import { AgentExecutionMode , AgentInvocationMode , AgentTool , getAgentInvokableTools , resolveToolId } from "./agentToolRegistry" ;
1919import { createHeadlessLogger , invokeHeadlessTool } from "./headlessToolRuntime" ;
20+ import { JsonObjectSchema } from "./schemaConverter" ;
2021
2122const MCP_AUTH_HEADER = "x-mcp-auth-token" ;
2223const MCP_AUTH_HEADER_DISPLAY_NAME = "X-MCP-Auth-Token" ;
@@ -98,6 +99,54 @@ function stripInvocationMeta(args: Record<string, unknown>): Record<string, unkn
9899 return clone ;
99100}
100101
102+ /**
103+ * Parses validation errors to find top-level required-field-missing errors.
104+ * Only top-level fields ($.fieldName) are returned since elicitation supports flat schemas only.
105+ */
106+ function extractMissingFieldNames ( errors : string [ ] ) : string [ ] {
107+ const missing : string [ ] = [ ] ;
108+ for ( const err of errors ) {
109+ const match = / ^ \$ \. ( [ ^ . : ] + ) : r e q u i r e d f i e l d i s m i s s i n g $ / . exec ( err ) ;
110+ if ( match ?. [ 1 ] ) {
111+ missing . push ( match [ 1 ] ) ;
112+ }
113+ }
114+ return missing ;
115+ }
116+
117+ /**
118+ * Builds a flat elicitation requestedSchema from missing required fields in the tool's input schema.
119+ * Returns null if any missing field cannot be represented as a primitive (e.g. object or array types),
120+ * since form-based elicitation only supports flat primitive values.
121+ */
122+ function buildElicitationProperties ( inputSchema : JsonObjectSchema , fieldNames : string [ ] ) : Record < string , PrimitiveSchemaDefinition > | null {
123+ const properties = isRecord ( inputSchema . properties ) ? inputSchema . properties : { } ;
124+ const result : Record < string , PrimitiveSchemaDefinition > = { } ;
125+
126+ for ( const name of fieldNames ) {
127+ const prop = isRecord ( properties [ name ] ) ? ( properties [ name ] as Record < string , unknown > ) : undefined ;
128+ const type = typeof prop ?. type === "string" ? prop . type : undefined ;
129+ const description = typeof prop ?. description === "string" ? prop . description : undefined ;
130+ const title = typeof prop ?. title === "string" ? prop . title : undefined ;
131+ const enumValues = Array . isArray ( prop ?. enum ) ? ( prop . enum as unknown [ ] ) . filter ( ( v ) : v is string => typeof v === "string" ) : undefined ;
132+
133+ if ( type === "boolean" ) {
134+ result [ name ] = { type : "boolean" , ...( title ? { title } : { } ) , ...( description ? { description } : { } ) } ;
135+ } else if ( type === "number" || type === "integer" ) {
136+ result [ name ] = { type : "number" , ...( title ? { title } : { } ) , ...( description ? { description } : { } ) } ;
137+ } else if ( enumValues && enumValues . length > 0 ) {
138+ result [ name ] = { type : "string" , enum : enumValues , ...( title ? { title } : { } ) , ...( description ? { description } : { } ) } ;
139+ } else if ( type === "string" || type === undefined ) {
140+ result [ name ] = { type : "string" , ...( title ? { title } : { } ) , ...( description ? { description } : { } ) } ;
141+ } else {
142+ // Object or array — cannot be entered via a flat elicitation form
143+ return null ;
144+ }
145+ }
146+
147+ return Object . keys ( result ) . length > 0 ? result : null ;
148+ }
149+
101150function createInvocationError ( text : string ) : CallToolResult {
102151 return {
103152 content : [ { type : "text" , text } ] ,
@@ -643,7 +692,7 @@ export class McpServerManager {
643692 const toolIdFromName = resolveToolId ( request . params . name ) ;
644693 const toolArgs = isRecord ( request . params . arguments ) ? request . params . arguments : { } ;
645694 const invocationMeta = parseInvocationMeta ( toolArgs ) ;
646- const prefillData = stripInvocationMeta ( toolArgs ) ;
695+ let prefillData = stripInvocationMeta ( toolArgs ) ;
647696
648697 const agentTools = await this . getAgentTools ( ) ;
649698 const matchedTool = agentTools . find ( ( tool ) => tool . toolId === toolIdFromName ) ;
@@ -664,16 +713,68 @@ export class McpServerManager {
664713 const displayName = matchedTool . displayName ;
665714 const inputValidationErrors = validateAgainstSchema ( prefillData , matchedTool . inputSchema ) ;
666715 if ( inputValidationErrors . length > 0 ) {
667- const errorText = `Input validation failed: ${ inputValidationErrors . join ( "; " ) } ` ;
668- logInvocationWithMeta ( {
669- toolId,
670- toolName : displayName ,
671- connectionId : null ,
672- prefillData,
673- outcome : "rejected" ,
674- error : errorText ,
675- } ) ;
676- return createInvocationError ( errorText ) ;
716+ const missingFields = extractMissingFieldNames ( inputValidationErrors ) ;
717+ let elicitedSuccessfully = false ;
718+
719+ // Attempt elicitation only when every validation error is a missing required field
720+ // and all of those fields can be represented as flat primitives in a form.
721+ if ( missingFields . length > 0 && missingFields . length === inputValidationErrors . length ) {
722+ const elicitProperties = buildElicitationProperties ( matchedTool . inputSchema , missingFields ) ;
723+ if ( elicitProperties ) {
724+ try {
725+ const clientCaps = server . server . getClientCapabilities ( ) ;
726+ if ( clientCaps ?. elicitation ?. form ) {
727+ const elicitParams : ElicitRequestFormParams = {
728+ message : `The tool "${ displayName } " requires the following parameters. Please provide values to continue.` ,
729+ requestedSchema : {
730+ type : "object" ,
731+ properties : elicitProperties ,
732+ required : missingFields ,
733+ } ,
734+ } ;
735+ const elicitResult = await server . server . elicitInput ( elicitParams ) ;
736+
737+ if ( elicitResult . action === "accept" && isRecord ( elicitResult . content ) ) {
738+ const mergedPrefill = { ...prefillData , ...elicitResult . content } ;
739+ const revalidationErrors = validateAgainstSchema ( mergedPrefill , matchedTool . inputSchema ) ;
740+ if ( revalidationErrors . length === 0 ) {
741+ prefillData = mergedPrefill ;
742+ elicitedSuccessfully = true ;
743+ } else {
744+ const errorText = `Input validation failed after elicitation: ${ revalidationErrors . join ( "; " ) } ` ;
745+ logInvocationWithMeta ( { toolId, toolName : displayName , connectionId : null , prefillData, outcome : "rejected" , error : errorText } ) ;
746+ return createInvocationError ( errorText ) ;
747+ }
748+ } else {
749+ logInvocationWithMeta ( {
750+ toolId,
751+ toolName : displayName ,
752+ connectionId : null ,
753+ prefillData,
754+ outcome : "rejected" ,
755+ error : "User declined or cancelled parameter elicitation" ,
756+ } ) ;
757+ return createInvocationError ( `Tool invocation cancelled: required parameters were not provided for "${ displayName } ".` ) ;
758+ }
759+ }
760+ } catch {
761+ // Elicitation not supported or failed; fall through to the validation error below
762+ }
763+ }
764+ }
765+
766+ if ( ! elicitedSuccessfully ) {
767+ const errorText = `Input validation failed: ${ inputValidationErrors . join ( "; " ) } ` ;
768+ logInvocationWithMeta ( {
769+ toolId,
770+ toolName : displayName ,
771+ connectionId : null ,
772+ prefillData,
773+ outcome : "rejected" ,
774+ error : errorText ,
775+ } ) ;
776+ return createInvocationError ( errorText ) ;
777+ }
677778 }
678779
679780 let executionMode : AgentExecutionMode ;
0 commit comments