-
Notifications
You must be signed in to change notification settings - Fork 7
Document MCP server generator #6778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| --- | ||
| title: Authentication | ||
| description: How your API's security schemes become the environment variables an MCP server reads at startup. | ||
| availability: beta | ||
| --- | ||
|
|
||
| The generated MCP server doesn't need a separate auth configuration. It reads one environment variable per security scheme declared under your spec's `components.securitySchemes` and forwards it as the corresponding header on every request that needs it. | ||
|
|
||
| ## How the variable name is derived | ||
|
|
||
| The variable's base name is the security scheme's key, upper-snake-cased. A scheme named `widgetKey` becomes `WIDGET_KEY`; a scheme named `apiToken` becomes `API_TOKEN`. | ||
|
|
||
| - **HTTP Basic** schemes get two variables: `{BASE}_USERNAME` and `{BASE}_PASSWORD`. | ||
| - Every other scheme type — API key, bearer token, another HTTP scheme — gets one: `{BASE}` itself. | ||
|
|
||
| A scheme named `basicAuth` with type `http`/`basic`, for example, produces `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD`. | ||
|
|
||
| Generation fails if a scheme's derived base isn't a legal environment variable name, or if it collides with another scheme's base or with one of the two reserved names, `BASE_URL` or `MCP_TOOLSET` — so a naming conflict surfaces at generation time instead of as a runtime mystery. | ||
|
|
||
| ## Always-present variables | ||
|
|
||
| Every generated server also reads: | ||
|
|
||
| - **`BASE_URL`** — the API's base URL. | ||
| - **`MCP_TOOLSET`** — selects which [toolset or preset](/learn/mcp-generator/get-started/configuration#presets) the server exposes at startup. Its description lists every valid value: `default`, plus whatever you've configured. | ||
|
|
||
| Because these two names are reserved, no security scheme in your spec can be named `baseUrl` or `mcpToolset` — rename the scheme if it collides. | ||
|
|
||
| ## Where the description comes from | ||
|
|
||
| Each environment variable's description comes from the security scheme's own OpenAPI `description`, when you've written one. Otherwise the generator fills in a fallback naming the credential type, such as "Credential for the widgetKey security scheme (API key)." | ||
|
|
||
| ## Missing credentials | ||
|
|
||
| Every environment variable is optional at startup, so the server always starts even with none set. A tool call that needs a credential you haven't set fails when it's made, not before. [`fern mcp dev`](/learn/mcp-generator/get-started/local-development) checks for this up front: it detects which credentials the toolset you're running actually needs and prints the exact `export` command for anything missing, instead of leaving you to debug a 401. | ||
|
|
||
| ## Current limits | ||
|
|
||
| OAuth client-credentials schemes aren't supported as a server's only auth source — the generated server has no token-acquisition flow to run, so generation fails outright if every security scheme your spec declares is OAuth client-credentials. Add an API key or bearer scheme for the endpoints an MCP client should reach, or [scope the toolset](/learn/mcp-generator/get-started/tool-selection) away from the ones that require it. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| --- | ||
| title: Configuration reference | ||
| description: Configure MCP server generation in generators.yml — server identity, tool selection, and per-tool overrides. | ||
| availability: beta | ||
| --- | ||
|
|
||
| Configure the MCP generator in `generators.yml`. Options nested under `config` are specific to the MCP generator. The rest (`output`, `github`, `audiences`, `smart-casing`, `metadata`, `api`) behave the same way as for [SDK generators](/learn/sdks/reference/generators-yml). | ||
|
|
||
| ```yaml title="generators.yml" {6-21} | ||
| groups: | ||
| mcp: | ||
| generators: | ||
| - name: fernapi/fern-mcp-server | ||
| version: 0.1.0 | ||
| config: | ||
| server-name: acme-public-api | ||
| instructions: >- | ||
| Refunds must stay under $100. | ||
| tools: | ||
| intent: >- | ||
| Support agents look up customers/payments, refunds under $100, no admin. | ||
| mode: static | ||
| budget: { max-tools: 40, max-tokens: 60000 } | ||
| include: | ||
| - { tag: payments, method: GET } | ||
| - { tag: customers, method: GET } | ||
| - { path-prefix: /v1/refunds } | ||
| exclude: | ||
| - { tag: internal } | ||
| overrides: | ||
| POST /v1/payments: | ||
| name: create_payment | ||
| description: "Create a payment. Amounts are in minor units." | ||
| response-fields: [id, status, amount] | ||
| output: | ||
| location: npm | ||
| package-name: "@acme/mcp" | ||
| github: | ||
| repository: acme/acme-mcp | ||
| mode: pull-request | ||
| ``` | ||
|
|
||
| ## `config` options | ||
|
|
||
| <ParamField path="server-name" type="string" required={false} toc={true}> | ||
| Name shown in an MCP client's server list, and the basis for the generated package name. Defaults to the API definition's display name (kebab-cased) with `-mcp` appended. | ||
| </ParamField> | ||
|
|
||
| <ParamField path="instructions" type="string" required={false} toc={true}> | ||
| Server-level instructions passed to MCP clients on connect — the same role a system prompt plays for a model, scoped to this server. Anything the [AI-curated preset](/learn/mcp-generator/get-started/tool-selection#ai-curated-rulesets) can't express as a tool selector, like a spending limit or an escalation rule, is written here. | ||
|
|
||
| ```yaml | ||
| config: | ||
| instructions: "Refunds must stay under $100; escalate anything larger." | ||
| ``` | ||
| </ParamField> | ||
|
|
||
| ## `tools` options | ||
|
|
||
| These options live under `config.tools` in `generators.yml`. | ||
|
|
||
| <ParamField path="intent" type="string" required={false} toc={true}> | ||
| Free-text description of what the MCP should let an agent do, stored verbatim. Set by the [AI-curated preset](/learn/mcp-generator/get-started/tool-selection#ai-curated-rulesets); a later `fern mcp tools --refine --ai` re-runs against this stored intent to propose rule updates once the spec changes, instead of asking you to restate what the server is for. | ||
| </ParamField> | ||
|
|
||
| <ParamField path="mode" type="string" default="static" toc={true}> | ||
| `static` generates one tool per matched endpoint. `dynamic` collapses the toolset into three meta-tools — list, describe, invoke — covered under [dynamic mode](/learn/mcp-generator/get-started/tool-selection#presets). Set by hand or via the Refine loop; it isn't offered as a top-level preset. | ||
| </ParamField> | ||
|
|
||
| <ParamField path="budget" type="object" required={false} toc={true}> | ||
| Overrides the default [budget verdict](/learn/mcp-generator/get-started/tool-selection#the-budget-verdict) thresholds for this group. | ||
| </ParamField> | ||
| <Indent> | ||
| <ParamField path="budget.max-tools" type="number" default="40"> | ||
| Tool count above which `fern mcp init` and `fern mcp tools` score the toolset amber. More than 3 times this value scores red. | ||
| </ParamField> | ||
| <ParamField path="budget.max-tokens" type="number" default="60000"> | ||
| Estimated token cost above which the toolset scores amber. More than 3 times this value scores red. | ||
| </ParamField> | ||
| </Indent> | ||
|
|
||
| <ParamField path="include" type="array of objects" required={false} toc={true}> | ||
| Endpoints to expose as tools. Entries are OR'd together; fields within a single entry are AND'd. Omit `include` to start from every endpoint the spec exposes. | ||
|
|
||
| ```yaml | ||
| config: | ||
| tools: | ||
| include: | ||
| - { tag: payments, method: GET } # read-only payments | ||
| - { path-prefix: /v1/refunds } # coarse cut — the primary unit for untagged specs | ||
| - { operation-id: "payments_*" } # glob | ||
| - { endpoint: POST /v1/refunds } # a literal endpoint — the escape hatch | ||
| ``` | ||
|
|
||
| Each entry supports `tag`, `method`, `path-prefix`, `operation-id` (glob), and `endpoint` (a literal `METHOD /path`). Prefer the others over `endpoint` so a selection keeps matching as the spec evolves instead of freezing a list of paths. | ||
| </ParamField> | ||
|
|
||
| <ParamField path="exclude" type="array of objects" required={false} toc={true}> | ||
| Endpoints to drop from the resolved toolset, using the same selector shape as `include`. `exclude` always wins over `include`. | ||
|
|
||
| ```yaml | ||
| config: | ||
| tools: | ||
| exclude: | ||
| - { tag: internal } | ||
| - { method: DELETE } | ||
| ``` | ||
| </ParamField> | ||
|
|
||
| <ParamField path="overrides" type="object" required={false} toc={true}> | ||
| Per-tool polish, keyed by `METHOD /path`. | ||
| </ParamField> | ||
| <Indent> | ||
| <ParamField path="overrides.<endpoint>.name" type="string" required={false}> | ||
| Overrides the generated tool name. | ||
| </ParamField> | ||
| <ParamField path="overrides.<endpoint>.description" type="string" required={false}> | ||
| Overrides the generated tool description. | ||
| </ParamField> | ||
| <ParamField path="overrides.<endpoint>.read-only" type="boolean" required={false}> | ||
| Overrides the generated `readOnlyHint` MCP annotation. | ||
| </ParamField> | ||
| <ParamField path="overrides.<endpoint>.destructive" type="boolean" required={false}> | ||
| Overrides the generated `destructiveHint` MCP annotation. | ||
| </ParamField> | ||
| <ParamField path="overrides.<endpoint>.response-fields" type="array of strings" required={false}> | ||
| Restricts the tool's response to the named fields, dropping the rest before it reaches the agent. The [Refine loop](/learn/mcp-generator/get-started/tool-selection#refining-an-over-budget-toolset) writes this automatically when you project down an oversized response. | ||
| </ParamField> | ||
| <ParamField path="overrides.<endpoint>.deprecated" type="boolean" required={false}> | ||
| Marks the tool deprecated in its description instead of removing it — the migration path for a [published server](/learn/mcp-generator/get-started/maintaining#breaking-change-handling) when removing the tool outright would break existing agent integrations. | ||
| </ParamField> | ||
| </Indent> | ||
|
|
||
| <ParamField path="presets" type="object" required={false} toc={true}> | ||
| Named subsets of the group's resolved toolset, using the same selector schema as `include`/`exclude`. A client can connect to one preset instead of the whole server, and each preset gets its own verdict in `fern mcp tools`. | ||
|
|
||
| ```yaml | ||
| config: | ||
| tools: | ||
| presets: | ||
| read-only: | ||
| include: [{ method: GET }] | ||
| support: | ||
| intent: "support agents: lookups + refunds, no admin" | ||
| include: [{ tag: customers }, { tag: payments, method: GET }] | ||
| ``` | ||
|
|
||
| A preset defined here is the same object the dashboard's preset picker edits — changing one updates the other. | ||
| </ParamField> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| --- | ||
| title: Local development | ||
| description: Build, inspect, and connect a generated MCP server to Claude, Cursor, or Codex. | ||
| availability: beta | ||
| --- | ||
|
|
||
| ## Run the inspector | ||
|
|
||
| ```bash | ||
| fern mcp dev --group mcp | ||
| ``` | ||
|
|
||
| `fern mcp dev` builds the group's server and attaches the [MCP inspector](https://modelcontextprotocol.io/legacy/tools/inspector), so you can call tools directly and check their input schemas and responses before wiring up a real client. If the toolset you're running needs a credential you haven't [set](/learn/mcp-generator/get-started/authentication), it prints the exact `export` command instead of starting a server where every call fails. | ||
|
|
||
| ## Build and run manually | ||
|
|
||
| Every generated project also builds and runs as a standalone Node package, without the inspector: | ||
|
|
||
| ```bash | ||
| cd path/to/generated/mcp | ||
| npm run setup # installs dependencies and builds | ||
| npm start # runs the server over stdio | ||
| ``` | ||
|
|
||
| Node 20 or later is required. | ||
|
|
||
| ## Connect a client | ||
|
|
||
| ```bash | ||
| fern mcp install --local --group mcp | ||
| ``` | ||
|
|
||
| This wires the generated server into your local Claude, Cursor, or Codex configuration, pointing at the built `dist/index.js` and filling in the environment variables it needs. | ||
|
|
||
| To wire it up by hand instead, every generated project's README includes the client config as JSON: | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "acme-public-api": { | ||
| "command": "node", | ||
| "args": ["/absolute/path/to/dist/index.js"], | ||
| "env": { | ||
| "WIDGET_KEY": "", | ||
| "BASE_URL": "" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| and, for Claude Code specifically, a ready-to-run install command: | ||
|
|
||
| ```bash | ||
| claude mcp add acme-public-api --env WIDGET_KEY=<WIDGET_KEY> --env BASE_URL=<BASE_URL> -- node "/absolute/path/to/dist/index.js" | ||
| ``` | ||
|
|
||
| The client starts the server itself over stdio the first time it needs it — there's no separate process to keep running or port to open. | ||
|
|
||
| ## Next steps | ||
|
|
||
| <CardGroup cols={2}> | ||
| <Card title="Maintaining MCP servers" icon="fa-duotone fa-arrows-rotate" href="/learn/mcp-generator/get-started/maintaining"> | ||
| Regenerate on spec changes and catch breaking tool changes before you publish. | ||
| </Card> | ||
| <Card title="Multiple servers" icon="fa-duotone fa-diagram-project" href="/learn/mcp-generator/get-started/multiple-servers"> | ||
| Generate separate MCP servers for different audiences from one spec. | ||
| </Card> | ||
| </CardGroup> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||
| --- | ||||||
| title: Maintaining MCP servers | ||||||
| description: Regenerate on spec changes, catch breaking tool changes before you publish, and keep a published server's toolset stable. | ||||||
| availability: beta | ||||||
| --- | ||||||
|
|
||||||
| Maintaining a generated MCP server follows the same loop as an SDK — edit config, regenerate — with a few commands on top for a surface an SDK doesn't have: a live toolset that downstream agents depend on staying stable. | ||||||
|
|
||||||
| | Command | What it does | | ||||||
| |---|---| | ||||||
| | `fern mcp list` | Table of every configured MCP group: server name, presets, tool count, token estimate, output target, and toolset overlaps with other groups. | | ||||||
| | `fern mcp tools [--group g] [--preset p]` | Resolved toolset for a group, with per-tool token cost, the [budget verdict](/learn/mcp-generator/get-started/tool-selection#the-budget-verdict), and a quality lint (missing or derived-from-junk descriptions, tool name collisions, ambiguous near-duplicate tools, oversized responses). `--refine` opens the mutation loop; `--diff` compares the resolved toolset against `tools.lock`. | | ||||||
| | `fern generate --group g` | Regenerates the server from the current config and spec, same as any other generator. | | ||||||
| | `fern generator upgrade` | Upgrades the pinned generator version, same as for SDKs. | | ||||||
| | `fern check` | Validates config, including the [MCP-specific rules](#fern-check-rules). | | ||||||
|
|
||||||
| ## `tools.lock` | ||||||
|
|
||||||
| `fern generate` writes a `tools.lock` file next to `generators.yml` — the resolved tool names, input schema hashes, and token costs for that group, at that generation. It's the baseline `fern mcp tools --diff` compares against, and the artifact that makes a breaking-change classification reviewable in a pull request alongside the config change that caused it. | ||||||
|
|
||||||
| ## Breaking-change handling | ||||||
|
|
||||||
| A renamed or removed tool breaks any agent already integrated against it — a worse failure than a breaking SDK change, because nobody recompiles an agent's tool list. `fern mcp tools --diff` classifies every delta against `tools.lock`: | ||||||
|
|
||||||
| - **Additive** — a new tool, or a longer/clarified description. | ||||||
| - **Breaking** — a tool removed or renamed, a required parameter added, response fields dropped, or the server's identity changed (`server-name` or package name, on a group that's already published). | ||||||
|
|
||||||
| A breaking delta produces a `fern check` warning and a major-version-bump recommendation on publish; an additive one recommends a minor bump. This release detects and warns — it doesn't prevent a breaking change from generating or publishing. | ||||||
|
|
||||||
| To retire a tool without breaking existing integrations immediately, mark it deprecated instead of removing it: | ||||||
|
|
||||||
| ```yaml | ||||||
| config: | ||||||
| tools: | ||||||
| overrides: | ||||||
| POST /v1/charges: | ||||||
| deprecated: true | ||||||
| ``` | ||||||
|
|
||||||
| This keeps the tool available but marks it deprecated in its description, giving downstream agents a migration window instead of an outage. | ||||||
|
|
||||||
| ## `fern check` rules | ||||||
|
|
||||||
| Beyond the general SDK checks, `fern check` also flags, for MCP groups: | ||||||
|
|
||||||
| - A [budget](/learn/mcp-generator/get-started/tool-selection#the-budget-verdict) warning. | ||||||
| - An `include`/`exclude` selector that matches nothing. | ||||||
| - A destructive endpoint with no `destructive` annotation override. | ||||||
| - A new spec endpoint matching no configured group. | ||||||
| - An **orphaned override** — an `overrides` entry keyed to an endpoint no longer in the spec, so a hand-written description doesn't just silently stop applying. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 [vale] <FernStyles.Hedges> reported by reviewdog 🐶
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 [vale] <FernStyles.Adverbs> reported by reviewdog 🐶
Suggested change
|
||||||
| - A `server-name` or package rename on an already-published group, which `tools --diff` can't see on its own since it compares tools, not identity. | ||||||
| - A `response-fields` projection set under a generator version that doesn't support it. | ||||||
|
|
||||||
| All of these print as warnings, not blockers, in CI and `--json` output. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 [vale] <Microsoft.Wordiness> reported by reviewdog 🐶
Suggested change
|
||||||
|
|
||||||
| ## Updating a server | ||||||
|
|
||||||
| - **Spec changed** — regenerate; `fern mcp tools --diff` reports what moved. | ||||||
| - **Renaming or pruning tools** — edit `overrides` or `exclude`, then regenerate. | ||||||
| - **New generator version** — `fern generator upgrade`. | ||||||
| - **Publishing** — the same `github` and npm output blocks, and the same autorelease infrastructure, as SDK generators. | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| navigation: | ||
| - section: Get started | ||
| contents: | ||
| - page: Overview | ||
| path: ./overview.mdx | ||
| slug: overview | ||
| - page: Quickstart | ||
| path: ./quickstart.mdx | ||
| slug: quickstart | ||
| - page: Tool selection | ||
| path: ./tool-selection.mdx | ||
| slug: tool-selection | ||
| - page: Multiple servers | ||
| path: ./multiple-servers.mdx | ||
| slug: multiple-servers | ||
| - page: Local development | ||
| path: ./local-development.mdx | ||
| slug: local-development | ||
| - page: Maintaining MCP servers | ||
| path: ./maintaining.mdx | ||
| slug: maintaining | ||
| - page: Authentication | ||
| path: ./authentication.mdx | ||
| slug: authentication | ||
| - page: Configuration reference | ||
| path: ./configuration.mdx | ||
| slug: configuration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 [vale] <FernStyles.Acronyms> reported by reviewdog 🐶
'AND' has no definition.