Ruddr's MCP server puts more than 90 query tools across 19 namespaces behind two tool definitions, scoped to the permissions of the one person who authorized the connection. Every one of those numbers is a design decision, and most MCP servers make them differently.
Most MCP servers are a REST API in a costume
The fastest way to ship an MCP server is to take the REST API you already have and generate a tool per endpoint. It works, in the sense that the tools appear and the calls return 200s. It is also how you get a server that burns 40,000 tokens describing itself, hands the model a thousand raw rows, and watches it do arithmetic badly.
The mismatch is that a REST API is designed for a programmer who reads documentation once and then writes code that runs a million times. An MCP server is read fresh by a model on every conversation, and that model has to get it right on the first try, without asking anyone.
Those are different jobs. We built for the second one. What follows is what that changed.
Answers, not rows
Ask a CRUD-shaped MCP server "which projects are trending over budget?" and the model does what it can: pull time entries, multiply rates, sum, and compare to the budget. Every step is a chance to be wrong, and none of it is auditable.
Ruddr's server exposes the same analytical layer that our application uses. Alongside the entity lookups there are summary tools across five views of our customer's business, on both the project and the member dimension:
- Actuals: What happened: revenue, time, performance to date
- Plan: What was originally planned
- Forecast: What is now expected
- Budget: What was authorized (in total)
- Monthly budget: What was authorized (per month)
A summary tool returns grouped rows and a totals block that the database computed over those same rows. The model does not add up a column; it reads a subtotal. When a member asks for utilization against plan, the comparison happens in SQL, on the same code path that renders the number in the Ruddr UI.
Multicurrency works the same way. Conversion is done server-side, and a money cell arrives as a map keyed by reporting currency with the primary currency declared in the field catalog. The model never sees a bare number it has to guess the denomination of, and it never applies an exchange rate itself.
This is the difference between an integration that retrieves your data and one that answers questions about it.
The permission model is the product
The concern we hear from customers first is never about capability. It is "I am not connecting our project margins and salary-adjacent data to an AI assistant."
That is the right instinct, and the usual MCP answer is bad: one service account, one API key, full workspace access, and a hope that the model behaves. Every question anyone asks runs with the same god-mode credential.
Ruddr does not work that way. A connection is authorized by one member, through OAuth, and every call runs as that member. The tool list is rebuilt on each request from two gates: what that member's security role permits, and which scopes they granted this particular client. A delivery lead and a controller can connect the same assistant to the same workspace and get genuinely different tool surfaces.
Two details we deliberately pursued:
- A withheld tool is invisible, not forbidden. When a member's role does not permit a tool, the server reports it as unknown rather than as denied. The tool surface never leaks the existence of data the role withholds.
- A missing scope says so. When the tool exists and the member could use it but the connection was not granted the scope, the error names the scope required. That is a fact about the connection, not about the workspace, so the client can go ask for a wider grant.
Read and write are separate grants. mcp:read is the default; nothing writes to your workspace unless someone deliberately granted mcp:write. A read-only connection does not receive write tools that fail at call time because it does not receive them at all.
There is one more piece that sounds like a footnote and is not. Every tool advertises a readOnlyHint to the client, and that hint is derived from the scope gating the tool rather than declared by hand. A tool cannot describe itself as read-only while sitting behind the write grant, because nobody is typing that annotation in.
Ninety-one tools, two tool definitions
Ruddr's query surface covers 19 namespaces — time, expenses, clients, projects, pipeline, billing, members, resourcing, and the actuals, plan, forecast and budget families on both dimensions. That is more than 90 tools, each with a real JSON Schema describing filters, grouping, sorting and date ranges.
Published directly, that surface would be a disaster. Every one of those schemas would be sent to the model on every conversation, before it knows which one it needs. It would crowd out the user's actual documents, and in a client where the user has connected several servers, it would crowd out the other servers too. A tool surface is not free — the person pays for it in the context they have left.
So we collapsed it. Queries run through one tool, ruddr-query; commands through another, ruddr-command.
The model sees a short directory of what exists, loads the full schema for the one or two tools it actually needs, and runs them. The 90+ schemas still exist and are still precise but they are just fetched on demand instead of broadcast.
The result: a surface that would have cost tens of thousands of tokens up front costs a directory listing, and the depth is still there for the query that needs it.
This is, to us, the clearest line between an MCP server someone generated and one someone designed. Generation optimizes for coverage. Design optimizes for what the model can hold in its head at the moment it has to choose.
Skills: the rules a JSON Schema cannot carry
A JSON Schema can say that where accepts an object. It cannot say that a namespace key and a boolean key must never appear at the same level, that array values are already ORed so a single leaf needs no and wrapper, or that a task's status lives on task, not on project.
Those rules are real, and a model that does not know them writes a filter that gets rejected. Then it guesses at a second shape, and a third. The user watches their assistant flail at a question that had a perfectly good answer.
So the server carries skills, which are instruction documents the model loads on demand, the same mechanism that our in-app assistant uses:
data-tools:The query grammar every read tool shares-
track-time:The preflight-then-save flow for logging time -
sitemap:Turning "take me to…" into a real deep link -
submit-ticket:Filing a support ticket, with a confirmation step
Six more cover our help center (knowledge base), API documentation, system status, public site content, feedback, and What's New product updates.
The server's own instructions tell the model to load data-tools once, before its first query, and to re-read the relevant skill if a tool rejects its arguments rather than guessing at a different shape. Recovery is part of the design, not an afterthought.
We think this is underrated. Most MCP servers assume the model will infer the conventions from tool names and descriptions. It sometimes does. Skills mean it does not have to.
Writes that ask first
Reading your data with an AI assistant is a modest risk. Writing to it is not, and time entries are the sharpest case: they feed billing and utilization, and a wrong time entry is not a typo, it is an invoice problem.
So time tracking over MCP is a three-step flow, and the last step saves only after the member has confirmed the entry.
-
track-time-suggestionsanswers the open-ended ask — "help me with my timesheet" — by returning the combinations that member is actually entitled to log against on that date, with their assigned role and any required task already filled in. Combinations that cannot be completed are dropped rather than offered, so every row the user sees is a valid one. -
track-time-preflighttakes what the user committed to, resolves names to records, applies defaults, and validates each row against the project's rules — without saving. It reports what is missing or wrong, with the choices to fix it. -
track-timecommits, and it takes only resolved UUIDs. It does no name resolution of its own. That is a deliberate constraint: the tool that writes cannot guess which project you meant.
The skill instructs the model to confirm with the user before that final call, every time. Picking a suggestion is not a shortcut around it.
The same shape applies to filing a support ticket — draft, present, confirm, send. Anywhere the server acts on your behalf in a way someone else will see, a human sees it first.
The unglamorous parts
The following are why our MCP server behaves on the thousandth call the way it did on the first.
- Results are columnar. A list of 300 rows sent as JSON objects repeats every key 300 times. Ruddr sends a field catalog once — key, label, type, role, and for money fields the reporting currencies and which is primary — then the rows as bare arrays. Same information, a fraction of the tokens, and the model gets told what a column means instead of inferring it from the key.
- Pagination has a stated contract. List tools page and return no totals; the server instructions tell the model to check
pagination.hasMorebefore calling a list complete. Summary tools do not page — they return every matching group up to a ceiling, with totals over exactly the rows in that response. The two behave differently, so we say so rather than letting the model find out. - Errors are useful and quiet. A failed call returns a self-correcting hint where one applies — a malformed filter points at the skill that explains the grammar. What it does not return is the exception. The caller gets a correlation id that means nothing to them or to anyone intercepting it, and that we can use to find the real failure.
- Rate limits are per member and per tool. Keyed to the member, not the workspace, so one person's runaway client cannot exhaust everyone's budget. The expensive grouped reporting queries meter separately from everything else, so one report loop cannot spend a member's whole allowance.
- Everything is traced. Each call records under the member's identity, with the connected application and the credential's scopes, and the trace holds exactly what left Ruddr. It never holds the bearer token. Admins can replay traffic per tool and see what an external agent was actually served.
No token, no logs of your prompts. The server is stateless.
We run evals against it
A tool surface is a prompt. Tool names, descriptions, schemas and server instructions are all text a model reads to decide what to do, and like any prompt, a small change to it can quietly make things worse.
So the MCP channel has its own eval suites in CI, separate from the ones covering the in-app assistant: whether the model picks the right query for a question, whether it uses the meta-tools correctly, whether it loads the skill it needs, and whether it respects scope boundaries. They sit alongside several hundred cases covering the underlying tools themselves.
This matters more than it sounds. When a server's tool descriptions are generated from an API spec, nobody is checking whether a model can actually navigate them — the schemas are valid, so the build is green. Valid and usable are different properties, and only one of them is what the user experiences.
We would rather find out that a description change broke tool selection from a failing eval than from a customer.
What it looks like in practice
The point of all of this is that the questions stay ordinary:
- "Which projects are trending over budget this quarter?"
- "What is my team's utilization against plan for Q3?"
- "Show me unbilled time on Acme's projects older than 30 days."
- "Which opportunities have slipped a stage since last month?"
- "Log 3 hours to the Acme redesign for yesterday."
- "Take me to the project's budget tab." — which returns a link, not a screenshot
Each of those is one or two tool calls against the same engine that renders the equivalent report in Ruddr. The numbers match because they are the same numbers.
Connecting
It is a remote server over streamable HTTP with OAuth. There is nothing to install and no API key to paste — add it in your MCP-compatible client, authorize as yourself, choose whether to grant write access, and the tool surface is built for your permissions on the first request.
For guidance on getting set up, check out this Help Center article: https://help.ruddr.io/integrations/ai-connectors-overview
If you build MCP servers, the summary is this: the model is your user now. It reads your tool surface cold, once, under a context budget, and has to get it right without asking anyone. Design for that reader and the rest follows.
