LFM2.5-VL-3B-WebGPU / src /tools /mcp-client.js
shubeydoo's picture
Initial release
6c30253
Raw
History Blame Contribute Delete
3.36 kB
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);
}