How We Build MCP Servers: Architecture, Authorization and Security for Business Systems

17 min read 1
Date Published: Nov 10, 2026
Pavlo Yablonskyi CTO & Co-Founder
How We Build MCP Servers: Architecture, Authorization and Security for Business Systems

Short answer (updated November 2026). A production MCP server is a thin protocol layer over code you would write anyway: typed tools that validate input and call services with the business logic, which call adapters to your ERP, CRM, database or document store. Design tools around tasks, not around every API endpoint. Decide early who the server acts for: a service account with narrow rights for automations, or the individual user via OAuth 2.1, as the current MCP specification (2026-07-28) defines it, with protected resource metadata, resource indicators and audience checks. Never pass a client's token through to the backend, never give a model a generic "run SQL" tool on an admin key, and treat every tool description and every piece of returned text as untrusted. Add audit logs, output limits and tests, and check what personal data leaves the company with each tool result.

At SDH we have built MCP layers for a field service ERP, a calculation engine and a music distribution platform, and we turned what worked into an internal project template. This article describes the architecture, the authorization options, the attacks that have hit real MCP servers, and a checklist for taking a server to production.

Where we use MCP

  • FieldHub, a SaaS ERP for field service and security integrators in the US: CRM, inventory, scheduling, accounting. Its API is too large to expose one tool per endpoint, so we built the MCP layer as a separate engine between the agent and the API instead of hand-writing hundreds of tools.
  • An astrology calculation server: the server computes natal charts and planetary positions precisely; the model only interprets. It runs over standard input/output for desktop clients and over HTTP on Google Cloud Run for an automation in n8n that answers users in a Telegram bot.
  • A music distribution platform (in development): an MCP layer over the release, metadata and royalty workflows that the platform runs against distributor APIs.
  • Our template: the structure that proved itself in these projects – layers, error handling, authentication for HTTP, Docker and Cloud Run deployment, end-to-end tests – packaged so that every new server starts from the same base.

MCP in late 2026: what changed

The protocol has moved quickly; much of what was written about MCP in 2025 is outdated.

  • The current specification is 2026-07-28. It makes the protocol stateless: the initial handshake and session IDs are gone, each request carries its version and capabilities, and state that spans several calls is passed as handles in tool arguments. The old HTTP+SSE transport and dynamic client registration are deprecated (changelog).
  • Authorization is OAuth 2.1. An MCP server is an OAuth resource server: it publishes protected resource metadata (RFC 9728), clients must name the server they want a token for (resource indicators, RFC 8707), and the server must check that a token was issued for it (authorization). Clients now register through client ID metadata documents.
  • Enterprise single sign-on arrived as an official extension in June 2026: access to MCP servers is provisioned centrally through the company's identity provider (MCP blog).
  • Neutral governance. MCP joined the Agentic AI Foundation under the Linux Foundation in December 2025, co-founded by Anthropic, Block and OpenAI (MCP blog).
  • Broad client support: Claude, the OpenAI Responses API and ChatGPT, Microsoft Copilot Studio and Windows, VS Code, Google Cloud and JetBrains IDEs all speak MCP. The official SDKs reach close to half a billion downloads a month; Python and TypeScript are tier-1 SDKs.
  • The public registry is still a preview, so do not build production discovery on it yet (registry).

The architecture

Layers of a production MCP server: AI clients such as Claude, ChatGPT, Copilot or n8n connect through an optional gateway; the MCP server has a thin tool layer, a service layer with business logic and adapters to ERP, CRM, databases and documents; authentication, audit logging and limits sit at the edge
Figure 1. A production MCP server is a thin protocol layer over ordinary business code

Our servers follow the same structure:

  1. Tools are the only part that knows about MCP. A tool validates its input against a typed model, calls one service function and converts errors. No business logic lives here.
  2. Services hold the logic as plain functions without MCP imports. They are tested like any other code and can be reused by a REST API or a background job.
  3. Adapters talk to the systems behind: the ERP API, the CRM, the database, the document store. Credentials and retries live here.
  4. The edge handles transport (standard input/output for desktop clients, Streamable HTTP for remote ones), authentication, rate limits and audit logging.

The input models are part of the prompt. The model reads the tool name, its description and every field description to decide what to call and with which arguments; most of the quality of tool use comes from them. We write them like documentation for a new colleague.

Errors must tell the model how to fix the call. "customer_id not found; search by name with find_customer first" lets the agent recover. A stack trace leaks internals and teaches it nothing.

A minimal example

A read-only tool that lists a customer's open invoices from an ERP, in Python with FastMCP, the way our template structures it:

# models.py: the field descriptions are read by the model
from pydantic import BaseModel, Field

class OpenInvoicesInput(BaseModel):
    customer_id: str = Field(
        description="ERP customer number, for example C-10234. Use find_customer to look it up.")
    limit: int = Field(default=20, ge=1, le=100,
                       description="Maximum number of invoices to return")


# services/invoices.py: business logic, no MCP imports, unit-tested on its own
class DomainError(Exception):
    """The message must tell the model how to fix the call."""

