Skip to content

feat(mcp): scaffold a tool, an app, or a form with pnpm mcp:new - #351

Merged
dsklyar merged 8 commits into
mainfrom
danielsklyar/zel-8113-new-view-scaffolder
Aug 8, 2026
Merged

dsklyar merged 8 commits into
mainfrom
danielsklyar/zel-8113-new-view-scaffolder

Conversation

@dsklyar

@dsklyar dsklyar commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Last of the eleven. Adds pnpm mcp:new <kind> <package> <name>, one command for the three things a contributor actually adds to an MCP server:

pnpm mcp:new tool        docs      fetch-usage     # src/tools/docs_fetch_usage.ts
pnpm mcp:new app         inventory usage-chart     # a view, its ui:// resource, and the tool that opens it
pnpm mcp:new elicitation consent   confirm-optout  # a tool that collects its arguments through a form
Kind Writes Edits package.json
tool one defineTool, no variants no
app src/ui/<name>/<Name>View.tsx, src/apps/<name>.ts, src/tools/<name>_app.ts on a package's first view
elicitation one tool whose elicitation variant collects what the agent left out no

Why one command and not three

The kinds differ only in what they write. Resolving the package, deriving the names, refusing to clobber a file, and leaving registration alone are the same problem three times, so they live in scripts/lib/mcp-new/shared.ts and each kind is a template module under it.

More usefully, it makes a boundary explicit: only app touches a manifest. A tool that renders as text needs none of React, Vite, or Tailwind, and three sibling scripts would leave a standing risk that mcp:new-tool drags a browser toolchain into a package that ships no view. Getting a first view to render still means a prebuild hook, build:ui, typecheck:ui pointed at a tsconfig.ui.json extending the right base, a gitignore entry, and six catalog: devDependencies — none discoverable from the view directory, since the entry point and stylesheet that boot a view are synthesized at build time and exist nowhere on disk. The app kind knows that and then runs pnpm install, because skipping it fails as an unresolvable react inside mcp-app-entry.tsx, a file that is not on disk and whose error never mentions installing anything.

Two things the templates do rather than stub

  • elicitation emits a valid form — one required field, a real prompt. assertElicitFormSchema and the empty-message check run at construction, so a placeholder schema would generate a package that throws on boot. A generator emitting code that cannot run is worse than one emitting nothing.
  • app emits the MCP App variant only. To serve a form too, generate one and move its variant across; example_hello_app is the worked example of a single tool serving all three paths, and the generated file says so.

Left to the author

Registering the factory in src/tools/index.ts, for every kind. That line is where a name and description become public API on a published package, so it stays a person's decision — but the command prints the paste:

1. Register it, by adding these two lines to mcp-server-docs/src/tools/index.ts:

     import { createFetchUsageTool } from './fetch_usage.js';
     createFetchUsageTool(),

The kebab-case check is not cosmetic: the name becomes a directory name, a *View.tsx component name, the last segment of a ui:// uri, and the tail of a tool name, and only one spelling satisfies all four. docs + fetch-usage becomes docs_fetch_usage, or docs_usage_chart_app for the app kind, which takes the same _app suffix as example_hello_app — without it a tool and the view that renders its result would share a wire name.

The prefix is the package's own spelling, not its directory's: mcp-server-examples ships example_* and mcp-server-assessment ships assessments_*, so those two are listed rather than derived. ui:// uris and pnpm mcp:inspect keep taking the directory's short name, which is what both packages already serve. Generated scripts and the tsconfig.ui.json extends derive their way to the repo root from the package, since packages/mcp/* is three levels up and dev/* is two.

Ticket: ZEL-8113. No changeset — root-level scripts and docs only, nothing published changes.

Also here

  • An empty view directory is skipped rather than fatal. Discovery threw on a directory holding no *View.tsx, which is exactly the state a half-finished mkdir leaves behind.
  • Documentation: a new Scaffolding a tool, an app, or a form section, and the environment-variable tables move to sit before Contributing rather than interrupting the view chapters. Pure move, no wording change.

Where this sits in the stack

Eleven layers, each targeting the one below it, registered as a GitHub stack — so this diff is only its own commits, and merging a lower layer rebases and retargets everything above it automatically.

  1. fix(design-tokens): resolve tokens.css from source inside the monorepo #341 — design-tokens: resolve tokens.css from source inside the monorepo
  2. feat(mcp-server-base): negotiate client capabilities from the handshake #342 — negotiate client capabilities from the handshake
  3. feat(mcp-server-base): attribute stdio sessions to the detected host #343 — attribute stdio sessions to the detected host
  4. feat(mcp): serve ui:// views and per-capability tool variants #344 — serve ui:// resources and per-capability tool variants
  5. feat(mcp-server-base): add a browser-only ./ui subpath with useMcpApp #345 — browser-only ./ui subpath with useMcpApp
  6. feat(mcp-server-base): publish the Tailwind view theme #346 — publish the Tailwind view theme
  7. feat(mcp): build MCP App views with React and Vite #347 — build views into one self-contained document with Vite
  8. fix(mcp): restore the Inspector's missing app sandbox document #348 — restore the Inspector's missing app sandbox document
  9. feat(dev): add mcp-server-examples with the reference view and form flows #349 — dev/mcp-server-examples, the first consumer of all of it
  10. feat(mcp): add pnpm mcp:inspect with per-request dev view HTML #350 — pnpm mcp:inspect with per-request dev view HTML
  11. feat(mcp): scaffold a tool, an app, or a form with pnpm mcp:new #351 — the pnpm mcp:new scaffolder ← this PR

