Skip to content

feat(agent): agent-native MCP wave — bridges, core surface, instance registry, devframe connect - #145

Open
antfubot wants to merge 6 commits into
mainfrom
feat/agent-mcp-wave-phase-3
Open

feat(agent): agent-native MCP wave — bridges, core surface, instance registry, devframe connect#145
antfubot wants to merge 6 commits into
mainfrom
feat/agent-mcp-wave-phase-3

Conversation

@antfubot

@antfubot antfubot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Why

The full agent-native MCP wave (plan 031). Vercel's next-devtools-mcp 0.4.0 validated an architecture devframe was already positioned for: the real MCP endpoint lives inside the framework, and a thin external connector just discovers and proxies it. This wave builds both halves — the embedded surface and the connector — proven end-to-end, including the literal /_next/mcp shape on devframe primitives via @devframes/next.

Supersedes #142 (phase 1) and #144 (phase 2), both folded in here.

Phase 1 — bridges & structured errors

  • Bridge MCP forwarding. viteDevBridge and @devframes/next's createDevframeNextHandler gain an mcp option (forwarded to createDevServer) and advertise the side-car endpoint in their __connection.jsonConnectionMeta['mcp'] gains an optional port for side-car origins. Shared resolver resolveMcpConnectionMeta.
  • Structured diagnostic errors. formatMcpError emits { error: { code, message, fix?, docs? } } for nostics Diagnostics, so agents get the actionable next step and docs URL instead of a flattened string.
  • Docs. Agent-native guide documents the conventions: description-as-prompt, gateway tools, structured errors.

Phase 2 — core surface

  • createMcpFetchHandler(ctx, options) — the MCP Streamable-HTTP endpoint as a framework-agnostic web-standard Request → Response handler; mountMcpHttp is a thin h3 wrapper over it.
  • Built-in devframe:state:read tool — tool-shaped shared-state access (no key → key list, key → JSON value), honoring the exposeSharedState filter alongside the resource projection.
  • Hub commands → agent surface, as a lazy provider. New core API ctx.agent.registerToolProvider(() => AgentToolInput[]): a tool source queried at list/getTool/invoke time, the same on-demand projection applied to agent-flagged RPCs. The commands host registers one provider deriving tools from its commands map — single source of truth, nothing mirrored. Commands opt in via an agent field (description, safety, optional valibot args); agent on a handler-less group throws DF8404; the field never crosses the wire.
  • Schema-typed handlers may be async — the schema-typed definition branch types handlers as Thenable<InferReturnType<RS>>.
  • git agent surfacestatus/log/show/branches/diff agent-flagged with args/returns schemas (safety: 'read'); writes stay agent-invisible. Cross-refs plan 029.

Phase 3 — instance registry, connector, in-process Next MCP

  • Instance registry (devframe/node): registerDevframeInstance writes an atomic record to ~/.devframe/instances/<pid>-<port>.json; readers prune dead records on failed __connection.json probes, dedup same-port ghosts (newest wins), and adopt a dialable origin for family-ambiguous localhost binds. createDevServer registers automatically and unregisters on close; in-process hosts call it explicitly. DEVFRAME_INSTANCES_DIR / DEVFRAME_DISABLE_INSTANCE_REGISTRY override. Failures degrade to coded warnings (DF0042).
  • devframe bin + connect — the package's first bin. devframe connect runs a stdio MCP connector exposing two gateway tools: devframe:connect:list-instances (discover live instances + list their MCP tools; MCP-less instances carry a restart-with---mcp funnel hint) and devframe:connect:call-tool (proxy one tool call over Streamable-HTTP). Errors carry { message, fix }; a missing SDK peer throws DF0043. --port probes beside the registry.
  • In-process Next MCPDevframeNextHost.mountMcp(ctx, path) serves the fetch handler on the Next app's own origin (the /_next/mcp shape). The hub example mounts /__hub/__mcp, advertises it, registers the instance, and agent-flags its ping command.
  • MCP conformance fix — non-object outputSchema projections are dropped (a v.void() returns schema produced type: null, which SDK clients reject).

API surface

All wave tool names follow the devframe:<area>:<fn> convention. The public surface is deliberately minimal — only members with a consumer outside their own package are exported (createMcpFetchHandler, registerToolProvider, registerDevframeInstance + its two types, resolveMcpConnectionMeta, and the feature options). valibot→JSON-Schema conversion and the registry read/probe/prune helpers stay internal.

Verification

Full gate: pnpm lint && pnpm test && pnpm typecheck && pnpm build905 unit tests and 17/17 Playwright e2e, including two connector gates: files-inspector (discovery → gateway-tool round-trip → actionable errors) and minimal-next-devframe-hub (connector discovers the hub inside the Next dev server and calls its agent-flagged command through the in-process endpoint).


This PR was created with the help of an agent.

@antfubot

Copy link
Copy Markdown
Collaborator Author

