AI Engineering · MCP · Backend

Building a Production MCP Server: OAuth, Tool Boundaries and State

A local MCP demo can be little more than a transport plus a few functions. A production MCP service is a different system: it has identities, credentials, authorization boundaries, persistent state, upstream APIs and failure modes that an AI client can trigger repeatedly. The protocol is only one layer of the design.

Treat the MCP server as an application boundary

The easiest way to build an MCP integration is to expose an existing API almost directly. That is also an easy way to give an AI client more authority than it needs. In production, I prefer to treat the MCP server as its own application boundary with an intentionally small contract.

For Search Console Insights, the useful operations are specific: discover connected properties, query search performance, compare periods, inspect URL indexing and read sitemap status. The server exposes those workflows instead of becoming a generic proxy for every Google API operation.

Design tools around user intent, not upstream endpoints

A good tool should represent a task the assistant can reason about. That usually means fewer parameters, narrower result shapes and stronger defaults than the upstream API provides.

For example, a search-performance tool can constrain dimensions, date ranges and row counts. The model still has enough flexibility to analyze traffic, but it cannot accidentally request unbounded data or invoke unrelated Search Console operations.

const SearchPerformanceInput = z.object({
  siteUrl: z.string().min(1),
  startDate: z.string(),
  endDate: z.string(),
  dimensions: z.array(
    z.enum(["date", "query", "page", "country", "device"])
  ).max(3),
  rowLimit: z.number().int().min(1).max(25000),
});

OAuth is part of the architecture, not a login screen

A remote MCP service needs to know which user is calling it and which upstream account that identity is allowed to use. That makes OAuth part of the service architecture rather than a thin authentication step in front of the tools.

In this project the MCP client authenticates to the service, while Google refresh tokens remain server-side. The client receives the capabilities exposed by the MCP server, not the upstream Google credentials themselves. That separation reduces credential exposure and gives the service a stable place to enforce authorization.

Separate external data from workflow state

Not every piece of data that passes through an MCP server belongs in its database. Search Console performance data can be read from Google and cached briefly without becoming application-owned data.

Persistent storage is more appropriate for state the user creates inside the workflow: planned SEO actions, implementation notes and change history. Keeping those categories separate reduces unnecessary data retention and makes it clearer which system is authoritative for each record.

Isolation needs more than a user ID

Once persistent state exists, every record needs an explicit ownership boundary. In Search Console Insights, workflow records are isolated by the authenticated Google subject and the Search Console property they belong to.

That matters because one person can access multiple properties, and the service can support multiple connected identities. Authorization therefore needs to answer both who is making the request and which property the requested state belongs to.

Bound the operations an agent can repeat

AI clients are excellent at composing tools, but they can also retry, broaden a query or call the same operation many times while exploring a problem. Production tools should assume that behavior is normal.

I put explicit bounds around date ranges, result sizes and supported dimensions, validate every input and avoid arbitrary outbound URL fetching. These are not model-specific restrictions; they are ordinary backend controls applied to a client that happens to be an AI system.

Failure behavior is part of the tool contract

An MCP tool should fail in a way the client can understand without exposing secrets or implementation details. Upstream API errors, expired authorization and database failures need useful boundaries, but raw credentials, connection strings and internal stack information should stay on the server.

This also improves agent behavior. A clear, bounded error gives the assistant enough information to choose another action. An opaque crash encourages retries, while an overly detailed infrastructure error can leak information the client never needed.

Test the protocol and the application separately

There are at least two things to verify: the backend logic behind each operation and the MCP transport that exposes it. Type checking and unit tests cover input validation and service behavior, while protocol smoke tests verify that a real MCP client can initialize the server, discover tools and call them successfully.

That separation makes debugging much faster. If the service logic passes but tool discovery fails, the problem is in the protocol layer. If transport works but a Search Console query fails, the investigation moves to authentication, validation or the upstream API.

Production MCP is backend engineering with a new client

The interesting part of an MCP server is not the novelty of tool calling. The difficult parts are familiar backend concerns: authentication, authorization, contracts, rate and size limits, state ownership, persistence, observability and safe failure behavior.

The AI client changes how those capabilities are discovered and composed, but it does not remove the need for disciplined service design. My rule is simple: give the agent useful powers, then make the boundaries around those powers explicit enough that the system remains understandable when it is running unattended.

Related work

See the production architecture behind Search Console Insights

Related technical articles