Skip to Content
DocumentationTutorialMCP Integration

MCP Integration

MCP has emerged as a great way to connect your agents to external capabilities. Since Cascaide nodes are plain async functions, the set up is straightforward.

  • Initialise your MCP client from within a node’s exec
  • Simply call it!

We will walk through a practical implementation. You can find the code at https://github.com/cascaide-ts/cookbook.git .

In this guide, we will focus on the searchToolNode as that is what’s relevant. The set up is a ReAct agent. We will be connecting to Firecrawl MCP.


We will give the agent the following tools:

const mcp_discovery_tool = { type: 'function' as const, function: { name: 'mcp_discovery', description: 'Discovers available MCP tools from the connected server. Returns a list of tool names, their descriptions, and the exact JSON schema required for their arguments. ALWAYS use this first if you do not know the exact tool name or required arguments.', parameters: { type: 'object', properties: {}, required: [] } } }; const mcp_executor_tool = { type: 'function' as const, function: { name: 'mcp_executor', description: 'Executes a specific MCP tool discovered via mcp_discovery. You MUST provide the tool_name and a valid JSON object for tool_args.', parameters: { type: 'object', properties: { tool_name: { type: 'string', description: 'The exact name of the MCP tool to run (e.g., "firecrawl_scrape").' }, tool_args: { type: 'object', description: 'The argument payload for the tool. This MUST be an object. If the tool requires no arguments, pass an empty object {}.' } }, required: ['tool_name', 'tool_args'] } } };

The searchToolNode exec step is identical to a simple ReAct agent. All the logic relevant to MCP happens inside the executeTool function.

export async function searchToolNodeExec(prepOutput: any) { const { toolCallsToExecute, history } = prepOutput; const { executeTool } = await import('./searchAgentTools'); // Executes ALL tools in parallel const results = await Promise.all( toolCallsToExecute.map(async (toolCall: ExtendedToolCall) => { const functionName = toolCall.function.name; const toolCallId = toolCall.id; try { let toolResult; toolResult = await executeTool(toolCall); return { toolCall, toolResult }; } catch (err: any) { return { toolCall, toolResult: { role: 'tool', tool_call_id: toolCallId, content: JSON.stringify({ error: `Tool execution failed for ${functionName}: ${err.message || 'Unknown error'}` }), }, }; } }) ); return { results }; }

executeTool

We simply initialise an MCP client and provide it to the executeTool function to invoke.

import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; export type ToolCall = { id: string; type: 'function'; function: { name: string; arguments: string; // JSON string }; }; export type ToolResult = { role: 'tool'; tool_call_id: string; content: string; }; // ============================================================================ // MCP CONNECTION // ============================================================================ // Using HTTP transport instead of stdio for serverless compatibility. // Each request creates a fresh stateless connection — no child processes, // no singleton needed, works cleanly on Vercel and other serverless platforms. const createMcpClient = async (): Promise<Client> => { const transport = new StreamableHTTPClientTransport( new URL("https://mcp.firecrawl.dev/v2/mcp"), { headers: { Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY}` } } ); const client = new Client( { name: "my-ai-agent", version: "1.0.0" }, { capabilities: {} } ); await client.connect(transport); return client; }; export const executeTool = async (toolCall: ToolCall): Promise<ToolResult> => { const { name, arguments: argsString } = toolCall.function; try { const args = JSON.parse(argsString); let toolResultData: any; const client = await createMcpClient(); try { switch (name) { case 'mcp_discovery': const { tools } = await client.listTools(); toolResultData = tools.map(t => ({ name: t.name, description: t.description, required_schema: t.inputSchema })); break; case 'mcp_executor': if (!args.tool_name) { throw new Error("Missing 'tool_name'. You must specify which MCP tool to execute."); } const mcpResult = await client.callTool({ name: args.tool_name, arguments: args.tool_args || {} }); if (mcpResult.isError) { const errorText = (mcpResult.content as any[]) .filter((c: any) => c.type === 'text') .map((c: any) => c.text) .join('\n'); throw new Error(`MCP Execution Error: ${errorText}`); } toolResultData = (mcpResult.content as any[]) .filter((c: any) => c.type === 'text') .map((c: any) => c.text) .join('\n\n'); break; default: throw new Error(`Unknown tool: ${name}`); } } finally { // Always close the connection after use await client.close(); } return { role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify(toolResultData) }; } catch (error: any) { return { role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify({ error: error.message || 'Tool execution failed' }) }; } };
💡

That’s it. This gives you an easily extensible tool node that supports parallel execution. Want to add more MCPs/tools? Just add cases.

Last updated on