Follow-up commit d1d9373 (also addresses feedback that applies to #144's commands bridge):

  • Lazy tool providers replace imperative sync. New core API ctx.agent.registerToolProvider(() => AgentToolInput[]) — a tool source queried at list/getTool/invoke time, the same on-demand projection the agent host already applies to agent-flagged RPC definitions. The hub commands host now registers one provider deriving tools from its commands map (single source of truth); the registerAgentTools/unregisterAgentTools mirror and its handle bookkeeping are gone. handle.notifyChanged() still drives MCP tools/list_changed.
  • Tool names follow the devframe:<area>:<fn> convention (matching devframe:agent:list-tools etc.): read_statedevframe:state:read, devframe_indexdevframe:connect:list-instances, devframe_calldevframe:connect:call-tool.

Gates rerun: 905 unit tests, typecheck, lint, and the three connector e2e specs all green. (tsnapi flagged the removed bridge methods as a narrowing — allowed deliberately since they only ever existed in this unmerged stack.)

@antfubot antfubot changed the title feat(agent): agent-native wave phase 3 — instance registry, devframe connect, in-process Next MCP feat(agent): agent-native wave phases 2+3 — core surface, instance registry, devframe connect Jul 29, 2026
@antfubot
antfubot changed the base branch from feat/agent-mcp-wave-phase-2 to feat/agent-mcp-wave-phase-1 July 29, 2026 08:39
@antfubot

Copy link
Copy Markdown
Collaborator Author

Follow-up commit b4b9bb0 shrinks the public API surface added by this wave:

  • devframe/utils/valibot-json-schema subpath removed. AgentToolInput gains optional valibot args — the same shape RPC definitions carry ("schemas are carried by the definition; consumers convert on demand"). The agent host derives the JSON-Schema input internally, the hub passes command schemas through untouched, and valibot→JSON-Schema conversion is an implementation detail again. Net: −1 public subpath (−2 exported functions), +1 optional field on an existing @experimental type.
  • Instance registry trimmed from 9 exports to 3. devframe/node now exports only registerDevframeInstance + DevframeInstanceRecord / DevframeInstanceRegistration — what custom hosts actually need. The read/probe/prune/list helpers and env-var consts stay internal to the connector (same package, relative imports).

Gates rerun: 905 unit tests (incl. new args-projection coverage), typecheck, lint, 3/3 connector e2e.

@antfubot antfubot changed the title feat(agent): agent-native wave phases 2+3 — core surface, instance registry, devframe connect feat(agent): agent-native MCP wave — bridges, core surface, instance registry, devframe connect Jul 30, 2026
@antfubot
antfubot changed the base branch from feat/agent-mcp-wave-phase-1 to main July 30, 2026 01:25
antfubot added 5 commits July 30, 2026 01:42
…ommands bridge, git agent tools

- createMcpFetchHandler: framework-agnostic web-standard MCP endpoint
  extracted from mountMcpHttp (now a thin h3 wrapper); exported from
  devframe/adapters/mcp for custom hosts (Next App Router, etc.)
- built-in read_state(key?) MCP tool over shared state, honoring the
  exposeSharedState filter alongside the resource projection
- hub commands gain opt-in agent exposure: agent field (description,
  safety, valibot args) projects handler-bearing commands into ctx.agent;
  DF8404 rejects agent exposure on group-only commands
- valibot→JSON-Schema conversion moved to devframe/utils/valibot-json-schema
  (public) so SDK-free hosts can convert schemas
- rpc: schema-typed handlers may be async — Thenable<InferReturnType<RS>>
  in the schema-typed definition branch
- git plugin: status/log/show/branches/diff agent-flagged with valibot
  args/returns schemas (read-only surface; writes stay private)
- docs: hub commands-as-tools, read_state, custom-host mounting; DF8404 page
…connect, in-process Next MCP

- instance registry: registerDevframeInstance/readDevframeInstances/
  probeDevframeInstance/listLiveDevframeInstances in devframe/node —
  atomic same-dir writes, prune-on-read, ghost dedup per (port, basePath),
  dialable-origin adoption for family-ambiguous localhost binds;
  createDevServer registers automatically and unregisters on close;
  DEVFRAME_INSTANCES_DIR / DEVFRAME_DISABLE_INSTANCE_REGISTRY overrides
- first devframe bin: `devframe connect` runs the stdio MCP connector —
  devframe_index (discover instances + their tools, funnel hints for
  MCP-less servers) and devframe_call (proxy one tool call over
  Streamable-HTTP); errors carry actionable fix payloads; missing SDK
  peer throws coded DF0043
- @devframes/next: DevframeNextHost.mountMcp serves MCP in-process on the
  Next app's own origin (the /_next/mcp shape); hub example wires it,
  advertises it in connection meta, registers the instance, and
  agent-flags its ping command; catch-all route exports POST/DELETE
- mcp adapter: drop non-object outputSchema projections (MCP requires
  type object; a v.void() returns schema broke SDK clients)
- e2e: devframe-connect (files-inspector round-trip incl. gateway tool)
  and minimal-next-devframe-hub (in-process discovery + command call);
  hermetic per-suite registries; vitest keeps unit runs out of the
  global registry
- diagnostics DF0042/DF0043 + docs pages; connect/registry docs in the
  MCP adapter page
- ctx.agent.registerToolProvider(() => AgentToolInput[]): a lazy tool
  source queried at list/getTool/invoke time — the same on-demand
  projection applied to agent-flagged RPCs; earlier sources win on id
  collision; handle.notifyChanged() drives tools/list_changed
- hub commands host derives its agent projection through one provider:
  the commands map is the single source of truth, replacing the
  registerAgentTools/unregisterAgentTools mirror and its handle map
- built-in and connector tool names follow the devframe:<area>:<fn>
  convention: read_state -> devframe:state:read, devframe_index ->
  devframe:connect:list-instances, devframe_call ->
  devframe:connect:call-tool
- drop the devframe/utils/valibot-json-schema subpath: AgentToolInput
  gains valibot args (the same shape RPC definitions carry); the agent
  host derives the JSON-Schema input internally, and the hub passes
  schemas through untouched — conversion is an implementation detail
  again
- devframe/node exports only registerDevframeInstance (+ its two types)
  from the instance registry; the read/probe/prune helpers stay internal
  to the connector
…space

main renamed examples/minimal-next-devframe-hub -> examples/next-devframe-hub
and its RPC/command/instance ids to the example:next-devframe-hub
convention (#143); this wave's additions (MCP serverName, registry
instance id, e2e spec file + assertions, playwright cwd) now match.
Same fix for files-inspector's example:files-inspector id/namespace.

Also regenerates the recipes/common-rpc-functions + recipes/open-helpers
dts snapshots for phase 2's Thenable<InferReturnType<RS>> handler-return
widening (non-breaking — allowed to update without --allow-breaking).
@antfubot
antfubot force-pushed the feat/agent-mcp-wave-phase-3 branch from b4b9bb0 to ef2598e Compare July 30, 2026 03:45
@netlify

netlify Bot commented Jul 30, 2026

Copy link
Copy Markdown

Deploy Preview for devfra ready!

Name Link
🔨 Latest commit 158eb99
🔍 Latest deploy log https://app.netlify.com/projects/devfra/deploys/6a6c0a812af7e10008711573
😎 Deploy Preview https://deploy-preview-145--devfra.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@antfubot

Copy link
Copy Markdown
Collaborator Author

Rebased onto main and retargeted from the phase-1 branch — phase 1 (#142) is already merged there, so this PR is now the clean phases 2+3 delta on top of it, with no phase-1 duplication.

Conflicts resolved during the rebase (main moved 8 commits, incl. #143's example-dir rename and #141's openHelperscommonRpcFunctions deprecation):

  • tests/__snapshots__/tsnapi/devframe/recipes/open-helpers.snapshot.d.ts — took main's deprecated-alias version; later regenerated cleanly (non-breaking) once phase 2's Thenable<InferReturnType<RS>> handler-return widening applied to it and common-rpc-functions.
  • examples/{minimal-next-devframe-hub → next-devframe-hub}/... (route handler + test) — git's rename detection carried my edits into the renamed path; merged main's ensureNextDevframeHub rename with my POST/DELETE/GET handler addition.
  • Follow-up fixups (ef2598e) to fully align with chore: rename minimal-* example dirs, reorganize docs nav #143's rename: the MCP serverName, registry instance id, e2e spec (renamed minimal-next-devframe-hub-dev.spec.tsnext-devframe-hub-dev.spec.ts) all now use example:next-devframe-hub; same fix for files-inspector's example:files-inspector id/namespace in devframe-connect.spec.ts; playwright.config.ts's stale cwd.

Full gate rerun clean post-rebase: 911 unit tests, typecheck, lint, build, and 17/17 Playwright e2e.

Bare `devframe` (no subcommand) silently exited 0 — cac only shows help
automatically for an explicit -h/--help flag, not an unmatched command.
Check matchedCommand after parse() and fall back to outputHelp(), guarding
against the already-handled --help case so it doesn't print twice.
@antfubot

Copy link
Copy Markdown
Collaborator Author

Follow-up commit 158eb99: bare devframe (no subcommand) now shows help instead of silently exiting 0.

cac only auto-prints help for an explicit -h/--help flag — an unmatched command (bare invocation, or an unrecognized subcommand like devframe bogus) left matchedCommand unset with no fallback, so devframe alone did nothing. Now checks cli.matchedCommand after parse and falls back to cli.outputHelp(), guarded against cli.options.help so --help itself isn't printed twice (cac already handles that case internally).

4 new unit tests cover bare invocation, --help, an unrecognized subcommand, and a real subcommand's own help — asserting help prints exactly once in each case. Full gate green: 915 tests, typecheck, lint, build.

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