A founder DMs you on Slack at 9pm. They've been pair-programming with Claude Code on their app. They want to know if last week's TikTok campaign actually drove installs. They type the question. Claude calls the Grovs MCP server, runs get_link_analytics on the campaign's smart link, summarizes the result, and the conversation moves on. The founder never opened the dashboard.
This is fine.
A different founder, two weeks later, asks Claude to "clean up the inactive campaigns." Claude reads the list, picks a dozen that haven't been touched in 60 days, and starts archiving them. One of them turns out to be a dormant evergreen flow that gets hit twice a year by a B2B partner integration. The archived link returns 404 the next morning.
This is not fine.
Both scenarios are "the agent has API access." The difference between them is the difference between read and write, and that difference is the question every MCP server author has to answer before shipping. Most don't think about it carefully. The MCP spec doesn't make you. The dashboards we copy our API patterns from were designed for humans, who hesitate before they click "archive." Agents don't hesitate.
We ship 16 tools in our MCP server, and roughly half of them are read-only. That ratio wasn't an accident. This is how we thought it through, the design principles that fell out of it, and the questions we think every other platform shipping an MCP server should have an answer for.
The instinct, when you start designing tools for an agent, is to split the world in two. Reads on one side, writes on the other. Reads are safe, writes need confirmation.
That model is too coarse to be useful. It collapses three different categories of operation into one "write" bucket:
These are not the same operation. An agent creating a new draft link is not in the same risk class as an agent changing the iOS bundle ID on a SDK that ships to two million users tomorrow. Both are "writes." One is recoverable in a click. The other can take down every Universal Link in the app for the rest of the week.
If you treat them the same, you end up doing one of two things wrong. You either prompt the user for confirmation on every write, which trains them to click through confirmations they shouldn't, or you let the agent fire writes freely, which is how the second founder's Saturday morning goes badly.
What we landed on has five tiers, not two.
No state change. The same call made twice in a row returns the same answer (give or take whatever changed in the real world between the calls). An agent can call these in a loop. It can call them without warning. It can call them in parallel with other agents. Nothing breaks.
This is the tier where most of the actual value of an MCP server lives. The reason MCP exists at all is that the agent needs to know things about the platform before it can usefully act on or talk about them. Reads are the substrate. They're also the lowest-risk surface to ship, which means they're where a sensible MCP server should over-invest.
Creates new state but doesn't change or destroy existing state. The classic example is creating a draft link or a new campaign. If the agent gets it wrong, the cost is one extra row in a table somewhere and a 10-second cleanup later.
These are write operations but the blast radius is tightly bounded. The agent isn't touching anything that's already serving traffic. It's making a thing that exists only because the agent just made it.
Changes existing state. The state is recoverable (you can edit it back) but in the meantime, real users may be affected. Updating a deep link's preview image is reversible. Renaming a campaign is reversible. Both can confuse downstream systems and humans for the minutes or hours between the mistake and the correction.
This tier is where the "is this safe?" question genuinely starts to matter. The right behavior is usually to let the agent make the change but report it back to the user in the same turn, so they see what was done while they still remember asking.
Reversible in theory but affects the runtime behavior of a production system the moment it lands. Setting the iOS bundle ID for a project is reversible. Setting it wrong breaks every install attribution and every Universal Link until you set it back. Configuring the global fallback URL for a project is reversible. Setting it to the wrong host means every untracked click goes to a 404 page for as long as it takes someone to notice.
The defining feature of Tier 4 isn't whether it's recoverable. It's whether the user would have wanted to make the change manually, with the platform's UI showing them exactly what they were touching. For us, the answer is almost always yes. Configuration changes that touch production-serving infrastructure need a human at the keyboard.
Cannot be undone. Or can only be undone with engineering effort, support tickets, or backup restores. Archiving a campaign that deactivates dozens of links. Deleting a project. Rotating an API key. Revoking a user's access.
The right default for Tier 5 is not "ask the user for confirmation." It's "make the agent describe what it's about to do, in plain language, before the call happens, and require the user to type the confirmation themselves." Confirmation buttons get clicked through. Typing back the campaign name does not.
Some platforms ship Tier 5 tools in MCP servers at all. We do, sparingly. Some shouldn't. We'll come back to which.
We ship 16 tools across the five tiers. Here's where each one lands.
get_status: your account info and project listget_usage: MAU count, subscription statusget_analytics_overview: project-level analytics (views, opens, installs, revenue)get_link_analytics: analytics for a single linkget_top_links: top-performing links over a date rangeget_link: full details for one linksearch_links: list and filter linkslist_campaigns: list campaigns with metricsHalf the surface area of the server is read-only. That's deliberate. The most common things an agent needs to do (answer "how is this campaign doing?", "what's our top link this week?", "are we close to the MAU limit?") are pure reads. Investing in these makes the server useful for the daily use cases without taking on the design risk of mutation tools.
create_link: create a new deep linkcreate_campaign: create a campaign to group linkscreate_project: provision a new projectcreate_link and create_campaign are low-risk. The new link or campaign has zero traffic the moment it's created. If the agent makes one with the wrong name or the wrong tags, the user fixes it in seconds.
create_project is at the edge of this tier. A new project provisions production and test environments with new domains. The cost of an unwanted project is small (it sits there doing nothing) but if an agent gets confused about which project context it's working in across a long conversation, downstream calls can land on the wrong project. We let it through with a per-call namespace check on the user's side, but it was a real design discussion.
update_link: modify an existing link's name, path, preview, tags, or routingThis is the tool where we did the most thinking. A deep link's data payload, custom redirects, and preview metadata all affect what users see and what the app does when the link opens. Editing them while a campaign is live is not catastrophic but it's not invisible either. Our position is that the agent can make the change but the tool's response always includes the previous values, so the user can see what was overwritten without having to ask.
configure_sdk: set iOS bundle ID, Android package name, App Links SHA-256 fingerprintsconfigure_redirects: set per-platform and global fallback redirect URLsThese are the two tools we worried about most. Setting an Android SHA-256 fingerprint wrong breaks App Links verification on every device. Setting the iOS team ID wrong breaks Universal Links. Setting the global fallback URL wrong sends every untracked click to a 404.
We shipped them anyway, because not shipping them would have made the MCP server useless for the first-time setup workflow that's actually one of the highest-value use cases ("hey Claude, finish configuring my Grovs project from this Xcode build settings file"). But the tool descriptions are deliberately verbose, the agent gets explicit guidance to surface every parameter before calling, and the user-side dashboard surfaces every config change as a notification.
If we had to ship the server again from scratch, we might split these into more granular tools (set_ios_bundle_id as its own tool rather than a parameter on a multi-purpose configure_sdk) so the agent's intent is more legible to the user before the call lands. We'll probably do that in the next iteration.
archive_link: deactivate a deep link (cannot be undone)archive_campaign: archive a campaign and deactivate all its links (cannot be undone)Two destructive tools, both with "Always confirm with the user before calling" in their tool descriptions. The agent reads its own tool description and the major MCP clients (Claude Code, Cursor, Windsurf) respect this guidance in practice. We've watched agents propose archives in plain English and wait for the user to type a confirmation before firing the call.
The tools we left out of the server tell you as much about how we think as the tools we put in. The ones we discussed and rejected, in rough order of how tempting they were:
Bulk operations of any kind. No archive_all_inactive_links, no bulk_update_tags, no delete_links_older_than. The same operations are available one at a time. If a user really wants to archive 50 links, the agent loops, the user sees 50 individual archive proposals, and the user can stop after 12 if something looks off. This is slower than the bulk version. We think slower is correct here.
Tools that fire webhooks or trigger external systems. No tool that sends a postback to Meta, fires a Slack notification, or pushes data to a customer's analytics warehouse. The MCP server reads and writes Grovs state. It doesn't make the world outside Grovs do things on the user's behalf.
API key generation or rotation. No create_api_key, no revoke_api_key. Credential operations are firmly in the dashboard. The cost of an agent rotating a key that's wired into 14 production deployment pipelines is too high. The benefit of letting it do so is minor.
User and access management. No invite_user, no remove_user, no change_user_role. Even if the user's intent is clearly correct, the consequences of an agent getting confused about whose access it's modifying are bad enough that we keep this in a dashboard with named-user confirmation flows.
Anything billing-related. No upgrade_plan, no cancel_subscription. Money operations are reserved for the dashboard. Forever, probably.
There's an instinct, when you're shipping an MCP server, to map every dashboard button to a tool. We deliberately didn't. The agent should be able to do most of what's useful. It shouldn't be able to do everything the user can do, because the user is making decisions with their own attention and the agent is making decisions with whatever the last 50 tokens of context happened to suggest.
There's a tempting framing that says agents should be able to do anything a user could do, because anything else is paternalistic. We think this is the wrong way to look at it.
The agent doesn't have a user's context. It has a 500,000-token window of recent conversation, a tool list, and whatever priors its underlying model was trained on. A user knows that the campaign called "Q1 2025 winter promo" is technically inactive but is referenced by a partner contract. The agent doesn't, and there's no clean way to make it know that without giving it access to information the user might not want to put in an agent's context window.
Restricting what an agent can do isn't paternalistic. It's an acknowledgement that the user is the only entity in the loop with the full context, and the system should be designed so the user's hand stays on the wheel for any operation where context matters. Reads don't matter, mostly. Writes matter, sometimes. Configuration and destruction matter, always.
That's the principle behind the 16 tools. Half are pure reads because most agent work is reading. The mutation tools exist because some agent work is mutation, and that work is legitimately useful. The destructive tools are the smallest set because the smallest set is what we could defend.
Our 16 are public. They're at github.com/grovs-io/mcp. The tool descriptions, the destructive-confirmation strings, the deliberate omissions, all visible in the source. If you want to argue with any of it, the issues tab is open.
More articles you might find useful