6 min read

I Reduced My MCP Tools from 96 to 10. Here's the Pattern.

MCP tool bloat is choking LLMs. The Single Tool Resource Action Pattern (STRAP) applies REST-style routing to consolidate 96 operations into 10 tools with the same functionality.

MCP is an amazingly powerful concept… not perfect, but powerful. I now use it everywhere and in every project.

Recently, while working on Outlet, my open source email platform to replace my Sendy and ListMonk setups (that’s for another post… and why I was running both and why I had to stop) I ran into an issue with MCP tool bloat.

Every LLM has two limits we must work within: context window and how many MCP tools it can handle.

Using the traditional “one function, one tool” pattern, I was looking at 96 tools. That’s not a typo. Ninety-six. It would have used too much context and exceeded the tool limit for several LLMs.

The reason it has so many tools is I want full CRUD for setting up brands, email lists, emails, drip campaigns, subscribers, analytics… because my ultimate goal is a platform that AI can fully manage. So it must have connectivity with comprehensive control.

The problem is the MCP tool patterns published by almost everyone have created this massively adopted mental model that things must be done a certain way.

Here’s the currently promoted MCP tool pattern:

One function == one tool

CRUD makes 4 tools:

  • create_email
  • read_email
  • update_email
  • delete_email

Multiply that across 20+ entities and you’re drowning in tools.

It’s great for small projects. But when you add this pattern to any sophisticated system you end up with 96 tools which chokes most LLMs and creates massive context bloat.

There’s actually a GitHub issue on VS Code from December 2024 that says exactly this: “MCP tool loading causes context pollution” and suggests “MCP servers should provide fewer, parameterized tools.” But nobody had written up the pattern.

So I did.

The STRAP Pattern

I call it the Single Tool Resource Action Pattern or STRAP.

If you’ve been around software for a while you’ll recognize this immediately… it’s basically REST-style routing applied to MCP tools.

The idea is simple. Instead of one tool per operation, you create one tool per domain with resource and action parameters.

Old Pattern

create_email_list()
get_email_list()
update_email_list()
delete_email_list()
get_email_list_stats()
get_email_list_subscribers()
subscribe_to_email_list()
unsubscribe_from_email_list()
create_sequence()
get_sequence()
update_sequence()
delete_sequence()
get_sequence_stats()
create_template()
get_template()
update_template()
delete_template()
enroll_contact_in_sequence()
unenroll_contact_from_sequence()
pause_enrollment()
resume_enrollment()
... (96 tools later, you get the idea)

STRAP Pattern

email(resource: list, action: create, name: "Newsletter")
email(resource: list, action: subscribe, id: "1", email: "user@example.com")
email(resource: sequence, action: create, name: "Welcome Series", list_id: "1")
email(resource: enrollment, action: enroll, sequence_id: "uuid", contact_id: "uuid")
email(resource: queue, action: list, status: "pending")

96 operations became 10 domain tools. Same functionality. Works with every LLM now.

Why It Works

1. Context savings are massive

Tool definitions are one of the biggest context hogs in MCP. In Claude Code, tool definitions alone consume nearly 6% of your context window before you even say anything. Fewer tools means more room for actual conversation and code.

I cut context overhead by roughly 80%.

2. LLMs learn the pattern once

Once an LLM understands resource + action, it generalizes instantly. It stops guessing which of your 96 tools to use and just… gets it.

3. Scales infinitely

Adding a new resource or action is one line in an enum. Not a whole new tool definition with its own schema, description, and handler.

4. Works with every LLM

No more hitting tool limits on Claude, GPT, Gemini, or local models. Ten tools is nothing.

The Implementation

Here’s what a STRAP tool description looks like in practice (from Outlet’s actual email tool):

email(resource, action, ...)

Manage email lists, sequences, templates, enrollments, entry rules, and email queue.

Resources and Actions:

LIST RESOURCE:
- list.create: Create an email list (requires: name)
- list.list: List all email lists
- list.get: Get an email list (requires: id)
- list.update: Update an email list (requires: id)
- list.delete: Delete an email list (requires: id)
- list.stats: Get list statistics (requires: id)
- list.subscribers: List subscribers (requires: id)
- list.subscribe: Subscribe contact to list (requires: id, email)
- list.unsubscribe: Unsubscribe contact from list (requires: id, email)

SEQUENCE RESOURCE:
- sequence.create: Create a sequence (requires: name, list_id)
- sequence.list: List sequences
- sequence.get: Get a sequence with its emails (requires: id)
- sequence.update: Update a sequence (requires: id)
- sequence.delete: Delete a sequence (requires: id)
- sequence.stats: Get sequence statistics (requires: id)

ENROLLMENT RESOURCE:
- enrollment.enroll: Enroll contact in sequence (requires: sequence_id, contact_id)
- enrollment.unenroll: Remove contact from sequence (requires: sequence_id, contact_id)
- enrollment.pause: Pause contact's sequence (requires: sequence_id, contact_id)
- enrollment.resume: Resume contact's sequence (requires: sequence_id, contact_id)
- enrollment.list: List contact's enrollments (requires: contact_id)

Examples:
  email(resource: list, action: create, name: "Newsletter")
  email(resource: list, action: subscribe, id: "1", email: "user@example.com")
  email(resource: sequence, action: create, name: "Welcome Series", list_id: "1")
  email(resource: enrollment, action: enroll, sequence_id: "uuid", contact_id: "uuid")

The tool description documents all valid resources and actions in one place. Yes, it’s longer than a single-purpose tool description. But the net savings are huge because you’re documenting 31 operations in one tool instead of 31 separate tools.

The Tradeoff

I’ll be honest about the tradeoff.

Your tool descriptions get longer since you’re documenting everything in one spot. And there’s a small learning curve for the LLM on the first call… it has to understand the resource/action pattern.

But after that first call? It just works. The LLM pattern-matches and applies it consistently.

Real World Results

In Outlet I consolidated what would have been 96 individual tools across brands, lists, sequences, templates, enrollments, contacts, campaigns, analytics, webhooks, and GDPR compliance… down to 10 domain tools:

  1. email - lists, sequences, templates, enrollments, entry rules, queue
  2. brand - brand management and domain verification
  3. contact - subscriber management and tagging
  4. campaign - broadcast email campaigns
  5. transactional - transactional email sending
  6. webhook - webhook configuration
  7. stats - analytics and reporting
  8. design - email design templates
  9. blocklist - suppression and domain blocking
  10. gdpr - compliance and data requests

Same functionality. Same comprehensive control. But now it works with every LLM I’ve tested.

Context usage dropped dramatically. Tool selection errors dropped to near zero because the LLM isn’t choosing between 96 similarly-named functions anymore.

When to Use STRAP

Use it when:

  • You have more than ~15 tools
  • You’re building CRUD operations across multiple entities
  • You want AI to fully manage a system
  • You’re hitting context limits or tool limits

Don’t bother when:

  • You have a handful of unrelated tools
  • Each tool does something genuinely unique
  • You’re building a simple integration

TL;DR

Instead of create_thing(), read_thing(), update_thing(), delete_thing() for every entity… create domain(resource, action, ...) for each domain…

STRAP your MCP servers.


The full implementation is open source in my email platform Outlet: github.com/outlet-sh/outlet

Written by

Alma Tuck

Back to all articles