From 8df8569c8f24460f5eea359cf70eebc15631e2c0 Mon Sep 17 00:00:00 2001 From: Mervin Praison Date: Wed, 4 Jun 2025 09:05:08 +0100 Subject: [PATCH 1/2] Add MCP SSE example and dependency --- .../examples/README-tool-examples.md | 14 ++++ src/praisonai-ts/examples/tools/mcp-sse.ts | 35 ++++++++++ src/praisonai-ts/package.json | 3 +- src/praisonai-ts/src/tools/index.ts | 3 +- src/praisonai-ts/src/tools/mcpSse.ts | 70 +++++++++++++++++++ 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/praisonai-ts/examples/tools/mcp-sse.ts create mode 100644 src/praisonai-ts/src/tools/mcpSse.ts diff --git a/src/praisonai-ts/examples/README-tool-examples.md b/src/praisonai-ts/examples/README-tool-examples.md index cc2967b2b..180c26fa4 100644 --- a/src/praisonai-ts/examples/README-tool-examples.md +++ b/src/praisonai-ts/examples/README-tool-examples.md @@ -191,3 +191,17 @@ const agent = new Agent({ // Start the agent with a prompt that will trigger tool usage agent.start("What's the weather in Paris, the time in Tokyo, and what is 25 * 4?"); ``` + +## MCP SSE Tool Integration + +PraisonAI can use remote tools exposed via the Model Context Protocol over Server-Sent Events. + +1. Start the Python SSE server in another terminal: +```bash +python ../../praisonai-agents/tests/mcp-sse-direct-server.py --host 127.0.0.1 --port 8080 +``` + +2. Run the TypeScript example which connects to this server: +```bash +npx ts-node examples/tools/mcp-sse.ts +``` diff --git a/src/praisonai-ts/examples/tools/mcp-sse.ts b/src/praisonai-ts/examples/tools/mcp-sse.ts new file mode 100644 index 000000000..c05c20445 --- /dev/null +++ b/src/praisonai-ts/examples/tools/mcp-sse.ts @@ -0,0 +1,35 @@ +import { Agent } from '../../src/agent'; +import { MCP, MCPTool } from '../../src/tools/mcpSse'; + +async function main() { + // Connect to the running SSE server + const mcp = new MCP('http://127.0.0.1:8080/sse'); + await mcp.initialize(); + + // Create tool functions that call the remote MCP tools + const toolFunctions: Record Promise> = {}; + for (const tool of mcp) { + const paramNames = Object.keys((tool as any).inputSchema?.properties || {}); + toolFunctions[tool.name] = async (...args: any[]) => { + const params: Record = {}; + for (let i = 0; i < args.length; i++) { + params[paramNames[i] || `arg${i}`] = args[i]; + } + return tool.execute(params); + }; + } + + const agent = new Agent({ + instructions: 'Use the tools to greet people and report the weather.', + name: 'MCPAgent', + tools: mcp.toOpenAITools(), + toolFunctions + }); + + const result = await agent.start('Say hello to John and tell the weather in London.'); + console.log('\nFinal Result:', result); +} + +if (require.main === module) { + main(); +} diff --git a/src/praisonai-ts/package.json b/src/praisonai-ts/package.json index bd8f49b25..30d3bdc1f 100644 --- a/src/praisonai-ts/package.json +++ b/src/praisonai-ts/package.json @@ -62,7 +62,8 @@ "fast-xml-parser": "^4.5.1", "node-fetch": "^2.6.9", "openai": "^4.81.0", - "praisonai": "^1.0.19" + "praisonai": "^1.0.19", + "@modelcontextprotocol/sdk": "^1.12.1" }, "optionalDependencies": { "boxen": "^7.1.1", diff --git a/src/praisonai-ts/src/tools/index.ts b/src/praisonai-ts/src/tools/index.ts index 283b0e4be..f57b493e8 100644 --- a/src/praisonai-ts/src/tools/index.ts +++ b/src/praisonai-ts/src/tools/index.ts @@ -20,4 +20,5 @@ export class BaseTool implements Tool { } // Export all tool modules -export * from './arxivTools'; \ No newline at end of file +export * from './arxivTools'; +export * from './mcpSse'; diff --git a/src/praisonai-ts/src/tools/mcpSse.ts b/src/praisonai-ts/src/tools/mcpSse.ts new file mode 100644 index 000000000..ad29aa962 --- /dev/null +++ b/src/praisonai-ts/src/tools/mcpSse.ts @@ -0,0 +1,70 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { BaseTool } from './index'; + +export interface MCPPToolInfo { + name: string; + description?: string; + inputSchema?: any; +} + +export class MCPTool extends BaseTool { + private client: Client; + private inputSchema: any; + + constructor(info: MCPPToolInfo, client: Client) { + super(info.name, info.description || `Call the ${info.name} tool`); + this.client = client; + this.inputSchema = info.inputSchema || { type: 'object', properties: {}, required: [] }; + } + + async execute(args: any = {}): Promise { + const result: any = await this.client.callTool({ name: this.name, arguments: args }); + if (result.structuredContent) { + return result.structuredContent; + } + if (Array.isArray(result.content) && result.content.length > 0) { + const item = result.content[0]; + if (typeof item.text === 'string') return item.text; + } + return result; + } + + toOpenAITool() { + return { + type: 'function', + function: { + name: this.name, + description: this.description, + parameters: this.inputSchema + } + }; + } +} + +export class MCP implements Iterable { + tools: MCPTool[] = []; + private client: Client | null = null; + + constructor(private url: string, private debug = false) {} + + async initialize(): Promise { + this.client = new Client({ name: 'praisonai-ts-mcp', version: '1.0.0' }); + const transport = new SSEClientTransport(new URL(this.url)); + await this.client.connect(transport); + const { tools } = await this.client.listTools(); + this.tools = tools.map((t: any) => new MCPTool({ + name: t.name, + description: t.description, + inputSchema: t.inputSchema + }, this.client as Client)); + } + + [Symbol.iterator](): Iterator { + return this.tools[Symbol.iterator](); + } + + toOpenAITools() { + return this.tools.map(t => t.toOpenAITool()); + } +} From 385700f1e1eb5f15fea8e1b86a6a145e2a81bb93 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:23:07 +0000 Subject: [PATCH 2/2] feat: improve MCP SSE tooling with comprehensive error handling and type safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive error handling to MCP initialization and tool execution - Implement resource cleanup mechanisms with close() method and connection state tracking - Fix interface typo: MCPPToolInfo -> MCPToolInfo - Add schemaProperties getter to improve type safety in example code - Prevent re-initialization of MCP client with guard clause - Add debug logging throughout MCP lifecycle - Improve example code with proper error handling for network failures - Add better parameter mapping with object/positional argument support - Include validation warnings for missing tools and argument mismatches - Remove unused MCPTool import from example - Add proper resource cleanup in finally block These changes address the ENETUNREACH error mentioned in PR description by providing graceful error handling when the MCP SSE server is unavailable. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: MervinPraison --- src/praisonai-ts/examples/tools/mcp-sse.ts | 49 ++++++++++-- src/praisonai-ts/src/tools/mcpSse.ts | 86 +++++++++++++++++----- 2 files changed, 108 insertions(+), 27 deletions(-) diff --git a/src/praisonai-ts/examples/tools/mcp-sse.ts b/src/praisonai-ts/examples/tools/mcp-sse.ts index c05c20445..0c6293282 100644 --- a/src/praisonai-ts/examples/tools/mcp-sse.ts +++ b/src/praisonai-ts/examples/tools/mcp-sse.ts @@ -1,19 +1,47 @@ import { Agent } from '../../src/agent'; -import { MCP, MCPTool } from '../../src/tools/mcpSse'; +import { MCP } from '../../src/tools/mcpSse'; async function main() { // Connect to the running SSE server const mcp = new MCP('http://127.0.0.1:8080/sse'); - await mcp.initialize(); + try { + await mcp.initialize(); + } catch (error) { + console.error('Failed to connect to MCP SSE server:', error); + console.error('Please ensure the server is running at http://127.0.0.1:8080/sse'); + process.exit(1); + } // Create tool functions that call the remote MCP tools const toolFunctions: Record Promise> = {}; + + if (mcp.tools.length === 0) { + console.warn('Warning: No MCP tools available. Make sure the MCP server is running.'); + } + for (const tool of mcp) { - const paramNames = Object.keys((tool as any).inputSchema?.properties || {}); + if (!tool || typeof tool.name !== 'string') { + console.warn('Skipping invalid tool:', tool); + continue; + } + + const paramNames = Object.keys(tool.schemaProperties || {}); toolFunctions[tool.name] = async (...args: any[]) => { const params: Record = {}; - for (let i = 0; i < args.length; i++) { - params[paramNames[i] || `arg${i}`] = args[i]; + + if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) { + // If single object argument, use it directly as params + Object.assign(params, args[0]); + } else { + // Map positional arguments with validation + if (args.length > paramNames.length) { + console.warn( + `Tool ${tool.name}: Too many arguments provided. Expected ${paramNames.length}, got ${args.length}` + ); + } + for (let i = 0; i < Math.min(args.length, paramNames.length); i++) { + params[paramNames[i]] = args[i]; + } } return tool.execute(params); }; @@ -26,8 +54,15 @@ async function main() { toolFunctions }); - const result = await agent.start('Say hello to John and tell the weather in London.'); - console.log('\nFinal Result:', result); + try { + const result = await agent.start('Say hello to John and tell the weather in London.'); + console.log('\nFinal Result:', result); + } catch (error) { + console.error('Agent execution failed:', error); + } finally { + // Clean up MCP connection + await mcp.close(); + } } if (require.main === module) { diff --git a/src/praisonai-ts/src/tools/mcpSse.ts b/src/praisonai-ts/src/tools/mcpSse.ts index ad29aa962..46683a3da 100644 --- a/src/praisonai-ts/src/tools/mcpSse.ts +++ b/src/praisonai-ts/src/tools/mcpSse.ts @@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { BaseTool } from './index'; -export interface MCPPToolInfo { +export interface MCPToolInfo { name: string; description?: string; inputSchema?: any; @@ -12,22 +12,30 @@ export class MCPTool extends BaseTool { private client: Client; private inputSchema: any; - constructor(info: MCPPToolInfo, client: Client) { + constructor(info: MCPToolInfo, client: Client) { super(info.name, info.description || `Call the ${info.name} tool`); this.client = client; this.inputSchema = info.inputSchema || { type: 'object', properties: {}, required: [] }; } + get schemaProperties(): Record | undefined { + return this.inputSchema?.properties; + } + async execute(args: any = {}): Promise { - const result: any = await this.client.callTool({ name: this.name, arguments: args }); - if (result.structuredContent) { - return result.structuredContent; - } - if (Array.isArray(result.content) && result.content.length > 0) { - const item = result.content[0]; - if (typeof item.text === 'string') return item.text; + try { + const result: any = await this.client.callTool({ name: this.name, arguments: args }); + if (result.structuredContent) { + return result.structuredContent; + } + if (Array.isArray(result.content) && result.content.length > 0) { + const item = result.content[0]; + if (typeof item.text === 'string') return item.text; + } + return result; + } catch (error) { + throw new Error(`Failed to execute tool ${this.name}: ${error instanceof Error ? error.message : String(error)}`); } - return result; } toOpenAITool() { @@ -46,18 +54,37 @@ export class MCP implements Iterable { tools: MCPTool[] = []; private client: Client | null = null; - constructor(private url: string, private debug = false) {} + constructor(private url: string, private debug = false) { + if (debug) { + console.log(`MCP client initialized for URL: ${url}`); + } + } async initialize(): Promise { - this.client = new Client({ name: 'praisonai-ts-mcp', version: '1.0.0' }); - const transport = new SSEClientTransport(new URL(this.url)); - await this.client.connect(transport); - const { tools } = await this.client.listTools(); - this.tools = tools.map((t: any) => new MCPTool({ - name: t.name, - description: t.description, - inputSchema: t.inputSchema - }, this.client as Client)); + if (this.client) { + if (this.debug) console.log('MCP client already initialized'); + return; + } + + try { + this.client = new Client({ name: 'praisonai-ts-mcp', version: '1.0.0' }); + const transport = new SSEClientTransport(new URL(this.url)); + await this.client.connect(transport); + const { tools } = await this.client.listTools(); + this.tools = tools.map((t: any) => new MCPTool({ + name: t.name, + description: t.description, + inputSchema: t.inputSchema + }, this.client as Client)); + + if (this.debug) console.log(`Initialized MCP with ${this.tools.length} tools`); + } catch (error) { + if (this.client) { + await this.client.close().catch(() => {}); + this.client = null; + } + throw new Error(`Failed to initialize MCP client: ${error instanceof Error ? error.message : 'Unknown error'}`); + } } [Symbol.iterator](): Iterator { @@ -67,4 +94,23 @@ export class MCP implements Iterable { toOpenAITools() { return this.tools.map(t => t.toOpenAITool()); } + + async close(): Promise { + if (this.client) { + try { + await this.client.close(); + } catch (error) { + if (this.debug) { + console.warn('Error closing MCP client:', error); + } + } finally { + this.client = null; + this.tools = []; + } + } + } + + get isConnected(): boolean { + return this.client !== null; + } }