diff --git a/package.json b/package.json index 17be71ee59..3f95aee6d5 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch", "@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", "@mistralai/mistralai": "^1.7.5", - "@modelcontextprotocol/sdk": "^1.17.5", + "@modelcontextprotocol/sdk": "^1.23.0", "@mozilla/readability": "^0.6.0", "@notionhq/client": "^2.2.15", "@openrouter/ai-sdk-provider": "^1.2.8", @@ -207,6 +207,7 @@ "@types/content-type": "^1.1.9", "@types/cors": "^2.8.19", "@types/diff": "^7", + "@types/dotenv": "^8.2.3", "@types/express": "^5", "@types/fs-extra": "^11", "@types/he": "^1", diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 0b8db73930..3925376226 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -12,6 +12,7 @@ import { TraceMethod, withSpanFunc } from '@mcp-trace/trace-core' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { StreamableHTTPClientTransport, @@ -42,11 +43,14 @@ import { type MCPPrompt, type MCPResource, type MCPServer, - type MCPTool + type MCPTool, + MCPToolInputSchema, + MCPToolOutputSchema } from '@types' import { app, net } from 'electron' import { EventEmitter } from 'events' import { v4 as uuidv4 } from 'uuid' +import * as z from 'zod' import { CacheService } from './CacheService' import DxtService from './DxtService' @@ -343,7 +347,7 @@ class McpService { removeEnvProxy(loginShellEnv) } - const transportOptions: any = { + const transportOptions: StdioServerParameters = { command: cmd, args, env: { @@ -620,6 +624,8 @@ class McpService { tools.map((tool: SDKTool) => { const serverTool: MCPTool = { ...tool, + inputSchema: z.parse(MCPToolInputSchema, tool.inputSchema), + outputSchema: tool.outputSchema ? z.parse(MCPToolOutputSchema, tool.outputSchema) : undefined, id: buildFunctionCallToolName(server.name, tool.name, server.id), serverId: server.id, serverName: server.name, diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index 6d4210fd1d..c48ae8f794 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -7,6 +7,7 @@ import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServerTrust } from '@renderer/hooks/useMCPServerTrust' import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription' import type { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types' +import { parseKeyValueString } from '@renderer/utils/env' import { formatMcpError } from '@renderer/utils/error' import type { TabsProps } from 'antd' import { Badge, Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd' @@ -63,21 +64,6 @@ const PipRegistry: Registry[] = [ type TabKey = 'settings' | 'description' | 'tools' | 'prompts' | 'resources' -const parseKeyValueString = (str: string): Record => { - const result: Record = {} - str.split('\n').forEach((line) => { - if (line.trim()) { - const [key, ...value] = line.split('=') - const formatValue = value.join('=').trim() - const formatKey = key.trim() - if (formatKey && formatValue) { - result[formatKey] = formatValue - } - } - }) - return result -} - const McpSettings: React.FC = () => { const { t } = useTranslation() const { serverId } = useParams<{ serverId: string }>() diff --git a/src/renderer/src/types/tool.ts b/src/renderer/src/types/tool.ts index c803c76fcb..d4cad8f552 100644 --- a/src/renderer/src/types/tool.ts +++ b/src/renderer/src/types/tool.ts @@ -34,6 +34,15 @@ export const MCPToolInputSchema = z required: z.array(z.string()).optional() }) .loose() + .transform((schema) => { + if (!schema.properties) { + schema.properties = {} + } + if (!schema.required) { + schema.required = [] + } + return schema + }) export interface BuiltinTool extends BaseTool { inputSchema: z.infer diff --git a/src/renderer/src/utils/__tests__/env.test.ts b/src/renderer/src/utils/__tests__/env.test.ts new file mode 100644 index 0000000000..8ba3bad0a4 --- /dev/null +++ b/src/renderer/src/utils/__tests__/env.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest' + +import { parseKeyValueString } from '../env' + +describe('parseKeyValueString', () => { + it('should parse empty string', () => { + expect(parseKeyValueString('')).toEqual({}) + }) + + it('should parse single key-value pair', () => { + expect(parseKeyValueString('KEY=value')).toEqual({ KEY: 'value' }) + }) + + it('should parse multiple key-value pairs', () => { + const input = `KEY1=value1 +KEY2=value2 +KEY3=value3` + expect(parseKeyValueString(input)).toEqual({ + KEY1: 'value1', + KEY2: 'value2', + KEY3: 'value3' + }) + }) + + it('should handle quoted values', () => { + expect(parseKeyValueString('KEY="quoted value"')).toEqual({ KEY: 'quoted value' }) + }) + + it('should handle single quoted values', () => { + expect(parseKeyValueString("KEY='single quoted'")).toEqual({ KEY: 'single quoted' }) + }) + + it('should handle values with equals signs', () => { + expect(parseKeyValueString('URL=https://example.com?param=value')).toEqual({ + URL: 'https://example.com?param=value' + }) + }) + + it('should handle empty values', () => { + expect(parseKeyValueString('KEY=')).toEqual({ KEY: '' }) + }) + + it('should handle comments', () => { + const input = `KEY=value +# This is a comment +ANOTHER_KEY=another_value` + expect(parseKeyValueString(input)).toEqual({ + KEY: 'value', + ANOTHER_KEY: 'another_value' + }) + }) + + it('should handle whitespace around key-value pairs', () => { + expect(parseKeyValueString(' KEY=value \n ANOTHER=another ')).toEqual({ + KEY: 'value', + ANOTHER: 'another' + }) + }) + + it('should handle special characters in values', () => { + expect(parseKeyValueString('KEY=value with spaces & symbols!')).toEqual({ + KEY: 'value with spaces & symbols!' + }) + }) + + it('should handle multiline values', () => { + const input = `KEY="value +with +multiple +lines"` + expect(parseKeyValueString(input)).toEqual({ + KEY: 'value\nwith\nmultiple\nlines' + }) + }) + + it('should handle invalid lines gracefully', () => { + const input = `KEY=value +invalid line without equals +ANOTHER_KEY=another_value` + expect(parseKeyValueString(input)).toEqual({ + KEY: 'value', + ANOTHER_KEY: 'another_value' + }) + }) + + it('should handle duplicate keys (last one wins)', () => { + const input = `KEY=first +KEY=second +KEY=third` + expect(parseKeyValueString(input)).toEqual({ KEY: 'third' }) + }) + + it('should handle keys and values with special characters', () => { + expect(parseKeyValueString('API-URL_123=https://api.example.com/v1/users')).toEqual({ + 'API-URL_123': 'https://api.example.com/v1/users' + }) + }) +}) diff --git a/src/renderer/src/utils/env.ts b/src/renderer/src/utils/env.ts new file mode 100644 index 0000000000..c8b6da34d1 --- /dev/null +++ b/src/renderer/src/utils/env.ts @@ -0,0 +1,5 @@ +import { parse } from 'dotenv' + +export const parseKeyValueString = (str: string): Record => { + return parse(str) +} diff --git a/src/renderer/src/utils/mcp-tools.ts b/src/renderer/src/utils/mcp-tools.ts index 364b22e651..691689dcc4 100644 --- a/src/renderer/src/utils/mcp-tools.ts +++ b/src/renderer/src/utils/mcp-tools.ts @@ -136,7 +136,10 @@ export async function callMCPTool( topicId?: string, modelName?: string ): Promise { - logger.info(`Calling Tool: ${toolResponse.tool.serverName} ${toolResponse.tool.name}`, toolResponse.tool) + logger.info( + `Calling Tool: ${toolResponse.id} ${toolResponse.tool.serverName} ${toolResponse.tool.name}`, + toolResponse.tool + ) try { const server = getMcpServerByTool(toolResponse.tool) diff --git a/yarn.lock b/yarn.lock index 3bd2fc9278..22b6c581db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4747,11 +4747,12 @@ __metadata: languageName: node linkType: hard -"@modelcontextprotocol/sdk@npm:^1.17.5": - version: 1.17.5 - resolution: "@modelcontextprotocol/sdk@npm:1.17.5" +"@modelcontextprotocol/sdk@npm:^1.23.0": + version: 1.23.0 + resolution: "@modelcontextprotocol/sdk@npm:1.23.0" dependencies: - ajv: "npm:^6.12.6" + ajv: "npm:^8.17.1" + ajv-formats: "npm:^3.0.1" content-type: "npm:^1.0.5" cors: "npm:^2.8.5" cross-spawn: "npm:^7.0.5" @@ -4761,9 +4762,17 @@ __metadata: express-rate-limit: "npm:^7.5.0" pkce-challenge: "npm:^5.0.0" raw-body: "npm:^3.0.0" - zod: "npm:^3.23.8" - zod-to-json-schema: "npm:^3.24.1" - checksum: 10c0/182b92b5e7c07da428fd23c6de22021c4f9a91f799c02a8ef15def07e4f9361d0fc22303548658fec2a700623535fd44a9dc4d010fb5d803a8f80e3c6c64a45e + zod: "npm:^3.25 || ^4.0" + zod-to-json-schema: "npm:^3.25.0" + peerDependencies: + "@cfworker/json-schema": ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + "@cfworker/json-schema": + optional: true + zod: + optional: false + checksum: 10c0/b0291f921ad9bda06bbf1a61b1bb61ceca1173da5d74d39a411c40428d6ca50a95f0de3a1631f25a44b439220b15c30c1306600bf48bef665ab7ad118d528260 languageName: node linkType: hard @@ -8524,6 +8533,15 @@ __metadata: languageName: node linkType: hard +"@types/dotenv@npm:^8.2.3": + version: 8.2.3 + resolution: "@types/dotenv@npm:8.2.3" + dependencies: + dotenv: "npm:*" + checksum: 10c0/af9178da617959cddc8259aaa3f16c474523ead469f4a03490de2f2d1cafc8615c5d0d1ed3fad837096218126421c38cd46b4065548bb5aee3cc002c518b69f7 + languageName: node + linkType: hard + "@types/estree-jsx@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree-jsx@npm:1.0.5" @@ -10046,7 +10064,7 @@ __metadata: "@libsql/client": "npm:0.14.0" "@libsql/win32-x64-msvc": "npm:^0.4.7" "@mistralai/mistralai": "npm:^1.7.5" - "@modelcontextprotocol/sdk": "npm:^1.17.5" + "@modelcontextprotocol/sdk": "npm:^1.23.0" "@mozilla/readability": "npm:^0.6.0" "@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch" "@notionhq/client": "npm:^2.2.15" @@ -10094,6 +10112,7 @@ __metadata: "@types/content-type": "npm:^1.1.9" "@types/cors": "npm:^2.8.19" "@types/diff": "npm:^7" + "@types/dotenv": "npm:^8.2.3" "@types/express": "npm:^5" "@types/fs-extra": "npm:^11" "@types/he": "npm:^1" @@ -10403,6 +10422,20 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:^3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10c0/168d6bca1ea9f163b41c8147bae537e67bd963357a5488a1eaf3abe8baa8eec806d4e45f15b10767e6020679315c7e1e5e6803088dfb84efa2b4e9353b83dd0a + languageName: node + linkType: hard + "ajv-keywords@npm:^3.4.1": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -10412,7 +10445,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.0, ajv@npm:^6.12.0, ajv@npm:^6.12.4, ajv@npm:^6.12.6": +"ajv@npm:^6.10.0, ajv@npm:^6.12.0, ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -10424,7 +10457,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.6.3": +"ajv@npm:^8.0.0, ajv@npm:^8.17.1, ajv@npm:^8.6.3": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -13425,6 +13458,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:*": + version: 17.2.3 + resolution: "dotenv@npm:17.2.3" + checksum: 10c0/c884403209f713214a1b64d4d1defa4934c2aa5b0002f5a670ae298a51e3c3ad3ba79dfee2f8df49f01ae74290fcd9acdb1ab1d09c7bfb42b539036108bb2ba0 + languageName: node + linkType: hard + "dotenv@npm:^16.1.4, dotenv@npm:^16.3.0, dotenv@npm:^16.3.1, dotenv@npm:^16.4.5": version: 16.6.1 resolution: "dotenv@npm:16.6.1" @@ -26353,6 +26393,15 @@ __metadata: languageName: node linkType: hard +"zod-to-json-schema@npm:^3.25.0": + version: 3.25.0 + resolution: "zod-to-json-schema@npm:3.25.0" + peerDependencies: + zod: ^3.25 || ^4 + checksum: 10c0/2d2cf6ca49752bf3dc5fb37bc8f275eddbbc4020e7958d9c198ea88cd197a5f527459118188a0081b889da6a6474d64c4134cd60951fa70178c125138761c680 + languageName: node + linkType: hard + "zod-validation-error@npm:^3.4.0": version: 3.4.0 resolution: "zod-validation-error@npm:3.4.0" @@ -26362,13 +26411,20 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.1": +"zod@npm:^3.22.4, zod@npm:^3.24.1": version: 3.25.56 resolution: "zod@npm:3.25.56" checksum: 10c0/3800f01d4b1df932b91354eb1e648f69cc7e5561549e6d2bf83827d930a5f33bbf92926099445f6fc1ebb64ca9c6513ef9ae5e5409cfef6325f354bcf6fc9a24 languageName: node linkType: hard +"zod@npm:^3.25 || ^4.0": + version: 4.1.13 + resolution: "zod@npm:4.1.13" + checksum: 10c0/d7e74e82dba81a91ffc3239cd85bc034abe193a28f7087a94ab258a3e48e9a7ca4141920cac979a0d781495b48fc547777394149f26be04c3dc642f58bbc3941 + languageName: node + linkType: hard + "zod@npm:^3.25.0 || ^4.0.0, zod@npm:^3.25.76 || ^4": version: 4.1.12 resolution: "zod@npm:4.1.12"