MP-301a · Module 1
Chained Tool Pipelines
4 min read
Tool chaining is the pattern where the output of one tool becomes the input of the next. In basic MCP usage the LLM orchestrates the chain — it calls tool A, reads the result, then calls tool B with data from A's output. But at the 301 level, you need to understand when to move chaining inside the server. Server-side chaining reduces round-trips, cuts token overhead, and enables atomic multi-step operations that must succeed or fail together. The trade-off is visibility: the LLM cannot inspect intermediate results or change course mid-chain.
A well-designed chain tool exposes its pipeline stages in the response. Instead of returning only the final result, include a "steps" array showing what each stage did, its input, and its output. This gives the LLM enough context to debug failures and explain the result to the user. Without stage visibility, a chain tool is a black box — and black boxes erode trust when things go wrong.
The critical design question is atomicity. If stage 2 of a 4-stage chain fails, what happens to the side effects from stage 1? If you are writing to a database, you need transaction semantics — roll back everything if any stage fails. If you are calling external APIs, rollback may be impossible, so you need compensating actions (undo what stage 1 did) or idempotency guarantees (safe to retry the whole chain). Choose your chain boundaries based on what you can safely undo.
// Server-side chain: lookup → enrich → score
async function enrichContact(email: string) {
const steps: { stage: string; status: string; data?: unknown }[] = [];
// Stage 1: Lookup
const contact = await crm.findByEmail(email);
if (!contact) {
return { isError: true, steps, error: `No contact found for ${email}` };
}
steps.push({ stage: "lookup", status: "ok", data: { id: contact.id } });
// Stage 2: Enrich from external API
let enrichment;
try {
enrichment = await clearbit.enrich(email);
steps.push({ stage: "enrich", status: "ok" });
} catch (err) {
steps.push({ stage: "enrich", status: "skipped", data: { reason: (err as Error).message } });
enrichment = null; // Graceful degradation, not failure
}
// Stage 3: Score
const score = scoreContact(contact, enrichment);
steps.push({ stage: "score", status: "ok", data: { score } });
return { contact: { ...contact, ...enrichment, score }, steps };
}
- Map the dependency graph Before building a chain, draw which stages depend on which outputs. If two stages are independent, they should be parallel (fan-out), not sequential.
- Define rollback semantics For each stage with side effects, document what happens on failure: rollback, compensate, or accept partial state. This determines your error handling strategy.
- Expose stage results Return a steps array with each stage's name, status, and relevant data. The LLM and the user both need visibility into what happened inside the chain.