paso vs. Writing MCP Servers by Hand

On this page

There are two ways to create an MCP server. You can write TypeScript or Python using the MCP SDK. Or you can declare your API in YAML and let paso generate the server.

Both work. This post compares them honestly.

The manual approach

The MCP SDK gives you full control. You define tool schemas, write request handlers, manage transport, and handle errors yourself.

Here’s a minimal MCP server for a weather API with two tools:

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: 'weather',
  version: '1.0.0',
});

server.tool(
  'get_forecast',
  'Get weather forecast for a city',
  {
    city: z.string().describe('City name'),
    days: z.number().min(1).max(7).default(3).describe('Number of days'),
  },
  async ({ city, days }) => {
    const res = await fetch(
      `https://api.weather.example/v1/forecast?city=${encodeURIComponent(city)}&days=${days}`,
      { headers: { 'X-API-Key': process.env.WEATHER_API_KEY! } }
    );
    if (!res.ok) {
      return {
        content: [{ type: 'text', text: `Error: ${res.status} ${res.statusText}` }],
        isError: true,
      };
    }
    const data = await res.json();
    return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
  }
);

server.tool(
  'get_alerts',
  'Get active weather alerts for a region',
  {
    region: z.string().describe('Region code (e.g. US-CA)'),
  },
  async ({ region }) => {
    const res = await fetch(
      `https://api.weather.example/v1/alerts?region=${encodeURIComponent(region)}`,
      { headers: { 'X-API-Key': process.env.WEATHER_API_KEY! } }
    );
    if (!res.ok) {
      return {
        content: [{ type: 'text', text: `Error: ${res.status} ${res.statusText}` }],
        isError: true,
      };
    }
    const data = await res.json();
    return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

That’s ~50 lines for two read-only tools. No input validation beyond Zod types. No permission model. No consent gates. No rate limiting.

A production server with five tools, error handling, auth forwarding, and permission checks typically runs 200-400 lines.

The paso approach

The same two tools as a paso declaration:

version: "1.0"

service:
  name: Weather
  description: Weather forecasts and alerts
  base_url: https://api.weather.example/v1
  auth:
    type: api-key
    header: X-API-Key

capabilities:
  - name: get_forecast
    description: Get weather forecast for a city
    method: GET
    path: /forecast
    permission: read
    inputs:
      city:
        type: string
        required: true
        description: City name
        in: query
      days:
        type: integer
        default: 3
        description: Number of days (1-7)
        in: query
        constraints:
          max_value: 7

  - name: get_alerts
    description: Get active weather alerts for a region
    method: GET
    path: /alerts
    permission: read
    inputs:
      region:
        type: string
        required: true
        description: Region code (e.g. US-CA)
        in: query

Run usepaso serve. You have the same MCP server with automatic protocol compliance, auth forwarding, input validation, and error formatting.

The comparison

DimensionManual (MCP SDK)paso (YAML declaration)
Lines of code200-400 for 5 tools0 (YAML only)
Time to first server1-4 hours5-15 minutes
Auth handlingYou write itAutomatic from auth block
Input validationZod/Pydantic schemas in codeDeclared in YAML
Permission modelYou design and implement itBuilt-in (read/write/admin tiers)
Consent gatesYou implement UI hooksconsent_required: true
Rate limitingYou implement itmax_per_hour constraint
Error formattingYou format responsesAutomatic
Custom logicFull controlNot supported
Database queriesYesNo
Multi-step workflowsYesNo
Protocol updatesRewrite handlersUpdate paso
MaintenancePer-tool, per-APIOne YAML file

When to write by hand

Use the MCP SDK directly when your server needs:

  • Custom business logic. A tool that queries a database, runs computations, or calls multiple APIs in sequence before returning a result.
  • Stateful interactions. Tools that maintain session state across calls (multi-turn workflows, shopping carts, document editing).
  • Non-REST backends. GraphQL APIs, gRPC services, WebSocket connections, or local file system access.
  • Complex transformations. When the API response needs significant processing before it’s useful to an agent.

The MCP SDK is the right tool for these cases. paso doesn’t replace it.

When to use paso

Use paso when your MCP server maps to a REST API:

  • REST endpoint mapping. Each tool corresponds to one HTTP endpoint. Most SaaS APIs fit this pattern.
  • Multiple APIs. You want to expose 3-5 APIs to agents. Writing and maintaining 3-5 manual servers is a multiplication problem.
  • Rapid prototyping. You want to test whether agents can use your API before committing to a full implementation.
  • Team handoff. A YAML file is easier to review, modify, and hand off than a TypeScript codebase.
  • OpenAPI import. You already have an OpenAPI spec and want an MCP server without writing code.

The maintenance argument

The initial build time is one cost. Maintenance is the ongoing cost.

When the MCP SDK releases a breaking change, manual servers need handler updates. paso servers need a npm update usepaso. When you add an endpoint to your API, a manual server needs a new handler function with schema, auth, error handling, and tests. A paso server needs a new YAML block.

For one API, this is manageable. For three APIs with 30 capabilities total, the maintenance difference compounds.

They’re not mutually exclusive

You can use both. paso for the REST APIs that map cleanly to YAML declarations. Manual servers for the tools that need custom logic.

A team might run a paso server for their Stripe, Slack, and Notion integrations, and a hand-written server for their internal data pipeline that requires multi-step orchestration.

The MCP client doesn’t care how the server was built. It cares that the tools are well-described and the responses are correct.

Related:

Get started with paso.