Skip to content

Commit ea68f9a

Browse files
authored
fix(mcp): preserve tool input schema types and required fields (#6248)
* fix(mcp): preserve tool input schema types and required fields * Update OpenAIAssistant.ts * replace zod-to-json-schema with toolSchemaToJsonSchema for schema normalization * enhance isZodSchema validation and handle null schemas in toolSchemaToJsonSchema
1 parent 1e21e61 commit ea68f9a

6 files changed

Lines changed: 52 additions & 57 deletions

File tree

packages/components/nodes/agentflow/Agent/Agent.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ILLMMessage, IResponseMetadata } from '../Interface.Agentflow'
1919
import { Tool } from '@langchain/core/tools'
2020
import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents'
2121
import { flatten } from 'lodash'
22-
import zodToJsonSchema from 'zod-to-json-schema'
22+
import { toolSchemaToJsonSchema, type ToolJsonSchema } from '../../../src/utils'
2323
import { getErrorMessage } from '../../../src/error'
2424
import { DataSource } from 'typeorm'
2525
import { randomBytes } from 'crypto'
@@ -68,7 +68,7 @@ interface IKnowledgeBaseVSEmbeddings {
6868
interface ISimpliefiedTool {
6969
name: string
7070
description: string
71-
schema: any
71+
schema: ToolJsonSchema
7272
toolNode: {
7373
label: string
7474
name: string
@@ -744,10 +744,7 @@ class Agent_Agentflow implements INode {
744744
}
745745
const componentNode = options.componentNodes[agentSelectedTool]
746746

747-
const jsonSchema = zodToJsonSchema(tool.schema as any)
748-
if (jsonSchema.$schema) {
749-
delete jsonSchema.$schema
750-
}
747+
const jsonSchema = toolSchemaToJsonSchema(tool.schema)
751748

752749
return {
753750
name: tool.name,
@@ -801,10 +798,7 @@ class Agent_Agentflow implements INode {
801798

802799
toolsInstance.push(retrieverToolInstance as Tool)
803800

804-
const jsonSchema = zodToJsonSchema(retrieverToolInstance.schema)
805-
if (jsonSchema.$schema) {
806-
delete jsonSchema.$schema
807-
}
801+
const jsonSchema = toolSchemaToJsonSchema(retrieverToolInstance.schema)
808802
const componentNode = options.componentNodes['retrieverTool']
809803

810804
availableTools.push({
@@ -876,10 +870,7 @@ class Agent_Agentflow implements INode {
876870

877871
toolsInstance.push(retrieverToolInstance as Tool)
878872

879-
const jsonSchema = zodToJsonSchema(retrieverToolInstance.schema)
880-
if (jsonSchema.$schema) {
881-
delete jsonSchema.$schema
882-
}
873+
const jsonSchema = toolSchemaToJsonSchema(retrieverToolInstance.schema)
883874
const componentNode = options.componentNodes['retrieverTool']
884875

885876
availableTools.push({

packages/components/nodes/agentflow/Tool/Tool.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { updateFlowState } from '../utils'
33
import { processTemplateVariables } from '../../../src/utils'
44
import { Tool } from '@langchain/core/tools'
55
import { ARTIFACTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents'
6-
import zodToJsonSchema from 'zod-to-json-schema'
6+
import { toolSchemaToJsonSchema } from '../../../src/utils'
77

88
interface IToolInputArgs {
99
inputArgName: string
@@ -153,19 +153,16 @@ class Tool_Agentflow implements INode {
153153
// Combine schemas from all tools in the array
154154
const allProperties = toolInstance.reduce((acc, tool) => {
155155
if (tool?.schema) {
156-
const schema: Record<string, any> = zodToJsonSchema(tool.schema)
156+
const schema = toolSchemaToJsonSchema(tool.schema) as { properties?: ICommonObject }
157157
return { ...acc, ...(schema.properties || {}) }
158158
}
159159
return acc
160160
}, {})
161161
toolInputArgs = { properties: allProperties }
162+
} else if (!toolInstance.schema) {
163+
toolInputArgs = {}
162164
} else {
163-
// Handle single tool instance
164-
toolInputArgs = toolInstance.schema ? zodToJsonSchema(toolInstance.schema as any) : {}
165-
}
166-
167-
if (toolInputArgs && Object.keys(toolInputArgs).length > 0) {
168-
delete toolInputArgs.$schema
165+
toolInputArgs = toolSchemaToJsonSchema(toolInstance.schema) as ICommonObject
169166
}
170167

171168
return Object.keys(toolInputArgs.properties || {}).map((item) => ({

packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { DataSource } from 'typeorm'
1313
import { getCredentialData, getCredentialParam } from '../../../src/utils'
1414
import fetch from 'node-fetch'
1515
import { flatten, uniqWith, isEqual } from 'lodash'
16-
import { zodToJsonSchema } from 'zod-to-json-schema'
16+
import { toolSchemaToJsonSchema } from '../../../src/utils'
1717
import { AnalyticHandler } from '../../../src/handler'
1818
import { Moderation, checkInputs, streamResponse } from '../../moderation/Moderation'
1919
import { formatResponse } from '../../outputparsers/OutputParserHelpers'
@@ -1163,7 +1163,7 @@ interface JSONSchema {
11631163
}
11641164

11651165
const formatToOpenAIAssistantTool = (tool: any): OpenAI.Beta.FunctionTool => {
1166-
const parameters = zodToJsonSchema(tool.schema) as JSONSchema
1166+
const parameters = toolSchemaToJsonSchema(tool.schema) as JSONSchema
11671167

11681168
// For strict tools, we need to:
11691169
// 1. Set additionalProperties to false

packages/components/nodes/tools/MCP/Pipedream/PipedreamMCP.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { getCredentialData, getCredentialParam, getVars, prepareSandboxVars } fr
44
import { DataSource } from 'typeorm'
55
import { MCPToolkit } from '../core'
66
import axios from 'axios'
7-
import { z, ZodTypeAny } from 'zod'
87
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'
98
import type { CallToolRequest, CallToolResult, TextContent, Tool as McpTool } from '@modelcontextprotocol/sdk/types.js'
109

@@ -40,17 +39,6 @@ Once the user has connected their account, retry the original request.`
4039
return text
4140
}
4241

43-
function createSchemaModel(inputSchema: { type: string; properties?: Record<string, any> }): z.ZodObject<any> {
44-
if (inputSchema.type !== 'object' || !inputSchema.properties) {
45-
throw new Error('Invalid schema type or missing properties')
46-
}
47-
const schemaProperties = Object.entries(inputSchema.properties).reduce((acc, [key]) => {
48-
acc[key] = z.any()
49-
return acc
50-
}, {} as Record<string, ZodTypeAny>)
51-
return z.object(schemaProperties)
52-
}
53-
5442
async function createPipedreamTool(toolkit: MCPToolkit, name: string, description: string, argsSchema: any): Promise<Tool> {
5543
return tool(
5644
async (input): Promise<string> => {
@@ -345,7 +333,7 @@ class Pipedream_MCP implements INode {
345333
}
346334

347335
const toolPromises = rawTools.map((t: McpTool) =>
348-
createPipedreamTool(toolkit, t.name, t.description || t.name, createSchemaModel(t.inputSchema))
336+
createPipedreamTool(toolkit, t.name, t.description || t.name, t.inputSchema ?? { type: 'object', properties: {} })
349337
)
350338
const settled = await Promise.allSettled(toolPromises)
351339
const tools = settled.filter((r): r is PromiseFulfilledResult<Tool> => r.status === 'fulfilled').map((r) => r.value)

packages/components/nodes/tools/MCP/core.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { CallToolRequest, CallToolResultSchema, ListToolsResult, ListToolsResult
22
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
33
import { StdioClientTransport, StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js'
44
import { BaseToolkit, tool, Tool } from '@langchain/core/tools'
5-
import { z, type ZodTypeAny } from 'zod/v3'
65
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
76
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
87
import { checkDenyList, secureFetch } from '../../../src/httpSecurity'
@@ -124,11 +123,12 @@ export class MCPToolkit extends BaseToolkit {
124123
if (this.client === null) {
125124
throw new Error('Client is not initialized')
126125
}
126+
const argsSchema = tool.inputSchema ?? { type: 'object', properties: {} }
127127
return await MCPTool({
128128
toolkit: this,
129129
name: tool.name,
130130
description: tool.description || tool.name,
131-
argsSchema: createSchemaModel(tool.inputSchema)
131+
argsSchema
132132
})
133133
})
134134
const res = await Promise.allSettled(toolsPromises)
@@ -177,24 +177,6 @@ export async function MCPTool({
177177
)
178178
}
179179

180-
function createSchemaModel(
181-
inputSchema: {
182-
type: 'object'
183-
properties?: Record<string, unknown>
184-
} & { [k: string]: unknown }
185-
): z.ZodObject<Record<string, ZodTypeAny>> {
186-
if (inputSchema.type !== 'object' || !inputSchema.properties) {
187-
throw new Error('Invalid schema type or missing properties')
188-
}
189-
190-
const schemaProperties = Object.entries(inputSchema.properties).reduce((acc, [key]) => {
191-
acc[key] = z.any()
192-
return acc
193-
}, {} as Record<string, ZodTypeAny>)
194-
195-
return z.object(schemaProperties)
196-
}
197-
198180
export const validateArgsForLocalFileAccess = (args: string[]): void => {
199181
const dangerousPatterns = [
200182
// Absolute paths

packages/components/src/utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { NodeVM } from 'vm2'
2222
import { Sandbox } from '@e2b/code-interpreter'
2323
import { secureFetch, checkDenyList, secureAxiosRequest } from './httpSecurity'
2424
import JSON5 from 'json5'
25+
import zodToJsonSchema, { type JsonSchema7Type } from 'zod-to-json-schema'
2526

2627
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
2728
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
@@ -2303,3 +2304,39 @@ export const isReasoningModelOpenAI = (name: string): boolean => {
23032304
if (name.includes('gpt-5')) return true
23042305
return false
23052306
}
2307+
2308+
/**
2309+
* JSON Schema shape returned by {@link toolSchemaToJsonSchema}, extended with the
2310+
* optional `$schema` marker that `zod-to-json-schema` emits.
2311+
*/
2312+
export type ToolJsonSchema = JsonSchema7Type & { $schema?: string; [key: string]: unknown }
2313+
2314+
type ZodToJsonSchemaInput = Parameters<typeof zodToJsonSchema>[0]
2315+
2316+
/**
2317+
* Type guard detecting a Zod schema without importing Zod's types directly.
2318+
*
2319+
* Using `Parameters<typeof zodToJsonSchema>[0]` keeps the guard compatible with
2320+
* whichever Zod major version (`^3 || ^4`) TypeScript resolves at the call site.
2321+
*/
2322+
export const isZodSchema = (schema: unknown): schema is ZodToJsonSchemaInput =>
2323+
typeof schema === 'object' && schema !== null && '_def' in schema && typeof (schema as { parse?: unknown }).parse === 'function'
2324+
2325+
/**
2326+
* Normalizes a tool schema into a plain JSON Schema object.
2327+
*
2328+
* LangChain tools may expose their `schema` as either a Zod schema (has `_def`)
2329+
* or an already-plain JSON Schema (e.g. MCP tools). This helper handles both,
2330+
* deep-clones plain objects to prevent accidental mutation, and strips the
2331+
* `$schema` marker so the result is safe to embed in LLM tool definitions.
2332+
*/
2333+
export const toolSchemaToJsonSchema = (schema: unknown): ToolJsonSchema => {
2334+
if (schema == null) return { type: 'object', properties: {} }
2335+
const jsonSchema: ToolJsonSchema = isZodSchema(schema)
2336+
? (zodToJsonSchema(schema) as ToolJsonSchema)
2337+
: cloneDeep(schema as ToolJsonSchema)
2338+
if (jsonSchema.$schema) {
2339+
delete jsonSchema.$schema
2340+
}
2341+
return jsonSchema
2342+
}

0 commit comments

Comments
 (0)