DEV Community

Shiva Padakanti
Shiva Padakanti

Posted on • Originally published at usepaso.dev

The Complete Guide to MCP Servers

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:

  1. Exposes tools. Each tool has a name, description, and typed inputs. The agent reads these to decide what to call.
  2. Handles requests. When the agent calls a tool, the server validates inputs, constructs the HTTP request, and forwards it to your API.
  3. 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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then:

usepaso serve
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Python:

pip install usepaso
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
usepaso serving "Sentry" (6 capabilities). Agents welcome.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
valid (Sentry, 6 capabilities, 0 regrets)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Flags best-practice issues: DELETE without consent gates, short descriptions, write operations without constraints.

Inspect

usepaso inspect
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Previews the exact HTTP request without sending it. Verify URL construction, parameter placement, and auth headers.

Doctor

usepaso doctor
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Consent gates

Force the agent to ask the user before executing sensitive operations.

- name: delete_issue
  permission: admin
  consent_required: true
Enter fullscreen mode Exit fullscreen mode

Constraints

Rate limits and guardrails.

constraints:
  - max_per_hour: 100
    description: Deletion is rate-limited
Enter fullscreen mode Exit fullscreen mode

Forbidden list

Explicitly block capabilities from being exposed to agents.

permissions:
  read:
    - list_issues
  write:
    - resolve_issue
  forbidden:
    - drop_database
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

paso writes .cursor/mcp.json in your project root. Restart Cursor to connect.

VS Code

usepaso connect vscode
Enter fullscreen mode Exit fullscreen mode

paso writes .vscode/mcp.json in your project root. Reload VS Code to connect.

Windsurf

usepaso connect windsurf
Enter fullscreen mode Exit fullscreen mode

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

  1. Add pagination. Don't let agents fetch unbounded lists.
  2. Use constraints. Rate limits prevent agents from overwhelming your API.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

All guides in this series:

Get started now.

Top comments (0)