DEV Community

Shiva Padakanti
Shiva Padakanti

Posted on • Originally published at usepaso.dev

How to Create an MCP Server

An MCP server exposes your API as tools that AI agents can call. The Model Context Protocol defines the transport and schema. You provide the tools.

There are two ways to build one.

The manual way

Install the SDK and write TypeScript:

npm install @modelcontextprotocol/sdk
Enter fullscreen mode Exit fullscreen mode
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 for a project',
  {
    organization_slug: z.string(),
    project_slug: z.string(),
  },
  async ({ organization_slug, project_slug }) => {
    const res = await fetch(
      `https://sentry.io/api/0/projects/${organization_slug}/${project_slug}/issues/`,
      { headers: { Authorization: `Bearer ${process.env.SENTRY_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 six tools with input validation, error handling, auth forwarding, and permission checks, you're looking at a few hundred lines. For each API you want to expose.

The paso way

Write YAML. Run one command.

npm install -g usepaso
usepaso init --name "Sentry"
Enter fullscreen mode Exit fullscreen mode

This creates a usepaso.yaml file. Fill in your capabilities:

version: "1.0"

service:
  name: Sentry
  description: Error monitoring for software teams
  base_url: https://sentry.io/api/0
  auth:
    type: bearer

capabilities:
  - name: list_issues
    description: List issues for a project
    method: GET
    path: /projects/{organization_slug}/{project_slug}/issues/
    permission: read
    inputs:
      organization_slug:
        type: string
        required: true
        description: The organization slug
        in: path
      project_slug:
        type: string
        required: true
        description: The project slug
        in: path
Enter fullscreen mode Exit fullscreen mode

Validate it:

usepaso validate
Enter fullscreen mode Exit fullscreen mode
valid (Sentry, 1 capability, 0 regrets)
Enter fullscreen mode Exit fullscreen mode

Start the server:

USEPASO_AUTH_TOKEN=your-sentry-token usepaso serve
Enter fullscreen mode Exit fullscreen mode
usepaso serving "Sentry" (1 capability). Agents welcome.
Enter fullscreen mode Exit fullscreen mode

That's a production MCP server. paso handles protocol compliance, request routing, auth forwarding, input validation, and error formatting.

What you don't write

No TypeScript request handlers. No Zod schema definitions. No transport setup. No auth forwarding logic. No error formatting.

Each new capability is a few lines of YAML:

  - name: resolve_issue
    description: Resolve an issue
    method: PUT
    path: /issues/{issue_id}/
    permission: write
    consent_required: true
    inputs:
      issue_id:
        type: string
        required: true
        description: The issue ID
        in: path
      status:
        type: enum
        required: true
        values: [resolved, unresolved, ignored]
        description: New status for the issue
Enter fullscreen mode Exit fullscreen mode

Six capabilities. One YAML file. No TypeScript to maintain.

Side-by-side

Manual MCP server paso
Language TypeScript or Python YAML
Lines of code 100-500+ per API 0
Schema definitions Zod / Pydantic in code Declared in YAML
Auth handling You write it Automatic
Input validation You write it Automatic
New endpoint New handler function New YAML block
Protocol changes Rewrite handlers Update paso
Custom logic Full control Not supported

Already have an OpenAPI spec?

Generate the declaration from it:

usepaso init --from-openapi ./openapi.json --name "Sentry"
Enter fullscreen mode Exit fullscreen mode

paso reads the spec, generates the YAML, and you edit it to curate which endpoints agents can access.

Connect to Claude Desktop

usepaso connect claude-desktop
Enter fullscreen mode Exit fullscreen mode

paso writes the config for you. Restart Claude Desktop. Your API is now agent-ready.

Verify before you ship

paso gives you five checks:

usepaso validate --strict    # Structure + best practices
usepaso inspect              # Review what agents will see
usepaso test --all --dry-run # Verify all requests build correctly
usepaso doctor               # End-to-end setup check
usepaso serve                # Ship it
Enter fullscreen mode Exit fullscreen mode

The tradeoff

The manual approach gives you full control over every handler. You can run database queries, call multiple APIs in sequence, or apply complex business logic before returning a result. paso gives you an MCP server in minutes with no protocol code. If your API follows REST conventions and you want agents to call it, paso is faster. If your MCP server needs custom orchestration logic, write it by hand.

Questions

Does paso work with Cursor and other MCP clients?
Yes. paso generates a standard MCP server. Any MCP client that supports the Model Context Protocol can connect to it: Claude Desktop, Cursor, Windsurf, and others.

Can I use paso with Python?
Yes. pip install usepaso. Same CLI, same YAML format, same output. Read more about Python support.

What if my API needs custom logic per request?
paso handles REST endpoint mapping. If a capability requires database queries, multi-step orchestration, or custom transformations, write that handler manually using the MCP SDK.

Related:

Top comments (1)

Collapse
 
sidclaw profile image
SidClaw

nice writeup. the consent_required: true field in the resolve_issue example caught my eye -- it's declaring that an action needs approval before executing. but the MCP protocol itself doesn't have a standard for actually enforcing that. there's no spec for "hold this tool call, route it to a reviewer, wait for a decision."

so right now consent_required is a hint to the client, not enforcement at the server level. the client can ignore it. curious whether paso has plans to handle the enforcement side -- actually pausing execution until someone approves.