UNPKG

@xynehq/jaf

Version:

Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools

293 lines 10.7 kB
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { z } from 'zod'; import { safeConsole } from '../utils/logger.js'; /** * Create an MCP client using the STDIO transport. * Suitable for local MCP servers started as subprocesses (e.g., npx/uvx/python/node). */ export async function makeMCPClient(command, args = []) { const transport = new StdioClientTransport({ command, args, }); const client = new Client({ name: "jaf-client", version: "2.0.0", }); await client.connect(transport); return { async listTools() { try { const response = await client.listTools(); return response.tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); } catch (error) { safeConsole.error('Failed to list MCP tools:', error); return []; } }, async callTool(name, args) { try { const response = await client.callTool({ name, arguments: args }); if (response.content && Array.isArray(response.content) && response.content.length > 0) { return response.content.map((c) => { if (c.type === 'text') { return c.text; } return JSON.stringify(c); }).join('\n'); } return JSON.stringify(response); } catch (error) { return JSON.stringify({ error: 'mcp_tool_error', message: error instanceof Error ? error.message : String(error), tool_name: name }); } }, async close() { await client.close(); } }; } /** * Create an MCP client using the Streamable HTTP transport (SSE). * * This connects to a remote MCP server that exposes the single MCP endpoint * supporting POST and GET with Server-Sent Events per the 2025-06-18 spec. * * Example: * const mcp = await makeMCPClientSSE('https://example.com/mcp', { * headers: { Authorization: `Bearer ${token}` } * }) */ export async function makeMCPClientSSE(url, opts) { const endpoint = new URL(url); // Ensure EventSource is available in Node environments. // The MCP SDK's SSE transport expects a global EventSource (available in browsers). // In Node.js, install the 'eventsource' package and set it on globalThis. if (typeof globalThis.EventSource === 'undefined') { try { const mod = await import('eventsource'); const ES = mod.default ?? mod; globalThis.EventSource = ES; } catch (e) { const msg = e instanceof Error ? e.message : String(e); throw new Error(`EventSource is not defined. Install the 'eventsource' package or run in a browser. Cause: ${msg}`); } } // NOTE: Current SDK type signature expects only the URL; some versions may // accept an options bag. To keep compatibility with this repository, we pass // only the endpoint here. If your server requires custom headers, consider // configuring it to accept token/query params, or upgrade the SDK accordingly. const transport = new SSEClientTransport(endpoint); const client = new Client({ name: "jaf-client", version: "2.0.0", }); await client.connect(transport); return { async listTools() { try { const response = await client.listTools(); return response.tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); } catch (error) { safeConsole.error('Failed to list MCP tools (SSE):', error); return []; } }, async callTool(name, args) { try { const response = await client.callTool({ name, arguments: args }); if (response.content && Array.isArray(response.content) && response.content.length > 0) { return response.content.map((c) => { if (c.type === 'text') { return c.text; } return JSON.stringify(c); }).join('\n'); } return JSON.stringify(response); } catch (error) { return JSON.stringify({ error: 'mcp_tool_error', message: error instanceof Error ? error.message : String(error), tool_name: name }); } }, async close() { await client.close(); } }; } /** * Create an MCP client using the Streamable HTTP transport. * * This connects to a remote MCP server that implements the Streamable HTTP transport * specification using HTTP POST for sending messages and HTTP GET with Server-Sent Events * for receiving messages. * * Example: * const mcp = await makeMCPClientHTTP('https://example.com/mcp', { * headers: { Authorization: `Bearer ${token}` }, * sessionId: 'my-session-123' * }) */ export async function makeMCPClientHTTP(url, opts) { const endpoint = new URL(url); // Ensure EventSource is available in Node environments for the underlying SSE functionality if (typeof globalThis.EventSource === 'undefined') { try { const mod = await import('eventsource'); const ES = mod.default ?? mod; globalThis.EventSource = ES; } catch (e) { const msg = e instanceof Error ? e.message : String(e); throw new Error(`EventSource is not defined. Install the 'eventsource' package or run in a browser. Cause: ${msg}`); } } const transportOpts = { requestInit: { ...opts?.requestInit, headers: { ...opts?.requestInit?.headers, ...opts?.headers } }, fetch: opts?.fetch, // Only set sessionId if explicitly provided and not empty // Otherwise let the server generate a new session ID ...(opts?.sessionId && opts.sessionId.trim() ? { sessionId: opts.sessionId } : {}) }; const transport = new StreamableHTTPClientTransport(endpoint, transportOpts); const client = new Client({ name: "jaf-client", version: "2.0.0", }); await client.connect(transport); return { async listTools() { try { const response = await client.listTools(); return response.tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); } catch (error) { safeConsole.error('Failed to list MCP tools (HTTP):', error); return []; } }, async callTool(name, args) { try { const response = await client.callTool({ name, arguments: args }); if (response.content && Array.isArray(response.content) && response.content.length > 0) { return response.content.map((c) => { if (c.type === 'text') { return c.text; } return JSON.stringify(c); }).join('\n'); } return JSON.stringify(response); } catch (error) { return JSON.stringify({ error: 'mcp_tool_error', message: error instanceof Error ? error.message : String(error), tool_name: name }); } }, async close() { await client.close(); } }; } export function mcpToolToJAFTool(mcpClient, mcpToolDef) { let zodSchema = jsonSchemaToZod(mcpToolDef.inputSchema || {}); // Ensure top-level OBJECT parameters for function-calling providers if (!(zodSchema instanceof z.ZodObject)) { zodSchema = z.object({ value: zodSchema }).describe('Wrapped non-object parameters'); } const baseTool = { schema: { name: mcpToolDef.name, description: mcpToolDef.description ?? mcpToolDef.name, parameters: zodSchema, }, execute: (args, _) => mcpClient.callTool(mcpToolDef.name, args), }; return baseTool; } function jsonSchemaToZod(schema) { if (!schema || typeof schema !== 'object') { return z.any(); } if (schema.type === 'object') { const shape = {}; if (schema.properties) { for (const [key, prop] of Object.entries(schema.properties)) { let fieldSchema = jsonSchemaToZod(prop); if (!schema.required || !schema.required.includes(key)) { fieldSchema = fieldSchema.optional(); } if (prop.description) { fieldSchema = fieldSchema.describe(prop.description); } shape[key] = fieldSchema; } } return z.object(shape); } if (schema.type === 'string') { let stringSchema = z.string(); if (schema.description) { stringSchema = stringSchema.describe(schema.description); } if (schema.enum) { return z.enum(schema.enum); } return stringSchema; } if (schema.type === 'number' || schema.type === 'integer') { return z.number(); } if (schema.type === 'boolean') { return z.boolean(); } if (schema.type === 'array') { return z.array(jsonSchemaToZod(schema.items)); } return z.any(); } //# sourceMappingURL=mcp.js.map