Open source · MIT · pre-release

MCP servers from a YAML spec, or from TypeScript

Kervan is a framework for Model Context Protocol servers. Describe HTTP tools in a kervan.yaml file and serve them with one command, or write tools in TypeScript on the official MCP SDK. Both kinds of tool get validated arguments, masked errors and a timeout on every call; spec tools add SSRF protection, secret redaction and rate limits.

Kervan is not on npm yet: the quickstart builds it from source. Version 0.1 is in preparation.

One server, two ways to write tools

Both examples are files in the repository, shown unchanged, and what they print or answer comes from running them (the spec's tool calls reached the public Open-Meteo APIs on October 8, 2026).

Show the example written as

A YAML spec

The spec kervan run kervan.yaml

examples/spec/kervan.yaml, the first tool
specVersion: 1
name: open-meteo
version: 0.1.0
description: Weather and city lookup backed by the public Open-Meteo APIs (no API key needed).

defaults:
  http: { timeoutMs: 8000, maxResponseBytes: 262144 }
  rateLimit: { perMinute: 30, concurrency: 5 }

tools:
  - name: search_city
    title: Search city
    description: Finds up to 5 places by name and returns their coordinates.
    annotations: { readOnlyHint: true }
    input:
      type: object
      properties:
        name: { type: string, minLength: 2, maxLength: 100, description: "City name, e.g. Ankara" }
      required: [name]
    http:
      url: https://geocoding-api.open-meteo.com/v1/search
      query: { name: "{{input.name}}", count: 5, language: en, format: json }
    output:
      # Only the fields the model needs; never the whole API response.
      select: "results[].{name: name, country: country, latitude: latitude, longitude: longitude}"
Show the full file (51 lines, 2 tools)
examples/spec/kervan.yaml
specVersion: 1
name: open-meteo
version: 0.1.0
description: Weather and city lookup backed by the public Open-Meteo APIs (no API key needed).

defaults:
  http: { timeoutMs: 8000, maxResponseBytes: 262144 }
  rateLimit: { perMinute: 30, concurrency: 5 }

tools:
  - name: search_city
    title: Search city
    description: Finds up to 5 places by name and returns their coordinates.
    annotations: { readOnlyHint: true }
    input:
      type: object
      properties:
        name: { type: string, minLength: 2, maxLength: 100, description: "City name, e.g. Ankara" }
      required: [name]
    http:
      url: https://geocoding-api.open-meteo.com/v1/search
      query: { name: "{{input.name}}", count: 5, language: en, format: json }
    output:
      # Only the fields the model needs; never the whole API response.
      select: "results[].{name: name, country: country, latitude: latitude, longitude: longitude}"

  - name: get_current_weather
    title: Current weather
    description: Current temperature (°C), wind speed (km/h) and WMO weather code at a coordinate.
    annotations: { readOnlyHint: true }
    input:
      type: object
      properties:
        latitude: { type: number, minimum: -90, maximum: 90 }
        longitude: { type: number, minimum: -180, maximum: 180 }
      required: [latitude, longitude]
    http:
      url: https://api.open-meteo.com/v1/forecast
      query:
        latitude: "{{input.latitude}}"
        longitude: "{{input.longitude}}"
        current: temperature_2m,wind_speed_10m,weather_code
    output:
      select: "{temperatureC: current.temperature_2m, windKmh: current.wind_speed_10m, weatherCode: current.weather_code}"
      schema:
        type: object
        properties:
          temperatureC: { type: number }
          windKmh: { type: number }
          weatherCode: { type: integer }
        required: [temperatureC, windKmh, weatherCode]

The tools clients see tools/list

  • search_city · read-only

    Finds up to 5 places by name and returns their coordinates.

  • get_current_weather · read-only

    Current temperature (°C), wind speed (km/h) and WMO weather code at a coordinate.

A call and its result tools/call

request
{
  "arguments": {
    "latitude": 39.92,
    "longitude": 32.85
  },
  "name": "get_current_weather"
}
structuredContent
{
  "temperatureC": 21.1,
  "weatherCode": 2,
  "windKmh": 5.9
}

Connect it to Claude Code stdio

claude mcp add
claude mcp add open-meteo -- node /absolute/path/to/kervan/packages/cli/bin/kervan.js run /absolute/path/to/kervan/examples/spec/kervan.yaml

Next: build an MCP server from a YAML file, step by step, or read the complete kervan.yaml reference.

TypeScript

The tool createApp, app.tool

examples/calculator/src/calculator.ts
import { createApp, ToolError, z } from "@kervan/core"
import { createTestClient } from "@kervan/transport/testing"

const app = createApp({ name: "calculator", version: "0.1.0" })

app.tool("add", {
  description: "Adds two numbers.",
  input: z.object({ a: z.number(), b: z.number() }),
  output: z.object({ sum: z.number() }),
  annotations: { readOnlyHint: true },
  handler: ({ a, b }) => {
    if (!Number.isFinite(a + b)) throw new ToolError("The sum is too large.")
    return { sum: a + b }
  },
})

const client = await createTestClient(app)
const result = await client.callTool({ name: "add", arguments: { a: 2, b: 3 } })
console.log(`sum: ${(result.structuredContent as { sum: number }).sum}`)
await client.close()

Run it Node.js 22.23.3+ or 24.21.0+

from a clone of the repository
node examples/calculator/src/calculator.ts
output
sum: 5

What it shows

  • Input and output are Zod schemas; the client gets both as JSON Schema.
  • createTestClient connects the official MCP client in memory, the way tests call tools.
  • A ToolError message reaches the model; any other error would be masked.
  • To serve the tool instead, await serve(app) from @kervan/transport/node.

Next: tools in TypeScript, and YAML or TypeScript: the same tool written both ways.

What every tool gets

A spec tool and a TypeScript tool run on the same core. Spec tools, which call HTTP APIs for you, get more.

Every tool

  • Arguments validated against the input schema before any of your code runs.
  • Errors masked: only a ToolError's message reaches the client; anything else is logged with a reference.
  • A timeout on every call (30 seconds unless you set another), and the client's cancellation.
  • Results checked against the output schema, when there is one.

Spec tools also

  • SSRF protection: public addresses only, DNS resolved once and the connection pinned to the checked address.
  • Secret values redacted from results, errors and logs.
  • A rate limit per tool: 60 calls a minute and 10 at once, unless the spec sets others.
  • select run in a separate process without network or environment.

The details: what every tool gets, and what spec tools add.

How it fits together

Three libraries, a command line and a project generator. @kervan/core knows nothing about YAML or transports, so both kinds of tool are the same once registered.

How Kervan's packages fit togetherTools come from a kervan.yaml spec or from TypeScript. @kervan/spec-runtime turns a spec into ordinary tool definitions; @kervan/core holds every tool and validates, times and masks each call; @kervan/transport serves the app over stdio, Streamable HTTP or a fetch handler to MCP clients such as Claude Code. The kervan command line loads specs and code and serves them; create-kervan runs kervan create.kervan.yamlHTTP tools, no codeTypeScriptapp.tool() with Zod@kervan/spec-runtimespec → tool definitions@kervan/corevalidation · timeoutsmasked errors · middlewareregistry · list_changed@kervan/transportstdio · HTTP · fetchMCP clientsClaude Code, any clientkervan (CLI): create · dev · runloads specs and code and serves themcreate-kervannpm create kervan

Read how Kervan works: the packages and the path of a tool call.

Run it anywhere, connect any client

  • stdio

    The client starts your server as a process; nothing listens on the network. kervan run kervan.yaml, or serve(app) in code.

  • Stateless Streamable HTTP

    No sessions: every request carries its own context. Host and Origin are checked against allowedHosts and allowedOrigins.

  • A fetch handler

    The same app as a standard Fetch API handler: toFetchHandler(app). Tested on Node.

  • MCP 2026-07-28, and 2025 clients

    The stateless revision with server/discover, and clients of the 2025 revisions from the same app. Over HTTP, 2025 clients get no change notifications (why).

Any MCP client can connect. With Claude Code, the example spec over stdio:

claude mcp add
claude mcp add open-meteo -- node /absolute/path/to/kervan/packages/cli/bin/kervan.js run /absolute/path/to/kervan/examples/spec/kervan.yaml

More: transports and authentication, and connecting a server to Claude Code.

Get started

Kervan is not on npm yet, so this builds it from a clone of the repository: Node.js 22.23.3 or a later 22.x, or 24.21.0 or later, and pnpm (through corepack enable). pnpm try:new creates a project from the local packages and runs its tests; once the packages are published, npm create kervan replaces it.

sh
git clone https://github.com/aligoren/getkervan.git kervan
cd kervan
corepack enable
pnpm install
pnpm try:new ../my-server
cd ../my-server
npm run dev

Then the framework quickstart: run the example spec and call its tools.

Security by design

Each of these is implemented and covered by tests. The checks were also mutation-tested: broken on purpose, one at a time, to see a test fail (how).

  • SSRF protection

    Spec tools reach public addresses only. DNS is resolved once and the connection pinned to the checked address; redirects are refused unless allowed, then checked again; cloud metadata addresses are always refused.

  • Secret redaction

    Secret values are removed from results, errors and logs, in every encoding Kervan knows (raw, URL, form, JSON), before select sees the response.

  • Rate limits

    Each spec tool has a per-minute and a concurrency limit; serveHttp limits requests per minute.

  • Sandboxed select

    select expressions run in a separate process: empty environment, memory limit, the tool's timeout, no network, no file writes, no child processes.

  • Host and Origin checks

    HTTP servers accept only allowedHosts and allowedOrigins (localhost by default) and refuse to listen beyond loopback without them.

  • Limits by default

    Timeouts, response size limits and input size limits are on without configuration.

Studio (optional) adds users and roles, an encrypted secret vault, an audit log the database refuses to change, and CSRF and origin checks: see the Studio threat model.

The details: the framework security model. Report vulnerabilities privately to security@getkervan.dev (security.txt).

Kervan Studio, optional

An optional self-hosted web UI to manage servers, secrets, users and logs. Nothing is locked in: every server exports as kervan.yaml and runs with kervan run. The framework works without it.

See Studio

Kervan Studio's editor with the weather spec open: the kervan.yaml on the left and the playground on the right, the server published as version 2.