Depends on #350, for viewHtml and discoverMcpPackages. Everything below that is ordering, not need.

Test plan

  • format, lint, typecheck, typecheck:ui, and the full pnpm run test (40 tasks) are green
  • 33 unit tests in scripts/mcp-new.test.ts cover all three kinds against a temp fixture package: files written, script order, refuse-to-overwrite, kebab-case rejection, the tool prefix per package, the repo-root path at both package depths, and that tool and elicitation leave the manifest byte-identical
  • Generated all three kinds into mcp-server-examples and checked the output is real code rather than plausible text: the view built through Vite into a self-contained document, and all four files were in the tsc program for both typecheck and typecheck:ui
  • Constructed each generated tool, which is what proves the elicitation template's form survives assertElicitFormSchema rather than only compiling
  • The generated view satisfies the conventions it is meant to model: mcp-app-views.test.ts passes (exports the component its filename promises, no hand-written entry or stylesheet)
  • Error paths: no kind, unknown kind, a PascalCase name, and the umbrella package each fail with a message naming the fix
  • Probe fully reverted; git status clean apart from this PR's files

Demo(s)

Recorded before the rename, so it shows the older mcp:new-view spelling and the app kind's output; the files it writes are unchanged.

Screen.Recording.2026-08-02.at.23.59.01.mov

@linear-code

linear-code Bot commented Aug 2, 2026

Copy link
Copy Markdown

ZEL-8113

@pkg-pr-new

pkg-pr-new Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@transcend-io/airgap.js-types

pnpm add https://pkg.pr.new/@transcend-io/airgap.js-types@351
yarn add https://pkg.pr.new/@transcend-io/airgap.js-types@351.tgz

@transcend-io/cli

pnpm add https://pkg.pr.new/@transcend-io/cli@351
yarn add https://pkg.pr.new/@transcend-io/cli@351.tgz

@transcend-io/design-tokens

pnpm add https://pkg.pr.new/@transcend-io/design-tokens@351
yarn add https://pkg.pr.new/@transcend-io/design-tokens@351.tgz

@transcend-io/internationalization

pnpm add https://pkg.pr.new/@transcend-io/internationalization@351
yarn add https://pkg.pr.new/@transcend-io/internationalization@351.tgz

@transcend-io/privacy-types

pnpm add https://pkg.pr.new/@transcend-io/privacy-types@351
yarn add https://pkg.pr.new/@transcend-io/privacy-types@351.tgz

@transcend-io/sdk

pnpm add https://pkg.pr.new/@transcend-io/sdk@351
yarn add https://pkg.pr.new/@transcend-io/sdk@351.tgz

@transcend-io/type-utils

pnpm add https://pkg.pr.new/@transcend-io/type-utils@351
yarn add https://pkg.pr.new/@transcend-io/type-utils@351.tgz

@transcend-io/utils

pnpm add https://pkg.pr.new/@transcend-io/utils@351
yarn add https://pkg.pr.new/@transcend-io/utils@351.tgz

@transcend-io/mcp

pnpm add https://pkg.pr.new/@transcend-io/mcp@351
yarn add https://pkg.pr.new/@transcend-io/mcp@351.tgz

@transcend-io/mcp-server-admin

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-admin@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-admin@351.tgz

@transcend-io/mcp-server-assessment

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-assessment@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-assessment@351.tgz

@transcend-io/mcp-server-base

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-base@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-base@351.tgz

@transcend-io/mcp-server-consent

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-consent@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-consent@351.tgz

@transcend-io/mcp-server-discovery

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-discovery@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-discovery@351.tgz

@transcend-io/mcp-server-docs

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-docs@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-docs@351.tgz

@transcend-io/mcp-server-dsr

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-dsr@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-dsr@351.tgz

@transcend-io/mcp-server-inventory

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-inventory@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-inventory@351.tgz

@transcend-io/mcp-server-preferences

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-preferences@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-preferences@351.tgz

@transcend-io/mcp-server-workflows

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-workflows@351
yarn add https://pkg.pr.new/@transcend-io/mcp-server-workflows@351.tgz

commit: d0dbe35

