Tool Chaining with MCP
Problem
Complex tasks require multiple MCP tool calls in sequence:
- Search → Read → Analyze → Write
- Query database → Process → Store results
- Fetch data → Transform → Send notification
Manually orchestrating tool calls is:
- Error-prone
- Difficult to manage state
- Hard to handle failures
- Not reusable
Solution
Implement intelligent tool chaining with:
- Automatic tool selection
- State management
- Error recovery
- Result validation
- Chain composition
Code
import { NeuroLink } from "@juspay/neurolink";
type ChainStep = {
toolName: string;
args: Record<string, any>;
validateResult?: (result: any) => boolean;
onError?: (error: Error) => "retry" | "skip" | "abort";
};
type ChainContext = {
steps: ChainStep[];
results: any[];
currentStep: number;
metadata: Record<string, any>;
};
class ToolChain {
private neurolink: NeuroLink;
private context: ChainContext;
constructor() {
this.neurolink = new NeuroLink({
mcpServers: {
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
},
github: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN || "",
},
},
},
});
this.context = {
steps: [],
results: [],
currentStep: 0,
metadata: {},
};
}
/**
* Add step to chain
*/
addStep(step: ChainStep): this {
this.context.steps.push(step);
return this; // Fluent interface
}
/**
* Execute tool chain
*/
async execute(): Promise<{
success: boolean;
results: any[];
errors: Error[];
}> {
const errors: Error[] = [];
console.log(
`🔗 Executing chain with ${this.context.steps.length} steps...\n`,
);
for (let i = 0; i < this.context.steps.length; i++) {
this.context.currentStep = i;
const step = this.context.steps[i];
console.log(
`📍 Step ${i + 1}/${this.context.steps.length}: ${step.toolName}`,
);
try {
const result = await this.executeStep(step);
// Validate result if validator provided
if (step.validateResult && !step.validateResult(result)) {
throw new Error("Result validation failed");
}
this.context.results[i] = result;
console.log(`✅ Step ${i + 1} completed\n`);
} catch (error: any) {
console.error(`❌ Step ${i + 1} failed:`, error.message);
errors.push(error);
// Handle error
const action = step.onError?.(error) || "abort";
if (action === "abort") {
console.log("🛑 Aborting chain");
return { success: false, results: this.context.results, errors };
}
if (action === "retry") {
console.log("🔄 Retrying step...");
i--; // Retry current step
continue;
}
if (action === "skip") {
console.log("⏭️ Skipping step");
this.context.results[i] = null;
continue;
}
}
}
console.log("✅ Chain execution complete");
return {
success: errors.length === 0,
results: this.context.results,
errors,
};
}
/**
* Execute single step
*/
private async executeStep(step: ChainStep): Promise<any> {
// Replace placeholders in args with previous results
const processedArgs = this.processArgs(step.args);
// Use AI to execute tool
const result = await this.neurolink.generate({
input: {
text: `Execute the tool "${step.toolName}" with these arguments: ${JSON.stringify(processedArgs)}`,
},
enableTools: true,
});
return this.extractToolResult(result);
}
/**
* Process args to replace placeholders with previous results
*/
private processArgs(args: Record<string, any>): Record<string, any> {
const processed: Record<string, any> = {};
for (const [key, value] of Object.entries(args)) {
if (typeof value === "string" && value.startsWith("$")) {
// Reference to previous result
const stepIndex = parseInt(value.slice(1));
processed[key] = this.context.results[stepIndex];
} else {
processed[key] = value;
}
}
return processed;
}
/**
* Extract tool result from AI response
*/
private extractToolResult(response: any): any {
// Implementation depends on response format
return response.toolResults?.[0] || response.content;
}
/**
* Get chain context
*/
getContext(): ChainContext {
return this.context;
}
/**
* Reset chain
*/
reset(): this {
this.context = {
steps: [],
results: [],
currentStep: 0,
metadata: {},
};
return this;
}
}
/**
* Pre-built chain templates
*/
class ChainTemplates {
/**
* Search → Read → Summarize chain
*/
static searchAnalyzeChain(query: string, maxFiles: number = 3): ToolChain {
const chain = new ToolChain();
return chain
.addStep({
toolName: "search_files",
args: { query, max_results: maxFiles },
})
.addStep({
toolName: "read_file",
args: { path: "$0" }, // Use result from step 0
})
.addStep({
toolName: "analyze_content",
args: { content: "$1" },
});
}
/**
* Fetch → Process → Save chain
*/
static fetchProcessSaveChain(url: string, outputPath: string): ToolChain {
const chain = new ToolChain();
return chain
.addStep({
toolName: "fetch_url",
args: { url },
validateResult: (result) => result.status === 200,
})
.addStep({
toolName: "process_data",
args: { data: "$0" },
})
.addStep({
toolName: "write_file",
args: {
path: outputPath,
content: "$1",
},
onError: () => "retry",
});
}
/**
* GitHub workflow: Create issue → Create branch → Push → Create PR
*/
static githubWorkflowChain(
repo: string,
issueTitle: string,
branchName: string,
): ToolChain {
const chain = new ToolChain();
return chain
.addStep({
toolName: "github_create_issue",
args: {
repo,
title: issueTitle,
body: "Auto-generated issue",
},
})
.addStep({
toolName: "github_create_branch",
args: {
repo,
branch: branchName,
from: "main",
},
})
.addStep({
toolName: "github_push_files",
args: {
repo,
branch: branchName,
files: [],
message: `Fixes #$0`, // Reference issue from step 0
},
})
.addStep({
toolName: "github_create_pr",
args: {
repo,
title: `Fix: ${issueTitle}`,
head: branchName,
base: "main",
body: `Closes #$0`,
},
});
}
}
// Usage Example 1: File Processing Chain
async function example1_FileProcessing() {
const chain = new ToolChain();
chain
.addStep({
toolName: "list_directory",
args: { path: "./docs" },
})
.addStep({
toolName: "read_file",
args: { path: "$0" }, // Read first file from listing
validateResult: (content) => content.length > 0,
})
.addStep({
toolName: "analyze_content",
args: { content: "$1" },
});
const result = await chain.execute();
console.log("\n=== Results ===");
console.log("Success:", result.success);
console.log("Results:", result.results);
}
// Example 2: Data Pipeline Chain
async function example2_DataPipeline() {
const chain = new ToolChain();
chain
.addStep({
toolName: "query_database",
args: {
query: "SELECT * FROM users WHERE active = true",
},
})
.addStep({
toolName: "transform_data",
args: { data: "$0" },
onError: () => "skip", // Skip transformation errors
})
.addStep({
toolName: "send_notification",
args: {
message: "Data pipeline completed: $1",
},
});
await chain.execute();
}
// Example 3: Using Pre-built Templates
async function example3_Templates() {
// Search and analyze
const searchChain = ChainTemplates.searchAnalyzeChain("authentication", 5);
await searchChain.execute();
// GitHub workflow
const githubChain = ChainTemplates.githubWorkflowChain(
"myorg/myrepo",
"Fix authentication bug",
"fix/auth-bug",
);
await githubChain.execute();
}
// Main
async function main() {
console.log("=== Example 1: File Processing ===\n");
await example1_FileProcessing();
console.log("\n=== Example 2: Data Pipeline ===\n");
await example2_DataPipeline();
console.log("\n=== Example 3: Templates ===\n");
await example3_Templates();
}
main();
Explanation
1. Fluent Interface
Chain steps with method chaining:
chain
.addStep({...})
.addStep({...})
.addStep({...});
2. Result References
Reference previous step results:
args: {
content: "$1";
} // Use result from step 1
3. Validation
Validate step results:
validateResult: (result) => result.status === 200;
4. Error Handling
Control flow on errors:
- "abort": Stop chain
- "retry": Retry current step
- "skip": Continue to next step
5. Reusable Templates
Pre-built chains for common patterns:
ChainTemplates.searchAnalyzeChain(query);
Variations
Conditional Chains
Branch based on results:
class ConditionalChain extends ToolChain {
addConditionalStep(
condition: (context: ChainContext) => boolean,
trueStep: ChainStep,
falseStep: ChainStep,
) {
return this.addStep({
...trueStep,
args: condition(this.context) ? trueStep.args : falseStep.args,
});
}
}
// Usage
chain.addConditionalStep(
(ctx) => ctx.results[0].count > 100,
{ toolName: "process_large", args: {} },
{ toolName: "process_small", args: {} },
);
Parallel Chains
Execute independent chains in parallel:
async function executeParallel(chains: ToolChain[]) {
const results = await Promise.all(chains.map((chain) => chain.execute()));
return {
success: results.every((r) => r.success),
results: results.map((r) => r.results),
errors: results.flatMap((r) => r.errors),
};
}
// Usage
await executeParallel([
ChainTemplates.searchAnalyzeChain("auth"),
ChainTemplates.searchAnalyzeChain("database"),
]);
Loop Chains
Repeat steps until condition met:
class LoopChain extends ToolChain {
async executeLoop(
step: ChainStep,
condition: (result: any) => boolean,
maxIterations: number = 10,
) {
let iterations = 0;
let result: any;
while (iterations < maxIterations) {
result = await this.executeStep(step);
if (condition(result)) {
break;
}
iterations++;
}
return result;
}
}
// Usage: Retry until success
await chain.executeLoop(
{ toolName: "check_status", args: { id: "123" } },
(result) => result.status === "complete",
20,
);
Chain Composition
Combine multiple chains:
class CompositeChain {
private chains: ToolChain[] = [];
add(chain: ToolChain): this {
this.chains.push(chain);
return this;
}
async execute() {
const results = [];
for (const chain of this.chains) {
const result = await chain.execute();
results.push(result);
if (!result.success) {
break; // Stop on first failure
}
}
return results;
}
}
Common Patterns
Data Processing Pipeline
Fetch → Validate → Transform → Store → Notify
Content Workflow
Search → Read → Analyze → Summarize → Publish
GitHub Automation
Create Issue → Create Branch → Commit → Push → Create PR
Monitoring Pipeline
Query Metrics → Analyze → Alert → Create Ticket → Notify
Best Practices
- Keep chains short: 3-5 steps maximum
- Validate early: Check results at each step
- Handle errors: Define recovery strategy
- Use templates: Standardize common patterns
- Log extensively: Track chain execution
- Test chains: Verify each step independently
- Document dependencies: Clear step relationships