MCP vs REST tools â six months in
We started taking MCP seriously in the second half of 2025. Six months and five servers later (admin-mcp, finance-mcp, logistics-mcp, sales-ai-mcp, engine-mcp), it is worth writing down what we won by using it and where we stayed on REST.
The growth
- December 2025: 0 MCP tools
- June 2026: 919 registered tools across five servers
- admin-mcp: ~478
- finance-mcp: 144
- consulting-mcp: 193
- logistics-mcp: 77
- sales-ai-mcp: 47
- assistant-mcp: 27
This is not a tool-count brag. It is a map of how much of each domain can be cleanly exposed as structured tools.
Where MCP wins
1. Schema discovery
With REST, every integration started with the client team asking "give us an OpenAPI spec". With MCP, the chat agent calls tools/list itself and sees the signatures. There is no separate "keep the spec in sync" debt. The agent discovers what is available within a single run.
2. Per-tenant scoping
Every MCP tool reads tenantId from context (NestJS ALS). On the REST side we had to pass it explicitly everywhere, and every new customer integration meant a pass through all requests checking that the header was set. With MCP the tenant scope lives in the tool-call's internal context. We set it in one place; it is enforced everywhere.
3. Token cost
Describing a REST API in the agent's system prompt is roughly 8-12k tokens. An MCP server's tool list, fetched on demand with only the relevant tools landing in context, averages 2.1k tokens. On a 100k-conversation day, the difference is measurable in USD.
Where REST still wins
1. Long-running jobs
An 8-minute report generation or a 2-minute image render is a bad fit for an MCP tool. MCP is request-response, and past the 60-second timeout the agent gives up. REST + job-id polling or webhook is better. We return job-ids from long-running endpoints, and getJobStatus(jobId) lives as an MCP tool. The agent polls; the endpoint stays REST.
2. Streaming responses
LLM streaming, server-sent events, websocket push: not an MCP fit. REST fragment + websocket channel wins.
3. OAuth-protected upstreams
Google, Microsoft, Stripe â anywhere there is an external OAuth flow between the tenant and the upstream, you can wrap MCP tools around it, but the failure modes (token refresh, scope widening, consent screen redirect) are awkward. REST plus our own proxy is simpler.
The hybrid pattern we settled on
For every new feature we now run a small decision tree:
- Read operation (idempotent, < 5 sec) â MCP tool, mandatory
- Write operation (mutation, < 5 sec) â MCP tool, audit trail mandatory
- Anything > 5 sec â REST endpoint with job-id + optional
getJobStatusMCP tool - Streaming â REST/websocket, no MCP
- OAuth handshake â REST + own callback handler
This lets the tool catalogue keep growing without long-running or streaming cases ruining the chat experience.
The audit-trail piece
Every write MCP tool mandatorily writes an audit_event row to Postgres. Tool name, args (PII masked), tenant, actor, timestamp, success flag. This is one of those things we would have forgotten on REST, but on MCP we made it part of the tool implementation. In six months we have never had a hard time answering "what did the bot do three weeks ago?".
Mistakes we made
- Tool descriptions were too short in the first months. The agent picked the wrong tool. Every description is now at least 60 words with sample args.
- Too many tools on one server. Admin-mcp's 478 tools strained the agent context. We now filter by domain selector and only send the relevant subdomain's tools.
getByIdtools returned the full entity. Too many tokens. They are now projection-based: the agent picks which fields it wants.
What we would do differently
Starting over: we would not go 0 to 478. One domain, one server, 30 tools, then measure. MCP is not about tool quantity. It is about tool quality.