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).
A YAML spec
The spec kervan run 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}"Show the full file (51 lines, 2 tools)
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-onlyFinds up to 5 places by name and returns their coordinates.
get_current_weather· read-onlyCurrent temperature (°C), wind speed (km/h) and WMO weather code at a coordinate.
A call and its result tools/call
{
"arguments": {
"latitude": 39.92,
"longitude": 32.85
},
"name": "get_current_weather"
}{
"temperatureC": 21.1,
"weatherCode": 2,
"windKmh": 5.9
}Connect it to Claude Code stdio
claude mcp add open-meteo -- node /absolute/path/to/kervan/packages/cli/bin/kervan.js run /absolute/path/to/kervan/examples/spec/kervan.yamlNext: build an MCP server from a YAML file, step by step, or read the complete kervan.yaml reference.
TypeScript
The tool createApp, app.tool
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+
node examples/calculator/src/calculator.tssum: 5What it shows
- Input and output are Zod schemas; the client gets both as JSON Schema.
createTestClientconnects the official MCP client in memory, the way tests call tools.- A
ToolErrormessage 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.
selectrun 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.
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, orserve(app)in code.Stateless Streamable HTTP
No sessions: every request carries its own context.
HostandOriginare checked againstallowedHostsandallowedOrigins.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 open-meteo -- node /absolute/path/to/kervan/packages/cli/bin/kervan.js run /absolute/path/to/kervan/examples/spec/kervan.yamlMore: 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.
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 devThen 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
selectsees the response.Rate limits
Each spec tool has a per-minute and a concurrency limit;
serveHttplimits requests per minute.Sandboxed select
selectexpressions 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
allowedHostsandallowedOrigins(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).
Documentation
Framework documentation
Concepts, YAML and TypeScript tools, transports, the CLI, deployment and the security model.
kervan.yaml reference
Every field of the spec, generated from its JSON Schema.
Guides
From what MCP is to exposing a REST API as tools and connecting Claude Code.
Studio documentation
For the optional web UI: install, users and roles, the secret vault, publishing and backups.
