@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
293 lines • 10.7 kB
JavaScript
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