Skip to content

The intent surface: journeys, grouping, budgets, and the agent skill - #4

Merged
SouravInsights merged 41 commits into
mainfrom
feat/intent-tools
Sep 12, 2026
Merged

SouravInsights merged 41 commits into
mainfrom
feat/intent-tools

Conversation

@SouravInsights

@SouravInsights SouravInsights commented Sep 11, 2026 •

Copy link
Copy Markdown
Owner

Journeys, grouping, budgets, and the agent skill (0.9)

This PR changes what @webmcp-stack/codegen thinks its job is. It used to take an OpenAPI spec and emit one WebMCP tool per endpoint, then give you a dashboard to edit them by hand. That model broke for two reasons: nobody edits 75 tools by hand, and the person editing them isn't a person at all anymore. It's a coding agent with a lazy prompt. So the safe defaults became the product, and "how does your own agent learn the rules?" became the problem worth solving.

This PR answers that. The rules now ship into the repo the agent reads, the tool surface an agent picks from is a few intent-level tools instead of a 1:1 mirror of your routes, and those choices are locked in by CI. The built-in LLM layer, which existed to guess what the user's agent would do, is gone.

npx @webmcp-stack/codegen generate reads your OpenAPI spec, emits one .webmcp.ts file per endpoint (each one withheld by default: the file exists with its schema and hints baked in, but it isn't registered until you enable it), scaffolds a skill file your agent reads, and wires registration. Journeys sit on top: small TypeScript files you or your agent write that register step tools and a submit gate, composing the withheld endpoint tools' raw HTTP callers instead of re-implementing them. verify measures the whole surface and exits 1 on failure, so a sloppy hand edit breaks CI. The dashboard keeps its editing surface, writing overrides that survive regeneration.

What's in it

Journeys — the new concept, and the one to read closely. A journey file is a small TypeScript file in src/webmcp/journeys/ for goals that take a few calls with shared state: an agent can't invent a fully populated locationObject from scratch, and a paid action needs a human in the loop first:

// src/webmcp/journeys/document-trip.webmcp.ts
import { createJourney } from "../journey.webmcp";
import { getAutocompleteTool, fetchGetAutocomplete } from "../get-autocomplete.webmcp";
import { executeCreateTrip, type CreateTripInput } from "../create-trip.webmcp";

export const documentTrip = createJourney({
  name: "document-trip",
  goal: "Record a trip you've been on and open the editor to write its story",
  steps: {
    // A step that calls the real get-autocomplete tool (an endpoint you
    // already own). It stores the result on the draft; the input schema,
    // description, and annotations come from the generated tool definition.
    "search-places": {
      tool: getAutocompleteTool,
      call: (input, signal) => fetchGetAutocomplete(input as never, signal),
      store: (places) => ({ locationObject: places }),
      provides: ["locationObject"],
    },
    // A step that takes input and stores it, with no API call of its own.
    "set-details": {
      description: "Set the trip's title.",
      input: { type: "object", properties: { title: { type: "string" } }, required: ["title"] },
      provides: ["title"],
    },
  },
  // The gate. It refuses until every step's `provides` is filled on the
  // draft, prompts the human through the same confirmation flow writes
  // already use, then clears the draft. The agent can't skip it.
  submit: {
    description: "Create the trip and open it in the editor.",
    build: (draft) => draft as never as CreateTripInput,
    run: executeCreateTrip,
  },
});

The detail that makes this safe: journey.webmcp.ts is a file the generator owns and rewrites on every run, the same way the runtime is. Your journey files import createJourney from it, so you can't edit the gate logic out, because you don't get to touch the file that is the gate logic. The barrel auto-registers every journeys/*.webmcp.ts it finds, and verify enforces the journey rules: gate present, at most five steps, no direct fetch inside a step.

sequenceDiagram
    actor You as Human
    actor Agent as Your coding agent
    participant Browser as Page (WebMCP)
    participant App as Your backend

    Note over Browser: documentTrip.register() ran on load
    Browser->>Agent: tools registered: search-places, set-details, submit (+ the normal tools)
    Agent->>Browser: call document-trip-search-places(input)
    Browser->>App: callApi(getAutocompleteTool)
    App-->>Browser: places[]
    Browser->>Agent: "Still needed: document-trip-set-details"
    Note right of Agent: result stored on an in-memory draft
    Agent->>Browser: call document-trip-set-details({ title })
    Browser->>Agent: "Still needed: document-trip-submit"
    Agent->>Browser: call document-trip-submit()
    Browser->>You: confirm "Create trip 'Kyoto 2025'?"
    You-->>Browser: approve
    Browser->>App: executeCreateTrip(build(draft))
    App-->>Browser: trip created
    Browser-->>Agent: "Done. Trip trip_123 created."
    Note over Browser: draft cleared
Loading

Handshake grouping — one tool per endpoint works until your API splits a single user action across two calls. POST /v1/media/request-upload and POST /v1/media/uploads/{uploadId}/complete are one action (upload a file) that happen to need two HTTP calls. groupHandshakes detects begin/end pairs like that, threads the first response into the second call by exact name (a {uploadId} path param has to find a property named uploadId on the first response's schema, or the pair is skipped with a note), and appends a merged upload-media tool next to the pair as a withheld proposal. You enable it like any write tool; the members stay withheld. Fuzzy cases are skipped deterministically, because a decision you don't have to make is a decision the tool doesn't get wrong.

Chrome's character budgets, applied as guidance — names ≤30, parameter descriptions ≤150, tool descriptions ≤500, single tool output ≤1.5K. Chrome documents these; the spec itself only rejects an empty description or a name outside 1-128 chars, so this PR treats them as authoring guidance, not a rule. Machine-drafted text is composed to fit (sentence cuts at boundaries); author and spec text is never silently shortened; verify warns on an overrun rather than failing CI. The runtime still caps every toolResult at ~1.5K in the generator-owned layer where a hand edit can't remove it.

The agent skill file (.agents/skills/webmcp-tools/SKILL.md) — scaffolded like the runtime, rewritten on every generate, at the cross-client path agents already scan (AGENTS.md, Claude Code, the generic standards). It tells the agent: the character budgets, the description format (Side effect:/Reads from: lines), the confirmation requirement for writes, how to compose journey files (fetchX for data, execute for the agent-facing contract, never fetch inside the file), and the eval harness at packages/codegen/evals/skill/ with 9 cases and deterministic graders. node run.mjs --selftest passes; live agent runs need a CLI available (AGENT_CMD='codex exec --json "{PROMPT}"' node run.mjs).

Spec sync (97da8f5) — the spec evolved since codegen's first pass. We now emit title (the human label Chrome's UI shows), auto-set consequentialHint on destructive tools, and expose exposedTo (origin-scoping) as a config pass-through. References to Chrome 146 in three docs files were stale and are now at 149/150.

Deleted — --llm, --suggest, the provider flow, the config options, the llm.ts module. ~1,050 lines. The skill file is how rules reach models; the CLI itself never calls a model.

The assumptions baked in

If you're reading this from the Chrome team, or from WebMCP at large, this is the section that earns the review:

  • Withheld-by-default is the posture. Every write-tier tool ships unregistered; the developer turns it on. The alternative (register everything, let the agent pick) is the failure mode this CLI exists to prevent.
  • A journey's shared state is in-memory only. The draft object lives for the page load; a tab close or refresh wipes it. That's intentional, a half-finished journey should evaporate rather than linger. And it means two parallel journeys on one page will collide and overwrite each other, an accepted edge case, worth saying out loud.
  • The gate is a registered tool, not a config. WebMCP today has no native journey construct, so the submit gate is a step tool with logic that lives in the file you don't get to edit. When the spec ships one (the skills/tool-groups thread is #161), the factory swaps its internals and user-written journey files don't change.
  • The confirmation prompt is window.confirm, not a fancy native thing. Chrome's own guidance hints at a requestUserInteraction() API for this; it isn't in the spec yet. When it lands, the change is one file, the confirmation logic sits in the same owned region the gate sits in.
  • Grouping is deterministic-only. Begin/end pairs merge when they match exact rules; fuzzy matches are skipped with a note. A clever guess is how plan-trip got written.
  • A step composes a generated endpoint tool, never duplicates it. fetchGetAutocomplete (the raw caller) is exported explicitly so steps reuse one function. Reuse keeps the schema single-sourced.

What to review

Skim these first:

  • packages/codegen/assets/journey.webmcp.ts — the factory; ~215 lines, no new deps.
  • packages/codegen/src/group.ts and src/group.test.ts — the handshake detector and its pinned cases (the beenthere upload pair, the unthreadable pair, the cross-resource pair, the GETs-only negative).
  • packages/codegen/src/verify.ts — the new checks.
  • packages/codegen/assets/skill/SKILL.md and packages/codegen/evals/skill/run.mjs — the contract with the agent, and the graders.
  • packages/codegen/src/outputs/tools-templates.ts — the generated region (titles, annotations, the fetchX raw callers, the gated execute).
  • packages/codegen/src/outputs/generated-code.test.ts — parses every generated template output with the TypeScript compiler, the guard against the runtime bug below.
  • site/content/docs/journeys.mdx and journeys-faq.mdx — the public walkthrough (rewritten around a booking example) and the plain-terms FAQ.
  • site/content/docs/why-agent-tools.mdx, after-you-generate.mdx, working-with-your-agent.mdx, prompt-cookbook.mdx, tool-or-journey.mdx, testing.mdx — the new guides, and the agent-readable /llms.txt and /llms-full.txt feeds.

Updates after review

A review pass on this branch found one real bug and several docs that over-promised. All are fixed, in separate commits:

  • The generated runtime did not parse. The 1.5K truncation notice was written as a "\n…" string inside the outer template literal, so the emitted runtime.webmcp.ts carried a real newline inside a double-quoted string. Every generated runtime was a syntax error. Fixed, and generated-code.test.ts now parses every template output so it cannot recur.
  • Character budgets no longer silently shorten text. Author and spec text is kept in full, machine text is composed to fit, and verify warns instead of failing CI.
  • Journey steps no longer claim read-only when the composed tool writes. The factory inherits the composed tool's readOnlyHint, resolves the WebMCP context at registration time, and composes step descriptions within budget.
  • The surface check counts journey tools (steps plus the submit gate), which the docs claimed but the code did not do.
  • Docs. The journeys guide was rewritten around a relatable example and an explicit mental model; the new pages above cover why it matters, what to do after generating, how to work with your coding agent, prompts, choosing a shape, and testing. The CLI and READMEs point at the new domain, https://webmcp.souravinsights.com.
  • A changeset was added, which the first push was missing.

What landed

66 files changed, +5,934/−1,218. 229 tests pass (232 before the LLM removal: 12 removed with the deleted layer, 9 added for grouping, budgets, journeys, and generated-output parsing). tsc and biome clean. On a beenthere-shaped fixture spec the CLI prints: Grouped create-media-request-upload + complete-media-upload into upload-media. Verify reports: ✓ Journeys: 1 journey file, gates and budgets in order.

Restructure after review: plain language throughout, version claims
verified against source, audit package and dashboard rework marked out
of scope. Journeys section now shows the actual helper contract and
composition-based examples from beenthere's real surface.
journey.webmcp.ts gains ToolStep — a step references the generated tool
object, calls its raw caller, and declares only what lands in the draft
(store), so tool definitions live in one place and never drift. FreeStep
keeps the freeform form for steps with no backend call. SKILL.md updated
to the same shape, with the no-direct-fetch rule.
The concept page walks a complete agent session on beenthere's
document-trip end to end, then the ship/write split and the rules. The
FAQ answers the questions a newcomer actually asks — what the draft is,
why in-memory, what the agent does vs. the human, why journeys shrink
the tool surface instead of growing it.
Verified against the local clone of webmachinelearning/webmcp: budgets
unchanged, naming compliant, withheld-by-default aligned. Gaps found:
the spec's new title member and consequentialHint annotation, exposedTo
now real (our flag-the-gap note is actionable), stale Chrome version
claims. Watch items recorded: requestUserInteraction, skills #161,
output schema #9, native validation #92, declarative API.
Generated tool definitions now carry the spec's USVString title (derived
from the tool name) and set consequentialHint on destructive-confirm
tools; the journey factory does the same for step and submit tools. The
tools output accepts exposedTo origins and passes them to registerTool,
matching the spec's cross-origin exposure option. Chrome version
references corrected to the live 149/150 origin trials.
… 1.5K output cap

Generation now composes within the published budgets: tool descriptions
fit 500 chars and parameter descriptions 150, cut at a sentence boundary
when inherited spec text overflows (fitBudget). Verify measures the
final text and fails CI on offenders — new Budgets check is error-level,
parameter names over 30 join the Names warnings. The runtime caps every
toolResult at ~1.5K with a truncation notice; living in the shared
runtime means it cannot be edited away per tool.
The tools output now drops assets/skill/SKILL.md at
.agents/skills/webmcp-tools/SKILL.md — the cross-client skills location
that Claude Code and other compliant agents scan. Regenerated wholesale
like the runtime (the header comment says why; project-specific rules
stack via the user's own skill directory). Bundled assets ship in the
package (files: dist + assets) and are read with a layout-tolerant
resolver.
… emitted, verify lints them

Every generate now drops journey.webmcp.ts next to the runtime and scans
<outDir>/journeys/*.webmcp.ts into the barrel, which registers every
createJourney() export at page load. Endpoint-backed tools emit a live
fetchX raw caller in the generated region — the default execute composes
it, and journey steps have an honest composition path (withheld tools
included). Verify gains journey checks: createJourney and submit gate
present, descriptions within budget (errors), step count and direct
fetch usage (warnings). Journeys live inside the tools directory; docs
and skill file paths updated to match.
evals/skill/: a real generated surface (beenthere-lite, skills dir
installed), nine cases across core/negative/control kinds, and a runner
that grades files, not transcripts — marker preservation byte-for-byte,
budget measurements, regex allow/forbid lists, tree diffs. The sharpest
case is journey-document-trip: retrospective naming passes, plan-trip is
an automatic fail; its skill-removed twin detects model absorption.
Self-test (node run.mjs --selftest) verifies every grader in both
directions and passes. Running against real agents requires an agent CLI
via AGENT_CMD; results stay local.
POST pairs like request-upload + complete-upload are one action the API
split in two. groupHandshakes detects them deterministically (paired
begin/end verbs, shared noun, same resource, exact-name threading of the
second call's path params from the first response — anything fuzzy is
skipped with a note) and appends a merged withheld tool whose fetchX
runs both calls in order. Members stay untouched; adoption is enabling
the merged tool, and overrides keep that decision across regenerations.
Pipeline notes now print in the CLI summary so the proposal is visible
without opening the dashboard.
The user's own agent with the skill file covers what the layer proposed;
a second model path in the CLI was weight that fought the lean direction.
Deletes llm.ts, both flags and their provider-resolution flow, the config
llm options, the suggestions field and rendering, and the suggest-only
helpers (findSchemaModules, schemaExportsToJson, importSchemaModule).
Plain generate is the whole story: deterministic always.
All six steps landed on this branch: spec sync, budgets, skill + evals,
journeys wiring, handshake grouping, LLM layer removal. The doc records
what each step became, so the plan now reads as history with the parked
items (audit package, dashboard report, URL audit) still named.
@vercel

vercel Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
webmcp-stack Ready Ready Preview Sep 12, 2026 9:06am UTC

The notice was written as a "\n…" string inside the outer template literal
that builds runtime.webmcp.ts, so the emitted file contained a real newline
inside a double-quoted string. Every generated runtime was a syntax error,
which means no generated tool compiled.

Emit the escape instead, and add generated-code.test.ts, which runs every
template output through the TypeScript compiler so an escaping mistake cannot
ship again. Regenerate the eval fixture's runtime to match.
Chrome's 500/150 character budgets are authoring advice, not browser rules:
the spec only rejects an empty description or a name outside 1-128 chars.

Generation no longer silently shortens text a person or a spec wrote. Machine
drafted text is composed to fit; author text is kept in full, and verify
reports an overrun as a warning for both tools and journey files. fitBudget
also reserves room for its ellipsis, so an unbroken token can no longer come
back one character over budget.

The surface check now counts journey tools too (one per step plus the submit
gate), because they register at runtime and previously escaped the total.
…tions

A step's call and run are user code, so the factory cannot know whether a
step writes. Steps now inherit the composed tool's readOnlyHint (a free step
with no run reads as a read) instead of always claiming read-only, with an
opt-in readOnly override for the cases the author knows better.

Resolve the WebMCP context at registration time, matching generated tools, so
a browser or polyfill that installs WebMCP after the module loads still gets
the journey registered.

Compose step descriptions within the 500-char budget: keep the journey name,
drop the repeated goal, and fit the base when the goal would overflow.
Root README: drop the deleted LLM flags, mark the character budgets, output
cap, and exposedTo as shipped, describe grouping and journeys and the skill
file, correct the generated-tool example, and fix the example package name.

journeys.md spec: record that the shipped shape is TypeScript files that call
createJourney, not the config block this early spec described.

Journeys docs and FAQ: the surface check warns and now counts journey tools;
absorption and step safety are described as conventions the developer follows,
not guarantees the tool enforces.

Skill file: stop claiming a name override in .webmcp-codegen.json (only
description, enabled, and field text are supported), and teach the derived
read-only step hint.
Three gaps were filled:

- "Why give your site tools for agents" explains the problem, why a small
  intent-shaped surface beats a mirror of the API, what the tool does and does
  not do, and where WebMCP honestly is today.
- "After you generate" is the loop that turns a generated surface into one you
  trust: review it, enable writes deliberately, fix descriptions, test in the
  browser, write journeys, wire CI, regenerate.
- "Working with your coding agent" covers the shipped skill file, how to prompt
  an agent to improve descriptions and write journeys, example prompts, and what
  to check when it is done.

The Introduction is reframed around the same goal, with an honest "what it does
not do" section and links into the new pages; the Quickstart points onward.
Both pages were missing the closing `---`, so the title and description were
parsed as body text and came back undefined in page metadata and in the
generated llms.txt.
Publish /llms.txt as a curated index of every docs page, /llms-full.txt as the
whole docs set in one markdown file for a single read, a robots.txt that allows
crawlers and points at the sitemap, and sitemap.xml from the docs source.
run.mjs imported execFile/promisify only to void them. group.test.ts asserts
an emitted template literal, which biome's noTemplateCurlyInString reads as a
mistake; silencing it beats rewriting the assertion to satisfy the linter.
Covers the shipped journeys, grouping, budgets, skill file, and the removed
LLM layer, plus the fixes in this branch. Publishing stays with the maintainer.
The review gained the runtime escaping bug found while typechecking the
journey factory, the budget-is-guidance reasoning, and a short section on what
was implemented.
… model

The page told a story about a specific product and left the reader to extract
how journeys actually work. It now:

- opens with the problem (a goal that takes several calls and needs a value the
  agent cannot invent), so the reader knows why the feature exists;
- frames a journey as a form filled in steps, with the submit as the one
  confirmed Send button;
- walks one complete example, booking a ride by resolving a destination, then
  setting a pickup time, then requesting the ride behind the gate;
- shows the agent's side of the protocol, including the "still needed" replies;
- explains how state moves through call, store, provides, and build.

The FAQ and the two guides added earlier use the same example so the docs read
as one set.
Steps for procedures, Tabs for alternatives, Accordions for optional detail,
and Files for directory trees, registered once so docs pages use them without
an import.
Three practical gaps:

- Prompt cookbook: copy-paste prompts for the recurring jobs (improve a
  description, write a journey, review the surface, fix verify, do a safety
  pass), each with what to check afterwards.
- "Tool, group, or journey?": the decision guide for which shape a capability
  takes, with signs for and against, and the edge cases in accordions.
- Testing your tools: how to test a generated tool without a browser, what
  throws and why, how to test a journey's store and build, and the one thing
  that still needs a browser.
- Quickstart: Tabs for the Next.js and Vite registration edits.
- After you generate: a Files tree of the tools directory.
- Journeys: inline code annotations on store, provides, and build, the lines
  the state-movement section talks about.
- Working with your coding agent: point at the prompt cookbook.
The full-text feed is raw markdown, so Fumadocs' presentation-only
"// [!code highlight]" markers were leaking into it.
The origin was hardcoded in four files. It now lives in lib/site-url.ts,
overridable with NEXT_PUBLIC_SITE_URL, so page metadata, sitemap, robots, and
the llms.txt feeds cannot drift apart again.
The production site moved to https://webmcp.souravinsights.com, so the docs
links printed by the CLI and shown in both READMEs follow it.
Replace special punctuation with characters any keyboard can type, across the
public docs, the CLI output, and the files the generator writes.

The generated region marker becomes plain hyphens; reads still accept the old
box-drawing marker so files generated by an earlier version migrate on the next
run instead of looking hand-edited. Fixture and example regenerated to match.
The changeset for this release read like a feature inventory. It now leads
with the user outcome, keeps the journey example, and folds the docs-domain
note into the docs paragraph so the release is one entry.
Drop the removed `llm` config, use the withheld wording the tool now uses, and
cover the release's headline features: grouping, journeys, the agent skill
file, the budget policy, and `verify`.

Also replace the vague opening with what the tool actually turns into tools,
and add a short "Where this fits" section: it generates from a contract you
maintain, not from a codebase scan. Absolute docs links so they work on npm.
Add a short "Who this is for" on the why page, a matching bullet on the docs
landing, and a line in the root README. The point, in one place: it generates
from a contract you maintain, not from a scan of your code, so a vague
contract makes vague tools. That is a deliberate trade, not a hidden gap.
Wire up Fumadocs' Mermaid support: `remarkMdxMermaid` turns a ```mermaid block
into `<Mermaid chart="..." />`, and the client component renders it with the
official renderer. Mermaid is imported on demand, so pages without a diagram
never load it.
The journeys page trades its text transcript for a sequence diagram of the
agent filling the draft, the human confirming, and the real write running. The
visible-effects page gains the same view of one call, including the UI update
that is easy to forget. Both stay as text in the raw markdown, so they also
read well in llms-full.txt.
The docs render the title from frontmatter above the body, so a leading `#`
heading repeated it on the journeys FAQ and the visible-effects page.

Reading the rest of the pages turned up the same content written twice across
pages:

- working-with-your-agent repeated three prompts that already live, verbatim,
  in the prompt cookbook.
- the cookbook's warning restated the agent-exposure section on that page, now
  a link instead.
- the introduction and the why page shared the same browser-support sentence.
- the guides page carried a full dashboard section that duplicates the CLI
  reference.

Title headings, duplicate answers, and cross-page copies are all in one commit
so the cleanup is easy to review or revert together.
"Why give your site tools for agents" made the site the owner of the tools
and read like a teaser. "Why make your site agent-ready" says the same thing
in plain words. The intro card matches, and the URL is unchanged.
@SouravInsights
SouravInsights merged commit 193a696 into main Sep 12, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Preview — 1a49cad5 Deployed Sep 12, 2026 by vercel[bot]
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.

1 participant