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:
co-authored by
autofix-ci[bot]
LemonNeko
parent
7d934f9c67
commit
8f4909db3e
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user