MCP in practice: giving AI agents real access to your product
Model Context Protocol went from Anthropic side-project to industry standard in eighteen months. What building an MCP server for a real product taught us about tools, auth, and agent-friendly design.
In 2026, 'does it have an MCP server?' is a question prospects actually ask. Model Context Protocol โ the open standard for connecting AI assistants to external tools and data โ has become the USB port of the agent ecosystem: one server, and your product is usable from Claude, IDE agents, and every agentic browser that speaks the protocol. Here's what building one for a production SaaS actually involves.
The model: tools, resources, prompts
An MCP server exposes three things. Tools are functions the model can call (create_ticket, search_docs) โ each with a JSON Schema for inputs and a natural-language description that the model reads to decide when to call it. Resources are data the client can load as context (a config file, a dashboard's current state). Prompts are reusable templates the user can invoke. In practice, tools carry 90% of the value; start there.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({ name: 'appesto', version: '1.0.0' });
server.tool(
'search_docs',
'Search Appesto product documentation. Returns the most relevant sections. Use this before answering any question about how an Appesto product works.',
{ query: z.string().describe('Natural-language search query') },
async ({ query }) => {
const hits = await docsSearch(query, { limit: 5 });
return {
content: [{ type: 'text', text: formatHits(hits) }],
};
},
);Tool descriptions are prompt engineering
The single highest-leverage line of code in an MCP server is the tool description. The model chooses tools by reading them, the way a developer reads API docs โ except it reads them on every single turn. 'Search docs' produces a tool that gets ignored; 'Use this before answering any question about how an Appesto product works' produces a tool that gets called at the right moments. Write descriptions that say when to use the tool, not just what it does, and state what the tool returns so the model doesn't have to guess.
Design tools around intent, not your REST API
- Wrong: expose list_subscriptions, get_plan, get_usage as three tools and make the model orchestrate them - every extra round trip is latency and a chance to go off the rails.
- Right: expose get_billing_summary(user) that returns everything a billing question needs in one call.
- Return text the model can reason over (formatted summaries with IDs), not raw JSON dumps of your database rows.
- Cap list results aggressively and say so in the description - an agent that requests 10,000 rows will happily blow its own context window.
Auth: the part that's finally settled
Remote MCP servers standardized on OAuth 2.1 with PKCE, and the 2025-06 spec revision made resource indicators mandatory โ tokens are scoped to your server specifically, which killed the token-passthrough antipattern. Practical guidance: treat the MCP server as a public OAuth client, scope tokens down to what the exposed tools genuinely need, and log every tool invocation with its arguments. Agents make more calls than humans, and your audit trail is how you notice when one goes weird.
What we'd tell a team starting now
Ship a read-only server first: search, summaries, status lookups. Zero risk, immediate utility, and you learn how models actually use your tools before you hand them write access. When you do add writes, make destructive tools require explicit confirmation parameters and return a preview of what will change. And test with a real agent on real tasks โ the failure modes (wrong tool chosen, arguments hallucinated, results misread) only show up when a model, not a human, is driving.
Written by Appesto Engineering.