feat(stage-*): basic mcp implementation (#1115)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: LemonNeko <17664845+lemonnekogh@users.noreply.github.com>
This commit is contained in:
Doji
2026-03-05 00:01:33 +08:00
committed by GitHub
co-authored by autofix-ci[bot] LemonNeko
parent 7d934f9c67
commit 8f4909db3e
21 changed files with 810 additions and 68 deletions
@@ -435,6 +435,14 @@ pages:
mcp-server:
description: Connect and manage MCP server and tools
title: MCP Server
config-path: Config file path
runtime-title: Runtime status
actions:
open-config: Open mcp.json
apply-and-restart: Save and restart stdio MCP
messages:
opened: Config file opened at {path}
restarted: MCP servers restarted. Started {started}, failed {failed}, skipped {skipped}
providers:
explained:
chat: Text generation model providers. e.g. OpenRouter, OpenAI, Ollama.
@@ -420,6 +420,14 @@ pages:
mcp-server:
description: 连接和管理 MCP 服务器及工具
title: MCP 服务器
config-path: 配置文件路径
runtime-title: 运行状态
actions:
open-config: 打开 mcp.json
apply-and-restart: 保存并重启 MCP
messages:
opened: 已打开配置文件:{path}
restarted: 已重启 MCP 服务。成功 {started},失败 {failed},跳过 {skipped}
providers:
explained:
chat: 文本生成模型服务来源,例如 OpenRouter, OpenAI, Ollama
-1
View File
@@ -69,7 +69,6 @@
"@proj-airi/stage-shared": "workspace:^",
"@proj-airi/stage-ui-live2d": "workspace:^",
"@proj-airi/stage-ui-three": "workspace:^",
"@proj-airi/tauri-plugin-mcp": "workspace:^",
"@proj-airi/ui": "workspace:^",
"@ricky0123/vad-web": "^0.0.30",
"@shikijs/rehype": "^3.23.0",
+3
View File
@@ -293,6 +293,9 @@ export const useChatOrchestratorStore = defineStore('chat-orchestrator', () => {
await llmStore.stream(options.model, options.chatProvider, newMessages as Message[], {
headers,
tools: options.tools,
// NOTICE: xsai stream may emit `finish` before tool steps continue, so keep waiting until
// the final non-tool finish to avoid ending the chat turn with no assistant reply.
waitForTools: true,
onStreamEvent: async (event: StreamEvent) => {
switch (event.type) {
case 'tool-call':
@@ -0,0 +1,42 @@
export interface McpToolDescriptor {
serverName: string
name: string
toolName: string
description?: string
inputSchema: Record<string, unknown>
}
export interface McpCallToolPayload {
name: string
arguments?: Record<string, unknown>
}
export interface McpCallToolResult {
content?: Array<Record<string, unknown>>
structuredContent?: Record<string, unknown>
toolResult?: unknown
isError?: boolean
}
interface McpToolBridge {
listTools: () => Promise<McpToolDescriptor[]>
callTool: (payload: McpCallToolPayload) => Promise<McpCallToolResult>
}
let bridge: McpToolBridge | undefined
export function setMcpToolBridge(nextBridge: McpToolBridge) {
bridge = nextBridge
}
export function clearMcpToolBridge() {
bridge = undefined
}
export function getMcpToolBridge(): McpToolBridge {
if (!bridge) {
throw new Error('MCP tool bridge is not available in this runtime.')
}
return bridge
}
-2
View File
@@ -9,8 +9,6 @@ describe('tools mcp schema', () => {
const tools = await mcp()
const toolNames = [
'mcp_list_tools',
'mcp_connect_server',
'mcp_disconnect_server',
'mcp_call_tool',
]
+37 -35
View File
@@ -1,34 +1,20 @@
import { callTool, connectServer, disconnectServer, listTools } from '@proj-airi/tauri-plugin-mcp'
import { tool } from '@xsai/tool'
import { z } from 'zod'
import { getMcpToolBridge } from '../stores/mcp-tool-bridge'
const tools = [
tool({
name: 'mcp_list_tools',
description: 'List all tools available on the MCP server',
description: 'List all tools available on the connected MCP servers',
execute: async (_, __) => {
return await listTools()
},
parameters: z.object({}).strict(),
}),
tool({
name: 'mcp_connect_server',
description: 'Connect to the MCP server. If "success", the connection to the MCP server is successful. Otherwise, the connection fails.',
execute: async ({ command, args }) => {
await connectServer(command, args)
return 'success'
},
parameters: z.object({
command: z.string().describe('The command to connect to the MCP server'),
args: z.array(z.string()).describe('The arguments to pass to the MCP server'),
}).strict(),
}),
tool({
name: 'mcp_disconnect_server',
description: 'Disconnect from the MCP server. If "success", the disconnection from the MCP server is successful. Otherwise, the disconnection fails.',
execute: async () => {
await disconnectServer()
return 'success'
try {
return await getMcpToolBridge().listTools()
}
catch (error) {
console.warn('[mcp_list_tools] failed to list tools:', error)
return []
}
},
parameters: z.object({}).strict(),
}),
@@ -36,23 +22,39 @@ const tools = [
name: 'mcp_call_tool',
description: 'Call a tool on the MCP server. The result is a list of content and a boolean indicating whether the tool call is an error.',
execute: async ({ name, parameters }) => {
const parametersObject = Object.fromEntries(parameters.map(({ name, value }) => [name, value]))
const result = await callTool(name, parametersObject)
return result satisfies {
content: {
type: string
text: string
}[]
isError: boolean
try {
const parametersObject = Object.fromEntries(parameters.map(({ name, value }) => [name, value]))
const result = await getMcpToolBridge().callTool({
name,
arguments: parametersObject,
})
return result satisfies {
content?: Record<string, unknown>[]
isError?: boolean
structuredContent?: Record<string, unknown>
toolResult?: unknown
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error)
return {
isError: true,
content: [
{
type: 'text',
text: message,
},
],
}
}
},
parameters: z.object({
name: z.string().describe('The name of the tool to call'),
name: z.string().describe('The qualified tool name to call. Use format "<serverName>::<toolName>"'),
parameters: z.array(z.object({
name: z.string().describe('The name of the parameter'),
value: z.union([z.string(), z.number(), z.boolean(), z.object({}).strict()]).describe('The value of the parameter, it can be a string, a number, a boolean, or an object'),
value: z.unknown().describe('The value of the parameter'),
}).strict()).describe('The parameters to pass to the tool'),
}),
}).strict(),
}),
]