Build an MCP Server for Your SaaS: A Practical Symfony Implementation

#MCP server implementation
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Founder & Lead Developer

Expert in software development and legacy code optimization

The request usually arrives through sales, not engineering: a prospect asks whether your product "works with Claude" or "has an MCP server", and suddenly an MCP server implementation is on the roadmap with a quarter-end deadline attached. The good news is that for a Symfony-based SaaS, a solid implementation is a few weeks of focused work, not a replatform. The bad news is that most first attempts get the same three things wrong: they expose the REST API one-to-one, they bolt on API keys instead of proper authorization, and they skip evaluation entirely.

This post is the implementation guide we wish those teams had read first. If you still need the business case and the build-versus-wait framing, start with our earlier explainer on what Model Context Protocol means for SaaS founders. This one assumes the decision is made and gets into the Symfony specifics.

What an MCP Server Implementation Actually Includes

Model Context Protocol is a JSON-RPC 2.0 based protocol that lets AI clients (Claude, ChatGPT, agent frameworks, IDE assistants) discover and call capabilities your product exposes. A production MCP server for a SaaS has five parts:

  1. A transport endpoint. For a multi-tenant SaaS this is Streamable HTTP, the remote transport in the current spec. Stdio transport is for local tools and has no place in your production architecture.
  2. Tools. Named operations with JSON Schema input definitions that the model can call: create_invoice, search_tickets, summarize_account_activity.
  3. Resources and prompts (optional but valuable). Read-only data the client can pull into context, and reusable prompt templates for common workflows in your domain.
  4. Authorization. The MCP spec builds on OAuth 2.1 for remote servers. This is where tenant isolation lives or dies.
  5. Operational wrapping. Logging, rate limits, evals, and audit trails, because you are letting a non-deterministic caller execute actions inside customer accounts.

Everything below maps these parts onto a Symfony application.

Step 1: Choose the SDK and Wire the Transport

The PHP ecosystem matured fast here. There is an official PHP SDK for MCP, developed in collaboration with the PHP Foundation and the Symfony team, and Symfony ships MCP integration as part of its AI initiative. Check the current state of both before you start, because the spec still evolves and transport details have changed once already (the older HTTP+SSE transport was superseded by Streamable HTTP). Pin the protocol version you support and record it in your compliance docs.

In a Symfony app the endpoint is a controller route, not a separate service:

#[Route('/mcp', name: 'mcp_endpoint', methods: ['POST', 'GET', 'DELETE'])]
public function handle(Request $request, McpServer $server): Response
{
    // The SDK handles JSON-RPC parsing, session negotiation,
    // and capability discovery. You handle auth and wiring.
    return $server->process($request);
}

Keep it inside the main application rather than a sidecar service. Your tools need your Doctrine entities, your voters, and your business rules. A separate microservice that re-implements permission checks is how tenant data leaks happen.

Step 2: Design Tools Around Jobs, Not Endpoints

The strongest predictor of whether an MCP integration feels good in practice is tool design. The tempting shortcut is to generate one tool per REST endpoint. Resist it. A model facing 80 fine-grained tools with overlapping names picks wrong constantly, burns tokens on retries, and produces support tickets.

Design tools the way you would design a CLI for a competent but literal-minded operator:

  • Few and coarse. Five to fifteen tools that map to user jobs ("find overdue invoices for a customer") beat fifty CRUD wrappers. Each extra tool dilutes the model's accuracy on all the others.
  • Descriptions are prompts. The tool description is the only documentation the model reads. State what the tool does, when to use it, when NOT to use it, and what the parameters mean in domain terms.
  • Strict input schemas. Enums instead of free strings, explicit required fields, and tight formats. Every loose parameter is an invitation for the model to invent values. The same discipline that makes structured LLM outputs reliable applies on the receiving side.
  • Plain-language errors. Return "customer 4711 has no draft invoices; use search_invoices to list payable ones" rather than a stack trace or a bare 422. The model will read the error and self-correct; give it something to work with.

In Symfony, a tool is a small invokable service. Validate the input with the Validator component exactly as you would an API payload, then delegate to the same application service your controllers use. No business logic in the tool class itself.

Step 3: Authorization and Multi-Tenant Scoping

This is the step that separates a demo from a product, and the one we examine first when we audit an MCP implementation. The spec's authorization model is OAuth 2.1: your MCP server is a resource server, tokens arrive as Bearer tokens, and the client discovers your authorization server through protected resource metadata.

