feat(MCP): outputschema (#7881)

* feat(MCP): outputschema

* fix: mark outputschema optional

* fix: upgrade zod v4
This commit is contained in:
SuYao 2025-07-23 10:12:00 +08:00 committed by GitHub
parent eebed6d399
commit d0649d29fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 65 additions and 58 deletions

View File

@ -257,7 +257,8 @@
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"word-extractor": "^1.0.4",
"zipread": "^1.3.3"
"zipread": "^1.3.3",
"zod": "^3.25.74"
},
"optionalDependencies": {
"@cherrystudio/mac-system-ocr": "^0.2.2"

View File

@ -1,9 +1,8 @@
// inspired by https://dify.ai/blog/turn-your-dify-app-into-an-mcp-server
import { loggerService } from '@logger'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import * as z from 'zod/v4'
const logger = loggerService.withContext('DifyKnowledgeServer')
@ -39,10 +38,6 @@ interface DifySearchKnowledgeResponse {
}>
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ToolInputSchema = ToolSchema.shape.inputSchema
type ToolInput = z.infer<typeof ToolInputSchema>
const SearchKnowledgeArgsSchema = z.object({
id: z.string().describe('Knowledge ID'),
query: z.string().describe('Query string'),
@ -96,7 +91,7 @@ class DifyKnowledgeServer {
{
name: 'search_knowledge',
description: 'Search knowledge by id and query',
inputSchema: zodToJsonSchema(SearchKnowledgeArgsSchema) as ToolInput
inputSchema: SearchKnowledgeArgsSchema
}
]
}

View File

@ -2,14 +2,13 @@
import { loggerService } from '@logger'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { createTwoFilesPatch } from 'diff'
import fs from 'fs/promises'
import { minimatch } from 'minimatch'
import os from 'os'
import path from 'path'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
import * as z from 'zod/v4'
const logger = loggerService.withContext('MCP:FileSystemServer')
@ -120,10 +119,6 @@ const GetFileInfoArgsSchema = z.object({
path: z.string()
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ToolInputSchema = ToolSchema.shape.inputSchema
type ToolInput = z.infer<typeof ToolInputSchema>
interface FileInfo {
size: number
created: Date
@ -345,7 +340,7 @@ class FileSystemServer {
'Handles various text encodings and provides detailed error messages ' +
'if the file cannot be read. Use this tool when you need to examine ' +
'the contents of a single file. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput
inputSchema: ReadFileArgsSchema
},
{
name: 'read_multiple_files',
@ -355,7 +350,7 @@ class FileSystemServer {
"or compare multiple files. Each file's content is returned with its " +
"path as a reference. Failed reads for individual files won't stop " +
'the entire operation. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput
inputSchema: ReadMultipleFilesArgsSchema
},
{
name: 'write_file',
@ -363,7 +358,7 @@ class FileSystemServer {
'Create a new file or completely overwrite an existing file with new content. ' +
'Use with caution as it will overwrite existing files without warning. ' +
'Handles text content with proper encoding. Only works within allowed directories.',
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput
inputSchema: WriteFileArgsSchema
},
{
name: 'edit_file',
@ -371,7 +366,7 @@ class FileSystemServer {
'Make line-based edits to a text file. Each edit replaces exact line sequences ' +
'with new content. Returns a git-style diff showing the changes made. ' +
'Only works within allowed directories.',
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
inputSchema: EditFileArgsSchema
},
{
name: 'create_directory',
@ -380,7 +375,7 @@ class FileSystemServer {
'nested directories in one operation. If the directory already exists, ' +
'this operation will succeed silently. Perfect for setting up directory ' +
'structures for projects or ensuring required paths exist. Only works within allowed directories.',
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput
inputSchema: CreateDirectoryArgsSchema
},
{
name: 'list_directory',
@ -389,7 +384,7 @@ class FileSystemServer {
'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
'prefixes. This tool is essential for understanding directory structure and ' +
'finding specific files within a directory. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput
inputSchema: ListDirectoryArgsSchema
},
{
name: 'directory_tree',
@ -398,7 +393,7 @@ class FileSystemServer {
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
'Files have no children array, while directories always have a children array (which may be empty). ' +
'The output is formatted with 2-space indentation for readability. Only works within allowed directories.',
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput
inputSchema: DirectoryTreeArgsSchema
},
{
name: 'move_file',
@ -407,7 +402,7 @@ class FileSystemServer {
'and rename them in a single operation. If the destination exists, the ' +
'operation will fail. Works across different directories and can be used ' +
'for simple renaming within the same directory. Both source and destination must be within allowed directories.',
inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput
inputSchema: MoveFileArgsSchema
},
{
name: 'search_files',
@ -417,7 +412,7 @@ class FileSystemServer {
'is case-insensitive and matches partial names. Returns full paths to all ' +
"matching items. Great for finding files when you don't know their exact location. " +
'Only searches within allowed directories.',
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput
inputSchema: SearchFilesArgsSchema
},
{
name: 'get_file_info',
@ -426,7 +421,7 @@ class FileSystemServer {
'information including size, creation time, last modified time, permissions, ' +
'and type. This tool is perfect for understanding file characteristics ' +
'without reading the actual content. Only works within allowed directories.',
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput
inputSchema: GetFileInfoArgsSchema
},
{
name: 'list_allowed_directories',

View File

@ -289,12 +289,6 @@
"topics.unpinned": "Αποστέλλω",
"translate": "Μετάφραση"
},
"html_artifacts": {
"code": "Κώδικας",
"generating": "Δημιουργία",
"preview": "Προεπισκόπηση",
"split": "Διαχωρισμός"
},
"code_block": {
"collapse": "συμπεριληφθείς",
"disable_wrap": "ακύρωση αλλαγής γραμμής",
@ -428,6 +422,12 @@
"search.topics.empty": "Δεν βρέθηκαν σχετικά θέματα, πατήστε Enter για να αναζητήσετε όλα τα μηνύματα",
"title": "Αναζήτηση θεμάτων"
},
"html_artifacts": {
"code": "Κώδικας",
"generating": "Δημιουργία",
"preview": "Προεπισκόπηση",
"split": "Διαχωρισμός"
},
"knowledge": {
"add": {
"title": "Προσθήκη βιβλιοθήκης γνώσεων"
@ -645,9 +645,9 @@
"close": "Κλείσιμο της εφαρμογής",
"devtools": "Εργαλεία προγραμματιστή",
"minimize": "Ελαχιστοποίηση της εφαρμογής",
"openExternal": "Άνοιγμα στον περιηγητή",
"open_link_external_off": "Τρέχον: Άνοιγμα συνδέσμου χρησιμοποιώντας το προεπιλεγμένο παράθυρο",
"open_link_external_on": "Τρέχον: Άνοιγμα συνδέσμου στον περιηγητή",
"openExternal": "Άνοιγμα στον περιηγητή",
"refresh": "Ανανέωση",
"rightclick_copyurl": "Αντιγραφή URL με δεξί κλικ"
},
@ -905,9 +905,9 @@
},
"settings": {
"about": "Περί μας",
"about.checkingUpdate": "Ελέγχω ενημερώσεις...",
"about.checkUpdate": "Έλεγχος ενημερώσεων",
"about.checkUpdate.available": "Άμεση ενημέρωση",
"about.checkingUpdate": "Ελέγχω ενημερώσεις...",
"about.contact.button": "Ταχυδρομείο",
"about.contact.title": "Επικοινωνία μέσω ταχυδρομείου",
"about.description": "Ένα AI ασιστάντα που έχει σχεδιαστεί για δημιουργούς",

View File

@ -290,12 +290,6 @@
"topics.unpinned": "Quitar fijación",
"translate": "Traducir"
},
"html_artifacts": {
"code": "Código",
"generating": "Generando",
"preview": "Vista previa",
"split": "Dividir"
},
"code_block": {
"collapse": "Replegar",
"disable_wrap": "Deshabilitar salto de línea",
@ -429,6 +423,12 @@
"search.topics.empty": "No se encontraron temas relacionados, presione Enter para buscar todos los mensajes",
"title": "Búsqueda de temas"
},
"html_artifacts": {
"code": "Código",
"generating": "Generando",
"preview": "Vista previa",
"split": "Dividir"
},
"knowledge": {
"add": {
"title": "Agregar base de conocimientos"
@ -646,9 +646,9 @@
"close": "Cerrar la aplicación",
"devtools": "Herramientas de desarrollo",
"minimize": "Minimizar la aplicación",
"openExternal": "Abrir en el navegador",
"open_link_external_off": "Actual: Abrir enlaces en ventana predeterminada",
"open_link_external_on": "Actual: Abrir enlaces en el navegador",
"openExternal": "Abrir en el navegador",
"refresh": "Actualizar",
"rightclick_copyurl": "Copiar URL con clic derecho"
},
@ -906,9 +906,9 @@
},
"settings": {
"about": "Acerca de nosotros",
"about.checkingUpdate": "Verificando actualizaciones...",
"about.checkUpdate": "Comprobar actualizaciones",
"about.checkUpdate.available": "Actualizar ahora",
"about.checkingUpdate": "Verificando actualizaciones...",
"about.contact.button": "Correo electrónico",
"about.contact.title": "Contacto por correo electrónico",
"about.description": "Una asistente de IA creada para los creadores",

View File

@ -289,12 +289,6 @@
"topics.unpinned": "Annuler le fixage",
"translate": "Traduire"
},
"html_artifacts": {
"code": "Code",
"generating": "Génération",
"preview": "Aperçu",
"split": "Diviser"
},
"code_block": {
"collapse": "Réduire",
"disable_wrap": "Désactiver le retour à la ligne",
@ -428,6 +422,12 @@
"search.topics.empty": "Aucun sujet correspondant trouvé, appuyez sur Entrée pour rechercher tous les messages",
"title": "Recherche de sujets"
},
"html_artifacts": {
"code": "Code",
"generating": "Génération",
"preview": "Aperçu",
"split": "Diviser"
},
"knowledge": {
"add": {
"title": "Ajouter une base de connaissances"
@ -645,9 +645,9 @@
"close": "Закрыть мини-программу",
"devtools": "Инструменты разработчика",
"minimize": "Свернуть мини-программу",
"openExternal": "Открыть в браузере",
"open_link_external_off": "Текущий: открывать ссылки в окне по умолчанию",
"open_link_external_on": "Текущий: открывать ссылки в браузере",
"openExternal": "Открыть в браузере",
"refresh": "Обновить",
"rightclick_copyurl": "Скопировать URL через правую кнопку мыши"
},
@ -905,9 +905,9 @@
},
"settings": {
"about": "À propos de nous",
"about.checkingUpdate": "Vérification des mises à jour en cours...",
"about.checkUpdate": "Vérifier les mises à jour",
"about.checkUpdate.available": "Mettre à jour maintenant",
"about.checkingUpdate": "Vérification des mises à jour en cours...",
"about.contact.button": "Courriel",
"about.contact.title": "Contactez-nous par courriel",
"about.description": "Un assistant IA conçu pour les créateurs",

View File

@ -291,12 +291,6 @@
"topics.unpinned": "Desfixar",
"translate": "Traduzir"
},
"html_artifacts": {
"code": "Código",
"generating": "Gerando",
"preview": "Visualizar",
"split": "Dividir"
},
"code_block": {
"collapse": "Recolher",
"disable_wrap": "Desativar quebra de linha",
@ -430,6 +424,12 @@
"search.topics.empty": "Nenhum tópico relacionado encontrado, clique em Enter para procurar todas as mensagens",
"title": "Procurar Tópicos"
},
"html_artifacts": {
"code": "Código",
"generating": "Gerando",
"preview": "Visualizar",
"split": "Dividir"
},
"knowledge": {
"add": {
"title": "Adicionar Base de Conhecimento"
@ -647,9 +647,9 @@
"close": "Fechar aplicativo",
"devtools": "Ferramentas de Desenvolvedor",
"minimize": "Minimizar aplicativo",
"openExternal": "Abrir no navegador",
"open_link_external_off": "Atual: Abrir links em janela padrão",
"open_link_external_on": "Atual: Abrir links no navegador",
"openExternal": "Abrir no navegador",
"refresh": "Atualizar",
"rightclick_copyurl": "Copiar URL com botão direito"
},
@ -906,9 +906,9 @@
},
"settings": {
"about": "Sobre Nós",
"about.checkingUpdate": "Verificando atualizações...",
"about.checkUpdate": "Verificar atualizações",
"about.checkUpdate.available": "Atualizar agora",
"about.checkingUpdate": "Verificando atualizações...",
"about.contact.button": "E-mail",
"about.contact.title": "Contato por e-mail",
"about.description": "Um assistente de IA criado para criadores",

View File

@ -2,6 +2,7 @@ import type { WebSearchResultBlock } from '@anthropic-ai/sdk/resources'
import type { GenerateImagesConfig, GroundingMetadata, PersonGeneration } from '@google/genai'
import type OpenAI from 'openai'
import type { CSSProperties } from 'react'
import * as z from 'zod/v4'
export * from './file'
import type { FileMetadata } from './file'
@ -657,6 +658,12 @@ export interface MCPToolInputSchema {
properties: Record<string, object>
}
export const MCPToolOutputSchema = z.object({
type: z.literal('object'),
properties: z.record(z.string(), z.unknown()),
required: z.array(z.string())
})
export interface MCPTool {
id: string
serverId: string
@ -664,6 +671,7 @@ export interface MCPTool {
name: string
description?: string
inputSchema: MCPToolInputSchema
outputSchema?: z.infer<typeof MCPToolOutputSchema>
}
export interface MCPPromptArguments {

View File

@ -7307,6 +7307,7 @@ __metadata:
winston-daily-rotate-file: "npm:^5.0.0"
word-extractor: "npm:^1.0.4"
zipread: "npm:^1.3.3"
zod: "npm:^3.25.74"
dependenciesMeta:
"@cherrystudio/mac-system-ocr":
optional: true
@ -21068,6 +21069,13 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.25.74":
version: 3.25.74
resolution: "zod@npm:3.25.74"
checksum: 10c0/59e38b046ac333b5bd1ba325a83b6798721227cbfb1e69dfc7159bd7824b904241ab923026edb714fafefec3624265ae374a70aee9a5a45b365bd31781ffa105
languageName: node
linkType: hard
"zustand@npm:^4.4.0":
version: 4.5.6
resolution: "zustand@npm:4.5.6"