@dsklyar
dsklyar force-pushed the danielsklyar/zel-8113-new-view-scaffolder branch from 237ac7c to a513eda Compare August 3, 2026 06:02
@dsklyar
dsklyar force-pushed the danielsklyar/zel-8113-new-view-scaffolder branch from f235867 to dc1825d Compare August 3, 2026 23:47
@dsklyar
dsklyar force-pushed the danielsklyar/zel-8113-new-view-scaffolder branch from dc1825d to 00b56b5 Compare August 4, 2026 20:24
@dsklyar
dsklyar force-pushed the danielsklyar/zel-8113-new-view-scaffolder branch from 00b56b5 to 4e47b2d Compare August 5, 2026 22:33
@dsklyar dsklyar changed the title feat(mcp): scaffold a new view with pnpm mcp:new-view feat(mcp): scaffold a tool, an app, or a form with pnpm mcp:new Aug 6, 2026
@dsklyar
dsklyar requested review from a team as code owners August 6, 2026 22:22
@dsklyar
dsklyar force-pushed the danielsklyar/zel-8113-new-view-scaffolder branch from 16aba6b to 883f659 Compare August 6, 2026 22:22
Base automatically changed from danielsklyar/zel-8113-mcp-inspect-command to main August 7, 2026 22:50
dsklyar added 3 commits August 7, 2026 15:50
Generate the component and the ui:// resource module, and add the
package-level view wiring when a package has none yet, so adding a view
is two files to fill in rather than seven to remember.
The generator wrote a view that could not be reached without hand-writing a
tool, and left the install that view build needs as an instruction. Skipping it
surfaced as a resolver error naming a synthesized file, so the view build now
checks its dependencies first and says what to run.

The generated tool stays unregistered: adding it to the package's tool list is
where its name becomes public API, which is a person's call.
dsklyar added 3 commits August 7, 2026 15:50
Deleting a view leaves its directory behind, because git cannot track an empty
one. Discovery then refused to run at all, which broke re-scaffolding the view
it belonged to.

Files present but no *View.tsx still fails: that is a renamed component, and
skipping it is how a view silently stops existing.
Replaces `pnpm mcp:new-view` with `pnpm mcp:new <app|tool|elicitation>
<package> <name>`. The three kinds shared nearly all their machinery —
resolving the package, spelling the generated names, refusing to clobber a
file, and leaving registration to a person — so they now share it, and each
kind is a template module.

Only `app` touches a package manifest, which is what keeps React, Vite, and
Tailwind out of packages that ship no view. The `elicitation` template emits
a valid form rather than a placeholder, since assertElicitFormSchema runs at
construction and a stub would throw on boot.
The scaffolded doc comment promised enforcement by the styling test, which no
longer exists. Keeps the reason a view should avoid arbitrary color and length
values, without naming a check.
@dsklyar
dsklyar force-pushed the danielsklyar/zel-8113-new-view-scaffolder branch from 4aac7cf to d07c4a6 Compare August 7, 2026 22:50
With v1 support gone there is one spec, so `V2` in the name carried no
information and the comment argued a choice nobody makes. The pin still needs
justifying: `restoreSandboxProxy` reads the install's `clients/web/static`
layout, which a major bump could restructure into a silent warning.

@dawson-turechek-transcend dawson-turechek-transcend left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice scaffolder — the “only app touches the manifest” boundary and leaving registration manual both feel right. A few things I’d adjust before this becomes the path everyone copies:

Comment thread scripts/lib/mcp-new/app.ts Resolved
Comment thread scripts/lib/mcp-new/shared.ts Resolved
Comment thread dev/mcp-server-examples/README.md Resolved
The generated `prebuild`, `build:ui`, and `tsconfig.ui.json` hardcoded three
levels to the repo root, which is right for `packages/mcp/*` and resolves
outside the repo from `dev/*` — the depth of the only view package there is.
Derived from the package's own directory instead.

Tool names now match the package they land in. The prefix is a package's own
spelling rather than its directory's: `mcp-server-examples` ships `example_*`
and `mcp-server-assessment` ships `assessments_*`, disagreeing in opposite
directions, so both are listed rather than derived. `ui://` uris and
`pnpm mcp:inspect` keep taking the directory's short name, which is what those
packages already serve.

The `app` kind takes an `_app` suffix, as `example_hello_app` does. It shared a
wire name with the `tool` kind for the same argument, and registering both would
collide somewhere `writeNew` cannot see, being two filenames.

The elicitation template also parses a host's answer against the fields its form
required, rather than the all-optional argument schema, so an empty acceptance
reports `malformed` instead of echoing back as an answer.
@dsklyar
dsklyar added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 55d684e Aug 8, 2026
5 checks passed
@dsklyar
dsklyar deleted the danielsklyar/zel-8113-new-view-scaffolder branch August 8, 2026 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants