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..0c6293282 --- /dev/null +++ b/src/praisonai-ts/examples/tools/mcp-sse.ts @@ -0,0 +1,70 @@ +import { Agent } from '../../src/agent'; +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'); + 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) { + 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 = {}; + + 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); + }; + } + + const agent = new Agent({ + instructions: 'Use the tools to greet people and report the weather.', + name: 'MCPAgent', + tools: mcp.toOpenAITools(), + toolFunctions + }); + + 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) { + 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..46683a3da --- /dev/null +++ b/src/praisonai-ts/src/tools/mcpSse.ts @@ -0,0 +1,116 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { BaseTool } from './index'; + +export interface MCPToolInfo { + name: string; + description?: string; + inputSchema?: any; +} + +export class MCPTool extends BaseTool { + private client: Client; + private inputSchema: any; + + 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 { + 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)}`); + } + } + + 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) { + if (debug) { + console.log(`MCP client initialized for URL: ${url}`); + } + } + + async initialize(): Promise { + 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 { + return this.tools[Symbol.iterator](); + } + + 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; + } +}