The rules that matter for a multi-tenant SaaS:

  • Tokens carry the tenant, the user, and a reduced scope. An MCP session should never have broader permissions than the user who connected it. Ideally it has fewer: read-heavy scopes by default, write scopes only where the use case demands them.
  • Resolve tenant context from the token on every call. Never from a tool parameter. If tenant_id is something the model can pass in, you have built a confused-deputy vulnerability with extra steps.
  • Run tool execution through your existing authorization layer. In Symfony that means the same security voters your controllers use. If a user cannot delete an invoice in the UI, the model acting on their behalf cannot either, and the denial should produce a readable error, not a silent failure.
  • Make destructive actions interactive. For anything irreversible, return a confirmation step or require an explicit confirm: true parameter that the client surfaces to the human. Agents are enthusiastic; your job is to make enthusiasm safe.

Log every tool call with the token subject, tenant, arguments, and outcome. Enterprise buyers will ask for this audit trail in the same security review where they asked about MCP in the first place.

Step 4: Operate It Like a Public API, Priced Like an LLM Feature

An MCP server is a public API with an unusual client, so the standard hardening applies: per-tenant rate limits, request size caps, timeouts on every downstream call. Two additions are MCP-specific.

First, expect bursty, redundant traffic. Agents retry, explore, and call the same search three different ways. Idempotency on write tools is not optional, and caching on read tools pays for itself quickly.

Second, watch the cost path. If any of your tools call an LLM internally (summarization, classification), an agent loop on the client side can multiply your inference spend invisibly. Put the same per-tenant budgets around tool-triggered inference that you put around your own AI features; our post on LLM cost control for SaaS covers the budget pattern in detail.

Testing: Unit Tests Are Not Enough

Test an MCP server at three levels:

  1. Protocol level. The SDK's test utilities or the MCP Inspector verify discovery, schema validity, and transport behavior. Cheap, fast, run in CI.
  2. Contract level. PHPUnit tests that call each tool with valid, invalid, and malicious inputs (cross-tenant IDs, oversized payloads, injection attempts in string fields) and assert on responses and side effects.
  3. Behavioral level. A small eval suite: 20 to 50 realistic prompts run against a real model connected to a staging server, scored on whether the right tools were called with the right arguments. This is the layer that catches bad tool descriptions, which no deterministic test can see. Rerun it whenever you change a description, because descriptions are prompts and prompt changes regress silently.

Common Mistakes We See in MCP Audits

  • Mirroring the REST API one-to-one, producing dozens of tools the model cannot choose between.
  • Accepting a static API key because OAuth "took too long", which makes per-user permissions and revocation impossible.
  • Trusting model-supplied identifiers for tenant or user context.
  • Returning raw exception messages, which leak schema details and confuse the model into dead-end retries.
  • Shipping without evals and discovering through support tickets that the model misuses a tool that has an ambiguous description.
  • No protocol version pinning, so a client update breaks the integration in production.

None of these are exotic. They are the same API design and security fundamentals that apply everywhere, stressed by a client that reads documentation literally and never gets tired. If your team is weighing where MCP fits in the broader product architecture, that is a tech stack strategy conversation worth having before the first sprint, not after.

Frequently Asked Questions

Do I need a separate service for the MCP server? Usually not. Inside your Symfony monolith you reuse entities, voters, and services directly. Split it out only if you have hard isolation or scaling requirements, and accept the duplicated authorization logic as a real cost.

Streamable HTTP or stdio? Streamable HTTP for anything multi-tenant or remote. Stdio is for local desktop integrations and internal developer tooling only.

How many tools should we expose? Start with three to five covering your highest-value workflows. Expand based on observed usage. It is far easier to add a tool than to retire one customers have wired into agent workflows.

How long does an MCP server implementation take? For a well-structured Symfony application with existing OAuth infrastructure, a first production version with three to five tools, scoping, and an eval suite is typically two to four weeks. Codebases without a clean service layer take longer, because the tools have nothing safe to call.

Is MCP stable enough to build on in 2026? The protocol has changed (notably the transport), but adoption across major AI clients makes it the de facto standard for this integration pattern. Pin the protocol version, isolate spec-touching code behind the SDK, and treat upgrades as scheduled work.

Ship It With Confidence

Wolf-Tech builds and audits AI-facing integrations for B2B SaaS teams, from custom software development to focused code quality and security reviews of existing MCP and LLM features. If you want a second pair of senior eyes on your tool design or your tenant isolation before enterprise customers connect their agents, write to hello@wolf-tech.io or find us at wolf-tech.io.