mcp-gen
by zodromon
README.md
# mcp-gen
> Turn your typed TypeScript functions into an MCP server. No schema library, no decorators, no boilerplate — the schema is inferred from your types.

`mcp-gen` reads exported TypeScript functions and generates [Model Context Protocol](https://modelcontextprotocol.io) **tool, resource, and prompt** definitions from them — using the TypeScript type checker (via [ts-morph](https://ts-morph.com)) to turn each parameter type into a JSON Schema and each JSDoc comment into a description. It can emit the schemas as JSON, serve a live MCP server, or open a **live playground** where you call your tools in a browser while you edit.
If your functions are typed, they're already MCP tools.
---
## Quick start
Write plain, typed functions with ordinary JSDoc:
```ts
// tools.ts
/**
* Greets a person by name and age.
* @param name - The person's name
* @param age - The person's age in years
*/
export function greet(name: string, age: number): string {
return `Hello ${name}, age ${age}`;
}
/**
* Echoes a message back after a tick.
* @param msg - The message to echo back
*/
export async function slowEcho(msg: string): Promise<string> {
return `echo: ${msg}`;
}
```
Generate the tool schemas:
```bash
mcp-gen tools.ts
```
```jsonc
{
"tools": [
{
"name": "greet",
"description": "Greets a person by name and age.",
"inputSchema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "The person's name" },
"age": { "type": "number", "description": "The person's age in years" }
},
"required": ["name", "age"]
}
}
// ... slowEcho
]
}
```
The fastest way to actually try them — the **live playground**. It watches your file, renders an input form for each tool from the inferred schema, and runs it right in your browser while you edit:
```bash
mcp-gen dev tools.ts # then open the printed http://127.0.0.1:4000/ URL
```
Or run it as a real MCP server:
```bash
mcp-gen serve tools.ts --port 3000
```
`serve` binds **`127.0.0.1` (loopback) by default** — it executes your local code when its tools are called, so it isn't reachable off-machine unless you ask. To expose it on the network, opt in explicitly with `--host` (e.g. `mcp-gen serve tools.ts --host 0.0.0.0`); doing so prints a warning, since the endpoint is then reachable by other machines.
Before exposing it, turn on **bearer-token authentication**. Configure one or more keys with `--api-key <key>` (repeatable) or the comma-separated `MCP_GEN_API_KEYS` environment variable — if any key is set, auth is **on** and every request to `/mcp` must carry `Authorization: Bearer <key>` (a missing, malformed, or wrong key is rejected with `401` before any tool runs). With no keys set, auth is off and behavior is unchanged. When auth is on, the off-machine "no authentication" warning is replaced by a one-line confirmation that a token is required. See [Deploying to production](#deploying-to-production).
That's it. The parameter names, types, required-ness, and descriptions all come from the code you already wrote.
## How it works
- **Types become schemas.** Each exported function's parameters are converted to a JSON Schema `inputSchema` by the TypeScript type checker. Optional params (`age?: number`) are omitted from `required`; return types are captured (and `Promise<T>` is unwrapped).
- **JSDoc becomes documentation.** The function's summary becomes the tool `description`; each `@param` becomes that property's `description`.
- **Fail-loud, never fail-silent.** A function that can't be converted to a valid schema — for example an unbound generic in input position (`identity<T>(value: T)`) — is **excluded and reported**, never emitted as something broken. Clean functions are still generated; the exit code tells you if any failed (see below).
## What it handles
It's built to work on real codebases, not just single-file toys:
- generics (constrained and unbound), and a clear error on the ones that can't be represented
- types imported from other modules and re-exported under aliases
- multiple export styles (named, default, aliased)
- `tsconfig.json` discovery and path-alias resolution
- detection of non-serializable types (instead of emitting invalid schema)
- `async` functions (awaited return types); a throwing tool returns `isError` rather than taking the server down
## Resources & prompts
MCP servers can expose three kinds of things — **tools** (actions), **resources** (data), and **prompts** (reusable templates). `mcp-gen` infers all three from the same typed exports; a single JSDoc tag picks which one. **An untagged function is a tool, exactly as before — nothing changes for existing code.**
### Resources — `@resource <uri>`
Tag a function with `@resource` and a URI. If the URI has `{placeholders}` that match parameter names, it becomes a **resource template** (the params validate the URL); with no placeholders it's a **static resource**. The return value is the content — a `string` is served as `text/plain`, anything else as `application/json`. Add `@mime <type>` to override.
```ts
/**
* Read a user record by id.
* @resource users://{id} — templated: {id} matches the `id` param
* @param id - The user id
*/
export function getUser(id: string): { id: string; name: string } {
return { id, name: `User ${id}` };
}
/**
* The current app configuration.
* @resource config://app — static: no placeholders
*/
export function appConfig() {
return { theme: "dark", version: "1.1.0" };
}
/**
* @resource info://build
* @mime text/plain — override the content type
*/
export function buildInfo(): string {
return "mcp-gen build 1.1.0";
}
```
### Prompts — `@prompt`
Tag a function with `@prompt`. Its parameters become the prompt's arguments (names and descriptions from `@param`). Return a **string** for a single user message, or an array of `{ role, content }` messages to pass them through as-is.
```ts
/**
* A code-review prompt.
* @prompt
* @param language - The programming language
* @param code - The code to review
*/
export function reviewPrompt(language: string, code: string): string {
return `Please review this ${language} code:\n\n${code}`;
}
```
Run `mcp-gen serve` and a connected MCP client can list and read your resources and get your prompts, alongside calling tools. Resources and prompts go through the **exact same path** as tools — same type inference, same validation, same execution — and the same fail-loud rule applies: e.g. a `@resource` template whose `{var}` doesn't match a parameter is excluded and reported, never half-registered.
## CLI
```
mcp-gen <file.ts> [--debug] [--tsconfig <path>] Generate tool schemas as JSON (stdout)
mcp-gen serve <file.ts> [--port N] [--host <addr>] [--api-key <key>]... [--tsconfig <path>] Start a live MCP server (default port 3000; binds 127.0.0.1; PORT/HOST env honored)
mcp-gen dev <file.ts> [--port N] [--tsconfig <path>] Live playground UI, watches + reloads (default port 4000)
mcp-gen check <file.ts> [--update] [--snapshot <path>] [--tsconfig <path>] Guard the tool surface against breaking changes (CI)
```
Generation writes machine-readable JSON to **stdout** (always includes `tools`; includes `errors`/`warnings` when present); human-readable messages go to **stderr**. Exit codes:
| Code | Meaning |
|---|---|
| `0` | every exported function converted cleanly |
| `1` | one or more functions failed — clean ones are still emitted, failures listed in `errors` |
| `2` | file-level failure (not found / unparseable / nothing servable) |
### Deploying to production
To run `serve` as a network-reachable MCP server, bind a public interface **and** require a bearer token. Pass the keys through the environment (not `--api-key`) so the secret never lands in shell history or a process listing:
```bash
# one or more comma-separated keys; every caller must send `Authorization: Bearer <key>`
MCP_GEN_API_KEYS="$(openssl rand -hex 32)" mcp-gen serve tools.ts --host 0.0.0.0
```
- **Keys via env in prod, not the CLI.** `--api-key` is convenient for local testing, but a flag is visible to anyone who can list processes (`ps`); `MCP_GEN_API_KEYS` keeps the secret out of `argv`. Both sources are unioned, comma-split, and trimmed, and any non-empty key turns auth on.
- **`PORT` / `HOST` are honored.** When you omit `--port` / `--host`, `serve` falls back to the `PORT` and `HOST` environment variables before its defaults (`3000` / `127.0.0.1`) — so it drops straight into a platform that injects `PORT`. An explicit `--port` / `--host` flag always overrides the env var.
- **Auth is fail-closed.** With keys configured, a request to `/mcp` carrying a missing, malformed, or non-matching token is rejected with `401 {"error":"unauthorized"}` *before* the MCP transport — so no tool, resource, or prompt code runs. Keys are compared in constant time.
- **No keys = exposed and unauthenticated.** Bind a non-loopback `--host` (or `HOST`) without any key configured and `serve` keeps printing the loud exposure warning. Configure a key to gate the endpoint (and silence the warning).
On a platform that injects `PORT` (and optionally `HOST`), you supply only the keys:
```bash
MCP_GEN_API_KEYS="key-a,key-b" mcp-gen serve tools.ts --host 0.0.0.0 # PORT taken from the environment
```
### Guarding the contract — `check`
`check` is **`tsc` for your tool surface**: it snapshots the generated tools to a committed file and, on later runs, fails the build on **breaking** changes — so an agent-facing tool can't silently change shape under its callers.
```bash
mcp-gen check tools.ts --update # write the baseline (the `jest -u` of tool contracts) — commit it
mcp-gen check tools.ts # in CI: fail if the surface broke
```
Breaking changes (exit `1`) are judged from the perspective of an existing caller: a tool removed or renamed, a property removed, a **new required** parameter, an optional param made required, a type change, an enum value removed, or any other narrowing of a nested sub-schema. Purely additive or loosening changes — a new tool, a new *optional* param, a relaxed requirement, a new enum value — are **safe** and never fail. Description and return-type changes are reported as **notices**.
| Code | Meaning |
|---|---|
| `0` | no breaking changes (or a successful `--update`) |
| `1` | at least one breaking change — named on stderr, full change list as JSON on stdout |
| `2` | file-level failure, bad usage, or a missing snapshot (never silently created — prints how to create a baseline) |
The snapshot is deterministically normalized (tools and keys sorted, stable formatting), so it stays byte-stable and reviews cleanly in a PR. Defaults to `<file>.mcp-snapshot.json`; override with `--snapshot`.
### Live playground — `dev`
`dev` is a type-aware playground for the server a file defines. It watches the file, regenerates the tool surface on every save, and serves a tiny **localhost** web UI:
```bash
mcp-gen dev tools.ts # then open the printed URL, e.g. http://127.0.0.1:4000/
```
Open the printed URL in a browser. For each tool it renders an input form from the inferred schema (string → text, number → number, boolean → checkbox, enum → dropdown, arrays/objects → a raw-JSON box), runs the tool on demand, and shows the **result**, the **generated `inputSchema`**, and the **raw JSON-RPC** request/response — the inspector view. Fail-loud excluded functions are listed greyed out with their reasons. Save the file and the page reloads itself, preserving what you'd typed.
Crucially, the playground runs each tool through the **exact same path** as `mcp-gen serve` — the same module loader and the same named→positional dispatch — so what you see in the browser is what the served server does. It binds `127.0.0.1` only (it executes your local code on request, so it is never exposed off-machine), defaults to port `4000`, and follows the same exit-code discipline as `serve`.
> **Commit the `*.mcp-snapshot.json` file** — it's the baseline every later `check` compares against, not build output. Don't add it to `.gitignore`; check it in alongside your code so a PR's diff shows exactly how the tool surface changed.
## Install
```bash
npm install -g @zodromon/mcp-gen
```
The package is scoped (`@zodromon/mcp-gen`), but the command you run is just `mcp-gen`:
```bash
mcp-gen tools.ts
```
Or run it without installing, via `npx`:
```bash
npx @zodromon/mcp-gen tools.ts
```
**From source** (to develop or contribute):
```bash
git clone https://github.com/zodromon/mcp-gen && cd mcp-gen
npm install
npm run build # → dist/
node dist/generate-mcp-schemas.js tools.ts
```
During development you can run it directly without building:
```bash
npm run generate -- tools.ts # via tsx
```
Requires Node.js. Dependencies: `@modelcontextprotocol/sdk`, `ts-morph`, `typescript`, `jiti`.
## Scope, honestly
**Good for:** quickly exposing existing typed functions as MCP tools — internal tools, prototypes, anything where you'd rather not hand-write tool schemas.
**Not trying to be** the biggest MCP framework. It does one thing. If you want decorators, a plugin system, or a managed platform, other good tools fit better:
- [FastMCP](https://github.com/punkpeye/fastmcp) — mature and popular; you declare params via a schema library (Zod/ArkType/Valibot).
- [simply-mcp-ts](https://github.com/Clockwork-Innovations/simply-mcp-ts) — decorator / functional / programmatic APIs.
- [The official MCP SDK](https://github.com/modelcontextprotocol) — maximal control, more boilerplate.
`mcp-gen`'s only real difference is taste: **nothing is added to your functions** — no schema library, no annotations beyond the JSDoc you'd write anyway. If that appeals, use it. If not, the others are great.
## License
MIT. Free to use, fork, or ignore.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues