From REST to MCP (2/2): The Design Shift
Design around intent
REST APIs expose operations that are ideally built around resources. They impose some level of granularity that might shift some of the orchestration/aggregation work to the client code. That work is developed and tested before the application is deployed.
With MCP, the agent often builds that workflow at runtime. Which means the agent is now responsible for figuring out how to combine different tools to fulfill the user intent. That added responsibility simply means higher error probability and more context usage. To reduce that, we should design around user intents not resources. A tool should represent one coherent user intent and push more of the deterministic orchestration into the server. This does not mean creating a manage_everything tool. The useful boundary is an outcome that the agent can sensibly choose.
Example
Setting up a Sentry project requires separate REST operations across project, repository, and client-key resources:
# Create the project
POST /api/0/teams/{organization_slug}/{team_slug}/projects/
# Link its repository
POST /api/0/projects/{organization_slug}/{project_slug}/repo/
# Retrieve an active client key and its DSN
GET /api/0/projects/{organization_slug}/{project_slug}/keys/
# Create a key only if no usable one exists
POST /api/0/projects/{organization_slug}/{project_slug}/keys/
Sentry documents these operations separately under Create a New Project, Link a Repository to a Project, and the endpoints to list or create client keys. Its MCP server exposes the complete setup through one create_project tool:
create_project(
organizationSlug="acme",
teamSlug="backend",
name="payments-api",
platform="python",
repository="acme/payments"
)
The tool creates the project, connects the repository, retrieves an active DSN, and creates a fallback client key when needed. The agent chooses the setup outcome without carrying the project slug through the sequence or adding every intermediate response to the conversation.
Batch repeated operations
REST's uniform interface does not define a standard bulk-operation primitive, so APIs commonly scope changes to one item. That worked well enough when a person used a UI: create one page, fill it, then create the next. Human-paced interaction rarely needed many complete items at once.
An agent creates a different workload. A user can ask for a research workspace, project plan, or book and expect several pages from one request. Without batching, the agent repeats the same tool call and rebuilds its arguments before checking each result. Every call consumes more context and gives the agent another chance to skip an item or let the inputs drift. A batch tool accepts the collection once and moves the loop into deterministic server code, or even a more efficient native bulk operation that our datastore supports.
Example
Notion's Create a page endpoint creates one page per request. Creating three pages under the same parent requires three calls:
POST /v1/pages# "API migration"
POST /v1/pages# "Launch checklist"
POST /v1/pages# "Incident review"
Notion's notion-create-pages tool accepts the collection directly:
notion-create-pages(
parent={ page_id: "engineering_page_id" },
pages=[
{ properties: { title: "API migration" }, content: "..." },
{ properties: { title: "Launch checklist" }, content: "..." },
{ properties: { title: "Incident review" }, content: "..." }
]
)
Adding another page changes the collection, not the number of tool calls. Batching still needs a clear boundary. The tool should document its input limits, whether it processes items atomically or independently, and how it reports a partial failure.
Consolidate closely related operations
REST can place related operations under different methods and routes. MCP puts every tool in one flat selection surface. Create and update are different actions, but separate tools can repeat the same context and compete for selection when they share most of their language and inputs.
Consider an issue tracking conversation with an AI agent: a user may say "make sure this bug is tracked with the notes we discussed so far" without specifying whether to create an issue or update one. A single combined tool should handle both scenarios, whether the agent is delegating the decision into the tool implementation (e.g., upsertIssue(key, value)) or the agent is making the choice via an additional parameter (e.g., writeIssue(id?, value, operation = 'create' | 'update')). Any of those would be better than registering 2 tools. This saves context and can also improve selection accuracy. The ToolScope study supports this narrower selection claim, not a rule to merge every related operation.
Example
GitHub's REST API exposes issue creation and update as 2 separate endpoints:
POST /repos/{owner}/{repo}/issues
PATCH /repos/{owner}/{repo}/issues/{issue_number}
GitHub's issue_write tool groups them behind one explicit parameter:
issue_write(
issue_number: number, # gets ignored if passed method is create
method="create" | "update",
owner: string,
repo: string,
title: string,
# ... other 9 fields
)
The method parameter keeps create and update distinct. If we were to keep create and update as 2 separate tools, that means we would be copying all the documentation for all 13 parameters across both tools, which would eventually bloat the context.
Make semantics explicit
With REST, the URL identifies a resource and the HTTP method describes the operation. Status codes classify the result, while headers add protocol-level context. Application code interprets those signals through branches written by a developer.
With MCP, an agent usually sees only a tool call and its result. It doesn't have a client-side logic that translates an HTTP status to a specific user-facing message. A bare Unauthorized leaves several questions open: what failed, whether state changed, and what the user should do next. The tool should turn that implicit contract into explicit inputs and results. Detailed semantics give the model fewer interpretations and a safer recovery path.
Example
A REST API may rely on the method, route, and status code:
PATCH /orders/ORD-42
Content-Type: application/json
{ "shipping_address": "12 Market Street, Cairo" }
401 Unauthorized
The programmed client's 401 branch may already know how authentication works and which sign-in screen to open. The equivalent update_order(order_id="ORD-42", shipping_address="12 Market Street, Cairo") tool should return that knowledge:
{
"error": "AUTHENTICATION_REQUIRED",
"message": "The shipping address for ORD-42 was not updated because the store account session has expired.",
"state_changed": false,
"recovery": "Ask the user to reconnect the store account through the account connection UI, then retry update_order with the same arguments. Do not ask for credentials in chat."
}
The MCP specification supports structured results, so explicit feedback does not require giving up machine-readable errors.
Treat documentation as control flow
With REST, developers usually read documentation while building an integration. Once the client ships, its integration logic keeps working even if the reference becomes incomplete, outdated, or buried in a Slack thread.
With MCP, the agent reads the tool name, description, and parameter descriptions at runtime. Documentation does not just support the integration. It prompts the agent and becomes an essential part of the control flow. The wording affects which tool the model selects and which arguments it builds. Changing a REST description does not change a client that has already compiled its integration logic. Changing an MCP description can change behavior while the schema and handler stay untouched. A commit named docs: clarify wording can now change production behavior. Documentation changes can therefore cause regressions and should be tested against representative user requests.
Example
Before invocation, the model may know a log-search capability only through this definition:
search_logs(service, query, start_time, end_time)
"Search logs."
The description leaves important decisions to the model. It does not say which logs the tool searches, what identifies a service, how it interprets time ranges, or whether it includes audit events. A useful definition carries the knowledge that would otherwise live in an API guide:
search_logs(service, query, start_time, end_time)
"Search application logs when investigating runtime behavior. `service` must match a deployed service name. Times are UTC and the range cannot exceed 24 hours. This tool does not search security audit events."
The expanded description tells the model when to select the tool and how to construct a valid call.
Suggest relevant next actions
HATEOAS (Hypermedia as the Engine of Application State) is part of REST's original uniform-interface constraint. It simply means the server should return links that describe valid next transitions. Most REST APIs ignore this part because they'd view it as impractical (even though the REST author is skeptical about APIs being called REST without HATEOAS). Anyways, this concept should now be revived; it will be more relevant in an MCP than in a REST (or REST-ish) API.
Again, an agent chooses the next step at runtime. If the result names a relevant tool, explains why it applies, and carries known arguments forward, the agent does not have to search the tool surface or infer the transition from raw state.
Example
Sentry's get_trace_details does this in its response. When a trace contains an AI conversation, the result includes the trace summary and a structured suggestedActions entry. The tool's response test covers this behavior:
{
"structuredContent": {
"suggestedActions": [
{
"type": "tool_call",
"toolName": "execute_sentry_tool",
"arguments": {
"name": "get_ai_conversation_details",
"arguments": {
"organizationSlug": "sentry-mcp-evals",
"conversationId": "conv-trace-snapshot"
}
},
"reason": "Fetch the full transcript for this AI conversation."
}
]
}
}
The response selects the next tool, explains why it applies, and carries forward the organization and conversation identifiers found in the trace. The agent can move from the trace overview to the full conversation without reconstructing that workflow from documentation.
Support field filtering
REST APIs commonly return a fixed resource representation, then let application code extract the fields it needs from it. Client-defined projections is considered one of the best practices by Microsoft, although many REST APIs do not provide them. That's why we hear that one of GraphQL's advantages is that it promotes against over-fetching.
With MCP, the tool should return the smallest complete result needed for the current decision. The model has to interpret every returned field, and common clients may keep the result in conversation history. Irrelevant data consumes context now and may keep consuming it on later turns. A smaller result also gives the model less state to distinguish before choosing its next action.
Example
A REST field projection may look like this:
GET /customers/C-17?fields=id,name,riskStatus
The MCP tool can expose the same filtering idea directly:
inspect_customer(
customer_id="C-17",
fields=["customer_id", "display_name", "risk_status", "risk_reasons"]
)
โ {
"customer_id": "C-17",
"display_name": "Contoso Ltd.",
"risk_status": "review",
"risk_reasons": ["billing address changed after payment"]
}
Both interfaces use the same projection pattern, and that is fine. With MCP, filtering matters more because every returned field may enter the model's context. The fields parameter lets the agent request only the properties needed for its current decision.
Guard destructive operations
With REST, an endpoint usually assumes that client code has already selected the intended operation and resource. This is enhanced by a good UI/UX that warns users of destructive operations, and the overall deterministic behavior of the app doesn't make this a big issue.
With MCP, a model makes that choice at runtime. Schema and permission checks can show that a mutation is well-formed and allowed, but they cannot show that the agent understood what the user wanted. A tool can guard destructive operations before and after execution. Before the call, its description can instruct the agent to warn the user and ask for explicit confirmation. For higher-risk operations, the client can enforce that confirmation. After the call, the result should confirm what changed and provide a recovery path through soft deletion or a restorable backup.
Example
remove_file(path)
"Remove a file. Before calling, warn the user that the file will be deleted and ask for explicit confirmation."
remove_file(path="/repo/config.yml")
โ {
"removed": "/repo/config.yml",
"state_changed": true,
"backup_path": ".backups/7fd2/config.yml",
"backup_expires_at": "2026-07-20T12:00:00Z"
}
This level of protection makes sense for deletion. Not every update needs the same ceremony.
Disclose tools progressively
A REST API may expose hundreds of endpoints because each application selects the subset it needs during development. That selection happens once, before the application runs. An agent cannot make the same permanent selection because the useful subset depends on the current request. MCP clients should progressively disclose specialist tools as the conversation context demands them, keeping the initial tool surface small and focused.
Comments
No comments yet. Start the discussion.