diff --git a/.llm/runs/docs-448-positioning-ai-mcp--docs/worklog.md b/.llm/runs/docs-448-positioning-ai-mcp--docs/worklog.md new file mode 100644 index 000000000..cdcd4314c --- /dev/null +++ b/.llm/runs/docs-448-positioning-ai-mcp--docs/worklog.md @@ -0,0 +1,121 @@ +# Worklog — docs/448-positioning-ai-mcp (issue #448, D9) + +Branch: `docs/448-positioning-ai-mcp` from `7f7ed76be66fbfcf1133f7c4bcab33737aa09c78`. +Slice: AI-stack + MCP positioning stories (`docs/site/ai/{index,mcp}.md`) plus elimination of the +stale "arrives from 0.0.1-beta.2 / publish:false" claims in `ai/engine.md` and `ai/durable-chat.md`. + +## Gate pre-flight (proposal §3.5 verification obligation) + +The issue was GATED on `@netscript/ai` publish state. Verified satisfied: + +| Check | Command | Result | +|---|---|---| +| `@netscript/ai` published | `curl jsr.io/@netscript/ai/meta.json` | `0.0.1-beta.7` present (2026-07-11T12:10:53Z) | +| Engine root surface | `deno doc jsr:@netscript/ai@0.0.1-beta.7` (scratch config, `minimumDependencyAge: "0"`) | `createAiRuntime` / `getAiRuntime` / model registry confirmed | +| `@netscript/plugin-ai` published | JSR meta.json | `0.0.1-beta.7` present | +| `@netscript/plugin-ai-core` published | JSR meta.json | `0.0.1-beta.7` present | +| `@netscript/fresh` beta line | JSR meta.json | `0.0.1-beta.7` present | +| `@netscript/fresh-ui` published | JSR meta.json | `0.0.1-beta.7` present, exports `./ai/render-ui` | + +Per proposal §3.5's owner lever: the engine has shipped, so engine content becomes present-tense +(no more caveated forward-ref for the engine itself). + +## Plan + +1. `ai/mcp.md` — fill the #433 stub with the D9 MCP story: elevator pitch → story spine → + mechanism (transports / auth / pool / registry registration / `ui://` render path, all + deno-doc-traceable) → one factual competitor comparison (Encore MCP blog, direction inverted: + Encore ships an MCP *server* for its backend; NetScript's shipped surface is an MCP *client* + stack) → cross-links. Explicit non-claim: no scaffolded NetScript MCP server. +2. `ai/index.md` — story-template pass (pitch, spine, one factual comparison: Medusa + agents-as-operators / Convex agent-building-blocks vs NetScript's published-layer stack); + replace the stale "beta.2-pending" callout with the beta.7 shipped state; fix + `/capabilities/*` redirect-stub links to canonical pillar paths; add the MCP card; correct the + `netscript generate ai` claim (codegen NOT shipped — CLI `generate` targets today are + `plugins`, `runtime-schemas`, `aspire`). +3. `ai/engine.md` — replace the "Available from 0.0.1-beta.2 / not published" callout with the + published beta.7 state; rewrite "Generated runtime registries" as a clearly-caveated + forward-reference (codegen unshipped); fix the "beta.2 engine" featureGrid copy; link the MCP + transports section to the new story page. +4. `ai/durable-chat.md` — drop the "beta.2 engine" phrasing; badge alpha → beta. + +## API-claim verification table (every present-tense symbol claim on the new/edited prose) + +All via `deno doc jsr:@netscript/ai@0.0.1-beta.7[/]` in a scratch dir with +`minimumDependencyAge: "0"` unless noted. + +| Claim (page) | Verification | Found | +|---|---|---| +| Export map `.` `./contracts` `./ports` `./tools` `./agent` `./mcp` `./anthropic` `./openai-compatible` `./openrouter` `./ollama` `./openai-embeddings` `./testing` (index, engine) | `0.0.1-beta.7_meta.json` exports | yes — exact match | +| `createMcpTransport(config: McpTransportConfig): McpTransportPort` (mcp) | `deno doc .../mcp` | yes | +| `McpTransportConfig` discriminants `stdio` / `streamable-http`; `MCP_TRANSPORT_KINDS` (mcp) | `deno doc .../mcp` | yes | +| `StreamableHttpMcpTransport` "Reconnectable Streamable-HTTP MCP transport"; `McpBackoffConfig`; `McpConnectionState = disconnected\|connecting\|connected\|reconnecting\|closed`; `onStateChange`/`McpStateChangeHandler` (mcp) | `deno doc .../mcp` | yes | +| `McpAuthConfig` = `none` / `api-token {token, headerName?, scheme?}` / `oauth {accessToken, tokenType?}` (mcp) | `deno doc .../mcp` | yes | +| `createMcpTransportPool(config)` "multi-server MCP pool from serializable transport configs"; `McpTransportPool` members `serverId`, `state`, `serverIds`, `server()`, `connect()` "prefixed tool descriptors", `reconnect()`, `listTools()` "without tearing down warm connections", `callTool()` "Call a prefixed pooled tool name and extract ui:// resources", `onStateChange`, `stop()`; `createMcpTransportPoolFromTransports` (mcp) | `deno doc --filter McpTransportPool .../mcp` | yes | +| `McpPooledToolResult extends McpToolResult` with `uiResources` (mcp) | `deno doc .../mcp` + `packages/ai/README.md` §MCP ("pooled tool results surface embedded ui:// resources as uiResources") | yes | +| `registerMcpTools(registry, transport)` → `McpToolRegistration { toolNames, stop() }` (mcp, engine) | `deno doc .../mcp` (module example + `McpToolRegistration` "Handle returned by registerMcpTools") | yes | +| `extractMcpUiResources(result): readonly McpUiResource[]` "data-only ui:// resources" (mcp) | `deno doc .../mcp` | yes | +| `createToolRegistry` / dispatch (mcp example) | `deno doc .../tools` (prior pass) + README | yes | +| Agent-loop telemetry: injected `TelemetryPort`, `gen_ai.chat` run span, `gen_ai.chat.turn` per-turn span, `gen_ai.tool.call` events (index) | `packages/ai/README.md` §"Agent telemetry (injected TelemetryPort)" (canonical README, PR #610) | yes | +| `createAiRuntime` pure wiring / `getAiRuntime` singleton (index, engine — pre-existing prose retained) | `deno doc jsr:@netscript/ai@0.0.1-beta.7` | yes | +| `@netscript/fresh-ui` `./ai/render-ui`: `RenderUiSurface`, `renderUiPayload`, `RENDER_UI_MAX_DEPTH = 6`, block types `stack\|grid\|section\|chart\|metric\|table\|list\|card`, fallback reasons `max-depth\|unknown-type\|invalid-node` (mcp/index cross-refs) | `deno doc jsr:@netscript/fresh-ui@0.0.1-beta.7/ai/render-ui` | yes | +| `McpUiWidget` island installs via the fresh-ui copy registry (`netscript ui:add ai`) (mcp) | `packages/fresh-ui/registry/islands/McpUiWidget.tsx` + `registry.manifest.ts`; `ui:add` in `packages/cli/src/public/features/ui/add/` | yes | +| `createMcpSandboxHandler` on `@netscript/fresh/ai/sandbox` (mcp cross-ref; durable-chat prose pre-existing) | existing durable-chat page + `@netscript/fresh@0.0.1-beta.7` published | yes | +| `netscript generate ai` NOT shipped (engine, index corrections) | `ls packages/cli/src/public/features/generate/` → `aspire`, `plugins`, `runtime-schemas` only | confirmed absent | +| No scaffolded NetScript MCP server (mcp non-claim) | context pack (#448 comment) + no server surface in `deno doc .../mcp` | confirmed absent | +| CLI E2E merge gate exercises a real streamable-HTTP MCP round trip into `McpUiWidget` (mcp proof line) | context pack (PR #600 / FAI-9, `scaffold.runtime` gate) | yes | + +## Competitor comparisons (one per page, factual) + +- `ai/index.md`: Medusa (agents-as-first-class-operators hero + Build/Optimize/Operate blog) and + Convex ("backend building blocks for your agents") — shape comparison only, no multipliers, no + superlatives, sourced from `research/D-positioning/competitor-teardown.md` (not `_plan/*`). +- `ai/mcp.md`: Encore MCP-server blog — factual direction contrast (server-side introspection vs + NetScript's client-side consumption), no borrowed numbers. + +## Resumption (predecessor died mid-flight on spend limit) + +Predecessor had committed nothing; working tree held finished `ai/{index,mcp}.md` plus this +worklog, but the planned `ai/engine.md` (step 3) and `ai/durable-chat.md` (step 4) edits were +never applied — `git status` showed only index/mcp modified and both files still carried +`beta.2` claims. Resumed in place on branch `docs/448-positioning-ai-mcp`. + +Reviewed predecessor `index.md`/`mcp.md`: prose is deno-doc-traceable against the API-claim table, +positioning-law-clean, one comparison per page, correct pillar cross-links. Accepted as-is. + +Completed the pillar (this session): + +- `ai/engine.md`: replaced the "Available from 0.0.1-beta.2 / not published" `important` callout + with a `note` "Published in 0.0.1-beta.7" (installable now, zero `@netscript/*` deps); rewrote + "Generated runtime registries" (which asserted a shipped `netscript generate ai`) as "Wiring tool + and agent registries" — direct `createToolRegistry`/`createAgentLoop` wiring plus a caveated + `note` that `generate ai` is NOT in the CLI (shipped targets: `plugins`, `runtime-schemas`, + `aspire`, confirmed by `ls packages/cli/src/public/features/generate/`); dropped "ahead of the + beta.2 engine" / "Ships today —" featureGrid framing now that the engine ships. +- `ai/durable-chat.md`: rewrote the "ships and installs today, independently of the [beta.2 engine]" + sentence to "composes with the [AI engine] but installs independently"; pinned the JSR line to + `@netscript/fresh@0.0.1-beta.7`. + +### Badge decision (deviation from predecessor plan step 4) + +Predecessor plan said "badge alpha → beta" on `durable-chat.md`. **Left as `alpha`.** The +`badge.vto` component supports only `stable|alpha|partial` (no `beta` variant — any other value +falls back to muted styling), and the site chrome brands NetScript site-wide as "Alpha software" +(`base.vto`). The badge is a maturity indicator, not a version/availability claim, so it is outside +the stale-`beta.2`/`publish:false` remediation scope and changing it would be both unsupported by +the component and inconsistent with the site framing. + +## Evidence + +Grep verdicts (whole `docs/site/ai/` pillar), post-edit: + +- `beta[.\- ]?2` / `publish.*false` → **(clean)** +- `not published` / `does not resolve` / `arrives in|from` → **(clean)** +- positioning-law (`honest|candor|throughput|fastest|best-in-class|blazing|Nx faster|N% faster`) → + **(clean)** + +`deno task verify` in `docs/site` — **GREEN**: + +- build → `🍾 Site built into _site` (500 files, 8.45s) +- `check:links` → `23029 internal links across 162 pages — all resolve` +- `check:caveats` → `27 caveat markers across 22 pages — all references resolve` diff --git a/docs/site/ai/durable-chat.md b/docs/site/ai/durable-chat.md index 37000c91a..e57bb5b14 100644 --- a/docs/site/ai/durable-chat.md +++ b/docs/site/ai/durable-chat.md @@ -16,10 +16,11 @@ stream rather than component state. {{ comp.badge({ status: "alpha" }) }} It composes three upstream libraries — `@durable-streams/tanstack-ai-transport`, `@tanstack/ai-preact`, and `@tanstack/ai` — and adds only NetScript glue: durable-stream URL resolution, server-side auth headers, and the projection law below. It does **not** -import `@netscript/ai`, which is why it ships and installs today, independently of the -[beta.2 engine](/ai/engine/). +import `@netscript/ai`, so it composes with the [AI engine](/ai/engine/) but installs +independently — you can adopt durable chat without pulling in the engine. -This subpath is published on JSR as part of `@netscript/fresh` and is usable now. +This subpath is published on JSR as part of `@netscript/fresh@0.0.1-beta.7` and is usable +now. ## The primitives at a glance diff --git a/docs/site/ai/engine.md b/docs/site/ai/engine.md index 87657cc77..9703376db 100644 --- a/docs/site/ai/engine.md +++ b/docs/site/ai/engine.md @@ -14,13 +14,13 @@ vocabulary, the capability seams, the model registries, the tool system, the age loop, MCP transports, and opt-in provider adapters. It wraps `@tanstack/ai*` and `@standard-schema/spec` and adds no schema DSL of its own. -{{ comp callout { type: "important", title: "Available from 0.0.1-beta.2" } }} -@netscript/ai is not published to JSR yet — it arrives in -0.0.1-beta.2. Do not run deno add jsr:@netscript/ai or -import @netscript/ai/* in an app today; the install does not resolve. This -page documents the surface you will compose against once beta.2 lands. What ships -now is the self-contained durable-chat runtime -and the chat UI, neither of which depends on this engine. +{{ comp callout { type: "note", title: "Published in 0.0.1-beta.7" } }} +@netscript/ai is published on JSR and installs today: +deno add jsr:@netscript/ai. Every subpath on this page resolves against the +published 0.0.1-beta.7 surface. The engine carries zero +@netscript/* dependencies, so you can adopt it on its own — the +durable-chat runtime and chat UI +each stand alone and neither requires it. {{ /comp }} ## The export map @@ -210,21 +210,21 @@ a network: `createFakeChatModelProvider(id, turns)`, `createFakeAgentLoop(chunks `createInMemoryToolRegistry()`, and `createFakeTelemetryPort()` (whose `.records` capture emitted telemetry). -## Generated runtime registries +## Wiring tool and agent registries -`netscript generate ai` produces the wiring that turns your app-owned tool and agent -files into engine-ready registries. It generates **two targets, and only two**: +Today you wire the engine's registries **directly** in your composition root: +`createToolRegistry(defs?)` from `@netscript/ai/tools` for tools and +`createAgentLoop(deps)` from `@netscript/ai/agent` for agent loops (both documented +above). No codegen step sits between your files and the runtime — you register against +the engine factories yourself. -{{ comp.apiTable({ - caption: "netscript generate ai — the two targets", - rows: [ - { name: "ai-tools", type: "name-keyed registry", desc: "Keyed by `descriptor.name`; values are `AiToolDefinition` (from `@netscript/ai/tools`)." }, - { name: "ai-agents", type: "stem-keyed registry", desc: "Keyed by file stem; values are a `() => AgentLoop` factory map." } - ] -}) }} - -There is no `skills` target in this cut. Like the rest of the engine, the codegen is part -of the beta.2 cut. +{{ comp callout { type: "note", title: "Not in this release: netscript generate ai" } }} +A netscript generate ai codegen — compiling app-owned tool and agent files +into name-keyed and stem-keyed registries — is a planned target but is not in +the CLI today. The shipped netscript generate targets are +plugins, runtime-schemas, and aspire. Until an AI +target lands, register tools and agent loops against the engine factories directly. +{{ /comp }} ## Reference @@ -242,13 +242,13 @@ of the beta.2 cut. icon: "←" }, { - title: "Ships today — durable chat", - body: "The self-contained runtime you can build on now, ahead of the beta.2 engine.", + title: "Durable chat", + body: "The self-contained Fresh runtime for a durable AI chat — composes with this engine but does not require it.", href: "/ai/durable-chat/", icon: "◆" }, { - title: "Ships today — chat UI", + title: "Chat UI", body: "The copy-registry components that render an agent transcript, including render_ui targets.", href: "/ai/chat-ui/", icon: "◆" diff --git a/docs/site/ai/index.md b/docs/site/ai/index.md index 2564365c3..85a4eb84e 100644 --- a/docs/site/ai/index.md +++ b/docs/site/ai/index.md @@ -3,7 +3,7 @@ layout: layouts/base.vto title: AI templateEngine: [vento, md] prev: null -next: { label: "Durable chat", href: "/ai/durable-chat/" } +next: { label: "MCP", href: "/ai/mcp/" } --- # AI @@ -11,22 +11,50 @@ next: { label: "Durable chat", href: "/ai/durable-chat/" } The NetScript AI stack is a set of composable seams for building agentic chat and tool-calling surfaces on the same contracts-first, hexagonal foundation as the rest of the framework. It is deliberately layered: a provider-agnostic **engine** at the -design center, a **durable-chat runtime** that turns a Fresh route into a chat whose -history survives reload and reconnect, and an app-owned **chat UI** you copy into -your workspace and keep. Each layer stands on its own — you can ship durable chat and -the chat UI today without pulling in the engine. - -{{ comp callout { type: "important", title: "What ships today, and what is beta.2-pending" } }} -Two pieces are published on JSR and usable now: the **durable-chat runtime** -(`@netscript/fresh/ai` + `@netscript/fresh/ai/sandbox`) and the **chat UI** -(the `@netscript/fresh-ui` ai copy-registry collection). The -provider-agnostic **engine** (`@netscript/ai`) and the AI plugin -(`@netscript/plugin-ai`, `@netscript/plugin-ai-core`) are **not** published yet — -they arrive in **0.0.1-beta.2**. Do not run deno add jsr:@netscript/ai -today; it resolves to nothing. Build on the durable-chat runtime and the chat UI now, -and treat the engine pages as the design you compose against once beta.2 lands. +design center, an **MCP client stack** that turns external MCP servers into agent +tools, a **durable-chat runtime** that turns a Fresh route into a chat whose history +survives reload and reconnect, and an app-owned **chat UI** you copy into your +workspace and keep. Each layer stands on its own and each is published — you can +adopt one without pulling in the others. + +That layering is the build-efficiency argument: whether the next slice of your app +is written by you or generated by an agent, each capability is one published seam +away — install it, wire the port, and the vocabulary already matches the layer +above — instead of a bespoke integration to design before the feature work starts. + +{{ comp callout { type: "important", title: "What ships in 0.0.1-beta.7" } }} +All four layers are published on JSR and installable now: the engine +(@netscript/aideno add jsr:@netscript/ai), the +MCP client stack (its ./mcp subpath), the +durable-chat runtime (@netscript/fresh/ai + +@netscript/fresh/ai/sandbox), and the chat UI (the +@netscript/fresh-ui ai copy-registry collection, installed +with netscript ui:add ai). The AI plugin pair +(@netscript/plugin-ai, @netscript/plugin-ai-core) is +published at the same version. {{ /comp }} +## The story: a chat that stops being a demo + +The first version of an AI feature is usually a chat box holding its transcript in +component state — and it works until it meets reality. A reload mid-answer loses the +in-flight tool call. The answer needs a chart, and now model output is being glued +into the DOM by hand. The agent needs a capability that lives behind an external +MCP server, and suddenly you are writing a protocol client. Each failure lands on a +different layer of this stack, and each layer answers exactly one of them: + +- **history that survives** — the [durable-chat runtime](/ai/durable-chat/) backs + the session with a durable stream and one projection reducer, so reload, + reconnect, and multi-tab replay all converge on the same transcript; +- **model-authored UI without raw HTML** — the [chat UI](/ai/chat-ui/) renders + `render_ui` payloads through a curated block vocabulary with depth limits and + safe fallbacks; +- **external capabilities as ordinary tools** — the [MCP client stack](/ai/mcp/) + pools servers, injects auth, and registers remote tools into the same registry + the agent loop dispatches; +- **one vocabulary underneath** — the [engine](/ai/engine/) owns the contracts all + of the above speak, so the layers compose instead of adapting to each other. + ## The two planes — never build chat on the wrong primitive NetScript models real-time state in two distinct planes, and the single most common @@ -50,9 +78,10 @@ questions. A chat needs the **session plane** because a tool call is a multi-chunk, mid-stream event: it moves through `pending → streaming → complete` and cannot be expressed as a single reconciled row without losing those intermediate states. Reach for -[durable streams](/capabilities/streams/) when you want live list/board/table data; -reach for [durable chat](/ai/durable-chat/) when you want a replayable conversation. -Conflating them is the documented root of the plane confusion — keep them distinct. +[durable streams](/durable-workflows/streams/) when you want live list/board/table +data; reach for [durable chat](/ai/durable-chat/) when you want a replayable +conversation. Conflating them is the documented root of the plane confusion — keep +them distinct. ## The engine is the design center @@ -67,7 +96,10 @@ the base engine pulls no vendor SDK you did not ask for. Models are addressed by Because the engine owns the vocabulary — the `Message`/`ContentPart` content model, the `AgentChunk` streaming union, the tool and MCP contracts — every layer above it -speaks the same types. The [engine reference](/ai/engine/) is the full map. +speaks the same types. And because telemetry is a port, an agent run is observable +the moment you inject a real `TelemetryPort`: the loop opens a `gen_ai.chat` span +per run and a `gen_ai.chat.turn` span per model turn, and records tool calls as +`gen_ai.tool.call` events. The [engine reference](/ai/engine/) is the full map. ## Thin plugins, centralized convention @@ -77,22 +109,40 @@ ch. 11): **R-PLUGIN-THIN** — every convention-bearing primitive lives in a cor **R-PLUGIN-SEAM** — a plugin's contract lives in its `-core` sibling and conforms to the base contract in `@netscript/plugin`. In practice the engine (`@netscript/ai`) owns the vocabulary, `@netscript/plugin-ai-core` owns the `/v1/ai` oRPC contract, and -`@netscript/plugin-ai` stays a thin delivery shell that declares contributions and a -scaffolder. The AI plugin and the `netscript generate ai` codegen — which emits an `ai-tools` -registry and an `ai-agents` registry (see the [engine page](/ai/engine/)) — land -alongside the engine in **0.0.1-beta.2** (not installable today). This split is what -keeps the published surface coherent — and it is the reason the runtime layers you use -today (`@netscript/fresh/ai`, the fresh-ui copy registry) are self-contained and ship -independently of the deferred engine. +`@netscript/plugin-ai` stays a thin delivery shell — all three published at +`0.0.1-beta.7`. This split is what keeps the published surface coherent: the runtime +layers (`@netscript/fresh/ai`, the fresh-ui copy registry) are self-contained, and +adopting the engine later changes nothing about them. + +{{ comp callout { type: "note", title: "Not in this release: netscript generate ai" } }} +A netscript generate ai codegen (app-owned tool and agent files compiled +into engine-ready registries) is planned but not in the CLI today — +the shipped generate targets are plugins, +runtime-schemas, and aspire. Wire tools and agent loops +through the engine's registries directly. +{{ /comp }} + +## One comparison + +Medusa's hero names "Agents and Developers" as co-audiences and its agent story is a +progressive narrative — agents build a store feature, then optimize it, then operate +the running store. Convex ships "backend building blocks for your agents" as +components on its reactive backend. NetScript's AI stack occupies the same +layered-investment shape with a different center of gravity: the layers here are the +**published framework surface itself** — a JSR engine package, subpath exports, and +a copy-registry you own — wired to the same contracts-first backend the rest of your +app is built on, rather than a product layer alongside it. The trade is explicit: +NetScript does not ship an agents-operate-your-app product tier today; it ships the +seams those products are made of. ## Where to go next {{ comp.cardsGrid({ columns: 3, cards: [ - { eyebrow: "Ships today", title: "Durable chat", body: "Turn a Fresh route into a durable AI chat: history and in-flight tool calls survive reload, reconnect, and multi-tab.", href: "/ai/durable-chat/", icon: "C" }, - { eyebrow: "Ships today", title: "Chat UI", body: "The fresh-ui `ai` copy-registry: composer, message thread, tool-call cards, and the generative-UI block renderer — copied into your app and owned by you.", href: "/ai/chat-ui/", icon: "U" }, - { eyebrow: "Beta.2", title: "AI engine", body: "@netscript/ai: the provider-agnostic runtime, contracts vocabulary, ports, tools, agent loop, MCP transports, and provider adapters.", href: "/ai/engine/", icon: "E" }, - { eyebrow: "Capability hub", title: "AI capability", body: "The one-screen orientation for the AI capability and how it composes with the rest of the framework.", href: "/capabilities/ai/", icon: "◎" }, - { eyebrow: "Related", title: "Durable streams", body: "The other real-time plane — live list/board/table data over the durable-stream server.", href: "/capabilities/streams/", icon: "🌊" }, + { eyebrow: "Guide", title: "MCP", body: "The MCP client stack: transports, injected auth, multi-server pooling, and safe rendering of ui:// resources from tool results.", href: "/ai/mcp/", icon: "M" }, + { eyebrow: "Guide", title: "Durable chat", body: "Turn a Fresh route into a durable AI chat: history and in-flight tool calls survive reload, reconnect, and multi-tab.", href: "/ai/durable-chat/", icon: "C" }, + { eyebrow: "Guide", title: "Chat UI", body: "The fresh-ui `ai` copy-registry: composer, message thread, tool-call cards, and the generative-UI block renderer — copied into your app and owned by you.", href: "/ai/chat-ui/", icon: "U" }, + { eyebrow: "Reference", title: "AI engine", body: "@netscript/ai: the provider-agnostic runtime, contracts vocabulary, ports, tools, agent loop, MCP transports, and provider adapters.", href: "/ai/engine/", icon: "E" }, + { eyebrow: "Related", title: "Durable streams", body: "The other real-time plane — live list/board/table data over the durable-stream server.", href: "/durable-workflows/streams/", icon: "🌊" }, { eyebrow: "Related", title: "Web Layer", body: "The Fresh page and island model the durable-chat runtime plugs into.", href: "/web-layer/", icon: "O" }, { eyebrow: "API Reference", title: "@netscript/ai", body: "Generated symbols for the AI engine package.", href: "/reference/ai/", icon: "R" }, { eyebrow: "API Reference", title: "@netscript/plugin-ai", body: "Generated symbols for the thin AI plugin delivery shell.", href: "/reference/plugin-ai/", icon: "R" }, diff --git a/docs/site/ai/mcp.md b/docs/site/ai/mcp.md index d09775f25..bf59cb774 100644 --- a/docs/site/ai/mcp.md +++ b/docs/site/ai/mcp.md @@ -2,8 +2,172 @@ layout: layouts/base.vto title: MCP templateEngine: [vento, md] +prev: { label: "AI", href: "/ai/" } +next: { label: "Durable chat", href: "/ai/durable-chat/" } --- # MCP -_Story page forthcoming._ +`@netscript/ai/mcp` is the **MCP client stack**: it turns external Model Context +Protocol servers into typed tools your agent loop can call — transports, injected +auth, multi-server pooling, and a safe rendering path for the `ui://` resources a +tool result can carry. It is published as part of `@netscript/ai@0.0.1-beta.7` and +installs with the engine: `deno add jsr:@netscript/ai`. + +{{ comp callout { type: "note", title: "Client, not server" } }} +NetScript's shipped MCP surface is client-side: your app +consumes external MCP servers as agent tools. There is no scaffolded +NetScript MCP server in this release — do not build on the assumption that +netscript hosts one for you. +{{ /comp }} + +## The story: one config instead of a protocol integration + +Your agent needs a capability that lives behind an MCP server — a docs-search +server, an internal ops server, a vendor's tool endpoint. Wiring that by hand is a +protocol integration, not a feature: JSON-RPC framing over stdio or HTTP, session +lifecycle, reconnect-with-backoff when the connection drops mid-run, auth headers +that must not be hardcoded, translating the server's tool schemas into whatever +your agent's tool registry expects, and deciding what to do when a result embeds +UI content you did not author. Every one of those steps is a place for an agent — +human or AI — to burn a turn on glue that has nothing to do with the feature. + +The `./mcp` subpath collapses that to configuration. You declare servers as data, +register the pool into the same tool registry the [agent loop](/ai/engine/) +already dispatches through, and the remote tools become ordinary registry entries: + +```ts +import { createMcpTransportPool, registerMcpTools } from "@netscript/ai/mcp"; +import { createToolRegistry } from "@netscript/ai/tools"; + +const pool = createMcpTransportPool({ + servers: [{ + kind: "streamable-http", + serverId: "search", + url: "https://mcp.example.com", + auth: { mode: "api-token", token: Deno.env.get("MCP_TOKEN")!, scheme: "Bearer" }, + }], +}); + +const registry = createToolRegistry(); +const registration = await registerMcpTools(registry, pool); +// registration.toolNames — the remote tools, now dispatchable like any local tool +// registration.stop() — detach them again +``` + +From here the agent loop treats a remote MCP tool exactly like a local +`defineAiTool` definition — same registry, same dispatch, same +[`AgentChunk` stream](/ai/engine/) on the wire. + +## Transports — stdio and reconnectable Streamable-HTTP + +`createMcpTransport(config)` takes a discriminated config — `{ kind: "stdio" }` or +`{ kind: "streamable-http" }` — and returns an `McpTransportPort`. The +Streamable-HTTP transport is built for long-lived agent sessions: it reconnects +with configurable backoff (`McpBackoffConfig`) and exposes its lifecycle as an +observable state machine. + +{{ comp.apiTable({ + caption: "Transport lifecycle — @netscript/ai/mcp", + rows: [ + { name: "McpConnectionState", type: "\"disconnected\" | \"connecting\" | \"connected\" | \"reconnecting\" | \"closed\"", desc: "The transport lifecycle union; `reconnecting` is a first-class state, not an error." }, + { name: "onStateChange(handler)", type: "(state, previous) => void", desc: "Subscribe to lifecycle transitions (`McpStateChangeHandler`); returns an unsubscribe function." }, + { name: "StreamableHttpMcpTransport", type: "class", desc: "The reconnectable Streamable-HTTP transport; configured via `StreamableHttpMcpTransportConfig` (serverId, url, auth, backoff)." }, + { name: "StdioMcpTransport", type: "class", desc: "The stdio transport for local process-based MCP servers." } + ] +}) }} + +Auth is **injected at the composition root**, never baked into a transport class. +`McpAuthConfig` is a three-mode union — `{ mode: "none" }`, +`{ mode: "api-token", token, headerName?, scheme? }`, or +`{ mode: "oauth", accessToken, tokenType? }` — so the same server declaration works +across environments with only the secret changing. + +## The pool — many servers, one tool namespace + +Real agents talk to more than one server. `createMcpTransportPool(config)` builds a +multi-server pool from **serializable** transport configs (so server declarations +can live in configuration, not code); `createMcpTransportPoolFromTransports` wraps +transports you already own. The pool is itself an `McpTransportPort`, so everything +that accepts one transport accepts a pool of them. + +{{ comp.apiTable({ + caption: "McpTransportPool — the multi-server surface", + rows: [ + { name: "connect(options)", type: "Promise", desc: "Open every pooled transport and return the discovered tools, name-prefixed by server id so two servers' tools never collide." }, + { name: "listTools(options)", type: "Promise", desc: "Re-list tools from all pooled transports without tearing down warm connections." }, + { name: "callTool(name, args, options)", type: "Promise", desc: "Route a prefixed tool name to its server and extract any `ui://` resources from the result." }, + { name: "state / onStateChange", type: "McpConnectionState", desc: "The aggregate lifecycle across all pooled transports." }, + { name: "server(serverId) / serverIds", type: "McpTransportPort | undefined", desc: "Reach a single pooled transport when you need it." }, + { name: "stop()", type: "Promise", desc: "Stop every pooled transport and clear discovered tool routes." } + ] +}) }} + +`registerMcpTools(registry, transport)` is the bridge into the agent: it surfaces +the remote tools into a `ToolRegistryPort` and returns an `McpToolRegistration` +whose `.stop()` detaches them — registration is reversible, not a global mutation. + +## `ui://` resources — tool results that carry UI, rendered safely + +An MCP tool result can embed **`ui://` resources** — UI content authored by the +server, not by you. The pool surfaces these on every `McpPooledToolResult` as +`uiResources`, and `extractMcpUiResources(result)` pulls the **data-only** +`McpUiResource` records from any raw tool result. The engine stops there by design: +it extracts, it never renders. + +Rendering belongs to the web layer, behind two guardrails you install with +`netscript ui:add ai`: + +- the **`McpUiWidget` island** (from the `@netscript/fresh-ui` copy registry) + renders a `ui://` resource in a sandboxed frame — sanitized, themed through the + sandbox route, `no-referrer`; +- the **`createMcpSandboxHandler`** route (on `@netscript/fresh/ai/sandbox`) + serves the resource with the active theme's tokens and a per-response CSP — see + [the durable-chat sandbox section](/ai/durable-chat/) for the handler itself. + +This is the same trust boundary as the [`render_ui` tool](/ai/chat-ui/): model- or +server-authored UI reaches the page only through a curated renderer or a sandbox, +never as raw HTML in your island tree. The whole path is exercised by the +framework's own merge gate — the CLI E2E suite drives a real Streamable-HTTP MCP +round trip through this client stack into a rendered `McpUiWidget` before a branch +can land. + +## One comparison + +Encore's MCP story points **inward**: it ships an MCP server that exposes a running +Encore backend's introspection surface to coding agents, so an agent can inspect +and verify the backend it is editing. NetScript's shipped MCP surface points +**outward**: a client stack for consuming external MCP servers as tools inside +your own agents, with a sandboxed path for the UI those tools return. The two +solve different halves of the MCP picture — if you need your backend to *be* an +MCP server today, NetScript does not scaffold that; if you need your product's +agent to *use* MCP servers, that path is published and gate-tested. + +## Where to go next + +{{ comp.featureGrid({ items: [ + { + title: "The engine — ports, tools, agent loop", + body: "The @netscript/ai surface this stack plugs into: the tool registry, the agent loop, and the McpTransportPort seam.", + href: "/ai/engine/", + icon: "E" + }, + { + title: "Durable chat — the sandbox route", + body: "createMcpSandboxHandler on @netscript/fresh/ai/sandbox: themed, CSP-guarded serving of ui:// resources.", + href: "/ai/durable-chat/", + icon: "◆" + }, + { + title: "Chat UI — rendering the transcript", + body: "The fresh-ui copy registry: tool cards, the render_ui block renderer, and the McpUiWidget island.", + href: "/ai/chat-ui/", + icon: "U" + }, + { + title: "Look up — @netscript/ai", + body: "The generated reference for the engine package, including every ./mcp export.", + href: "/reference/ai/", + icon: "≡" + } +] }) }}