MP-201a · Module 1

Tool Composition

3 min read

Tools that do one thing well are easier to describe, easier to test, and easier for LLMs to use correctly. The single-responsibility principle applies to MCP tools just as it applies to functions: a tool called "search_and_update_and_notify_customer" is doing three things, and the LLM has to want all three to justify calling it. Split it into search_customer, update_customer, and notify_customer, and the LLM can compose them as needed — searching without updating, updating without notifying, or chaining all three.

Tool composition happens at the LLM layer, not the server layer. The model calls tool A, reads the result, decides to call tool B with data from A's output, reads that result, and continues. Your server does not need to orchestrate this chain — the LLM does. Your job is to make each tool's output contain enough information for the LLM to decide what to do next. If search_customer returns a customer object with an ID, the LLM can pass that ID to update_customer without you building any pipeline logic.

There are legitimate reasons to combine operations into a single tool: atomic transactions (transfer money = debit + credit, must succeed or fail together), performance (fetching a customer and their recent orders in one database round-trip), and reducing token overhead (five tool calls cost more tokens than one). The test is whether the operations can meaningfully be used independently. If they always happen together, combine them. If they sometimes happen alone, keep them separate.

// Bad: monolithic tool that does too much
const badTool = {
  name: "manage_customer",
  description: "Search, create, update, or delete a customer",
  inputSchema: {
    type: "object",
    properties: {
      action: { type: "string", enum: ["search","create","update","delete"] },
      // ... different fields needed for each action
    },
  },
};

// Good: single-responsibility tools the LLM composes
const searchCustomer = {
  name: "search_customers",
  description: "Search customers by name, email, or account ID. Returns matching customer records with IDs usable in other customer tools.",
  inputSchema: { /* only search params */ },
};

const updateCustomer = {
  name: "update_customer",
  description: "Update a customer record by ID. Use search_customers first to find the correct customer_id.",
  inputSchema: { /* customer_id + fields to update */ },
};

const deleteCustomer = {
  name: "delete_customer",
  description: "Permanently delete a customer by ID. This action cannot be undone. Use search_customers to verify the customer before deleting.",
  inputSchema: { /* just customer_id */ },
};
  1. Apply the verb test Each tool name should contain exactly one verb: search, create, update, delete, send. If you need "and" in the name, you probably need two tools.
  2. Design outputs for chaining Every tool that creates or retrieves a record should return an ID or reference that other tools accept as input. This makes composition natural.
  3. Document the graph In each tool's description, mention related tools by name. "After creating a ticket with create_ticket, use add_comment to add notes." The LLM learns the workflow from the descriptions.