MCP server design: tools, authentication, and rate limits
By Flavio Copes
How to design a useful MCP interface for your product, with clear tools, account permissions, usage limits, predictable results, and safe writes.
An AI agent can use your product if you give it a useful way in.
That sounds like a small integration task. Expose a few API calls through MCP, add a connection button, and let people work from their favorite assistant.
But the choices you make here affect the product itself. What can someone accomplish without opening your dashboard? Which account pays for a generated image? Can an assistant publish something while its user is away? How does it recover when a request times out?
Those questions deserve as much attention as the tool registration code.
I think of an agent interface as another product interface. Someone will use it to do real work. They deserve clear results and predictable limits, just as someone clicking through the website does.
Start with a job someone wants done
Imagine a service that generates social preview images.
A person uses the website to choose a template, enter a title, adjust the layout, and download an image. An agent could use the same service while preparing a website launch. It already knows the page title and where the image belongs.
A useful request would be: create a preview image for this page using the account’s usual template, then return the image URL.
That gives us a concrete workflow to design around. We can ask what information the agent needs, what the operation costs, and what the result should contain.
Compare that with exposing every database table through a generic query tool. The latter gives the agent plenty of power, but makes it reconstruct the product’s rules on every request. It also makes those rules harder to enforce.
For this service, I would begin with listing templates and generating previews. I would leave billing administration and account deletion outside the first integration.
If MCP is new to you, my introduction to the Model Context Protocol explains the connection between an assistant, its client, and your server. The protocol gives them a shared language. You still decide what your product says in that language.
Design tools around completed actions
A tool called generate_preview communicates a purpose. A tool called execute_action with a large string argument leaves the client guessing.
I would want the generation tool to accept a template identifier, title, and optional subtitle. It should return an asset identifier and a URL, along with the generation status. The account comes from the authenticated connection.
An account identifier supplied in the arguments must never become proof that the caller owns that account.
Keep the tool description specific. Explain which fields are required, whether generation spends credits, and whether calling it creates a persistent asset. An assistant should not have to discover those details by making expensive mistakes.
The MCP tools specification provides input schemas and structured results for expressing these contracts. It also supports behavioral annotations. Those annotations help clients understand tools, but they are descriptions, not security enforcement.
For example, calling a tool read-only does not prevent its implementation from writing to a database. Your application code has to uphold the promise.
Small, coherent tools also give you better support conversations. You can ask which generation failed and find that operation in your logs. Debugging a generic command interpreter is much less pleasant.
Reuse the product’s business rules
The website and the MCP server should reach the same application operation.
For our image service, that operation checks the subscription, verifies access to the template, applies the title length limit, and reserves the required credits. It then creates the generation job.
If you copy that logic into a second implementation for MCP, it will drift. Eventually, the website will reject an action that the agent interface allows, or the agent interface will charge differently.
I would put the product rules behind a shared service function. The web route handles HTTP details. The MCP handler translates tool arguments and results. Both call the same operation.
This also makes testing more useful. A test of credit reservation protects both interfaces. Transport-specific tests can focus on translating invalid arguments or expired authentication into a clear response.
You do not need a large internal framework to achieve this. A small function with explicit dependencies is often enough. The important part is having one place that decides whether the operation may happen.
Authentication identifies the connection
For a remote product integration, users need a way to connect their own accounts and revoke access later.
The MCP authorization specification describes an OAuth-based authorization flow for HTTP transports. A local server using standard input and output has a different setup; you should not copy remote HTTP assumptions into every MCP deployment.
From the user’s point of view, the connection should answer a straightforward question: which account am I giving this assistant access to?
Show that account during setup. Make the connection visible in the product’s settings. Let the user revoke it without searching through the assistant’s configuration.
Then check authorization again inside each operation. A valid token does not grant access to every workspace or every paid feature. A collaborator who may view an image library might still lack permission to generate assets using the team’s credits.
I would avoid a single organization-wide credential shared by all users. It makes revocation harder and erases the difference between people with different roles. Per-user access gives you a clearer answer when someone asks who requested a change.
Put cost limits where the work happens
A generation tool can receive valid requests and still create an expensive problem.
An agent might retry an operation, loop over a large input, or misunderstand how many variants the user wanted. None of that requires a malicious user.
For the image service, I would enforce a maximum output count per request and a limit on concurrent jobs. The account would also have a usage budget that applies across the website and the agent interface.
Different limits solve different problems. Use a request rate limit to control how quickly callers arrive, and a credit budget to control spending. You can limit concurrent jobs separately to protect the generation workers. Limiting requests alone will not protect an expensive endpoint that accepts a huge batch.
Give the client a useful error when it reaches a limit. Say whether it can retry later, reduce the batch, or ask the user to change the account’s allowance. Avoid a vague failure that encourages the assistant to try the same operation repeatedly.
I would also distinguish development access from production use. Someone testing their integration should have a small, visible allowance and a way to generate a sample without accidentally consuming a team’s entire budget.
Reserve credits before starting expensive work
Suppose an account has one credit left. A person requests an image from the website while their assistant requests another through MCP.
If both operations read the balance before either changes it, both can see permission to proceed. Checking the balance in a shared function still leaves this race if the check and reservation happen separately.
I would lock the account balance while checking it, then reserve the credit and create the operation in the same transaction. A second request must wait before checking what remains. PostgreSQL’s row-locking documentation describes one mechanism for coordinating writes to the same account row. The implementation depends on the database, but the product rule is straightforward: concurrent requests must share the same remaining allowance.
The reservation also needs a lifecycle. For this service, I would keep the credit reserved while the job runs, consume it when generation succeeds, and release it after a confirmed failure if that matches the refund policy. Users should be able to see pending usage separately from completed charges.
I would keep the reservation until the generator confirms its outcome, because a timeout can occur while work continues. Returning the credit immediately could let another request spend it while the first image finishes.
Then consider the gap between saving a job and sending it to a queue. If the process stops there, the database can contain a reservation for work no worker received. I would retain enough durable job state for a background check to find and resubmit that operation. The worker must recognize the existing operation when it receives a duplicate.
These are useful failure cases for integration tests. Send two requests against the last credit. Stop processing after the reservation. Retry the same job after a worker restarts. Check the resulting asset and balance each time.
Retries need a product decision
Suppose the server creates an image, charges a credit, and sends the response. The network drops before the client receives it.
The client sees a failure. The product has already completed the work.
If retrying creates another image and charges another credit, the user pays for a connection problem. This can happen with ordinary API clients too, but agents make it easy to repeat the action while trying to recover.
For billable writes, I would support a request identifier with idempotency semantics. The server records the operation against that identifier. Repeating the same intended request returns the existing operation rather than creating another.
Define the details carefully. The identifier belongs to an account and a specific request. Reusing it with different arguments should produce a conflict. Its retention period should be documented, and the persistence mechanism must handle concurrent requests.
An in-memory map inside one server process will not protect a distributed service reliably.
Long operations also need a way to retrieve their status. Return a job identifier as soon as the job exists, then let the agent inspect it. A timeout should leave the client with a recovery path.
Approval should describe the exact effect
Generating a private draft image is one action. Publishing it to a live website is another.
I would separate them. Let the agent prepare the asset and show it to the user. Publishing should be a distinct operation with a clear destination.
For destructive or billable operations, the approval needs enough detail to make a decision. The account, affected resource, and price should be visible. A general instruction to help with a project is a poor substitute for approval of a specific irreversible purchase.
This is the approach I used for my Cloudflare domains MCP server. It can search and prepare a quote. Buying a domain requires approval of the exact live purchase.
The same idea applies to publishing a campaign or deleting a customer export. An assistant can do useful preparation before a person approves the final effect.
Be honest about where approval is enforced. A confirmation sentence inside a tool description is only guidance. Sensitive operations need an application-level boundary that the model cannot bypass by changing its wording.
Tool results can contain hostile text
Our image service might eventually accept a URL and extract a page title. That page is outside our control.
Its text could contain instructions directed at the assistant. A template description written by another user could do the same. Even a harmless-looking result can carry content that the assistant should treat as data.
The server should return only what the task needs. Fetching a title does not require returning the entire page, every embedded script, and all metadata. Reducing unnecessary content limits both exposure and confusion.
For server-side URL fetching, I would also restrict network access. An arbitrary URL field can become a way to request internal services if you do not validate destinations and redirects.
The MCP security guidance covers trust boundaries including token handling and server-side request forgery. Passing a token through to an unrelated upstream service is especially dangerous because it blurs who the token was issued for.
These protections belong in the integration. Asking the assistant to be careful cannot replace them.
Make the integration easy to operate
When a user reports a failed generation, support should be able to find it.
Record the authenticated actor, requested operation, affected resource, and outcome. Give the client an operation identifier it can include in a support request. Avoid logging access tokens or copying sensitive input into every diagnostic message.
I would track completed jobs and rejected requests separately. A connection count tells you very little about whether people can finish their work. Repeated failures on one template might reveal a product bug that dashboard users never encounter.
Tool changes need care too. Renaming an input or changing its meaning can break workflows people have saved. Keep old behavior stable where possible, and introduce new capabilities explicitly.
My first release would stay narrow. I would connect one client, generate one asset, test a failed request, revoke the connection, and confirm that the next request fails correctly. Then I would test duplicate requests and exhausted credits.
After that, I would watch which tasks users keep returning to the website to finish. Those gaps would tell me which tool to add next.
Want me to talk about your product? You can sponsor this site.
Related posts about ai: