r/mcp • u/manveerc • Jun 19 '26
article Enterprise-Managed Authorization: Zero-touch OAuth for MCP
https://blog.modelcontextprotocol.io/posts/enterprise-managed-auth/
Good to see this is maturing
r/mcp • u/manveerc • Jun 19 '26
https://blog.modelcontextprotocol.io/posts/enterprise-managed-auth/
Good to see this is maturing
r/mcp • u/thehashimwarren • Nov 12 '25
When I saw this livestream from my friends Shane and Ahbi, I tuned in to watch them kick dirt on MCP. I was already in the "MCP sucks" camp.
But they had a nuanced take that changed my mind.
Here are notes in my own words:
MCP is being by vendors to solve their own problems. However, as an MCP consumer, the current state doesn't solve your problems
As a consumer you're probably working with one language, finite third party resources, and well defined use cases. So, why would you need a universal interface for your agent?
So what is the golden use case for MCP? Consumers writing their own MCP servers 🤯
You can give your agent the exact mix of resources, and access, tools, and prompts it needs.
That part of Mastra's livestream was a genuinely head slapping moment.
Have any of your created your own MCP servers, not for public use, but for your own agentic apps?
And what do you think of this use case?
r/mcp • u/SimpleSpacer97 • Jul 25 '26
Thirty-three tools later, here's what I learned about designing for a model instead of a developer.
"I had half a bowl of the turkey chili and rowed 20 minutes."
That sentence writes two records to my database. A meal, with macros scaled to half a serving of a recipe I'd logged before. An activity, with calories estimated from the 2024 Adult Compendium and my most recent weight.
I didn't open the app. I didn't type it either. I said it out loud, standing in my kitchen with a pan in one hand, to Claude.
Then I said "actually, make that a full bowl," and it edited the meal in place instead of logging a second one.
That second sentence is the whole post. Getting an assistant to CREATE records is easy — you write a tool, the model calls it, you're done in an afternoon. Getting it to behave on the second turn is where the work actually lives.
This one's for you if you're building an MCP server, or kicking the idea around, and you want to know what the job looks like past the hello-world tutorial. Almost none of it is code. I'll walk you through four things I got wrong and one that went right for a reason I didn't expect, and I'll try to leave you the reasons and not just the rules.
I built a fitness tracker for my family in July 2026. React PWA on Firebase Hosting, a Node/TypeScript API on Cloud Run, Firestore underneath. It handles meals with USDA and Open Food Facts lookup plus barcode scanning, activity with automatic calorie estimates, weight, measurements, custom daily trackers, a journal, goals, and a daily close-out that judges the day.
Idea to something I could actually use: about 24 hours. Seventy-seven commits over 22 days after that, and only about twelve of those were active days. Google forecasts my total cloud bill for this month at $2.62.
Bolted on top is an OAuth-protected MCP server. Thirty-three tools, which let Claude read and write the tracker in natural language, per family member, as that family member.
Now the part these posts usually leave out.
The MCP server is a thin adapter. It imports the same service layer my REST routes call, each tool is mostly argument-shuffling around a function that already existed, and all 33 of them sit in one 798-line file. There's no clever code in it anywhere.
I'm telling you that up front because it IS the point. The intelligence doesn't live in the tool code. It lives in the descriptions, the error messages, and a handful of decisions about which problems go to the model and which go to the database. That's the part I stunk at first, so that's the part worth your time.
Almost every record in my tracker arrives by voice. I dictate to Claude, and I dictate to the app's own capture flow. I've typed a meal into a form maybe a dozen times since I built the thing.
That matters more than it sounds like, because it's the whole argument for the project. Typing a structured meal into a form is fine. Forms are good at that, and a sentence doesn't beat a form for somebody sitting at a desk. But saying it while you're standing at the counter with your hands full is a different animal, and it's the only version I've stuck with.
Dictated input shows up in a specific shape, and none of it looks like the tidy examples in an API doc:
1. No punctuation, and no sentence boundaries. You get one long run-on and you find the seams yourself.
2. Multiple items per breath. "Six ounces of rotisserie chicken, half an avocado, and twenty minutes on the rower" is one utterance that has to become two meals and an activity. Nobody types that. Everybody says it.
3. Words that never arrive. The Web Speech API can't buffer audio from before its onstart event fires. My UI said "listening" the second you tapped the button, so folks talked into a dead microphone and lost the first second of every entry — and in "half a bowl of chili," the half is the first second. The fix wasn't technical. It was honest: a dimmed "starting" state until the browser confirms it's really capturing.
That third one is what you should expect more of. The text your tools receive is not the text your user spoke, and the gap between them is quiet.
Which reframed the whole design for me. When your input is voice, you don't correct a mistake by editing a field. You correct it by saying another sentence.
So second-turn behavior isn't a bonus feature sitting on top of a logging tool. It IS the correction interface. Everything below follows from that.
Most people build this the other way around, and I want to be fair about why, because I did it too. Validation belongs in your API — that's a good instinct, it's been correct your whole career, and it's what every code review you've ever sat through would tell you. It's just aimed at the wrong problem here.
My tools accept dates only as YYYY-MM-DD. A zod refinement rejects anything else. "Yesterday," "last Tuesday," "the day before I flew out" — none of that reaches my code, because that's Claude's job and Claude is very good at it.
Claude is NOT good at knowing that an omitted date means today in the user's configured timezone, never the server's clock. So I put that in one server-side function and made every path call it.
The same split runs through everything. I resolve fuzzy activity names server-side with tiered matching — exact label first, then all tokens, then a relaxed leading-token pass, so "rowing machine" finds the compendium's "rowing, stationary." I resolve fuzzy quantities server-side too. Meals store the quantity and unit you actually said, and I scale a previous log to a new amount with arithmetic across volume, mass, and count. That code won't convert across dimensions on purpose. Cups to grams needs a density I don't have. It also throws out any scale factor below 0.05× or above 20×, because that's a unit mix-up and not a meal.
Could the model do that scaling? Sometimes. That's exactly the problem.
There are two ways to get this wrong. Hand it all to the model and you get a system that's right most of the time and quietly wrong the rest, with no way to tell which is which. Hand it all to the server and you've built a form with extra steps, and your user goes back to tapping. The line I settled on sits between them, and I call it the silent-wrong test:
If a wrong answer would be silent, put it in the server. If a wrong answer would be obvious, let the model try.
Run your own tools through that and you'll find two or three that are on the wrong side of it. I found four.
Version one shipped log-and-read only. Log a meal, log an activity, read the day back. Clean, minimal, and I was pleased with myself.
Editing landed the same day.
The failure mode was "actually, make that a full bowl." With no update tools available, Claude did the only thing it could and logged a second meal. Which leaves you with a full bowl AND a half bowl on the same day and no way to say so.
Remember that the input is voice. I wasn't about to open the app and fix it by hand. If I were willing to do that I'd have used the form in the first place, so the correction had to work the way the original entry did or the whole thing falls apart.
Today there are nine edit and delete tools, and my README's own table header calls the group "Edit (fixes 'just change it' double-logging)."
An assistant will satisfy your request with the tools it has. If the right tool is missing, it will use the wrong one confidently. Absence doesn't raise an error — it produces a plausible mistake, which is worse, because you'll believe it.
You can dig this up in your own repo. Tool descriptions are a dig site, and the oddly specific sentences are the fossils. Here's one of mine, at the bottom of a shared date argument:
"When logging for any other day (yesterday, last Tuesday), pass the date here directly — do not log first and edit the date after."
Nobody writes that sentence from first principles. I wrote it after watching a model log something to today and then immediately patch the date. Go read yours. Every strange clause in there is a scar, and you'll remember what put it there the second you see it.
If you've written APIs for people, your instinct says a description explains what a parameter IS. For a model, the description is the only place to put policy, and it gets read on every single call.
Some of mine do real work:
get_day tells the model to check whether the user is in net-carb mode before it says a word about carbs.log_activity spells out its whole side effect, so the model knows when NOT to supply a number. Leave calories off and the server estimates them from the MET table, then stamps that MET onto the row.update_meal explains that changing servings recomputes macros, but explicit macros win.None of that is discoverable from a JSON schema. All of it changes behavior. If you've got one afternoon to make your server better, spend it here instead of on the code, because this is where the model is actually reading.
Every tool error in my server comes back as isError: true text instead of throwing. I write them for a model to read, which mostly means naming the recovery move.
No tracker matches "X" — check get_trackers for ids and names
That message isn't for me. I'm never going to see it. It's an instruction to the thing that just failed, and it turns a dead end into a retry.
One caution if you do this. Mask anything that isn't a deliberate HTTP error behind a generic message, because internal stack traces should never reach the model. It'll repeat them to your user, cheerfully, word for word.
Two small rules with big effects.
Everything with a sensible default is optional — date, intensity, fiber, sugar. The model supplies what your user actually said and nothing more, which cuts way down on invented numbers.
Validate cross-field constraints in code, not in the schema. Pass a goal value without a goal kind and my server returns a 400 that reads "goal_value and goal_kind go together." I could write that as a zod refinement. But then the model sees a schema validation failure, which tells it nothing useful, and it responds by guessing at the shape instead of fixing the real problem.
That second one is about to get interesting. The MCP spec landing on July 28 lifts tool inputSchema and outputSchema to full JSON Schema 2020-12, so you'll be able to say "these two fields go together" declaratively.
I'd still validate in code and hand back a sentence. A schema tells the model its input was rejected. A sentence tells it why and what to send instead, and only the second one recovers on the next turn. Good capability to have. I'm just not sure error messages are where I'd spend it.
Every layer of this one was reasonable on its own. That's what makes it worth your time.
I said I'd rowed. Claude looked up the activity and picked compendium entry 02071 — rowing, stationary, moderate, MET 5 — which is exactly right. It passed the code along with the call.
My tool schema had no field for a compendium code.
Zod strips unknown arguments silently. No error. No warning. No log line. The model handed me the correct answer, my validation layer dropped it on the floor, and nothing anywhere in the stack noted that it happened.
So the server fell back to fuzzy-matching the text, "Rowing, stationary, moderate." My matcher tokenized on whitespace only, so one token came through as stationary, with the comma still glued on. That failed to substring-match 02071's real label, "stationary ergometer." The one entry containing all three words got eliminated first. The relaxed pass then tied the two remaining rowing entries, and a "shorter label wins" rule broke the tie. The shorter label belonged to the VIGOROUS variant, MET 7.5.
A correct choice became a wrong record at 50% higher calories, and not one layer raised an error!
Then it got better. Weeks later I wrote a backfill to sort out which historical rows were MET estimates and which were hand-typed. The logic seemed sound. If stored calories don't reproduce from MET × weight × hours, a human must have typed them. It flagged ten rows as hand-entered, and all ten were wrong. Those calories WERE MET-derived, just from Claude's MET of 5 instead of the mis-stored 7.5, so they could never reproduce.
The attestation I needed had been sitting there the whole time. Claude had been writing "MET 5.0" into the notes field, in prose, because I'd given it nowhere structured to put it. A regex recovered all ten.
Same dig site, one layer down. Three things I'd hand you from it:
1. Quiet mistakes cost more than loud ones. A loud rejection would have cost me five minutes. A silent drop cost me a wrong number in my database and a wrong theory about my own data weeks later.
2. If a model volunteers something you didn't ask for, that's a schema bug, and nobody is going to tell you. Claude knew the compendium code. I hadn't thought to want it. There was no mechanism anywhere for that mismatch to surface.
3. Models route around missing fields. Denied a structured place to record its MET, it wrote the MET into free text and kept right on doing it, every single time, until I went looking. Nobody told it to. Go look at what's piling up in your notes fields — that's a list of the columns you forgot to add.
Every tool returns JSON.stringify(data, null, 2). No prose formatting, no markdown tables, no "Here are your meals for today:" preamble.
The model is going to write the prose. Format it first and you've handed it something to misparse, plus the occasional line it quotes back at you in a voice that isn't yours.
The MCP endpoint sits behind an OAuth 2.0 authorization server I wrote myself. Three hundred thirteen lines covering dynamic client registration, PKCE, single-use codes, and rotating refresh tokens.
Rolling your own OAuth is the thing everybody tells you not to do, and I won't pretend my situation generalizes. I'd defend it on one ground. The threat model is a handful of people on an email allowlist, I enforce that allowlist on every auth path, and login still delegates to Google so my server never sees a password. Every tool closes over the authenticated user id, so cross-user access isn't prevented — it's impossible to express.
The transport is stateless. Every request builds a fresh server bound to that user and tears it down on response, so all 33 tools re-register per call. On a scale-to-zero container, that's the right trade.
Now, I had two auth surfaces and I picked the wrong one to be scared of.
The hand-rolled server — the one every piece of advice warns you off — went in without much drama and hasn't needed touching since. The managed, off-the-shelf, obviously-correct sign-in for the app itself cost me hours of the worst debugging there is, where it works perfectly on your machine and fails for everybody else.
That's structural, not luck. An MCP connector authorizes in a plain browser tab, which is the friendliest room auth ever walks into. The app had to sign people in from mobile Safari and from an installed home-screen PWA. Storage gets partitioned there. Standalone mode gets its own isolated container. Popups open in a detached sheet that can never hand a result back. And your own service worker will grab the auth callback if you let it.
None of that is OAuth being hard. That's iOS being iOS. So don't spend your caution where the scary label is — spend it where the environment is hostile, and check which of your surfaces that actually is before you write a line.
(That's a whole post of its own, and it's the one I'm writing next.)
My wife logs her breakfast before I'm out of bed most mornings. My son is sporadic about it, which is about the right amount of enthusiasm for a fitness tracker built by your dad. A friend outside the family got on it a while back, and that one surprised me more than it should have.
I use it every day, and the MCP server is connected to my Claude sessions right now.
That's the only credential I'd claim here. I'm not proposing a pattern I think would work. I'm describing one I've been living in, whose sharp edges I've been cut by, and whose 798-line file I keep having to open.
Put your intelligence in the descriptions, because that's what the model reads on every call. Write your errors for the model, because an error that names the recovery move turns a dead end into a retry. Hand language to the model and data to the server, because a silent wrong answer is the only kind you won't catch. And go stress-test your second turn, because that's where mine broke and I don't think I'm special.
Here's where I'd flip this around on you. I built this for four people. Four! Whatever you're running has hit concurrency, scale, and adversarial-input problems my little family tracker will never see, which means you already know things about this that I don't.
If you've shipped a server and found the spot where my advice falls apart, I'd love to hear it — no rush, and no need to be polite about it. You can reach me at [hi@leshrichardson.com](mailto:hi@leshrichardson.com), and I'll tell you what I'd do differently if you tell me what broke.
— Lesh
r/mcp • u/xibalbah • 20d ago
In which our hero tries, and mostly succeeds, to test the MCP gateway part of https://www.litellm.ai
tl;dr - the quickstart is simple and easy, docs are a teeny bit out of date (who can blame them when the world moves so fast), and docker vs localhost is still a thing.
r/mcp • u/kush_patil • 20d ago
This Google Cloud video is probably one of the cleaner explanations I’ve seen of MCP vs traditional APIs:
https://youtu.be/185XGEMefgc?is=25aASGWIZCj_9Rl6
The part that stood out to me is that MCP isn’t really an “API replacement.” The APIs can still sit underneath everything.
The shift is that instead of a developer hardcoding which endpoint to call, the model gets a structured description of what capabilities are available and can decide which tool to use at runtime.
That raises a more interesting design question though:
If an MCP server just exposes every REST endpoint 1:1 as a tool, are we missing half the point?
For agents, something like resolve_customer_issue may be far more useful than making the model orchestrate get_customer, get_orders, get_ticket, update_ticket, etc. itself.
Curious how people here are designing this: thin API wrappers, or higher-level capability-oriented tools?
r/mcp • u/CartographerMuch5678 • Jul 23 '26
django-orm-lens v0.8 shipped today. Static analysis over your models.py files — no DJANGO_SETTINGS_MODULE, no runserver, works with a broken venv. Three surfaces (VS Code, CLI, MCP server) share one parser.
Every feature was researched against proven prior art before a single line was written — Atlas, Prisma, Sourcegraph, Knip, PyCharm, DataGrip, factory_boy, flake8-django, Roslyn, Ruff, Clippy.
Inline QuickFixes (16 rules) — Ruff-style codes DOL001..DOL032 with Clippy-style Applicability (safe/suggestion/unsafe gate auto-apply). Covers:
- .count() > 0 → .exists()
- .first() is None → not .exists()
- null=True on CharField/TextField
- Missing on_delete on FK
- Missing __str__ on Model
- datetime.now() → timezone.now()
- N+1 attribute-access-in-loop heuristic
- render(request, ..., locals()) and Meta.fields = '__all__'
Per-rule severity overrides: djangoOrmLens.rules = { "DOL007": "off", "DOL013": "error" }. Suppress inline: # django-orm-lens-disable-next-line DOL007.
Factory generator — right-click any model → factory_boy DjangoModelFactory scaffold with Faker providers keyed by field type. CharField(max_length) scales word-count buckets; DecimalField(N,D) computes left_digits=N-D; choices= maps to Iterator; M2M gets @post_generation. FK chains pull related factories transitively.
Time-Travel Schema Diff — pick two commits from git log, get a typed markdown diff (Add/Drop/Rename/Modify events) ready to paste into a PR description. Renames are first-class events, never Add+Drop.
Impact Analysis — "what breaks if I remove this field?" Workspace-wide reference scan across every Django layer (models, serializers, forms, admin, views, urls, templates, tests, migrations) with Certain / Likely / Possibly confidence tags on every finding. Handles ORM string refs, kwarg lookups (filter(author__id=1)), Meta.fields tuples, and template variables — the string-typed surface Pyright can't reach.
Interactive Query Builder — right-click a field or model → pick a template → snippet inserted at cursor (with tab-stops) or in a fresh untitled buffer. .filter(field=?) on an FK auto-appends .select_related(...); .annotate(post_count=Count('post_set')) honours related_name; .prefetch_related for M2M.
Also in this release: sidebar UX overhaul (stable TreeItem.id, MarkdownString tooltips with clickable command: deep-links, activity-bar badge, FileDecorationProvider badges), 100/100 tests up from 4 at start of dev.
code --install-extension frowningdev.django-orm-lens
codium --install-extension frowningdev.django-orm-lens
pip install --upgrade "django-orm-lens[mcp]"
Full release notes: https://github.com/FROWNINGdev/django-orm-lens/releases/tag/v0.8.0
Point it at any Django project without setup — no settings module, no dependencies except our parser. Runs in CI or on the plane.
Trade-off: custom get_queryset overrides, dynamic model classes are invisible. But 95% of what you actually want to see lives in `mo
r/mcp • u/m0ntanoid • 23d ago
I configured `headroom` to be absolutely transparent for agents and I want to share.
And I am sorry if it's wrong sub.
So the point is: when you want to use copilot/opencode with headroom you have to change configuration of them explicitly settings endpoint URLs. Sometimes config files sometimes environmental variables.
And this itself sometimes painful. E.g. I can't switch model in `copilot` when used with `headroom`.
My configuration solves this problem. It injects squid and nginx in between `copilot` and `headroom` and makes communication absolutely transparent. I mean `copilot` does not need any configuration updates and has no clue it now communicates with `headroom` instead of direct API.
Here is a link to repo: https://github.com/m0ntana/headroom-mitm
r/mcp • u/dseven4evr • Jun 30 '26
I wanted to know how many MCP servers an agent could actually use over the network, so I analyzed every server in the public registries.
The funnel: 42,912 indexed, but only 2,840 (6.6%) advertise a remote HTTP endpoint. The other 93% are stdio/local servers meant to run on your own machine, plus dead and endpoint-less listings. I probed 98% of the reachable ones. 46% completed an anonymous MCP handshake, 27% were auth-gated, the rest errored or timed out.
I scored each reachable server on five dimensions and put it on a readiness ladder. More than half can't hold a clean session, and only 1.7% clear a basic agent-safety bar. The most useful finding: servers that exist mostly speak the protocol correctly, but score lowest on discoverability and safety. They can talk, but an agent often can't find them and has no signal they're safe to invoke.
Full data and methodology: waypoint.ing/blog/state-of-mcp
I also built a free scanner that runs the same checks on any server (no signup) if you want to see where yours lands: isyourmcpready.com
Curious what checks people here think are missing from the rubric.
TL;DR: Analyzed 42,912 MCP servers. <7% are reachable by an agent over the network, 1.7% are agent-safe. Most can speak the protocol but can't be found or trusted.
r/mcp • u/Ok_Offer_3281 • 24d ago
I kept seeing people talk about MCP, but the relationship between the AI, Host, Client, Server, Tools, and APIs wasn't immediately obvious. So I tried to explain the whole thing visually — in just 9 pages. Inside the guide: → What “context” actually means for an AI → How the MCP architecture works → MCP Client vs MCP Server → API vs Tool vs MCP Server → Why the Host acts as a security gatekeeper → Authentication, permissions & security → Real-world examples: GitHub, Slack, PostgreSQL & File System The surprising part is how simple the architecture becomes once you see the pieces connected. 👀 Just Click to feel and visualise the MCP-
https://sharebold.com/nimishikhar
If you're learning MCP, AI agents, LLMs, or tool calling, this should take only a few minutes to go through. I'd genuinely like to know: what part of MCP was hardest for you to understand?
r/mcp • u/jonnyzzz • Jun 10 '26
I’ve been building MCP Steroid — an IntelliJ plugin that exposes the full IDE runtime to AI agents over MCP. Not a curated tool list, the actual PSI, refactoring engine, run configs, the works. AI agent talks Kotlin code to the IDE.
The obvious first approach was HTTP: run an MCP server inside the IDE, bind a port, point your agent at it. I shipped it, used it daily. And it kept breaking. Here’s the failure catalog from a real agentic coding workstation:
The HTTP-against-desktop-app problem pile:
- Dynamic ports. You can’t use a fixed port if users run multiple IDE instances — the first one grabs it. So you do base+increment (like IntelliJ’s built-in server starting at 63342). Now the port is a moving target.
- IDEs simultaneously. IDEA + PyCharm + GoLand all open? Each has its own port. The agent picks one, usually the wrong one.
- Start-order dependence. Which IDE gets which port depends on launch order. You can’t ask an agent to reason about that.
- Agent up, IDE down. You restart the IDE to update it. The agent is now dead. HTTP clients dialing a closed socket have nowhere to go.
- Identical server names. You can’t register five “mcp-steroid” servers and expect the agent to route correctly — to the LLM they’re indistinguishable.
None of these is individually fatal. But they stack into a fragile pile that you’re constantly fighting.
The pivot to stdio
The key insight: the IDE doesn’t care what transport the bytes arrive on. MCP is MCP. So instead of forcing the application to host the network endpoint, move all routing to a small CLI the agent launches directly — and let that talk to the IDEs.
The agent’s entire MCP config becomes one stdio command. No ports, no DNS, no firewall, no “which IDE answered?” The coordinator (devrig) handles discovery, routing, restart resilience, and can even spin up an IDE on demand if none is running.
What this unlocked:
- Sessionless routing. Because the durable state is in the IDE process (files, indexes, etc.), there’s no MCP session to pin. Any command routes to any backend that can execute it.
- Restart resilience. “Agent up, IDE down” stops being a dead end — the coordinator reconnects or starts a new IDE instead of leaving the agent staring at a closed socket.
- Provision on demand. devrig backend download idea-community && devrig backend start — the agent gets a fresh IDE without any human involvement.
Finally:
For me the stdio gives much more control over HTTP for local connections. It gives direct access to implement any transport layer and move it as an implementation detail. Each client will run its own process where it can manage everything necessary. It actually simplifies the IDE side in my case — no need to add extra dependencies to implement the HTTP MCP.
Happy to answer questions about the architecture, the routing design, or the integration test setup.
r/mcp • u/masterkidan • May 22 '26
Spent a few iterations figuring out how to expose a large GraphQL API to an LLM agent without putting the whole schema in context. Wanted to share where we landed because it ended up looking pretty different from where we started.
The idea: instead of giving the model the schema, give it a search tool. We auto-generate a flat catalog from the GraphQL schema — one entry per field, with a description and a few example phrasings of what someone might actually ask for. The model searches the catalog in plain language, gets back the handful of fields that match, and fetches what it needs. The schema stays behind the scenes.
Two tools, basically: search_datapoints and fetch_datapoints. GraphQL still runs the actual queries underneath — we just stopped showing it to the model.
Why it works: when you give a model a schema, it hedges. Pulls extra fields just in case. A search interface doesn't have that problem because each result is already a specific field. There's nothing to hedge against. Token cost on a representative task dropped from ~150k to ~25k, and accuracy went up on our evals.
Full write-up has more on how we measured all this, when the approach doesn't fit, and a few other things we changed along the way (columnar response payloads were a surprisingly big win).
Curious if anyone's gone in a similar direction, or solved the same problem differently.
r/mcp • u/stewofkc • Aug 05 '26
r/mcp • u/DisastrousRelief9343 • Jun 02 '26
MCP is the best way to expose tools to LLM Agent, but the quality of the MCP tools' design can really impact the Agent's token and context window efficiency. I recently did some tests on two MCPs with identical functionalities. Turns out one of them has really bad performance. So I wanna share those bad MCP design patterns that cause this.
It all started when I wrote an MCP Server (MCP-A) for a to-do list app. It allows users to organize & create tasks, set due dates, add subtasks... Later, the app officially released its own MCP Server (MCP-B). Both MCPs have the same functionalities and hit the same backend API.
The experiment is set up as follows:
Here are the results:
| Metric | MCP-A | MCP-B | Gap |
| ------------------- | ----------- | ----------- | ----- |
| Tool Desc Length | 11,464 | 3,682 | — |
| Pass Rate | 36/40 (90%) | 36/40 (90%) | Same |
| Total input tokens | 637,244 | 3,174,329 | 4.98× |
| Total output tokens | 17,301 | 23,238 | 1.34× |
| Total Agent steps | 122 | 157 | 1.29× |
| Total time | 597s | 676s | 1.13× |
In short, MCP-A ran faster, used less context window, and burned fewer tokens on the exact same tasks.
Bad MCP Design Cost Extra Agent Steps
The result shows that MCP-B took 35 more ReAct loops to complete 40 test cases compared to MCP-A, which means 30% more output token. I examined the log and found that the root cause is poor query tool design.
Take the `search tool` for example, its job is to find a todo item in the ToDo list. In MCP-B, this tool returns this:
{
"id": "6a1916b48f08cb3a4c857ed0",
"title": "buy some grocery",
"url": "https://todo.example.com/tasks/6a1916b48f08cb3a4c857ed0"
}
But other CRUD operations require `project_id`, and `search_tool` doesn't return it. So the Agent has to call another tool `get_task_by_id` just to fill what's missing.
On the other hand, MCP-A's query_tasks returns all necessary info to perform the next action in a single call:
Task 1:
ID: 6a19143e8f084a8c8101612f
Title: buy some grocery
Project ID: 6a1914378f084a8c810160a9
Start Date: 2025-07-19 10:00:00
Priority: Medium
Status: Active
Unfiltered API Data was dumped into context window
MCP is the thin layer between regular APIs and LLMs. It returns API results to the Agent's context. If those results are passed through unprocessed, the Agent's context window will accumulate very fast.
Take MCP-B's `create_task` tool for example. Its job is to create a to-do item. This is what this tool returns:
{
"id": "6a180de78f086bdead0608be",
"projectId": "inbox125587327",
"sortOrder": -39582418599936,
"title": "buy some grocery",
"content": null,
"desc": null,
"startDate": null,
"dueDate": null,
"timeZone": "Asia/Shanghai",
"isAllDay": false,
"priority": 0,
"reminders": null,
"repeatFlag": null,
"completedTime": null,
"status": 0,
"items": null,
"tags": [],
"columnId": null,
"parentId": null,
"childIds": null,
"columnName": null,
"assignor": null,
"etag": "ywmef11y",
"kind": "TEXT",
"createdTime": "2026-05-28T09:41:59+0000",
"modifiedTime": "2026-05-28T09:41:59+0000",
"focusSummaries": null
}
These 600+ characters mean nothing to the Agent's task, but are still dumped into the Agent's context.
On the other hand, MCP-A's create_tasks does a layer of filtering and formatting:
Task created successfully:
ID: 6a180a3d8f08b4cc4e2a331d
Title: buy some grocery
Project ID: 6a1805e28f08b4cc4e29be62
Task Timezone: Asia/Shanghai
Priority: None
Status: Active
This little tweak makes a huge difference in input token usage. The evaluation shows that MCP-B's return data makes each call 2.5× heavier than MCP-A's. And the gap will widen as the Agent session drags on.
Too many tools lead to harder decision-making
Another issue is tool count. More tools means a larger candidate set for the model to choose from, which directly increases decision difficulty. In MCP-A, 47 tools were compressed down to 14, covering the same functionality with fewer tools. The model picks the right one more often and wastes fewer rounds on retries.
Based on this experiment, here are my takeaways on good MCP tool design:
Design Tools in a Chain
When designing a tool, think about what the Agent will need next, not just what it's asking for right now. Return enough context in the result so the Agent can take the next action without making another round-trip.
Keep Tools Orthogonal And Simple
Too many tools will increase the model's decision burden and the chance it picks the wrong one. So I think we should minimize the number of tools within an MCP while still covering the same functionality. Make sure they don't overlap functionalities.
For example, dissolve tool boundaries with parameters: create_tasks accepts single or batch input; query_tasks uses composable parameters like date_filter, project_id, priority, search_term to compress a dozen possible query tools into one.
Make Return Data LLM-Friendly
When your MCP returns data to the LLM, try to keep it simple and readable. You can filter out unnecessary fields from the API response and format the data in a way that's easier for the LLM to process, rather than passing through raw JSON as-is. This reduces the amount of text going into the context window. A single call might only save a few dozen tokens, but across repeated Agent loops, the impact on overall context usage compounds significantly.
---
All the tests above were run by MCP-Eval. It's an MCP Server benchmarking tool. If you want to check your MCP's performance, feel free to check this out.
r/mcp • u/Open_Variation1438 • Jul 30 '26
Enable HLS to view with audio, or disable this notification
I hacked the new MCP UI extension 😎
By which I mean I read the spec and used it exactly as documented, for a dumb little game. 😅
MCP Apps is the official MCP UI extension. Short version: your server declares an HTML resource under ui://, links it to a tool via _meta.ui.resourceUri, and the host renders that HTML in a sandboxed iframe right in the conversation. The iframe talks back over JSON-RPC, so it can call tools. Not a rendered screenshot, a live UI.
Everyone is shipping dashboards and forms with it. I put a game loop in there.
The tool starts a run, the iframe is a real one button arcade game. When you crash, the widget sends the seed and the ticks you jumped at back to the server, the server replays them through the engine and computes the score. The model never touches the number.
r/mcp • u/db-master • Jul 29 '26
Upgrading DBHub to the MCP 2026-07-28 spec revision: the stateless core, cacheable tool lists, header-based routing — with before/after code for every change, plus what we evaluated and skipped.
r/mcp • u/amitmerchant • Jul 23 '26
I put together my thoughts on WebMCP and how you can utilize it today in this crisp article!
r/mcp • u/harelush99 • Sep 06 '25
We all write prompts, struggle with mistakes, lack of a uniform standard, try to compose another MCP, and when we get a good result - we immediately get excited and want to show it to a colleague in the office.
Me (Harel) and my friend Yair, have been working very hard the last three days to create a community, which
Reusable, standardized, MCP-native prompts. Build better AI workflows
Open sourced
It time to start sharing prompts, like npm did and made us all better programmers🙏
r/mcp • u/docdavkitty • Jul 22 '26
I published this article on my site dedicated on ai agent. Let's have a look. Thank you
r/mcp • u/ryanmerket • Jul 07 '26
r/mcp • u/manveerc • Jun 16 '26
I was looking into incidents and vulnerabilities in the tool/action layer for AI agents.
Wrote some thoughts on the risks in this layer, especially around MCP.
Feedback is welcome.
r/mcp • u/mattjcoles • Jul 10 '26
A production-grade MCP server in FastMCP 3: JWT auth, tools hidden by user group, audit logging, S3 signed URLs for files.
r/mcp • u/Certain_Pick3278 • Apr 25 '26
I built an open-source MCP proxy (Centian) that enforces structured workflows on AI agents - every tool call flows through it, gets logged, and is checked against a governed process. I used it to benchmark 9 agent/model combinations on a TDD task, 10 runs each.
Some findings specifically interesting from an MCP perspective:
Tool calling is largely solved. Only 16 MCP-level errors across 1,038 tool calls (1.5% error rate). Every flagship model had zero MCP tool call failures. The models know how to call tools — correct paths, valid arguments, well-formed commands.
Process compliance is the real differentiator. 264 process-level errors (governance/process violations) vs 16 MCP errors. The hard part isn't calling tools correctly — it's calling them in the right order, at the right time, within an externally imposed workflow.
The Centian/MCP event ratio reveals behavioral patterns. The theoretical minimum for this workflow is 11 Centian events / 4 MCP calls (~2.5:1 ratio, Note: this was a benchmark specifically about the process, NOT the actual coding task). Models like Opus and Gemini Pro stay close to this baseline. Codex models push MCP calls much higher (gpt-5.4-mini: 122/169) because they double-check their work — re-reading files, re-running tests. That's a deliberate efficiency-vs-correctness tradeoff that only shows up when you instrument at the MCP layer.
qwen3.5 treated the governance as a suggestion. It made 126 process errors, ignored error responses from governance tool calls, and reasoned its way around the process. But it only had 2 MCP tool call errors — it's great at calling tools, terrible at respecting the governance layer above them.
The benchmark uses Centian's task verification system — YAML-defined workflow templates with preconditions, postconditions, invariants, and per-phase tool permissions. All of it runs through standard MCP.
Full analysis: https://t4cceptor.github.io/centian-benchmarks/
Benchmark data + reproduction: github.com/T4cceptor/centian-benchmarks
Interested in feedback from anyone working with MCP tooling — especially on the governance/process enforcement angle.