|
| 1 | +import type { Tool } from '@xsai/shared-chat' |
| 2 | + |
| 3 | +import { errorMessageFrom } from '@moeru/std' |
1 | 4 | import { tool } from '@xsai/tool' |
2 | 5 | import { z } from 'zod' |
3 | 6 |
|
4 | | -import { getMcpToolBridge } from '../stores/mcp-tool-bridge' |
5 | | - |
6 | | -const tools = [ |
7 | | - tool({ |
8 | | - name: 'builtIn_mcpListTools', |
9 | | - description: 'List all available MCP tools. Call this first to discover tool names before calling builtIn_mcpCallTool.', |
10 | | - execute: async () => { |
11 | | - try { |
12 | | - return await getMcpToolBridge().listTools() |
13 | | - } |
14 | | - catch (error) { |
15 | | - console.warn('[builtIn_mcpListTools] failed to list tools:', error) |
16 | | - return '' |
17 | | - } |
18 | | - }, |
19 | | - parameters: z.object({}).strict(), |
20 | | - }), |
21 | | - tool({ |
22 | | - name: 'builtIn_mcpCallTool', |
23 | | - description: 'Call an MCP tool by name. Use builtIn_mcpListTools first to get available tool names.', |
24 | | - execute: async ({ name, arguments: argsJson }) => { |
25 | | - try { |
26 | | - const args = argsJson ? JSON.parse(argsJson) : {} |
27 | | - return await getMcpToolBridge().callTool({ name, arguments: args }) |
28 | | - } |
29 | | - catch (error) { |
30 | | - return { |
31 | | - isError: true, |
32 | | - content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }], |
| 7 | +/** |
| 8 | + * Describes an MCP tool that can be exposed to the shared LLM runtime. |
| 9 | + * |
| 10 | + * Use when: |
| 11 | + * - A runtime needs to list available MCP tools before exposing them to models |
| 12 | + * |
| 13 | + * Expects: |
| 14 | + * - `name` is the fully-qualified tool name used for invocation |
| 15 | + * |
| 16 | + * Returns: |
| 17 | + * - The MCP tool descriptor metadata reported by the runtime |
| 18 | + */ |
| 19 | +export interface McpToolDescriptor { |
| 20 | + serverName: string |
| 21 | + name: string |
| 22 | + toolName: string |
| 23 | + description?: string |
| 24 | + inputSchema: Record<string, unknown> |
| 25 | +} |
| 26 | + |
| 27 | +/** |
| 28 | + * Payload for invoking an MCP tool through a runtime-specific transport. |
| 29 | + * |
| 30 | + * Use when: |
| 31 | + * - A runtime needs to forward a tool invocation into the MCP layer |
| 32 | + * |
| 33 | + * Expects: |
| 34 | + * - `name` matches a descriptor returned from `listTools` |
| 35 | + * - `arguments` is a JSON-compatible object when provided |
| 36 | + * |
| 37 | + * Returns: |
| 38 | + * - The MCP tool call input envelope |
| 39 | + */ |
| 40 | +export interface McpCallToolPayload { |
| 41 | + name: string |
| 42 | + arguments?: Record<string, unknown> |
| 43 | +} |
| 44 | + |
| 45 | +/** |
| 46 | + * Result returned from an MCP tool invocation. |
| 47 | + * |
| 48 | + * Use when: |
| 49 | + * - An MCP runtime returns tool output back to the shared LLM layer |
| 50 | + * |
| 51 | + * Expects: |
| 52 | + * - Error responses set `isError` when the tool execution failed |
| 53 | + * |
| 54 | + * Returns: |
| 55 | + * - Structured and unstructured MCP tool output |
| 56 | + */ |
| 57 | +export interface McpCallToolResult { |
| 58 | + content?: Array<Record<string, unknown>> |
| 59 | + structuredContent?: Record<string, unknown> |
| 60 | + toolResult?: unknown |
| 61 | + isError?: boolean |
| 62 | +} |
| 63 | + |
| 64 | +/** |
| 65 | + * Runtime contract for wiring MCP tool discovery and execution into `stage-ui`. |
| 66 | + * |
| 67 | + * Use when: |
| 68 | + * - A concrete runtime such as Electron needs to provide MCP access without a singleton bridge |
| 69 | + * |
| 70 | + * Expects: |
| 71 | + * - `listTools` and `callTool` are safe to call multiple times |
| 72 | + * |
| 73 | + * Returns: |
| 74 | + * - An object that can back `createMcpTools` |
| 75 | + */ |
| 76 | +export interface McpToolRuntime { |
| 77 | + listTools: () => Promise<McpToolDescriptor[]> |
| 78 | + callTool: (payload: McpCallToolPayload) => Promise<McpCallToolResult> |
| 79 | +} |
| 80 | + |
| 81 | +/** |
| 82 | + * Creates MCP proxy tools backed by a runtime-provided transport. |
| 83 | + * |
| 84 | + * Use when: |
| 85 | + * - A runtime wants to register MCP tools into the shared LLM tool store |
| 86 | + * |
| 87 | + * Expects: |
| 88 | + * - The runtime implements the `McpToolRuntime` contract |
| 89 | + * |
| 90 | + * Returns: |
| 91 | + * - xsai tool definition promises for MCP listing and invocation |
| 92 | + */ |
| 93 | +export function createMcpTools(runtime: McpToolRuntime): Array<Promise<Tool>> { |
| 94 | + return [ |
| 95 | + tool({ |
| 96 | + name: 'builtIn_mcpListTools', |
| 97 | + description: 'List all available MCP tools. Call this first to discover tool names before calling builtIn_mcpCallTool.', |
| 98 | + execute: async () => { |
| 99 | + try { |
| 100 | + return await runtime.listTools() |
33 | 101 | } |
34 | | - } |
| 102 | + catch (error) { |
| 103 | + console.warn('[builtIn_mcpListTools] failed to list tools:', error) |
| 104 | + return '' |
| 105 | + } |
| 106 | + }, |
| 107 | + parameters: z.object({}).strict(), |
| 108 | + }), |
| 109 | + tool({ |
| 110 | + name: 'builtIn_mcpCallTool', |
| 111 | + description: 'Call an MCP tool by name. Use builtIn_mcpListTools first to get available tool names.', |
| 112 | + execute: async ({ name, arguments: argsJson }) => { |
| 113 | + try { |
| 114 | + const args = argsJson ? JSON.parse(argsJson) : {} |
| 115 | + return await runtime.callTool({ name, arguments: args }) |
| 116 | + } |
| 117 | + catch (error) { |
| 118 | + return { |
| 119 | + isError: true, |
| 120 | + content: [{ type: 'text', text: errorMessageFrom(error) ?? String(error) }], |
| 121 | + } |
| 122 | + } |
| 123 | + }, |
| 124 | + // NOTICE: `arguments` is z.string() (JSON) because z.unknown() produces `{}` (no `type` key) |
| 125 | + // and z.record() emits `propertyNames`, both rejected by OpenAI. |
| 126 | + parameters: z.object({ |
| 127 | + name: z.string().describe('Tool name in "<serverName>::<toolName>" format'), |
| 128 | + arguments: z.string().describe('JSON object of tool arguments, e.g. {"query":"hello","limit":10}'), |
| 129 | + }).strict(), |
| 130 | + }), |
| 131 | + ] |
| 132 | +} |
| 133 | + |
| 134 | +function createUnavailableMcpToolRuntime(): McpToolRuntime { |
| 135 | + return { |
| 136 | + async listTools() { |
| 137 | + throw new Error('MCP tools are not available in this runtime.') |
| 138 | + }, |
| 139 | + async callTool() { |
| 140 | + throw new Error('MCP tools are not available in this runtime.') |
35 | 141 | }, |
36 | | - // NOTICE: `arguments` is z.string() (JSON) because z.unknown() produces `{}` (no `type` key) |
37 | | - // and z.record() emits `propertyNames`, both rejected by OpenAI. |
38 | | - parameters: z.object({ |
39 | | - name: z.string().describe('Tool name in "<serverName>::<toolName>" format'), |
40 | | - arguments: z.string().describe('JSON object of tool arguments, e.g. {"query":"hello","limit":10}'), |
41 | | - }).strict(), |
42 | | - }), |
43 | | -] |
44 | | - |
45 | | -export const mcp = async () => Promise.all(tools) |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +/** |
| 146 | + * Builds the default stage-ui MCP tool set without depending on runtime singletons. |
| 147 | + * |
| 148 | + * Use when: |
| 149 | + * - Shared code needs the MCP tool schema before a concrete runtime registers live implementations |
| 150 | + * |
| 151 | + * Expects: |
| 152 | + * - Runtime-specific callers override these tools through `useLlmToolsStore` |
| 153 | + * |
| 154 | + * Returns: |
| 155 | + * - MCP tool definitions with an unavailable-runtime fallback |
| 156 | + */ |
| 157 | +export async function mcp(): Promise<Tool[]> { |
| 158 | + return await Promise.all(createMcpTools(createUnavailableMcpToolRuntime())) |
| 159 | +} |
0 commit comments