const MAX_RESULT_CHARACTERS = 4000; const TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/; let client = null; export async function connectMcpServer(rawUrl) { const url = validateServerUrl(rawUrl); await disconnectMcpServer(); const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([ import('@modelcontextprotocol/sdk/client/index.js'), import('@modelcontextprotocol/sdk/client/streamableHttp.js'), ]); const nextClient = new Client({ name: 'lfm-webgpu', version: '1.0.0' }, { capabilities: {} }); try { await nextClient.connect(new StreamableHTTPClientTransport(url, { fetch: postOnlyFetch })); const response = await nextClient.listTools(); client = nextClient; return normalizeMcpTools(response.tools); } catch (error) { await nextClient.close().catch(() => {}); throw error; } } export async function disconnectMcpServer() { const active = client; client = null; if (active) await active.close().catch(() => {}); } export async function callMcpTool(name, args, signal) { if (!client) throw new Error('The MCP server is not connected.'); if (signal?.aborted) throw new DOMException('Generation stopped.', 'AbortError'); const response = await client.callTool({ name, arguments: args }); if (signal?.aborted) throw new DOMException('Generation stopped.', 'AbortError'); if (response.isError) throw new Error(errorMessage(response.content)); return compactMcpResult(response); } export function compactMcpResult(response) { const result = response.structuredContent ?? { content: response.content || [] }; const serialized = JSON.stringify(result); if (serialized.length <= MAX_RESULT_CHARACTERS) return result; const text = (response.content || []).filter(item => item.type === 'text').map(item => item.text).join('\n'); return { truncated: true, content: [{ type: 'text', text: (text || serialized).slice(0, MAX_RESULT_CHARACTERS) }], }; } export function normalizeMcpTools(tools = []) { return (Array.isArray(tools) ? tools : []).flatMap(normalizeDiscoveredTool); } function normalizeDiscoveredTool(tool) { const name = String(tool?.name || ''); const parameters = tool?.inputSchema; if (!TOOL_NAME.test(name) || !parameters || parameters.type !== 'object' || !parameters.properties || Array.isArray(parameters.properties)) return []; return [{ id: `mcp:${name}`, name, description: String(tool.description || `MCP tool: ${name}`), parameters: structuredClone(parameters), source: 'mcp', enabled: false, }]; } function validateServerUrl(rawUrl) { let url; try { url = new URL(String(rawUrl || '').trim()); } catch { throw new Error('Enter a valid MCP server URL.'); } const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname); if (url.protocol !== 'https:' && !localHttp) throw new Error('MCP servers must use HTTPS, except on localhost.'); return url; } function errorMessage(content) { const text = (content || []).filter(item => item.type === 'text').map(item => item.text).join('\n').trim(); return text || 'The MCP tool returned an error.'; } function postOnlyFetch(input, init = {}) { if (String(init.method || 'GET').toUpperCase() === 'GET') return Promise.resolve(new Response(null, { status: 405 })); return fetch(input, init); }