MCP (Model Context Protocol) is how AI agents connect to your API. An MCP server exposes your API's capabilities as tools that agents like Claude, Cursor, and Copilot can discover and call at runtime.
This guide covers everything: what MCP servers are, how to build one, how to test it, how to secure it, and how to deploy it. Whether you're writing one from scratch or generating one from a YAML file, this is the reference.
What is an MCP server?
An MCP server is a process that speaks the Model Context Protocol. It sits between your API and an AI agent, translating your endpoints into typed tools the agent can understand and call.
The protocol was created by Anthropic in late 2024 and donated to the Linux Foundation's Agentic AI Foundation in December 2025. It's an open standard. Over 5,000 MCP servers exist as of early 2026.
An MCP server does three things:
- Exposes tools. Each tool has a name, description, and typed inputs. The agent reads these to decide what to call.
- Handles requests. When the agent calls a tool, the server validates inputs, constructs the HTTP request, and forwards it to your API.
- Returns responses. The API response goes back through the MCP protocol to the agent.
The key difference between MCP and a regular REST API: MCP clients discover available actions at runtime. A developer hardcodes API calls. An agent discovers them dynamically, decides which to use based on context, and constructs the inputs itself.
For a deeper comparison with REST and GraphQL, see MCP vs REST vs GraphQL: What Changes When AI is the Client.
Two ways to build an MCP server
Option A: Write it by hand
The MCP TypeScript SDK gives you full control. You register tools, define input schemas, and write handler functions.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "Sentry",
version: "1.0.0",
});
server.tool(
"list_issues",
"List issues in a Sentry project",
{
organization_slug: z.string(),
project_slug: z.string(),
query: z.string().optional(),
},
async ({ organization_slug, project_slug, query }) => {
const url = new URL(
`https://sentry.io/api/0/projects/${organization_slug}/${project_slug}/issues/`
);
if (query) url.searchParams.set("query", query);
const res = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${process.env.SENTRY_AUTH_TOKEN}`,
},
});
const data = await res.json();
return { content: [{ type: "text", text: JSON.stringify(data) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
That's one tool. For a real API with 6 capabilities, you're looking at 200+ lines. Each tool needs input validation, URL construction, auth forwarding, and error handling. It works. It's just a lot of repetitive code.
Option B: Declare it with paso
paso takes a different approach. You describe your API in a YAML file and paso generates the MCP server.
version: "1.0"
service:
name: Sentry
description: Error monitoring and performance tracking
base_url: https://sentry.io/api/0
auth:
type: bearer
capabilities:
- name: list_issues
description: List issues in a project
method: GET
path: /projects/{organization_slug}/{project_slug}/issues/
permission: read
inputs:
organization_slug:
type: string
required: true
description: Organization slug
in: path
project_slug:
type: string
required: true
description: Project slug
in: path
query:
type: string
description: "Search query (e.g., 'is:unresolved')"
in: query
Then:
usepaso serve
Same MCP server. Same protocol compliance. 30 lines instead of 200+. The YAML file is called a paso declaration. See What is a paso Declaration? for a field-by-field breakdown.
For a detailed side-by-side comparison, see How to Create an MCP Server and paso vs Writing MCP Servers by Hand.
Getting started
Install
Node.js:
npm install -g usepaso
Python:
pip install usepaso
Both produce the same MCP server from the same YAML file. See paso Works the Same in Python for the Python walkthrough.
Create a declaration
usepaso init --name "Sentry"
This generates a usepaso.yaml template. Edit it to describe your API's capabilities.
If you have an OpenAPI spec, skip writing YAML by hand:
usepaso init --from-openapi ./openapi.json
paso converts OpenAPI 3.x specs into declarations. See OpenAPI to MCP in 60 Seconds for the walkthrough.
Serve
export USEPASO_AUTH_TOKEN="your-api-token"
usepaso serve
usepaso serving "Sentry" (6 capabilities). Agents welcome.
Your API is now accessible to MCP clients.
Testing before you ship
Never connect an untested MCP server to an agent. paso gives you five ways to verify your declaration before going live.
Validate
usepaso validate
valid (Sentry, 6 capabilities, 0 regrets)
Catches structural issues: missing fields, invalid URLs, path parameter mismatches, duplicate names. See Common MCP Server Errors for the seven errors you're most likely to hit and how to fix each one.
Strict mode
usepaso validate --strict
Flags best-practice issues: DELETE without consent gates, short descriptions, write operations without constraints.
Inspect
usepaso inspect
Shows exactly what MCP tools your declaration produces. This is what agents see.
Dry run
usepaso test list_issues \
-p organization_slug=my-org \
-p project_slug=my-project \
--dry-run
Previews the exact HTTP request without sending it. Verify URL construction, parameter placement, and auth headers.
Doctor
usepaso doctor
End-to-end check: file exists, YAML parses, validation passes, auth token is set, base URL is reachable. If doctor passes, you're ready to serve.
For the full testing workflow, see Five Ways to Test Before You Ship.
Permissions and safety
An MCP server without permissions is an API with no access controls. Any agent can call any tool. That includes DELETE.
paso provides four layers of safety. See What Happens When an Agent Calls DELETE for why each one matters.
Permission tiers
Every capability has a permission field: read, write, or admin.
- name: list_issues
permission: read # Safe. No data changes.
- name: resolve_issue
permission: write # Modifies data. Requires caution.
- name: delete_issue
permission: admin # High risk. Always requires consent.
Consent gates
Force the agent to ask the user before executing sensitive operations.
- name: delete_issue
permission: admin
consent_required: true
Constraints
Rate limits and guardrails.
constraints:
- max_per_hour: 100
description: Deletion is rate-limited
Forbidden list
Explicitly block capabilities from being exposed to agents.
permissions:
read:
- list_issues
write:
- resolve_issue
forbidden:
- drop_database
A capability in forbidden is never registered as an MCP tool. It doesn't exist as far as the agent is concerned.
Connecting to clients
Claude Desktop
usepaso connect claude-desktop
paso writes the config file for you. Restart Claude Desktop. Your capabilities appear as tools.
For the full walkthrough, see Connect Stripe to Claude Desktop in 5 Minutes. The same pattern works for any API.
Cursor
usepaso connect cursor
paso writes .cursor/mcp.json in your project root. Restart Cursor to connect.
VS Code
usepaso connect vscode
paso writes .vscode/mcp.json in your project root. Reload VS Code to connect.
Windsurf
usepaso connect windsurf
paso writes the Windsurf config file. Restart Windsurf to connect.
Other MCP clients
Any MCP-compatible client can connect. The server speaks standard MCP over stdio. No client-specific code needed.
Performance
paso adds minimal overhead on top of the MCP protocol.
| Metric | Value |
|---|---|
| Cold start | ~0.9s (including Node.js startup) |
| paso overhead at startup | ~50ms (YAML parsing + tool registration) |
| Per-request overhead | 2-5ms |
| Dominant latency | Your API's response time |
The cold start is mostly Node.js module loading. The per-request cost is input validation and HTTP request construction. single-digit milliseconds.
For detailed benchmarks and optimization tips, see MCP Server Performance: What to Expect.
Optimization tips
- Add pagination. Don't let agents fetch unbounded lists.
- Use constraints. Rate limits prevent agents from overwhelming your API.
- Keep declarations focused. 6 well-chosen capabilities serve agents better than 50.
How paso compares to hand-written servers
| Hand-written | paso | |
|---|---|---|
| Code to write | 200+ lines TypeScript | 30 lines YAML |
| Adding a tool | New function + registration + validation | 8 lines of YAML |
| Protocol compliance | Manual | Automatic |
| Auth forwarding | Manual | Automatic |
| Input validation | Manual (Zod schemas) | Automatic (from declaration) |
| Permission model | Build it yourself | Built-in tiers + consent + constraints |
| Multiple protocols | Rewrite per protocol | Declaration stays the same |
paso handles MCP today. When A2A or the next protocol arrives, your declaration doesn't change. The protocol layer is paso's problem, not yours.
Common patterns
One API, one declaration
Each usepaso.yaml describes one API. If you have Sentry, Stripe, and GitHub, that's three declarations, three servers.
Start with read, add write later
Expose read-only capabilities first. Verify they work. Then add write operations with consent gates.
Use OpenAPI as a starting point
If you have an OpenAPI spec, generate the declaration instead of writing it:
usepaso init --from-openapi ./openapi.json
Review the output, set appropriate permissions, remove capabilities you don't want exposed, then validate and serve.
CI validation
Add validation to your CI pipeline:
usepaso validate --strict --json
Returns structured output with a non-zero exit code on failure. Treat it like a linter.
What's next
MCP is one year old. The ecosystem is growing fast. Over 5,000 servers exist. The protocol is moving from local development tools to production infrastructure with Streamable HTTP transport for remote servers.
paso's bet: the protocol layer should be abstracted. You describe what your API can do. paso handles how it's exposed. When the protocol changes, your declaration stays the same.
Your API is one YAML file away from being agent-ready.
npx usepaso init --name "YourAPI"
All guides in this series:
- What is a paso Declaration?. field-by-field YAML breakdown
- How to Create an MCP Server. manual vs. paso, side by side
- paso vs Writing MCP Servers by Hand. line-by-line code comparison
- MCP vs REST vs GraphQL. what changes when AI is the client
- Common MCP Server Errors. seven errors and how to fix them
- Five Ways to Test Before You Ship. the complete testing workflow
- MCP Server Performance. benchmarks and optimization
- What Happens When an Agent Calls DELETE. permissions and safety
- Connect Stripe to Claude Desktop. end-to-end API integration
- OpenAPI to MCP in 60 Seconds. import from existing specs
- How to Make Your API Work with Claude. connecting to Claude Desktop
- paso Works the Same in Python. Python support
- Why We Built paso. the motivation
Top comments (0)