DEV Community

airmcp-dev
airmcp-dev

Posted on

A simpler way to build MCP servers — would love feedback

I've been building MCP servers for a few months now. Every time, same pattern — write a Zod schema, wrap the result in
{ content: [{ type: 'text', text: JSON.stringify(result) }] },
add try/catch, set up the transport. Repeat for every tool.

It's not hard, but it adds up.
I counted once: about 25 lines per tool with the official SDK — and once you add retry, caching, or auth, easily 60-70 lines. Most of that isn't actual logic. So I built a framework on top of it. It's called air and it's open source (Apache-2.0).

Before and After
With the MCP SDK:

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: 'my-server', version: '0.1.0' });

server.tool(
  'search',
  'Search documents',
  { query: z.string(), limit: z.number().optional() },
  async ({ query, limit }) => {
    try {
      const results = await doSearch(query, limit);
      return {
        content: [{ type: 'text', text: JSON.stringify(results) }],
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Error: ${error.message}` }],
        isError: true,
      };
    }
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
Enter fullscreen mode Exit fullscreen mode

With air:

import { defineServer, defineTool, retryPlugin, cachePlugin } from '@airmcp-dev/core';

const server = defineServer({
  name: 'my-server',
  transport: { type: 'sse', port: 3510 },
  use: [
    retryPlugin({ maxRetries: 3 }),
    cachePlugin({ ttlMs: 60_000 }),
  ],
  tools: [
    defineTool('search', {
      description: 'Search documents',
      params: { query: 'string', limit: 'number?' },
      handler: async ({ query, limit }) => doSearch(query, limit),
    }),
  ],
});

server.start();

Enter fullscreen mode Exit fullscreen mode

The second one includes retry and caching. The first one doesn't.

What it does differently

No Zod boilerplate.
Write 'string' instead of z.string().
Optional params? 'number?'. air generates the Zod and JSON schemas internally.

No content wrapping.
Just return whatever you want — string, number, object, array.
air converts it to the MCP content format.

Plugins instead of manual middleware.
There are 19 built-in plugins.

You add them to the use array and they run in order:

use: [
  authPlugin({ type: 'api-key', keys: [process.env.MCP_API_KEY!] }),
  sanitizerPlugin(),
  timeoutPlugin(10_000),
  retryPlugin({ maxRetries: 3 }),
  cachePlugin({ ttlMs: 60_000 }),
]
Enter fullscreen mode Exit fullscreen mode

No need to implement retry logic, caching, auth, or rate limiting yourself.

Built-in storage.

const store = await createStorage({ type: 'file', path: '.air/data' });
await store.set('users', 'u1', { name: 'Alice' }, 3600);
await store.append('logs', { action: 'login' });
await store.query('logs', { limit: 50, filter: { action: 'login' } });
Enter fullscreen mode Exit fullscreen mode

MemoryStore and FileStore included. TTL support.
Append-only logs with query and filter.

I tested it with a local LLM
Connected air to llama3.1 via Ollama.
Set up 4 tools — system info, notes, calculator, metrics.
Asked natural language questions.

Results:
"What's the CPU and memory?" → called system_info
"Create a note called Meeting Notes" → called note_create with correct
params
"What's 1234 times 5678?" → called calc, got 7,006,652
"Show my saved notes" → called note_list
"What is MCP?" → no tool call, answered from its own knowledge

All calls went through the plugin pipeline
(sanitizer → timeout → retry → cache) automatically.

What's included

19 plugins: timeout, retry, circuit breaker, fallback, cache, dedup, queue, auth, sanitizer, validator, cors, webhook, transform, i18n, json logger, per-user rate limit, dryrun

3 transports: stdio, SSE, Streamable HTTP

Storage: MemoryStore, FileStore

7-Layer Meter: classify calls from L1 (cache hit) to L7 (multi-step agent chain) for cost tracking

CLI: project scaffolding, dev mode with hot reload, client registration for Claude Desktop / Cursor / VS Code

Gateway: multi-server proxy with load balancing and health checks

Status

5 packages on npm, all Apache-2.0
165 tests passing
0 security vulnerabilities
(checked all 93 dependencies)
110-page docs in English and Korean

Try it

npm install @airmcp-dev/core
Enter fullscreen mode Exit fullscreen mode
import { defineServer, defineTool } from '@airmcp-dev/core';

const server = defineServer({
  name: 'hello',
  tools: [
    defineTool('greet', {
      params: { name: 'string' },
      handler: async ({ name }) => `Hello, ${name}!`,
    }),
  ],
});

server.start();

Enter fullscreen mode Exit fullscreen mode

GitHub ·
Docs ·
npm

This is my first time sharing something I built.
I'd genuinely appreciate any feedback — what looks useful,
what's missing, what you'd do differently. Thanks for reading.

Top comments (0)