Platform integration
Agent infrastructure
How to build an MCP server for a SaaS product: a 7-step guide covering scope, transport, tool design, OAuth, testing with MCP Inspector, and production pitfalls.

By the end of this guide you'll have a working MCP server that exposes part of your SaaS product to AI agents, authenticates per user, and survives basic testing with MCP Inspector. This is the step-by-step version — the architectural background lives in what is an MCP server.
Prerequisites: a SaaS product with an HTTP API, OAuth 2.0 or an equivalent auth mechanism, Node.js 20+ or Python 3.10+, and a scoped test tenant you can safely make writes against. Time required: two to four hours for a first pass covering three to five tools. Longer if your auth story isn't already in shape.
Don't wrap your whole API. That's the mistake most teams make on the first attempt, and it's what turns an agent-ready SaaS project into a two-quarter slog.
Pick one workflow an agent should be able to complete end-to-end. "Create a support ticket, attach a customer, and post an internal note" is a good scope. "Everything under /v2/tickets" is not.
Write the workflow down as three to seven verbs. Those verbs are your first draft of the tool list. If a verb needs more than one API call, that's fine — the tool hides the sequencing from the agent.
Use the official Model Context Protocol TypeScript SDK. Python has first-class support too. MCP was originated by Anthropic but is now a multi-vendor spec stewarded by the Model Context Protocol org.
There's no create-mcp-server initializer — you set up a plain Node project and install the SDK:
mkdir my-saas-mcp && cd my-saas-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
Write a minimal server with one example tool and a stdio transport. Get it running under MCP Inspector before you write a single tool of your own.
npx @modelcontextprotocol/inspector node ./build/index.js
If the example tool responds, your transport works. Everything after this is content.
MCP supports two transports: stdio (local process) and Streamable HTTP (remote). Streamable HTTP replaced the older HTTP+SSE transport in the March 2025 spec revision; it uses a single HTTP endpoint that handles POST and GET, and optionally upgrades to server-sent events for streaming responses. For a SaaS product exposing tools to hosted agents, Streamable HTTP is the answer. Stdio is for local subprocess integrations — CLI tools, IDE plugins, and local Claude Desktop extensions.
Streamable HTTP means your MCP server is a long-lived service you deploy. Same operational shape as any other API service you run. Same TLS, same load balancer, same observability. The architecture guide covers the trade-offs in more depth.
Switch the transport in your server entry point:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});
await server.connect(transport);
A tool is a name, a description, an input schema, and a handler. The description is the part agents actually read to decide whether to call the tool. Write it like a docstring for a colleague who has never seen your product.
server.tool(
"create_support_ticket",
"Create a support ticket in the authenticated user's workspace. Returns the ticket ID and URL. Use this when a user reports an issue that needs tracking.",
{
title: z.string().describe("Short summary, under 120 characters"),
description: z.string().describe("Full problem description in Markdown"),
priority: z.enum(["low", "medium", "high"]).default("medium"),
},
async ({ title, description, priority }, { authInfo }) => {
const ticket = await api.tickets.create(
{ title, description, priority },
{ token: authInfo.token }
);
return {
content: [{ type: "text", text: JSON.stringify(ticket) }],
};
}
);
One verb per tool. If the handler has an if statement branching on a mode parameter, split it into two tools. Agents pick tools better when the tools are narrower.
This is where most SaaS MCP projects fail security review. A single service account in the MCP server means every agent call executes as "the MCP server" — no user identity, no audit trail, no permission scoping. Don't ship that.
Use OAuth 2.1 with delegated access. The agent obtains a token on behalf of the end user, passes it to your MCP server, and your MCP server calls your API as that user. Permissions and audit trails work the way they already do in your product. The full setup is in OAuth for AI agents.
When you attach the SDK's requireBearerAuth middleware to your MCP endpoint, the validated token is exposed to tool handlers as extra.authInfo — the second argument to the handler — containing token, clientId, and scopes. Validate the token on every call. Don't cache the identity across sessions.
const authInfo = await validateToken(request.headers.authorization);
if (!authInfo) return unauthorized();
If you're weighing this against simpler options, agent authentication methods compared walks through when API keys are acceptable and when they're not. For a customer-facing MCP server, they're not.
MCP Inspector is the debugger. Launch it, connect to your running server, list the tools, call each one with valid and invalid inputs, and watch the JSON-RPC log.
The easiest path is the web UI — start Inspector and enter your server URL and bearer token in the sidebar:
npx @modelcontextprotocol/inspector
For scripted checks, use the CLI mode with a config file describing a streamable-http server:
{
"mcpServers": {
"my-saas": {
"type": "streamable-http",
"url": "https://mcp.yourdomain.com",
"headers": { "Authorization": "Bearer ${TOKEN}" }
}
}
}
npx @modelcontextprotocol/inspector --cli --config mcp.json --server my-saas --method tools/list
What to check:
content the agent can parse — usually a JSON string in a text block.The MCP Inspector walkthrough covers reading the log in detail. Do this step before you connect a real agent. Debugging with Inspector takes minutes; debugging inside an agent loop takes hours.
An MCP server is a production service. Treat it like one.
Log every tool call with: user ID, tool name, input hash, latency, outcome. Not the full input — some of it will be sensitive. The hash is enough to correlate repeat calls.
Add per-user and per-tool rate limits at the MCP layer, separate from whatever your API already enforces. Agents will retry aggressively on transient failures, and a hot loop can burn through a user's API quota in seconds. The rate limiting patterns for the agent era piece has the specifics.
Emit metrics you can alert on: tool call rate, error rate by tool, p95 latency by tool. When something breaks in production, the tool name is the first thing you'll want to filter by.
Wrapping the whole API. Every endpoint becomes a tool, the tool list balloons to 200 entries, and the agent picks the wrong one. Start with three to seven tools built around a workflow. Grow from evidence.
Service-account auth. Works in the demo, fails security review, leaves you with no audit trail when something goes wrong. Do delegated access from day one.
Descriptions that assume product knowledge. "Creates a ticket" tells the agent nothing about when to use the tool. Write descriptions that explain the intent, the inputs, and what the tool returns.
Hidden retries. If your handler retries on failure, the JSON-RPC log lies about what happened. Let the transport handle retries, or surface them explicitly.
No maintenance plan. Your product changes weekly. A hand-written MCP server drifts within a quarter. Either budget engineering time for maintenance or move to generation before the drift catches you — connector maintenance cost has the numbers.
Once three to five tools are running and being called in anger, the next question isn't "which tool do I write next." It's whether hand-writing is still the right approach at all. That's a decision worth making early, not after the fifteenth tool.
Stay up to date on the ever changing agentic landscape.