Jobs-Based Tool Design

Why mapping tools to user jobs instead of API endpoints produces more reliable, LLM-friendly MCP servers — with examples from the issue tracker.

March 17, 20263 min read2 / 2

The instinct when building an MCP server is to mirror your existing API surface. If your REST API has GET /issues, POST /issues, PATCH /issues/:id, DELETE /issues/:id — you build four tools that match them.

This is natural. It's also often wrong.

The problem is that REST APIs are designed for developers who understand the data model. MCP tools are used by an LLM that's trying to accomplish what a user asked for. Those are different things — and I had to unlearn the API-mirroring habit before my servers started working well.


Jobs-to-be-Done Framing

A better framing: what job is the user trying to accomplish? Design tools around those jobs.

User saysAPI-based approachJobs-based approach
"Mark all my open issues as in-progress"list_issues + update_issue × Nbulk_update_status(assignee, from_status, to_status)
"Close everything in the backlog sprint"list_issues + update_issue × Nclose_sprint_issues(sprint_id)
"Summarize what's been worked on this week"list_issues with date filterweekly_summary(start_date, end_date)

API-based: many calls, more chances for something to go wrong, more tokens used. Jobs-based: one call, deterministic, clear intent.

"Mark all my open issues as in-progress" — API vs Jobs Expand"Mark all my open issues as in-progress" — API vs Jobs


A Concrete Example: Bulk Status Update

API approach:

  1. LLM calls list_issues(assignee: "alice", status: "open")
  2. Gets back 12 issue IDs
  3. Calls update_issue(id: 1, status: "in-progress"), then update_issue(id: 2, ...), etc.
  4. 13 tool calls total, each with latency

Jobs approach:

TypeScript
server.tool( "bulk_update_status", "Update the status of all issues matching a filter in one operation. Use when the user wants to move multiple issues between states at once.", { from_status: z.enum(["open", "in-progress", "closed"]), to_status: z.enum(["open", "in-progress", "closed"]), assignee: z.string().optional().describe("Limit to this assignee only"), }, async ({ from_status, to_status, assignee }) => { let query = "UPDATE issues SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE status = ?"; const params: string[] = [to_status, from_status]; if (assignee) { query += " AND assignee = ?"; params.push(assignee); } const result = db.prepare(query).run(...params); return { content: [{ type: "text", text: `Updated ${result.changes} issue(s) from ${from_status} to ${to_status}.`, }], }; } );

1 tool call. The SQL handles the bulk operation. Clear, reliable, fast.


When to Keep API-Based Tools

Jobs-based tools don't replace everything. API-based tools still make sense for:

  • CRUD on individual records: creating a single issue, updating a specific field, deleting one item
  • Exploration: listing issues, searching, filtering — the LLM needs these to understand what exists
  • Composable operations: the user needs flexibility you can't predict in advance

Think of it as a layered approach: API-based tools for atomic operations, jobs-based tools for common compound workflows.


Describing Jobs-Based Tools Clearly

The tool description is even more important for jobs-based tools, because the operation isn't obvious from the name alone.

TypeScript
// Vague — LLM doesn't know when to use this "bulk_update", "Updates multiple issues", // Clear — LLM knows exactly when to call this "bulk_update_status", "Move all issues matching a status filter to a new status in one operation. Use when the user says 'mark all my open issues as done', 'close everything in the backlog', or similar bulk transitions.",

Include example phrasings in the description if the trigger isn't obvious. I've come to think of the description as the contract between me and the LLM.


Reliability at Scale

With 16 tools in the issue tracker, jobs-based design keeps the LLM's cognitive load low. Each tool has a clear, distinct purpose. There's no ambiguity about which tool to use for which operation.

This is especially important for complex multi-step requests where the LLM needs to chain several tools together — clearer individual tools make for more reliable chains.

Further Reading

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.