feat(cli): prototype stub-mode fern mcp init/list/tools/dev flow - #17544
feat(cli): prototype stub-mode fern mcp init/list/tools/dev flow#17544matlegault wants to merge 13 commits into
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
||
| const configurationAsRecord: Record<string, unknown> = isRecord(parsedFile) ? { ...parsedFile } : {}; | ||
| const groups = isRecord(configurationAsRecord.groups) ? { ...configurationAsRecord.groups } : {}; | ||
| groups[groupName] = { generators: [generatorEntry] }; |
There was a problem hiding this comment.
🔴 Writing an MCP group deletes other generators in it
writeMcpGroupToGeneratorsYml overwrites the whole target group with a single MCP entry (groups[groupName] = { generators: [generatorEntry] }). Running fern mcp init --group <existing-group> silently deletes every other generator in that group from generators.yml.
Prompt for agents
In writeMcpGroupToGeneratorsYml (packages/cli/cli/src/commands/mcp/prototype/mcpGeneratorsYml.ts), the line groups[groupName] = { generators: [generatorEntry] } replaces the entire group, discarding any pre-existing generators in that group. This causes silent data loss when the target group already contains other generators (for example an SDK generator, or when a user passes --group pointing at an existing group). Consider merging into the existing group: read the existing group's generators array, replace only the existing fernapi/fern-mcp-server entry (if any) or append the MCP entry, and preserve unrelated generators. The comment above already states the intent to preserve unrelated keys verbatim, so the same principle should apply to sibling generators within the group.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in c467bde: writeMcpGroupToGeneratorsYml now merges into the existing group — it preserves unrelated group properties and sibling generator entries, replacing only the existing fernapi/fern-mcp-server entry (or appending if none). Verified by adding a fernapi/fern-typescript-sdk entry to the mcp group and re-running fern mcp init --yes: the SDK entry survives.
| if (refine) { | ||
| toolsConfig = await runTrimLoop({ | ||
| cliContext, | ||
| endpoints: spec.endpoints, | ||
| initialConfig: toolsConfig | ||
| }); | ||
| await cliContext.runTaskForWorkspace(workspaceSpec.workspace, async (context) => { | ||
| await writeMcpGroupToGeneratorsYml({ | ||
| absolutePathToWorkspace, | ||
| context, | ||
| groupName: group, | ||
| serverName: found.config["server-name"], | ||
| toolsConfig, | ||
| presets: found.config.tools?.presets | ||
| }); | ||
| }); | ||
| cliContext.logger.info(chalk.green(`Updated group "${group}" in generators.yml.`)); | ||
| } |
There was a problem hiding this comment.
🟡 Refining a preset overwrites the group default toolset
With --preset X --refine, toolsConfig becomes preset X's config (toolsMcp.ts:58) and is then written back as the group's top-level tools. The group's default include/exclude is clobbered while preset X stays unchanged, the reverse of the intended refine.
Prompt for agents
In toolsMcp (packages/cli/cli/src/commands/mcp/prototype/toolsMcp.ts), when --preset and --refine are used together, the refined config is written to the group's top-level tools block instead of back into tools.presets[preset]. This overwrites the group's default toolset and leaves the named preset unchanged. When preset != null, the refined toolsConfig should be written into the presets map under that preset name (keeping the group's top-level tools intact), rather than passed as the top-level toolsConfig argument to writeMcpGroupToGeneratorsYml.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in c467bde: refining with --preset <name> now writes the refined config back into tools.presets[<name>] and leaves the group-level default tools block untouched. Verified end-to-end: refined pets-only preset gained exclude: [{method: POST}] while group-level include: [{method: GET}] was unchanged.
| if (args.intent != null && args.preset == null) { | ||
| presetKey = "ai-curated"; | ||
| toolsConfig = await promptForAiCuratedConfig({ cliContext, endpoints, initialIntent: args.intent }); |
There was a problem hiding this comment.
🟡 --json --intent emits non-JSON before the summary
With --json --intent, initMcp still takes the AI-curated branch and calls promptForAiCuratedConfig, which unconditionally logs the proposed ruleset. Those human-readable lines print before the JSON summary on the same stream, so the --json output no longer parses.
Prompt for agents
In initMcp (packages/cli/cli/src/commands/mcp/prototype/initMcp.ts), the AI-curated branch (args.intent set, args.preset null) calls promptForAiCuratedConfig, which prints the spinner line and the proposed ruleset via cliContext.logger.info. When json mode is active this pollutes the machine-readable output because the JSON summary is emitted on the same logger stream. Either suppress the ruleset logging when json is true (pass a flag / guard the logger.info calls in promptForAiCuratedConfig), or compute the AI proposal without logging in json mode.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in c467bde: in --json mode the AI helper skips the spinner and proposal logging (and the artificial delay) while still computing the ruleset and persisting intent:. Verified fern mcp init --json --intent "..." output now parses as valid JSON.
…oup merge, json output) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…es, and tool tables Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… prototype Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Description
UX prototype — all backend functionality is stubbed. This PR adds a representative stub-mode prototype of the customer-facing MCP creation flow so the full UX path can be demoed end-to-end. Real CLI commands, real OpenAPI spec parsing, and real
generators.ymlwriting — but the costing service, AI curation, code generation, and MCP runtime are all stubbed locally. No backend credentials are required.New subcommands under
fern mcp(alongside the existingfern mcp install):fern mcp init— wizard: diagnoses the spec (title, endpoint count, token estimate), prompts for a server name, offers toolset presets (Read-only / Main resources / AI-curated / Everything, plus any existingtools.presets), shows tool count + token estimate + verdict per choice, routes amber/red verdicts into a trim loop (remove by tag/method/path-prefix), and writes anmcpgroup with afernapi/fern-mcp-serverentry into the workspace's realgenerators.yml. Noninteractive flags:--name,--preset,--intent,--group,--yes,--json(verdicts become warnings).fern mcp list— lists MCP generator entries with group, server-name, output path, and resolved tool count/verdict.fern mcp tools— prints the resolved tool surface for a group (--group, defaultmcp;--preset,--json), with--refineentering the same trim loop and rewriting the config in place.fern mcp generate— prints a plausible stub generation transcript and writestools.locknext togenerators.yml(tool names, per-tool token estimates, deterministic local schema hash).fern mcp dev— prints local dev / MCP Inspector guidance (npx @modelcontextprotocol/inspector ...).All prototype logic lives under
packages/cli/cli/src/commands/mcp/prototype/:openapiSummary.ts— scans the workspace (incl.openapi/) for OpenAPI YAML/JSON, extracts endpoint summaries and rough token estimates.toolset.ts— compound include/exclude selectors (fields ANDed within a selector, selectors ORed, exclude wins), tool resolution, and the two-clause verdict (tool count and token cost judged independently; default budget 40 tools / 60k tokens; amber ≤3× budget, red beyond).presets.ts— read-only preset (GET + read-like POSTs by operationId/path/summary heuristics, ambiguous ones called out), main-resources preset backed by a multi-signal resource scorer (see below).aiCurated.ts— stubbed "Fern Agent": pattern-matches the intent against tags/methods, presents exclusions first, persists the intent asintent:in YAML.mcpGeneratorsYml.ts— parses/writes the MCP group ingenerators.yml(js-yaml, schema comment header, unrelated groups/keys preserved).trimLoop.ts,workspace.ts,initMcp.ts,listMcp.ts,toolsMcp.ts,generateMcp.ts— command implementations.Main-resources scorer (multi-signal)
buildMainResourcesPresetidentifies resources from three signals — tags, first meaningful path segment after version prefixes (/v1,/api), and component-schema references from request/response bodies — and treats a candidate as credible when ≥2 signals agree (case/plural-insensitive canonical name matching). Each credible resource is scored as:Selection is threshold + budget, not top-N: resources above
SCORE_THRESHOLD_RATIO(40%) of the top score are included, then the lowest-scoring resources are trimmed untilcomputeVerdictis green — the preset is within budget by construction. Selectors aretagwhen tags exist,path-prefixcompound selectors on untagged specs. Negative signals: name-regex exclusion (admin/internal/webhook/legacy/deprecated/beta/debug/test) plus operation-leveldeprecated: trueandx-internal: true(captured per-endpoint inopenapiSummary.ts), excluded via explicitendpointselectors. A confidence gate marks the preset unavailable when no credible resources exist or when scores are flat (top <FLAT_SCORE_SEPARATION_RATIO(1.5×) median with no strong-CRUD resource).Exception-based budget rendering
Human-facing output only "screams" budget when it's a problem. Within budget (green), option lines,
fern mcp list,fern mcp tools, and the init/generate summaries show only a quiet tool count (e.g.8 tools) — no token estimate, no PASS badge. Over budget (amber/red), the full treatment appears: count + token estimate + verdict badge/label, plus the existing routing into the trim/refine loop. The trim/refine loop itself keeps its full live count/token/verdict readout, since budget is the point there.--jsonoutput is unchanged and unconditionally includestoolCount,estimatedTokens, andverdict— only the human rendering goes quiet (budgetLineinui.ts).The interactive toolset menu always shows all four options with the cursor on the first one — no verdict-driven preselection. The per-option counts (quiet on green) carry the information; the only over-budget difference is that Main resources gets a green
(recommended)tag and Everything shows its over-budget line inline (still routing into refine if chosen).--yes/--presetand JSON behavior are unchanged.Changes Made
addMcpCommandinpackages/cli/cli/src/cli.tswithinit,list,tools,generate,devsubcommandspackages/cli/cli/src/commands/mcp/prototype/type: feat)Testing
pnpm vitest run src/commands/mcp/prototype/__test__→ 27 passednode build.dev.mjs) and ran the full flow against a scratch petstore workspace (19 endpoints). Full transcript below.End-to-end demo transcript (petstore workspace)
Notes:
pnpm turbo run compile --filter @fern-api/clipasses for the CLI package; a preexisting, unrelated compile error exists onmainin@fern-api/docs-validator(valid-changelog-slug.test.tsTS2322), so the dev CLI was bundled vianode build.dev.mjsdirectly.fernapi/fern-mcp-server@0.1.0generator referenced ingenerators.ymldoes not exist —fern generateon the written config is out of scope for this prototype;fern mcp generateprovides the stubbed path.Link to Devin session: https://app.devin.ai/sessions/9c59da7519e74af1a4a7a78d0e8e2abe
Open in Devin Desktop: https://app.devin.ai/desktop/session/9c59da7519e74af1a4a7a78d0e8e2abe?variant=devin
Requested by: @matlegault