Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/praisonai-ts/examples/README-tool-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The path ../../praisonai-agents/tests/mcp-sse-direct-server.py assumes a specific directory structure relative to the praisonai-ts package. This might be confusing or not work for users who have cloned only this repository or have a different setup.

Have you considered if this instruction could be made more robust or if a note about the expected directory layout should be added? For instance, mentioning where praisonai-agents should be located relative to praisonai-ts.

```

2. Run the TypeScript example which connects to this server:
```bash
npx ts-node examples/tools/mcp-sse.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The PR description mentions that running npx ts-node examples/tools/mcp-sse.ts results in an ENETUNREACH error. This is a critical issue for an example. Could you clarify if this example is expected to work out-of-the-box after starting the Python server as described?

If there are known issues or specific environmental configurations required (e.g., firewall settings, specific Python server behavior not covered by the command), it would be very helpful to document them here. An example that fails without clear troubleshooting guidance can be frustrating for users.

Comment on lines +204 to +206
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling guidance for the MCP SSE example.

The documentation should mention potential network errors (like the ENETUNREACH error mentioned in the PR objectives) and how users can troubleshoot them.

2. Run the TypeScript example which connects to this server:
```bash
npx ts-node examples/tools/mcp-sse.ts

+Troubleshooting:
+- If you encounter ENETUNREACH errors, ensure the Python server is running and accessible at http://127.0.0.1:8080/sse
+- Check that no firewall is blocking the connection
+- Verify the server is listening on the correct host and port


<details>
<summary>🤖 Prompt for AI Agents</summary>

In src/praisonai-ts/examples/README-tool-examples.md around lines 204 to 206,
add a troubleshooting section after the command to run the MCP SSE example.
Include guidance on handling network errors like ENETUNREACH by advising users
to ensure the Python server is running and accessible at
http://127.0.0.1:8080/sse, check for firewall blocks, and verify the server's
host and port configuration.


</details>

<!-- This is an auto-generated comment by CodeRabbit -->

```
70 changes: 70 additions & 0 deletions src/praisonai-ts/examples/tools/mcp-sse.ts
Original file line number Diff line number Diff line change
@@ -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<string, (...args: any[]) => Promise<any>> = {};

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<string, any> = {};

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();
}
3 changes: 2 additions & 1 deletion src/praisonai-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/praisonai-ts/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export class BaseTool implements Tool {
}

// Export all tool modules
export * from './arxivTools';
export * from './arxivTools';
export * from './mcpSse';
116 changes: 116 additions & 0 deletions src/praisonai-ts/src/tools/mcpSse.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> | undefined {
return this.inputSchema?.properties;
}

async execute(args: any = {}): Promise<any> {
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<MCPTool> {
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<void> {
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<MCPTool> {
return this.tools[Symbol.iterator]();
}

toOpenAITools() {
return this.tools.map(t => t.toOpenAITool());
}

async close(): Promise<void> {
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;
}
}
Loading