fix: relax MCP tool JSON Schema Zod validation to handle non-standard types (#6455)

* fix: relax MCP tool JSON Schema Zod validation to accept any type values

MCP servers may return JSON Schema with type values outside the
standard 6 types. Use z.any() for type and items fields to avoid
500 errors on /api/core/app/mcpTools/getTools.

- Remove SchemaInputValueTypeSchema enum and SchemaInputValueType
- Remove unnecessary .passthrough()
- Use plain string type for function parameters

Fixes #6451

* fix: mcp adapt

---------

Co-authored-by: c121914yu <yucongcong_test@163.com>
This commit is contained in:
Archer
2026-02-24 13:48:31 +08:00
committed by GitHub
parent a2250b3a44
commit be9317f601
6 changed files with 56 additions and 22 deletions

View File

@@ -121,6 +121,7 @@ description: FastGPT 文档目录
- [/docs/upgrading/4-14/41451](/docs/upgrading/4-14/41451)
- [/docs/upgrading/4-14/4146](/docs/upgrading/4-14/4146)
- [/docs/upgrading/4-14/4147](/docs/upgrading/4-14/4147)
- [/docs/upgrading/4-14/4148](/docs/upgrading/4-14/4148)
- [/docs/upgrading/4-8/40](/docs/upgrading/4-8/40)
- [/docs/upgrading/4-8/41](/docs/upgrading/4-8/41)
- [/docs/upgrading/4-8/42](/docs/upgrading/4-8/42)

View File

@@ -0,0 +1,18 @@
---
title: 'V4.14.8(进行中)'
description: 'FastGPT V4.14.8 更新说明'
---
## 🚀 新增内容
## ⚙️ 优化
1. 兼容 MCP 中 JSON Schema type 类型不在枚举类型里。
## 🐛 修复
1. 新 SDK 兼容:连续调用同一个 MCP 服务时,多次连接导致报错。

View File

@@ -1,5 +1,5 @@
{
"title": "4.14.x",
"description": "",
"pages": ["4147", "4146", "41451", "4145", "4144", "4143", "4142", "4141", "4140"]
"pages": ["4148", "4147", "4146", "41451", "4145", "4144", "4143", "4142", "4141", "4140"]
}

View File

@@ -205,4 +205,4 @@
"document/content/docs/use-cases/external-integration/openapi.mdx": "2026-02-12T18:45:30+08:00",
"document/content/docs/use-cases/external-integration/wecom.mdx": "2025-12-10T20:07:05+08:00",
"document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00"
}
}

View File

@@ -8,36 +8,26 @@ import type { OpenApiJsonSchema } from './tool/httpTool/type';
import { i18nT } from '../../../web/i18n/utils';
import z from 'zod';
const SchemaInputValueTypeSchema = z.enum([
'string',
'number',
'integer',
'boolean',
'array',
'object'
]);
type SchemaInputValueType = z.infer<typeof SchemaInputValueTypeSchema>;
export const JsonSchemaPropertiesItemSchema = z.object({
description: z.string().optional(),
'x-tool-description': z.string().optional(),
type: SchemaInputValueTypeSchema,
type: z.any(),
enum: z.array(z.string()).optional(),
minimum: z.number().optional(),
maximum: z.number().optional(),
items: z.object({ type: SchemaInputValueTypeSchema }).optional()
items: z.any().optional() // Array 时候有
});
export type JsonSchemaPropertiesItemType = z.infer<typeof JsonSchemaPropertiesItemSchema>;
export const JSONSchemaInputTypeSchema = z.object({
type: SchemaInputValueTypeSchema,
type: z.any().optional(),
properties: z.record(z.string(), JsonSchemaPropertiesItemSchema).optional(),
required: z.array(z.string()).optional()
});
export type JSONSchemaInputType = z.infer<typeof JSONSchemaInputTypeSchema>;
export const JSONSchemaOutputTypeSchema = z.object({
type: SchemaInputValueTypeSchema,
type: z.any().optional(),
properties: z.record(z.string(), JsonSchemaPropertiesItemSchema).optional(),
required: z.array(z.string()).optional()
});
@@ -47,21 +37,21 @@ export const getNodeInputTypeFromSchemaInputType = ({
type,
arrayItems
}: {
type: SchemaInputValueType;
arrayItems?: { type: SchemaInputValueType };
type: string;
arrayItems?: { type: string };
}) => {
if (type === 'string') return WorkflowIOValueTypeEnum.string;
if (type === 'number') return WorkflowIOValueTypeEnum.number;
if (type === 'integer') return WorkflowIOValueTypeEnum.number;
if (type === 'number' || type === 'integer') return WorkflowIOValueTypeEnum.number;
if (type === 'boolean') return WorkflowIOValueTypeEnum.boolean;
if (type === 'object') return WorkflowIOValueTypeEnum.object;
if (type !== 'array') return WorkflowIOValueTypeEnum.any;
if (!arrayItems) return WorkflowIOValueTypeEnum.arrayAny;
const itemType = arrayItems.type;
if (itemType === 'string') return WorkflowIOValueTypeEnum.arrayString;
if (itemType === 'number') return WorkflowIOValueTypeEnum.arrayNumber;
if (itemType === 'integer') return WorkflowIOValueTypeEnum.arrayNumber;
if (itemType === 'number' || itemType === 'integer') return WorkflowIOValueTypeEnum.arrayNumber;
if (itemType === 'boolean') return WorkflowIOValueTypeEnum.arrayBoolean;
if (itemType === 'object') return WorkflowIOValueTypeEnum.arrayObject;

View File

@@ -17,6 +17,7 @@ export class MCPClient {
private client: Client;
private url: string;
private headers: Record<string, any> = {};
private connectionPromise: Promise<Client> | null = null;
constructor(config: { url: string; headers: Record<string, any> }) {
this.url = config.url;
@@ -28,6 +29,28 @@ export class MCPClient {
}
private async getConnection(): Promise<Client> {
if (this.connectionPromise) {
return this.connectionPromise;
}
this.connectionPromise = this.doConnect().catch((error) => {
// 连接失败时清除缓存,允许下次重试
this.connectionPromise = null;
throw error;
});
this.client.onerror = (error) => {
logger.error('MCP client connection error', { url: this.url, error });
this.connectionPromise = null;
};
this.client.onclose = () => {
this.connectionPromise = null;
};
return this.connectionPromise;
}
private async doConnect(): Promise<Client> {
try {
const transport = new StreamableHTTPClientTransport(new URL(this.url), {
requestInit: {
@@ -37,6 +60,7 @@ export class MCPClient {
await this.client.connect(transport);
return this.client;
} catch (error) {
logger.debug('StreamableHTTP connect failed, falling back to SSE', { url: this.url, error });
await this.client.connect(
new SSEClientTransport(new URL(this.url), {
requestInit: {
@@ -72,6 +96,7 @@ export class MCPClient {
// 内部方法:关闭连接
async closeConnection() {
this.connectionPromise = null;
try {
await retryFn(() => this.client.close(), 3);
logger.debug('MCP client connection closed', { url: this.url });