def open_invoices(erp, data: OpenInvoicesInput) -> list[dict]:
    customer = erp.get_customer(data.customer_id)
    if customer is None:
        raise DomainError(
            f"Customer {data.customer_id} not found. Search with find_customer first.")
    invoices = erp.list_invoices(customer.id, status="open", limit=data.limit)
    # Only the fields the task needs: no bank details or personal notes leave the company
    return [{"number": i.number, "due": i.due_date.isoformat(),
             "amount": str(i.amount), "currency": i.currency}
            for i in invoices]


# tools/invoices.py: thin MCP layer
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

def register(mcp: FastMCP, erp) -> None:
    @mcp.tool(annotations={"readOnlyHint": True})
    def list_open_invoices(data: OpenInvoicesInput) -> list[dict]:
        """List a customer's unpaid invoices with number, due date and amount."""
        try:
            return open_invoices(erp, data)
        except DomainError as exc:
            raise ToolError(str(exc)) from exc
        except Exception as exc:
            raise ToolError(
                f"Internal error ({type(exc).__name__}); check the server logs.") from exc

And a test that runs the real server in memory, without a network, in CI:

import pytest
from fastmcp import Client
from fastmcp.exceptions import ToolError

async def test_unknown_customer_explains_the_fix(server):
    async with Client(server) as client:
        with pytest.raises(ToolError, match="find_customer"):
            await client.call_tool("list_open_invoices",
                                   {"data": {"customer_id": "C-0"}})

Note the readOnlyHint: it helps clients decide when to ask the user for confirmation, but it is only a hint. A malicious or buggy server can claim it and still delete data (MCP blog); the real protection is that this tool's adapter has no write access.

Large APIs: workflows, not endpoints

Mapping every endpoint to a tool fails once an API has more than a few dozen operations: the model must read every tool description, picks the wrong one more often, and the context fills up before the work starts. Anthropic's guidance is the same: build fewer tools around complete tasks, return only high-signal fields, paginate and truncate (Anthropic Engineering).

For an API the size of an ERP that is not enough, which is why we built FieldHub's MCP layer as a separate engine. The general options:

  • Curated task tools for the 10–⁠30 workflows that matter ("create a work order for a customer", "list overdue invoices"), each combining several API calls.
  • A discovery pattern for the long tail: a tool that searches the API catalogue, one that describes an operation, and one that executes it, restricted to an allowlist and to the caller's rights.
  • Several smaller servers by domain (CRM, inventory, accounting), so an agent only loads what its task needs.

Who does the server act for?

Two authorization models: a service account with a bearer token and narrow rights for automations such as n8n, versus per-user OAuth 2.1 where the MCP server validates a token issued for it and calls the backend on the user's behalf with the user's own rights
Figure 2. Service account or the individual user: decide before the first tool
Service account Per-user OAuth 2.1
Typical use Automations and workflows (n8n, back-office jobs), internal agents Assistants used by employees or customers (Claude, ChatGPT, Copilot)
How it works A static bearer token or client credentials; the server uses one technical account in the backend The client obtains a token for this MCP server; the server checks issuer, audience and scopes, then calls the backend with the user's own rights
Data access Everything the technical account may see, so its rights must be narrow and fixed Exactly what the user may see in the source system
Main risk A prompt injection reaches everything the account can reach Misconfigured scopes or token handling

We use both. Our template ships a bearer-token check for the HTTP endpoint, which is right for a server called by one automation. As soon as a server is used by people with different rights, it needs per-user authorization, and then the specification is explicit: validate that the token was issued for your server and never accept or pass through any other token (security best practices). If the backend needs its own token, the server exchanges or obtains it; it does not forward the client's.

What has gone wrong in real MCP deployments

Incident What happened The lesson
GitHub MCP server, May 2025 (Invariant Labs) A malicious public issue made an agent copy data from private repositories into a public pull request A broad personal token gave the agent access far beyond the task
Supabase MCP with Cursor, July 2025 (General Analysis) Instructions hidden in a support ticket led the agent to read and leak database tables with a key that bypassed row-level security Never give an agent a generic SQL tool on an admin key
Asana MCP, June 2025 (BleepingComputer) Data of about 1,000 customers was visible to other organisations; the feature was switched off for almost two weeks Check tenant boundaries in every cache and shared state
mcp-remote, CVE-2025-6514 (JFrog) A malicious server could execute commands on the client machine through a crafted authorization URL (CVSS 9.6) Keep MCP tooling updated; treat servers you connect to as untrusted
MCP Inspector, CVE-2025-49596 (The Hacker News) The developer tool could be taken over from a website (CVSS 9.4) Do not leave dev tooling listening on open interfaces
postmark-mcp, September 2025 (The Hacker News) A community server on npm started copying every email it sent to an attacker Pin and review third-party servers like any dependency
Audit of popular servers, March 2025 (Equixly) 43% had command injection, 22% path traversal, 30% server-side request forgery Never build shell commands or file paths from tool arguments

