Compare commits

..

1 Commits

Author SHA1 Message Date
suyao
18cd611316 fix: update @modelcontextprotocol/sdk to v1.23.0 and enhance MCP tool schemas 2025-12-02 04:39:24 +08:00
7 changed files with 133 additions and 132 deletions

View File

@@ -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",

View File

@@ -478,16 +478,13 @@ class FileStorage {
}
}
/**
* Core file reading logic that handles both documents and text files.
*
* @private
* @param filePath - Full path to the file
* @param detectEncoding - Whether to auto-detect text file encoding
* @returns Promise resolving to the extracted text content
* @throws Error if file reading fails
*/
private async readFileCore(filePath: string, detectEncoding: boolean = false): Promise<string> {
public readFile = async (
_: Electron.IpcMainInvokeEvent,
id: string,
detectEncoding: boolean = false
): Promise<string> => {
const filePath = path.join(this.storageDir, id)
const fileExtension = path.extname(filePath)
if (documentExts.includes(fileExtension)) {
@@ -507,7 +504,7 @@ class FileStorage {
return data
} catch (error) {
chdir(originalCwd)
logger.error('Failed to read document file:', error as Error)
logger.error('Failed to read file:', error as Error)
throw error
}
}
@@ -519,72 +516,11 @@ class FileStorage {
return fs.readFileSync(filePath, 'utf-8')
}
} catch (error) {
logger.error('Failed to read text file:', error as Error)
logger.error('Failed to read file:', error as Error)
throw new Error(`Failed to read file: ${filePath}.`)
}
}
/**
* Reads and extracts content from a stored file.
*
* Supports multiple file formats including:
* - Complex documents: .pdf, .doc, .docx, .pptx, .xlsx, .odt, .odp, .ods
* - Text files: .txt, .md, .json, .csv, etc.
* - Code files: .js, .ts, .py, .java, etc.
*
* For document formats, extracts text content using specialized parsers:
* - .doc files: Uses word-extractor library
* - Other Office formats: Uses officeparser library
*
* For text files, can optionally detect encoding automatically.
*
* @param _ - Electron IPC invoke event (unused)
* @param id - File identifier with extension (e.g., "uuid.docx")
* @param detectEncoding - Whether to auto-detect text file encoding (default: false)
* @returns Promise resolving to the extracted text content of the file
* @throws Error if file reading fails or file is not found
*
* @example
* // Read a DOCX file
* const content = await readFile(event, "document.docx");
*
* @example
* // Read a text file with encoding detection
* const content = await readFile(event, "text.txt", true);
*
* @example
* // Read a PDF file
* const content = await readFile(event, "manual.pdf");
*/
public readFile = async (
_: Electron.IpcMainInvokeEvent,
id: string,
detectEncoding: boolean = false
): Promise<string> => {
const filePath = path.join(this.storageDir, id)
return this.readFileCore(filePath, detectEncoding)
}
/**
* Reads and extracts content from an external file path.
*
* Similar to readFile, but operates on external file paths instead of stored files.
* Supports the same file formats including complex documents and text files.
*
* @param _ - Electron IPC invoke event (unused)
* @param filePath - Absolute path to the external file
* @param detectEncoding - Whether to auto-detect text file encoding (default: false)
* @returns Promise resolving to the extracted text content of the file
* @throws Error if file does not exist or reading fails
*
* @example
* // Read an external DOCX file
* const content = await readExternalFile(event, "/path/to/document.docx");
*
* @example
* // Read an external text file with encoding detection
* const content = await readExternalFile(event, "/path/to/text.txt", true);
*/
public readExternalFile = async (
_: Electron.IpcMainInvokeEvent,
filePath: string,
@@ -594,7 +530,40 @@ class FileStorage {
throw new Error(`File does not exist: ${filePath}`)
}
return this.readFileCore(filePath, detectEncoding)
const fileExtension = path.extname(filePath)
if (documentExts.includes(fileExtension)) {
const originalCwd = process.cwd()
try {
chdir(this.tempDir)
if (fileExtension === '.doc') {
const extractor = new WordExtractor()
const extracted = await extractor.extract(filePath)
chdir(originalCwd)
return extracted.getBody()
}
const data = await officeParser.parseOfficeAsync(filePath)
chdir(originalCwd)
return data
} catch (error) {
chdir(originalCwd)
logger.error('Failed to read file:', error as Error)
throw error
}
}
try {
if (detectEncoding) {
return readTextFileWithAutoEncoding(filePath)
} else {
return fs.readFileSync(filePath, 'utf-8')
}
} catch (error) {
logger.error('Failed to read file:', error as Error)
throw new Error(`Failed to read file: ${filePath}.`)
}
}
public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise<string> => {

View File

@@ -42,11 +42,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'
@@ -620,6 +623,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,

View File

@@ -39,7 +39,6 @@ import {
detectLanguage,
determineTargetLanguage
} from '@renderer/utils/translate'
import { documentExts } from '@shared/config/constant'
import { imageExts, MB, textExts } from '@shared/config/constant'
import { Button, Flex, FloatButton, Popover, Tooltip, Typography } from 'antd'
import type { TextAreaRef } from 'antd/es/input/TextArea'
@@ -67,7 +66,7 @@ const TranslatePage: FC = () => {
const { prompt, getLanguageByLangcode, settings } = useTranslate()
const { autoCopy } = settings
const { shikiMarkdownIt } = useCodeStyle()
const { onSelectFile, selecting, clearFiles } = useFiles({ extensions: [...imageExts, ...textExts, ...documentExts] })
const { onSelectFile, selecting, clearFiles } = useFiles({ extensions: [...imageExts, ...textExts] })
const { ocr } = useOcr()
const { setTimeoutTimer } = useTimer()
@@ -485,56 +484,33 @@ const TranslatePage: FC = () => {
const readFile = useCallback(
async (file: FileMetadata) => {
const _readFile = async () => {
let isText: boolean
try {
const fileExtension = getFileExtension(file.path)
// 检查文件是否为文本文件
isText = await isTextFile(file.path)
} catch (e) {
logger.error('Failed to check if file is text.', e as Error)
window.toast.error(t('translate.files.error.check_type') + ': ' + formatErrorMessage(e))
return
}
// Check if file is supported format (text file or document file)
let isText: boolean
const isDocument: boolean = documentExts.includes(fileExtension)
if (!isText) {
window.toast.error(t('common.file.not_supported', { type: getFileExtension(file.path) }))
logger.error('Unsupported file type.')
return
}
if (!isDocument) {
try {
// For non-document files, check if it's a text file
isText = await isTextFile(file.path)
} catch (e) {
logger.error('Failed to check file type.', e as Error)
window.toast.error(t('translate.files.error.check_type') + ': ' + formatErrorMessage(e))
return
}
} else {
isText = false
}
if (!isText && !isDocument) {
window.toast.error(t('common.file.not_supported', { type: fileExtension }))
logger.error('Unsupported file type.')
return
}
// File size check - document files allowed to be larger
const maxSize = isDocument ? 20 * MB : 5 * MB
if (file.size > maxSize) {
window.toast.error(t('translate.files.error.too_large') + ` (0 ~ ${maxSize / MB} MB)`)
return
}
let result: string
// the threshold may be too large
if (file.size > 5 * MB) {
window.toast.error(t('translate.files.error.too_large') + ' (0 ~ 5 MB)')
} else {
try {
if (isDocument) {
// Use the new document reading API
result = await window.api.file.readExternal(file.path, true)
} else {
// Read text file
result = await window.api.fs.readText(file.path)
}
const result = await window.api.fs.readText(file.path)
setText(text + result)
} catch (e) {
logger.error('Failed to read file.', e as Error)
logger.error('Failed to read text file.', e as Error)
window.toast.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e))
}
} catch (e) {
logger.error('Failed to read file.', e as Error)
window.toast.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e))
}
}
const promise = _readFile()

View File

@@ -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<typeof MCPToolInputSchema>

View File

@@ -136,7 +136,10 @@ export async function callMCPTool(
topicId?: string,
modelName?: string
): Promise<MCPCallToolResponse> {
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)

View File

@@ -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
@@ -10046,7 +10055,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"
@@ -10403,6 +10412,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 +10435,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 +10447,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:
@@ -26353,6 +26376,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 +26394,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"