Behind most of these is the combination Simon Willison called the "lethal trifecta": an agent that can read private data, sees untrusted content, and can send data out (Simon Willison). Prompt injection cannot be fully prevented by the model; it has to be contained by what the tools allow. OWASP now maintains an MCP Top 10 (in incubation) with the same themes: token exposure, scope creep, tool poisoning, supply chain, command injection, missing audit.

Our rules for production servers

  1. Narrow tools around tasks. No generic "run SQL", "call any endpoint" or "execute shell".
  2. Least privilege in the backend. Read and write in separate tools, and preferably separate credentials; the read tools' credentials cannot write.
  3. Deterministic confirmation for destructive actions. Deleting, paying or sending externally requires an explicit confirmation step enforced in server code, not a hint to the model.
  4. Per-user identity when people with different rights use it. Service accounts only for fixed automations, with fixed scopes.
  5. No token passthrough, audience checks on every token, minimal scopes, no wildcards.
  6. Treat tool output as untrusted input. Text from tickets, emails or documents can contain instructions; do not let it trigger actions on its own.
  7. Return only what the task needs, with pagination and size limits. Claude Code, for example, cuts tool output at 25,000 tokens by default.
  8. Actionable errors, no stack traces.
  9. Audit every call: who, which tool, which arguments, which result size; trace IDs through to the backend (the 2026 specification carries OpenTelemetry context).
  10. Pin and review dependencies and third-party servers; verify tool definitions do not change silently.
  11. Test in CI with an in-memory client and evaluation sets of real tasks; use MCP Inspector 0.14.1 or later for manual checks.
  12. Version tool schemas and change them like a public API.

Gateways help when several servers and teams are involved: central authentication, policies, logging to the SIEM and data loss prevention. Options include Cloudflare's MCP server portals (generally available since September 2026), Kong's AI MCP proxy, AWS Bedrock AgentCore Gateway and Azure API Management.

The European angle

  • Personal data leaves with every tool result. What a tool returns goes into the language model's context at the model provider. Minimise fields (GDPR Art. 5), have a data processing agreement with the provider, and check the transfer basis and where the model runs. EU-hosted model options exist through cloud providers; check the current regional offering before you rely on it.
  • Where the client runs matters. Claude, for example, calls remote MCP servers from Anthropic's cloud, so an internal server must be reachable from the internet or sit behind a gateway (Claude help). For many German companies this decides the architecture.
  • AI Act transparency has applied since 2 August 2026: users must know when they deal with an AI system. If your MCP server powers a customer-facing assistant, that assistant must say so.
  • NIS2. For the roughly 29,500 companies covered in Germany, an MCP server with access to core systems is part of the attack surface their risk management must cover.
  • There is no specific BSI guidance on MCP yet; the specification's security best practices and the OWASP list are the best references.

For the deadlines around these rules, see our compliance calendar for software.

How we help

SDH designs and builds MCP servers and integrations for ERP, CRM, databases and document stores, as part of our AI software development work: tool design, per-user authorization, audit and deployment in your cloud or ours. If you are deciding between MCP and a plain API integration, read MCP vs API: key differences and why MCP is becoming the standard for AI integration. Talk to us about connecting your systems to AI agents safely.

FAQ

What is the current MCP specification?

The current revision is 2026-07-28. It makes the protocol stateless, removes sessions and the initial handshake, deprecates the HTTP+SSE transport and dynamic client registration, and defines authorization on OAuth 2.1 with protected resource metadata and resource indicators.

How should an MCP server authenticate users?

For servers used by people with different rights, use per-user OAuth 2.1 as the specification describes: the server is an OAuth resource server, validates that each token was issued for it, and calls backends with the user's own rights. For a fixed automation, a service account with narrow, fixed rights is enough.

Should every API endpoint become an MCP tool?

No. Design tools around tasks and return only the fields the task needs. For very large APIs, combine a set of task tools with a discovery pattern or split the server by domain.

What are the main security risks of MCP servers?

Prompt injection through data the agent reads, tools with more rights than the task needs, token passthrough, missing tenant checks, command injection in tool arguments, and malicious or changed third-party servers. Most incidents so far combined access to private data, untrusted content and a way to send data out.

Is MCP compatible with the GDPR?

It can be. Tool results go into the model provider's context, so minimise personal data in tool outputs, have a data processing agreement with the model provider, check where the model runs and log access.

Sources

Categories

MCP AI Agents Security OAuth Integration Python

About the author

Pavlo Yablonskyi
Pavlo Yablonskyi
CTO & Co-Founder
View full profile

CTO & co-founder at Software Development Hub. Software engineer with 20+ years of experience. Python/Django-geek, software architect and IT team leader. Staying up-to-date with tech trends. Strong technical skills and diverse expertise in software structure design, development, team management and cybersecurity.

Share

Need a project estimate?

Drop us a line, and we provide you with a qualified consultation.

x

Partnership That Works for You

Your Trusted Agency for Digital Transformation and Custom Software Innovation.

Start typing to search...