diff --git a/.llm/runs/feat-openapi-mcp-read-tools--s6/context-pack.md b/.llm/runs/feat-openapi-mcp-read-tools--s6/context-pack.md
new file mode 100644
index 0000000000..6917f7b5b3
--- /dev/null
+++ b/.llm/runs/feat-openapi-mcp-read-tools--s6/context-pack.md
@@ -0,0 +1,11 @@
+# Context Pack — OMB S6
+
+Branch `feat/openapi-mcp-read-tools` is based on `origin/main` `f7558aa1c`. Issue 1132 and RFC 1123
+were read. S4 projection, S5 directory, and S8 receipt lifecycle are present. Live registry is 14,
+so this slice truthfully plans 14→17 despite the staged brief's stale 17→20 expectation. Plan and
+Design are complete; formal local evaluator passes are waived under the milestone composition rule.
+Implementation is complete locally: three flows, contracts, 14→17 registry wiring, CLI receipt
+composition, public exports, docs synchronization, and acceptance fixtures. Targeted tests pass
+10/10; the full package suite passes 98/98; scoped check/lint/fmt, quality gate, doc-lint, and
+publish dry-run pass. Remaining work is final diff review, commit/push/comment, composed
+draft→ready/OpenHands evaluation handoff, and close-gate body/evidence updates.
diff --git a/.llm/runs/feat-openapi-mcp-read-tools--s6/drift.md b/.llm/runs/feat-openapi-mcp-read-tools--s6/drift.md
new file mode 100644
index 0000000000..f13a1ccf4f
--- /dev/null
+++ b/.llm/runs/feat-openapi-mcp-read-tools--s6/drift.md
@@ -0,0 +1,34 @@
+# Drift — OMB S6
+
+## 2026-08-04 — registry baseline differs from staged brief
+
+- **Severity:** significant
+- **Expected:** 17 live tool names, producing 20 after this slice.
+- **Observed:** remote `origin/main` at `f7558aa1c` has 14 names in `TOOL_NAMES`; issue 1132 also
+ specifies 14→17. S4 added the projection subpath, not three registry tools.
+- **Disposition:** implement exactly the three issue tools and record the truthful 14→17 delta.
+ Do not invent unrelated placeholder tools.
+
+## 2026-08-04 — milestone evaluator composition
+
+- **Severity:** procedural
+- **Authority:** owner staged brief, milestone-run.md § Evaluator protocol, orchestrator ruling D6.
+- **Disposition:** no local formal PLAN-EVAL/IMPL-EVAL. Record composed waiver rows; use
+ draft→ready augment + OpenHands + orchestrator pre-merge gate, retaining opposite-family code
+ review.
+
+## 2026-08-04 — OpenHands provider qualification
+
+- **Severity:** infrastructure
+- **Observed:** the first dispatch passed `qwen/qwen3.7-max`; LiteLLM rejected it before model
+ execution because the provider prefix was absent.
+- **Disposition:** retry once with the dispatcher's documented literal id
+ `openrouter/qwen/qwen3.7-max`; retain the failed run as infrastructure evidence.
+
+## 2026-08-04 — stale CLI registry fixture
+
+- **Severity:** implementation reconcile
+- **Observed:** package-local registry and stdio fixtures passed at 17, but the repository-wide real
+ CLI stdio smoke retained the pre-S6 expectation of 14.
+- **Disposition:** synchronize that fixture to the verified live 14→17 delta and prove it with the
+ focused real CLI stdio test.
diff --git a/.llm/runs/feat-openapi-mcp-read-tools--s6/openhands-review-brief.md b/.llm/runs/feat-openapi-mcp-read-tools--s6/openhands-review-brief.md
new file mode 100644
index 0000000000..16c23539a8
--- /dev/null
+++ b/.llm/runs/feat-openapi-mcp-read-tools--s6/openhands-review-brief.md
@@ -0,0 +1,32 @@
+use harness
+
+Review PR 1204 as the OpenHands component of the milestone-run composed evaluator protocol. This is
+review-only: do not modify source, run artifacts, `deno.lock`, or any file; do not commit or push.
+
+## SKILL
+
+- `netscript-harness` — read the run artifacts and honor the milestone evaluator waiver.
+- `netscript-doctrine` — evaluate `packages/mcp` against Archetype 2.
+- `jsr-audit` — verify the public export and publish evidence.
+- `netscript-tools` — use authoritative gates and preserve lock hygiene.
+- `netscript-pr` — report a structured REVIEW verdict without changing PR metadata.
+- `openhands-handoff` — write the required OpenHands summary output.
+
+## Review scope
+
+Read issue 1132, RFC 1123, the full diff from `main`, and
+`.llm/runs/feat-openapi-mcp-read-tools--s6/`. Verify:
+
+1. `truncated: true` iff service or operation rows were actually dropped; no central silent cap.
+2. `operationCount` is absent, not zero, whenever no parsed spec was fetched.
+3. S5's `sources` block is surfaced verbatim.
+4. All three tools compose S4 projection and S5 directory rather than re-deriving either.
+5. Receipts use S8 post-output-validation settlement.
+6. Registry moves from the live 14 baseline to 17, contracts/exports/docs agree, and no unrelated
+ tool is invented.
+7. No new lint ignores, unsafe casts, lock churn, speculative abstractions, or out-of-scope live
+ scaffold/AppHost work.
+
+Run the smallest checks needed to verify claims. Report PASS or actionable findings with severity
+and file:line evidence. Include raw exit codes for any commands run. Write the required
+`OPENHANDS_SUMMARY_PATH`; do not trust or reuse a stale persistent summary.
diff --git a/.llm/runs/feat-openapi-mcp-read-tools--s6/plan-eval.md b/.llm/runs/feat-openapi-mcp-read-tools--s6/plan-eval.md
new file mode 100644
index 0000000000..9d30803f49
--- /dev/null
+++ b/.llm/runs/feat-openapi-mcp-read-tools--s6/plan-eval.md
@@ -0,0 +1,8 @@
+# PLAN-EVAL — composed milestone waiver
+
+**Status:** composed per milestone-run.md (orchestrator waiver)
+
+Per the owner's staged brief and milestone-run.md § Evaluator protocol, this per-PR run does not
+launch a local formal PLAN-EVAL. Evaluation composes draft→ready augment, OpenHands, and the
+orchestrator pre-merge gate. The plan-gate inputs are present: re-baselined research, locked plan,
+Design checkpoint, Archetype-2 gate set, JSR surface scan, risk/debt/deferred-scope sweeps.
diff --git a/.llm/runs/feat-openapi-mcp-read-tools--s6/plan.md b/.llm/runs/feat-openapi-mcp-read-tools--s6/plan.md
new file mode 100644
index 0000000000..21758db11d
--- /dev/null
+++ b/.llm/runs/feat-openapi-mcp-read-tools--s6/plan.md
@@ -0,0 +1,66 @@
+# Plan — OMB S6 three read tools
+
+## Scope and archetype
+
+Implement `list_api_services`, `list_service_operations`, and `get_operation_schema` in
+`packages/mcp` as Archetype-2 application flows. No frontend/service/docs overlay and no AppHost or
+scaffold run: acceptance is fixture-only.
+
+## Locked decisions
+
+1. Compose S4 and S5 directly: directory rows provide specs; projection functions provide indexing,
+ identity resolution, descriptions, and schema views.
+2. Forward S5's `sources` array verbatim from `list_api_services`.
+3. Omit `operationCount` whenever a parsed spec was not fetched; never substitute zero.
+4. Self-cap operation rows at 49 (below the central 50-row truncator), apply filter before cap, and
+ set `truncated` iff the filtered row set lost at least one row.
+5. Return uniform failures for unknown/unavailable services and unknown/ambiguous operations.
+6. Wrap all three flows with S8's existing receipt lifecycle at `cli.ts`; do not write receipts in
+ flow code.
+7. Live registry delta is 14→17. The staged 17→20 expectation is stale and recorded in drift.
+
+## Public surface
+
+- Three tool names and Standard Schema contracts.
+- Three flow factories with explicit input/output types exported from `mod.ts`.
+- `McpCliOptions.serviceEndpointDirectory` injection seam for fixtures/embedders; default composition
+ uses the existing S5 factory and project root.
+
+## Commit slices
+
+1. **Plan/bootstrap** — run artifacts, live-count divergence, locked contracts and gate map.
+2. **Contracts and flows** — three contracts, one flow per tool, fixture tests proving all three
+ issue boxes. Gate: targeted package test plus scoped check/lint/fmt.
+3. **Registry/composition/exports** — 14→17 registry, CLI wiring and receipts, public exports and
+ docs count references required by the existing drift test. Gate: package test and doc-lint.
+4. **Merge-readiness evidence** — Archetype-2 full column, quality gate, JSR audit, publish dry-run,
+ lock/lint-ignore verification, PR evidence and ready handoff.
+
+## Gate set
+
+- Targeted and full `packages/mcp` tests.
+- Scoped check/lint/fmt wrappers rooted at `packages/mcp`, `--ext ts,tsx`.
+- `deno task quality:gate` (quality scan + architecture fitness).
+- `deno task doc:lint --root packages/mcp --pretty`.
+- Package-local `deno task publish:dry-run`.
+- Consumer compile through package check and registry/protocol fixtures.
+- Diff gates: no new `deno-lint-ignore`, `as unknown as`, or `deno.lock` churn.
+
+## Risks and mitigations
+
+- **Silent truncation:** fixture with >49 filtered operations checks exact retained length and flag.
+- **False zero:** schema and fixture distinguish absence from numeric zero.
+- **Source transformation:** identity assertion and deep equality prove the exact S5 block is returned.
+- **Receipt timing regression:** CLI composition reuses S8 wrapper; existing receipt lifecycle tests
+ plus an S6 receipt fixture prove settlement after validated output.
+- **Public-surface slow types:** explicit return types plus doc-lint and dry-run.
+
+## Open-decision sweep
+
+- Safe to defer: live-scaffold discovery path (owned by S7), activation copy, execution tool.
+- Must resolve now: none.
+
+## Debt and deferred scope
+
+No new architecture debt expected. Invocation, activation, manifest emission, and contract
+enrichment remain owned by their board slices.
diff --git a/.llm/runs/feat-openapi-mcp-read-tools--s6/research.md b/.llm/runs/feat-openapi-mcp-read-tools--s6/research.md
new file mode 100644
index 0000000000..7fd1d12328
--- /dev/null
+++ b/.llm/runs/feat-openapi-mcp-read-tools--s6/research.md
@@ -0,0 +1,50 @@
+# Research — OMB S6 three read tools
+
+## Authority and baseline
+
+- Read issue 1132 and RFC PR 1123 before implementation.
+- Rebased the feature branch onto remote `origin/main` at `f7558aa1c` on 2026-08-04.
+- S4 is present as the pure `@netscript/mcp/openapi-projection` export, including operation index,
+ canonical identity resolution, description ladder, and schema views.
+- S5 is present as `ServiceEndpointDirectoryPort` plus the composed precedence
+ `override > aspire-cli > run-manifest > appsettings`; directory results retain opaque parsed specs
+ only for running rows and return the complete `sources` array.
+- S8 is present as runner-settled `withFlowReceipt`: settlement occurs after output validation.
+
+## Re-baselined facts
+
+1. The live `TOOL_NAMES` count is **14**, not the staged brief's expected 17. The issue itself says
+ registry 14→17. Adding the three accepted tools therefore makes the live delta **14→17**, not
+ 17→20. No placeholder tools will be invented to satisfy the stale count.
+2. `list_api_services` can compute `operationCount` only for a running row because only that row
+ contains `spec`. All other states must omit the property entirely.
+3. `list_service_operations` and `get_operation_schema` can reuse the exact retained spec and S4
+ projection; neither needs a new fetch port or a second OpenAPI parser.
+4. S5's `sources` value is already the desired discriminated source-outcome block and can be
+ forwarded unchanged by identity, preserving every property and order.
+5. S8's receipt lifecycle is attached at the CLI composition edge, so all three flows should use
+ the existing `withReceipt` wrapper rather than write evidence themselves.
+
+## JSR surface scan
+
+- Package metadata, three entrypoints, module docs, and explicit exported symbol types already
+ exist. New public flow factories/types need JSDoc and explicit return types.
+- New exports must be added through `mod.ts`; no self-referential package imports.
+- Required publish gates: full package doc-lint and package-local publish dry-run; slow types or
+ private type references are blockers.
+- No dependency change is required and no lock churn is expected.
+
+## Doctrine and debt
+
+- Selected Archetype 2 because flows compose a package-owned external directory port and existing
+ network/source adapters; no service/runtime overlay applies because all acceptance is fixture-only.
+- Current doctrine verdict has no explicit `packages/mcp` row; new code is held to the current
+ Archetype-2 rules without deepening recorded debt.
+- In-scope risks: AP-1 oversized files, AP-7 duplicate upstream/projection behavior, AP-9 helper
+ flags, AP-11 module-load side effects, AP-22 empty barrels, AP-23 inline composition bodies,
+ AP-25 side effects outside adapters/edges.
+
+## Open questions
+
+None that force rework. Output field names and failure envelopes follow issue/RFC wording and
+existing tool schema conventions.
diff --git a/.llm/runs/feat-openapi-mcp-read-tools--s6/supervisor.md b/.llm/runs/feat-openapi-mcp-read-tools--s6/supervisor.md
new file mode 100644
index 0000000000..7885844cf1
--- /dev/null
+++ b/.llm/runs/feat-openapi-mcp-read-tools--s6/supervisor.md
@@ -0,0 +1,26 @@
+# Supervisor Identity — feat-openapi-mcp-read-tools--s6
+
+| Field | Value |
+| --- | --- |
+| Model | Codex GPT-5.6 Sol |
+| Session | current Codex implementation-supervisor session |
+| Host | Linux workspace |
+| Checkout | `/home/codex/repos/ns005-s6` |
+| Worktree | `/home/codex/repos/ns005-s6` |
+| Branch | `feat/openapi-mcp-read-tools` |
+| Baseline | `f7558aa1c4e06f076114d924c7324feddf554e45` (`origin/main`, 2026-08-04) |
+| Run ID | `feat-openapi-mcp-read-tools--s6` |
+
+## Routes in force
+
+| Task lane | Provider / model / effort | Role in this run |
+| --- | --- | --- |
+| `complex_implementation` | Codex / GPT-5.6 Sol / high | plan, implementation, gates, PR supervision |
+| milestone composed evaluation | draft→ready augment + OpenHands + orchestrator pre-merge gate | formal evaluation composition |
+
+## Recorded lane/eval overrides
+
+Owner directive applies milestone-run.md § Evaluator protocol and orchestrator ruling D6: no local
+formal PLAN-EVAL or IMPL-EVAL session. Evaluation is composed from draft→ready augment, OpenHands,
+and the orchestrator pre-merge gate. Opposite-family code review remains required by the milestone
+protocol; run-artifact/evidence prose is covered by the owner-review substitution.
diff --git a/.llm/runs/feat-openapi-mcp-read-tools--s6/worklog.md b/.llm/runs/feat-openapi-mcp-read-tools--s6/worklog.md
new file mode 100644
index 0000000000..c5ec49518e
--- /dev/null
+++ b/.llm/runs/feat-openapi-mcp-read-tools--s6/worklog.md
@@ -0,0 +1,111 @@
+# Worklog — OMB S6 three read tools
+
+## Design
+
+### Public surface
+
+Three flow factories and their input/output types; three new tool contracts; one optional injected
+`ServiceEndpointDirectoryPort` at the CLI composition edge.
+
+### Domain vocabulary
+
+`ApiServiceSummary`, `ListApiServicesResult`, `ServiceOperationSummary`,
+`ListServiceOperationsResult`, and `GetOperationSchemaResult`. Existing `SourceOutcome`,
+`ServiceEndpointRow`, `SchemaViewName`, and S4 projection types remain authoritative.
+
+### Ports and composition
+
+Consume the existing `ServiceEndpointDirectoryPort`. No new port. Default composition remains in
+`cli.ts` through `createServiceEndpointDirectory`; fixtures inject a fake directory.
+
+### Constants
+
+`SERVICE_OPERATION_RESULT_LIMIT = 49` is the flow-owned row cap below the central 50-item cap.
+Existing `SCHEMA_VIEW_NAMES`, endpoint statuses, and source identifiers are reused.
+
+### Commit slices
+
+1. Bootstrap plan artifacts.
+2. Contracts + flows + acceptance fixtures.
+3. Registry + composition + exports + documentation count synchronization.
+4. Full gate/evaluation evidence and PR handoff.
+
+### Deferred scope
+
+No live AppHost/scaffold path, activation copy, execution tool, policy, or contract enrichment.
+
+### Contributor path
+
+Start at `tool-contracts.ts` for wire shape, follow the named flow in
+`src/application/flows/`, then find composition and receipt wrapping in `cli.ts`.
+
+## Phase status
+
+| Phase | Status |
+| --- | --- |
+| Research | complete |
+| Plan | complete |
+| PLAN-EVAL | composed per milestone-run.md (orchestrator waiver) |
+| Implement | complete |
+| Gate | complete |
+| IMPL-EVAL | composed per milestone-run.md (orchestrator waiver) — PASS |
+
+## Implementation evidence
+
+- Added three contract entries and three one-flow-per-tool modules.
+- `list_api_services` self-caps at 49, reports honest truncation, omits `operationCount` unless S5
+ returned a running row with a parsed spec, and forwards the exact `sources` reference.
+- `list_service_operations` composes S4 indexing/description, filters before a 49-row cap, and sets
+ `truncated` exactly from dropped matching rows.
+- `get_operation_schema` composes S4 canonical resolution/schema views and labels its curl output as
+ an unauthenticated template.
+- CLI composition injects or creates the S5 directory and wraps all three tools through S8's
+ post-validation receipt lifecycle.
+- Registry and documentation drift fixtures now record the truthful 14→17 live delta.
+
+## Gate evidence
+
+| Gate | Result |
+| --- | --- |
+| Acceptance/registry/stdio fixtures | PASS — 10/10 targeted tests |
+| Full `packages/mcp` test | PASS — 98/98 |
+| Scoped check wrapper | PASS — 92 files, 0 diagnostics |
+| Scoped lint wrapper with package config | PASS — 92 files, 0 findings |
+| Scoped fmt wrapper with package config | PASS — 92 files, 0 findings |
+| `quality:gate` | PASS — quality scan `ok:true`; arch checks exit 0 (baseline warnings only) |
+| Package doc-lint | PASS — combined total 0 |
+| Package publish dry-run | PASS — no slow-type failure; S6 files in intended publish list |
+| Lock hygiene | PASS after reversing Deno's unrelated queue lock resolution; final diff pending |
+
+## CI reconcile
+
+- The first OpenHands dispatch failed before model execution because the workflow received an
+ unqualified LiteLLM model id. Retried through the repo dispatcher with
+ `openrouter/qwen/qwen3.7-max`; composed evaluation remains in progress.
+- Branch CI found one stale cross-package fixture: the real CLI stdio smoke still asserted the
+ pre-S6 registry count of 14. Updated it to the live post-S6 count of 17; the focused smoke passes
+ 1/1 and its scoped format check passes.
+- The scaffold runtime reached 29 passing steps before an unrelated Aspire restore preparation
+ timed out after two 900-second attempts. The staged brief explicitly excludes AppHost/scaffold
+ runs for this fixture-only slice.
+
+## Slice reconcile
+
+- Issue 1132 remains open with three acceptance boxes; PR 1204 is draft with `status:plan` and
+ milestone 0.0.5. Closing keyword is intentionally deferred until all evidence and checkboxes are
+ truthful.
+
+## Opposite-family slice review
+
+- Claude Fable 5 low session `07579130-6ba6-47f0-9b01-3ad758e50b4c` returned **PASS**.
+- Three non-blocking observations were accepted into the sign-off slice: bound failure suggestions
+ to three, align the public limit schema (1–100) with the flow's 49-row self-cap, and narrow the
+ S6 receipt fixture name to its actual success-path assertion.
+
+## Composed evaluator result
+
+- OpenHands run `30891416446` returned **IMPL-EVAL PASS** in PR comment `5176464319`.
+- The evaluator independently verified all three issue contracts, S4/S5 composition, S8 receipt
+ settlement, the truthful 14→17 registry delta, 98/98 MCP tests, and lint/lock hygiene.
+- PR 1204 is ready for review with `status:impl-eval`; the orchestrator retains merge authority and
+ the pre-merge gate.
diff --git a/docs/site/ai/agent-tooling.md b/docs/site/ai/agent-tooling.md
index 906f84ec42..f61c35fb88 100644
--- a/docs/site/ai/agent-tooling.md
+++ b/docs/site/ai/agent-tooling.md
@@ -90,7 +90,7 @@ server over standard input/output. Its flags:
## What the server exposes
-Fourteen tools, every one returning a bounded structured result. Grouped by what
+Seventeen tools, every one returning a bounded structured result. Grouped by what
an agent is trying to do:
- **Read the running app** — seven telemetry read models: `get_app_status`,
@@ -108,6 +108,8 @@ an agent is trying to do:
and a bounded output tail.
- **Record drift** — `record_drift` appends an evidence-gated entry to the project
drift log (`.netscript/agent/drift.jsonl`) when authorized by a fresh successful diagnostic receipt.
+- **Inspect service APIs** — `list_api_services`, `list_service_operations`, and
+ `get_operation_schema` expose live OpenAPI contracts before an agent guesses with curl.
We keep the per-tool schemas, output bounds, and the full `execute_command`
policy in the [`@netscript/mcp` reference]({{ "ref:mcp" |> xref |> url }}) rather
@@ -206,7 +208,7 @@ deno test --allow-all packages/cli/e2e/tests/agent/agent-mcp-stdio_test.ts
```
The smoke starts the public CLI binary, initializes MCP over stdio, verifies the
-14-tool catalog, and checks docs, diagnostics, unreachable telemetry, and command
+17-tool catalog, and checks docs, diagnostics, unreachable telemetry, and command
denial behavior.
## Where to go next
diff --git a/docs/site/reference/mcp/index.md b/docs/site/reference/mcp/index.md
index efca86e1d8..6f2bd5b50d 100644
--- a/docs/site/reference/mcp/index.md
+++ b/docs/site/reference/mcp/index.md
@@ -11,7 +11,7 @@ surface and is maintained by hand; the authoritative, always-current symbol list
[`deno doc jsr:@netscript/mcp{{ releaseSpecifier }}`](https://jsr.io/@netscript/mcp/doc). For the
full index of packages and plugins return to the [reference overview](/reference/).
-`@netscript/mcp` publishes 14 token-bounded MCP tools that let a coding agent monitor a running app,
+`@netscript/mcp` publishes 17 token-bounded MCP tools that let a coding agent monitor a running app,
debug a correlated execution, read framework-semantic telemetry, run the doctor, search the public
documentation, and trigger allowlisted CLI commands — over newline-delimited JSON-RPC on stdio, with
no npm MCP SDK on the dependency graph.
@@ -31,7 +31,7 @@ Two entrypoints carry the surface:
| Symbol | Kind | Summary |
| ---------------------- | --------- | ------------------------------------------------------------------------------ |
| `createMcpServer` | function | Create the MCP server with `initialize` / `tools/list` / `tools/call` support. |
-| `createToolRegistry` | function | Immutable, enumerable definitions of the 14 tools. |
+| `createToolRegistry` | function | Immutable, enumerable definitions of the 17 tools. |
| `McpServer` | interface | Callable server subset: `handle(message)` plus the registered `tools`. |
| `McpServerOptions` | interface | Composition seams: `probe`, `environment`, `flows`, `truncation`. |
| `MCP_PROTOCOL_VERSION` | const | The MCP protocol version the runner implements (`2025-11-25`). |
@@ -73,6 +73,9 @@ input caps the result count server-side before truncation applies.
| `list_commands` | `filter`, `limit` | `count`, `commands` |
| `execute_command` | **`command`**, `args` | `exitCode`, `durationMs`, `outputTail`, `truncated`, `timedOut` |
| `record_drift` | **`resource`**, **`summary`**, `details` | `recorded`, `resource`, `receipt` |
+| `list_api_services` | — | Service status, URLs, optional operation count, conflicts, and verbatim discovery source outcomes |
+| `list_service_operations` | **`service`**, `filter`, `limit` | Bounded operation rows and `truncated` metadata |
+| `get_operation_schema` | **`service`**, **`operation`**, `view` | Projected schema view, operation identity, unauthenticated `curlExample`, and `authNote` |
**Truncation semantics.** After a flow succeeds, `truncateResult` recursively bounds the result
using `DEFAULT_TRUNCATION_POLICY` — arrays are capped at 50 elements and strings at 2,000 UTF-16
diff --git a/packages/cli/e2e/tests/agent/agent-mcp-stdio_test.ts b/packages/cli/e2e/tests/agent/agent-mcp-stdio_test.ts
index c1e5847bb6..1d07b8f8e4 100644
--- a/packages/cli/e2e/tests/agent/agent-mcp-stdio_test.ts
+++ b/packages/cli/e2e/tests/agent/agent-mcp-stdio_test.ts
@@ -98,7 +98,7 @@ Deno.test('agent mcp real CLI stdio smoke', async () => {
assertEquals(responses[0].result?.serverInfo?.name, '@netscript/mcp');
const tools = responses[1].result?.tools ?? [];
- assertEquals(tools.length, 14);
+ assertEquals(tools.length, 17);
assert(tools.some((tool) => tool.name === 'record_drift'));
const doctor = responses[2].result?.structuredContent;
diff --git a/packages/mcp/README.md b/packages/mcp/README.md
index 069721169d..1c1376c82c 100644
--- a/packages/mcp/README.md
+++ b/packages/mcp/README.md
@@ -4,7 +4,7 @@
[](https://github.com/rickylabs/netscript/actions/workflows/ci.yml)
[](https://rickylabs.github.io/netscript/)
-**The Model Context Protocol server for NetScript: 14 token-bounded tools that let a coding agent
+**The Model Context Protocol server for NetScript: 17 token-bounded tools that let a coding agent
monitor a running app, debug a correlated execution, read framework-semantic telemetry, run the
doctor, and search the docs — all over stdio.**
@@ -24,7 +24,7 @@ Aspire's own MCP server: Aspire speaks resources and containers; this server spe
## Why agents like it
-- **14 token-bounded tools** — every successful result is capped server-side (50 array items, 2,000
+- **17 token-bounded tools** — every successful result is capped server-side (50 array items, 2,000
characters per string) before it reaches the model; the analytics tools never return raw spans at
all.
- **Framework-semantic trace intelligence** — tools classify telemetry into `worker`, `saga`,
@@ -45,7 +45,7 @@ Aspire's own MCP server: Aspire speaks resources and containers; this server spe
```mermaid
flowchart LR
- A["Agent host
(Claude Code, VS Code, ...)"] <-- "JSON-RPC / stdio" --> S["netscript agent mcp
14 tools · bounded results"]
+ A["Agent host
(Claude Code, VS Code, ...)"] <-- "JSON-RPC / stdio" --> S["netscript agent mcp
17 tools · bounded results"]
S --> T["Telemetry endpoint
(OTLP read model)"]
S --> D["Docs corpus
(public Markdown)"]
S --> P["Command policy
(default-deny allowlist)"]
@@ -149,6 +149,9 @@ results, and `get_run` returns a structured `run_not_found` error the agent can
| `list_commands` | — | Live CLI command descriptors |
| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked |
| `record_drift` | `resource`, `summary` | Evidence-gated drift entry appended to project drift log |
+| `list_api_services` | — | Discovered services, live spec status, source outcomes, and operation counts |
+| `list_service_operations` | `service` | Bounded OpenAPI operation rows with honest truncation metadata |
+| `get_operation_schema` | `service`, `operation` | Request, response, and error views plus an unauthenticated curl template |
A top-level input/result field overview for every tool is on the
[MCP reference](https://rickylabs.github.io/netscript/reference/mcp/); the complete Standard Schema
@@ -273,7 +276,7 @@ The full flag reference, policy table, and composition options are on the docs s
## Docs
-- **MCP reference — the 14-tool field overview, policy, and exports**:
+- **MCP reference — the 17-tool field overview, policy, and exports**:
[rickylabs.github.io/netscript/reference/mcp/](https://rickylabs.github.io/netscript/reference/mcp/)
- **Agent tooling — install, flags, troubleshooting, CLI × skills × MCP**:
[rickylabs.github.io/netscript/capabilities/agent-tooling/](https://rickylabs.github.io/netscript/capabilities/agent-tooling/)
diff --git a/packages/mcp/cli.ts b/packages/mcp/cli.ts
index 201cc1ab44..8443a2081b 100644
--- a/packages/mcp/cli.ts
+++ b/packages/mcp/cli.ts
@@ -44,6 +44,11 @@ import { createRecordDriftFlow } from './src/application/flows/record-drift-flow
import type { ToolFlow } from './src/domain/tool-types.ts';
import type { DiagnosticEvidencePort } from './src/domain/diagnostic-evidence-port.ts';
import { withFlowReceipt } from './src/application/runner/receipt-lifecycle.ts';
+import { createListApiServicesFlow } from './src/application/flows/list-api-services-flow.ts';
+import { createListServiceOperationsFlow } from './src/application/flows/list-service-operations-flow.ts';
+import { createGetOperationSchemaFlow } from './src/application/flows/get-operation-schema-flow.ts';
+import { createServiceEndpointDirectory } from './src/application/service-endpoint-directory.ts';
+import type { ServiceEndpointDirectoryPort } from './src/ports/service-endpoint-directory-port.ts';
export * from './mod.ts';
@@ -65,6 +70,8 @@ export interface McpCliOptions {
/** Report a non-fatal failure to persist diagnostic evidence. */ readonly onEvidenceWarning?: (
message: string,
) => void;
+ /** Override service discovery and probing for embedders and fixtures. */ readonly serviceEndpointDirectory?:
+ ServiceEndpointDirectoryPort;
}
/** Resolve an explicit public documentation override from flags or environment. */
@@ -112,6 +119,9 @@ export function createMcpCliServer(options: McpCliOptions = {}): McpServer {
const probe = new FetchTelemetryProbe((endpoint) =>
createAspireDashboardFetch(endpoint, {}) ?? fetch
);
+ const serviceDirectory = options.serviceEndpointDirectory ?? createServiceEndpointDirectory({
+ projectRoot,
+ });
return createMcpServer({
probe,
environment,
@@ -169,6 +179,24 @@ export function createMcpCliServer(options: McpCliOptions = {}): McpServer {
warnEvidence,
),
record_drift: createRecordDriftFlow(evidence),
+ list_api_services: withReceipt(
+ createListApiServicesFlow(serviceDirectory),
+ evidence,
+ 'mcp list_api_services',
+ warnEvidence,
+ ),
+ list_service_operations: withReceipt(
+ createListServiceOperationsFlow(serviceDirectory),
+ evidence,
+ 'mcp list_service_operations',
+ warnEvidence,
+ ),
+ get_operation_schema: withReceipt(
+ createGetOperationSchemaFlow(serviceDirectory),
+ evidence,
+ 'mcp get_operation_schema',
+ warnEvidence,
+ ),
},
});
}
diff --git a/packages/mcp/mod.ts b/packages/mcp/mod.ts
index 35158102a5..6d7d7f063a 100644
--- a/packages/mcp/mod.ts
+++ b/packages/mcp/mod.ts
@@ -16,6 +16,27 @@ export {
recordDrift,
} from './src/application/flows/record-drift-flow.ts';
export type { RecordDriftInput } from './src/application/flows/record-drift-flow.ts';
+export {
+ API_SERVICE_RESULT_LIMIT,
+ createListApiServicesFlow,
+} from './src/application/flows/list-api-services-flow.ts';
+export type {
+ ApiServiceSummary,
+ ListApiServicesResult,
+} from './src/application/flows/list-api-services-flow.ts';
+export {
+ createListServiceOperationsFlow,
+ SERVICE_OPERATION_RESULT_LIMIT,
+} from './src/application/flows/list-service-operations-flow.ts';
+export type {
+ ListServiceOperationsResult,
+ ServiceOperationSummary,
+} from './src/application/flows/list-service-operations-flow.ts';
+export {
+ createGetOperationSchemaFlow,
+ OPENAPI_CURL_AUTH_NOTE,
+} from './src/application/flows/get-operation-schema-flow.ts';
+export type { GetOperationSchemaResult } from './src/application/flows/get-operation-schema-flow.ts';
export type {
DiagnosticEvidencePort,
DiagnosticEvidenceReceipt,
diff --git a/packages/mcp/src/application/flows/get-operation-schema-flow.ts b/packages/mcp/src/application/flows/get-operation-schema-flow.ts
new file mode 100644
index 0000000000..7c39fb0700
--- /dev/null
+++ b/packages/mcp/src/application/flows/get-operation-schema-flow.ts
@@ -0,0 +1,95 @@
+import { resolveCanonicalOperation } from '../../domain/openapi/canonical-identity.ts';
+import { indexOpenApiOperations } from '../../domain/openapi/operation-index.ts';
+import {
+ projectOperationSchemaViews,
+ SCHEMA_VIEW_NAMES,
+ type SchemaViewName,
+} from '../../domain/openapi/schema-views.ts';
+import type { ServiceEndpointDirectoryPort } from '../../ports/service-endpoint-directory-port.ts';
+import type { ToolExecutionResult, ToolFlow } from '../../domain/tool-types.ts';
+
+/** Explicit warning attached to every unauthenticated curl request template. */
+export const OPENAPI_CURL_AUTH_NOTE =
+ 'Unauthenticated request template only; add authorization explicitly when the service requires it.';
+
+/** Successful operation-schema value. */
+export interface GetOperationSchemaResult {
+ /** Selected service. */ readonly service: string;
+ /** Canonical resolved operation identity. */ readonly operation: string;
+ /** Canonical HTTP method. */ readonly method: string;
+ /** Exact OpenAPI path template. */ readonly path: string;
+ /** Selected projection view. */ readonly view: SchemaViewName;
+ /** S4-projected schema view. */ readonly schema: unknown;
+ /** Explicitly unauthenticated request template. */ readonly curlExample: string;
+ /** Authentication caveat for the request template. */ readonly authNote: string;
+}
+
+interface GetOperationSchemaInput {
+ readonly service: string;
+ readonly operation: string;
+ readonly view: SchemaViewName;
+}
+
+/** Create the schema-view flow from the existing endpoint directory and S4 projection. */
+export function createGetOperationSchemaFlow(directory: ServiceEndpointDirectoryPort): ToolFlow {
+ return async (input) => {
+ const parsed = parseInput(input);
+ if (!parsed) return failure('invalid_input', 'service and operation must be non-empty strings');
+ const result = await directory.list();
+ const row = result.entries.find((candidate) => candidate.name === parsed.service);
+ if (!row) return failure('service_unknown', `Unknown service "${parsed.service}".`);
+ if (row.status !== 'running') {
+ return failure(`service_${row.status}`, `Service "${parsed.service}" is ${row.status}.`);
+ }
+ const index = indexOpenApiOperations(row.spec);
+ const resolution = resolveCanonicalOperation(index, parsed.operation);
+ if (resolution.status !== 'resolved') {
+ const candidates =
+ (resolution.status === 'ambiguous' ? resolution.candidates : resolution.suggestions).slice(
+ 0,
+ 3,
+ );
+ return failure(
+ resolution.status === 'ambiguous' ? 'operation_ambiguous' : 'operation_unknown',
+ `Operation "${parsed.operation}" was not resolved. Candidates: ${
+ candidates.map((candidate) => candidate.canonicalId).join(', ') || 'none'
+ }.`,
+ );
+ }
+ const operation = resolution.operation;
+ const views = projectOperationSchemaViews(index.document, operation);
+ return {
+ ok: true,
+ value: {
+ service: parsed.service,
+ operation: operation.canonicalId,
+ method: operation.method,
+ path: operation.path,
+ view: parsed.view,
+ schema: views[parsed.view],
+ curlExample: curlExample(row.baseUrl, operation.method, operation.path),
+ authNote: OPENAPI_CURL_AUTH_NOTE,
+ } satisfies GetOperationSchemaResult,
+ };
+ };
+}
+
+function parseInput(input: unknown): GetOperationSchemaInput | undefined {
+ if (!input || typeof input !== 'object') return undefined;
+ const record = input as Record;
+ if (typeof record.service !== 'string' || !record.service) return undefined;
+ if (typeof record.operation !== 'string' || !record.operation) return undefined;
+ const view = record.view ?? 'all';
+ if (typeof view !== 'string' || !SCHEMA_VIEW_NAMES.includes(view as SchemaViewName)) {
+ return undefined;
+ }
+ return { service: record.service, operation: record.operation, view: view as SchemaViewName };
+}
+
+function curlExample(baseUrl: string, method: string, path: string): string {
+ return `curl -X ${method} '${baseUrl.replace(/\/$/, '')}${path}'`;
+}
+
+function failure(code: string, message: string): ToolExecutionResult {
+ return { ok: false, error: { code, message } };
+}
diff --git a/packages/mcp/src/application/flows/list-api-services-flow.ts b/packages/mcp/src/application/flows/list-api-services-flow.ts
new file mode 100644
index 0000000000..90595e845a
--- /dev/null
+++ b/packages/mcp/src/application/flows/list-api-services-flow.ts
@@ -0,0 +1,79 @@
+import { indexOpenApiOperations } from '../../domain/openapi/operation-index.ts';
+import type {
+ ServiceEndpointDirectoryPort,
+ ServiceEndpointRow,
+ SourceOutcome,
+} from '../../ports/service-endpoint-directory-port.ts';
+import type { ToolFlow } from '../../domain/tool-types.ts';
+
+/** Maximum service rows retained by this flow, below the central 50-row truncator. */
+export const API_SERVICE_RESULT_LIMIT = 49;
+
+/** One service rendered for OpenAPI introspection. */
+export interface ApiServiceSummary {
+ /** Stable service name. */ readonly name: string;
+ /** Current directory status. */ readonly status: ServiceEndpointRow['status'];
+ /** Selected discovery source. */ readonly source: ServiceEndpointRow['source'];
+ /** Lower-priority endpoint disagreements. */ readonly conflicts: ServiceEndpointRow['conflicts'];
+ /** Selected live base URL when one is known. */ readonly baseUrl?: string;
+ /** OpenAPI document URL derived from the selected base URL. */ readonly specUrl?: string;
+ /** Scalar documentation URL derived from the selected base URL. */ readonly docsUrl?: string;
+ /** Operation count, present only when a parsed spec was fetched. */ readonly operationCount?:
+ number;
+ /** Bounded degraded-state detail. */ readonly reason?: string;
+ /** HTTP status associated with an unavailable spec. */ readonly httpStatus?: number;
+ /** Operator remediation associated with an unavailable spec. */ readonly guidance?: string;
+}
+
+/** Complete service-list value, including the exact S5 source outcomes. */
+export interface ListApiServicesResult {
+ /** Stable service summaries. */ readonly services: readonly ApiServiceSummary[];
+ /** S5 source outcomes, forwarded without transformation. */ readonly sources:
+ readonly SourceOutcome[];
+ /** True exactly when at least one service row was dropped. */ readonly truncated: boolean;
+}
+
+/** Create the service-list flow from the existing endpoint directory. */
+export function createListApiServicesFlow(directory: ServiceEndpointDirectoryPort): ToolFlow {
+ return async () => {
+ const result = await directory.list();
+ return {
+ ok: true,
+ value: {
+ services: result.entries.slice(0, API_SERVICE_RESULT_LIMIT).map(serviceSummary),
+ sources: result.sources,
+ truncated: result.entries.length > API_SERVICE_RESULT_LIMIT,
+ } satisfies ListApiServicesResult,
+ };
+ };
+}
+
+function serviceSummary(row: ServiceEndpointRow): ApiServiceSummary {
+ const base = 'baseUrl' in row ? row.baseUrl : undefined;
+ const reason = 'reason' in row ? row.reason : undefined;
+ return {
+ name: row.name,
+ status: row.status,
+ source: row.source,
+ conflicts: row.conflicts,
+ ...(base === undefined ? {} : {
+ baseUrl: base,
+ specUrl: endpointUrl(base, '/api/openapi.json'),
+ docsUrl: endpointUrl(base, '/api/docs'),
+ }),
+ ...(row.status === 'running'
+ ? { operationCount: indexOpenApiOperations(row.spec).operations.length }
+ : {}),
+ ...(reason === undefined ? {} : { reason }),
+ ...(row.status === 'spec_unavailable' && row.httpStatus !== undefined
+ ? { httpStatus: row.httpStatus }
+ : {}),
+ ...(row.status === 'spec_unavailable' && row.guidance !== undefined
+ ? { guidance: row.guidance }
+ : {}),
+ };
+}
+
+function endpointUrl(baseUrl: string, path: string): string {
+ return `${baseUrl.replace(/\/$/, '')}${path}`;
+}
diff --git a/packages/mcp/src/application/flows/list-service-operations-flow.ts b/packages/mcp/src/application/flows/list-service-operations-flow.ts
new file mode 100644
index 0000000000..5dbcac2987
--- /dev/null
+++ b/packages/mcp/src/application/flows/list-service-operations-flow.ts
@@ -0,0 +1,114 @@
+import { describeOpenApiOperation } from '../../domain/openapi/description-ladder.ts';
+import { indexOpenApiOperations } from '../../domain/openapi/operation-index.ts';
+import type { ServiceEndpointDirectoryPort } from '../../ports/service-endpoint-directory-port.ts';
+import type { ToolFlow } from '../../domain/tool-types.ts';
+
+/** Maximum rows retained by this flow, below the central 50-row truncator. */
+export const SERVICE_OPERATION_RESULT_LIMIT = 49;
+
+/** One bounded operation row. */
+export interface ServiceOperationSummary {
+ /** Canonical dotted id or method-path fallback. */ readonly operation: string;
+ /** Canonical HTTP method. */ readonly method: string;
+ /** Exact OpenAPI path template. */ readonly path: string;
+ /** S4 description-ladder result. */ readonly summary: string;
+ /** Declared OpenAPI tags. */ readonly tags: readonly string[];
+}
+
+/** Successful operation-list value. */
+export interface ListServiceOperationsResult {
+ /** Selected service. */ readonly service: string;
+ /** Retained operation rows. */ readonly operations: readonly ServiceOperationSummary[];
+ /** True exactly when at least one matching row was dropped. */ readonly truncated: boolean;
+}
+
+interface ListServiceOperationsInput {
+ readonly service: string;
+ readonly filter?: string;
+ readonly limit?: number;
+}
+
+/** Create the operation-list flow from the existing endpoint directory and S4 projection. */
+export function createListServiceOperationsFlow(directory: ServiceEndpointDirectoryPort): ToolFlow {
+ return async (input) => {
+ const parsed = parseInput(input);
+ if (!parsed) return invalidInput('service must be a non-empty string');
+ const result = await directory.list();
+ const row = result.entries.find((candidate) => candidate.name === parsed.service);
+ if (!row) {
+ return serviceFailure(
+ 'service_unknown',
+ parsed.service,
+ result.entries.slice(0, 3).map((candidate) => candidate.name),
+ );
+ }
+ if (row.status !== 'running') return serviceFailure(`service_${row.status}`, parsed.service);
+
+ const needle = parsed.filter?.toLocaleLowerCase('en-US');
+ const matching = indexOpenApiOperations(row.spec).operations
+ .map((operation): ServiceOperationSummary => ({
+ operation: operation.canonicalId,
+ method: operation.method,
+ path: operation.path,
+ summary: describeOpenApiOperation(operation),
+ tags: Array.isArray(operation.operation.tags)
+ ? operation.operation.tags.filter((tag): tag is string => typeof tag === 'string')
+ : [],
+ }))
+ .filter((operation) => !needle || operationSearchText(operation).includes(needle));
+ const limit = Math.min(
+ parsed.limit ?? SERVICE_OPERATION_RESULT_LIMIT,
+ SERVICE_OPERATION_RESULT_LIMIT,
+ );
+ return {
+ ok: true,
+ value: {
+ service: parsed.service,
+ operations: matching.slice(0, limit),
+ truncated: matching.length > limit,
+ } satisfies ListServiceOperationsResult,
+ };
+ };
+}
+
+function parseInput(input: unknown): ListServiceOperationsInput | undefined {
+ if (!input || typeof input !== 'object') return undefined;
+ const record = input as Record;
+ if (typeof record.service !== 'string' || record.service.length === 0) return undefined;
+ if (record.filter !== undefined && typeof record.filter !== 'string') return undefined;
+ if (
+ record.limit !== undefined &&
+ (typeof record.limit !== 'number' || !Number.isInteger(record.limit) || record.limit < 1)
+ ) {
+ return undefined;
+ }
+ return {
+ service: record.service,
+ ...(typeof record.filter === 'string' ? { filter: record.filter } : {}),
+ ...(typeof record.limit === 'number' ? { limit: record.limit } : {}),
+ };
+}
+
+function operationSearchText(operation: ServiceOperationSummary): string {
+ return [
+ operation.operation,
+ operation.method,
+ operation.path,
+ operation.summary,
+ ...operation.tags,
+ ]
+ .join('\n').toLocaleLowerCase('en-US');
+}
+
+function invalidInput(message: string): Awaited> {
+ return { ok: false, error: { code: 'invalid_input', message } };
+}
+
+function serviceFailure(
+ code: string,
+ service: string,
+ known?: readonly string[],
+): Awaited> {
+ const suffix = known ? ` Known services: ${known.join(', ') || 'none'}.` : '';
+ return { ok: false, error: { code, message: `Service "${service}" is unavailable.${suffix}` } };
+}
diff --git a/packages/mcp/src/application/tool-registry.ts b/packages/mcp/src/application/tool-registry.ts
index 4fbd53341e..b283bf80a6 100644
--- a/packages/mcp/src/application/tool-registry.ts
+++ b/packages/mcp/src/application/tool-registry.ts
@@ -23,6 +23,9 @@ const kinds: Readonly> = {
list_commands: 'meta',
execute_command: 'mutate',
record_drift: 'mutate',
+ list_api_services: 'read',
+ list_service_operations: 'read',
+ get_operation_schema: 'read',
};
const summaries: Readonly> = {
get_app_status: 'Summarize NetScript app health.',
@@ -41,6 +44,12 @@ const summaries: Readonly> = {
'Execute one CLI command through an explicit allowlist gate and return only a bounded combined output tail.',
record_drift:
'Record drift only after a fresh successful diagnostic receipt for the same resource.',
+ list_api_services:
+ 'List discovered API services and live OpenAPI status instead of guessing endpoints with curl.',
+ list_service_operations:
+ 'List bounded operations for one service instead of guessing endpoint contracts with curl.',
+ get_operation_schema:
+ 'Get one operation request, response, and error schema before constructing a curl request.',
};
/** Build the immutable enumerable v1 tool registry. */
diff --git a/packages/mcp/src/domain/tool-contracts.ts b/packages/mcp/src/domain/tool-contracts.ts
index e46ad59b43..88e9a5d0a4 100644
--- a/packages/mcp/src/domain/tool-contracts.ts
+++ b/packages/mcp/src/domain/tool-contracts.ts
@@ -103,6 +103,17 @@ const inputShapes: Record>> = {
summary: stringProperty,
details: stringProperty,
}, ['resource', 'summary']),
+ list_api_services: objectSchema(),
+ list_service_operations: objectSchema({
+ service: stringProperty,
+ filter: stringProperty,
+ limit: limitProperty,
+ }, ['service']),
+ get_operation_schema: objectSchema({
+ service: stringProperty,
+ operation: stringProperty,
+ view: { enum: ['request', 'response', 'errors', 'all'] },
+ }, ['service', 'operation']),
};
const outputShapes: Record>> = {
@@ -239,6 +250,26 @@ const outputShapes: Record>> = {
resource: stringProperty,
receipt: { type: 'object' },
}, ['recorded', 'resource', 'receipt']),
+ list_api_services: objectSchema({
+ services: { type: 'array', maxItems: 49 },
+ sources: { type: 'array', maxItems: 4 },
+ truncated: { type: 'boolean' },
+ }, ['services', 'sources', 'truncated']),
+ list_service_operations: objectSchema({
+ service: stringProperty,
+ operations: { type: 'array', maxItems: 49 },
+ truncated: { type: 'boolean' },
+ }, ['service', 'operations', 'truncated']),
+ get_operation_schema: objectSchema({
+ service: stringProperty,
+ operation: stringProperty,
+ method: stringProperty,
+ path: stringProperty,
+ view: { enum: ['request', 'response', 'errors', 'all'] },
+ schema: { type: 'object' },
+ curlExample: stringProperty,
+ authNote: stringProperty,
+ }, ['service', 'operation', 'method', 'path', 'view', 'schema', 'curlExample', 'authNote']),
};
/** Standard-Schema input contracts for the complete v1 tool surface. */
diff --git a/packages/mcp/src/domain/tool-types.ts b/packages/mcp/src/domain/tool-types.ts
index ef1fc9ba15..4f2f097a39 100644
--- a/packages/mcp/src/domain/tool-types.ts
+++ b/packages/mcp/src/domain/tool-types.ts
@@ -16,6 +16,9 @@ export const TOOL_NAMES = [
'list_commands',
'execute_command',
'record_drift',
+ 'list_api_services',
+ 'list_service_operations',
+ 'get_operation_schema',
] as const;
/** Name of a registered v1 tool. */
diff --git a/packages/mcp/src/publish-assets.generated.ts b/packages/mcp/src/publish-assets.generated.ts
index d955fcea3a..143ba02474 100644
--- a/packages/mcp/src/publish-assets.generated.ts
+++ b/packages/mcp/src/publish-assets.generated.ts
@@ -6,4 +6,4 @@ export const MCP_PACKAGE_VERSION: string = '0.0.4';
/** Published MCP README embedded as the default documentation corpus. */
export const MCP_PACKAGE_README: string =
- '# @netscript/mcp\n\n[](https://jsr.io/@netscript/mcp)\n[](https://github.com/rickylabs/netscript/actions/workflows/ci.yml)\n[](https://rickylabs.github.io/netscript/)\n\n**The Model Context Protocol server for NetScript: 14 token-bounded tools that let a coding agent\nmonitor a running app, debug a correlated execution, read framework-semantic telemetry, run the\ndoctor, and search the docs — all over stdio.**\n\nPoint Claude Code or VS Code at a running NetScript app and the agent can ask _"is the app\nhealthy?"_, _"why did the last import job fail?"_, and _"what is slowing down `checkout`?"_ — and\nget compact, structured answers instead of raw logs. It can correlate one execution\'s spans, logs,\nand outcome by id; rank the queries hammering your database; and trigger allowlisted CLI commands\nthrough a default-deny policy. One command — `netscript agent init` — wires all of it into your\nagent host.\n\nGeneric observability tooling hands an agent raw spans and log lines and lets it burn its context\nwindow re-deriving structure the framework already knows. `@netscript/mcp` answers in NetScript\'s\nown vocabulary — jobs, sagas, triggers, streams, services — and bounds every result server-side, so\nthe agent gets percentiles, error rates, and ranked operations rather than the spans they were\ncomputed from. It reads the same OpenTelemetry data the Aspire dashboard shows you, and complements\nAspire\'s own MCP server: Aspire speaks resources and containers; this server speaks your app.\n\n## Why agents like it\n\n- **14 token-bounded tools** — every successful result is capped server-side (50 array items, 2,000\n characters per string) before it reaches the model; the analytics tools never return raw spans at\n all.\n- **Framework-semantic trace intelligence** — tools classify telemetry into `worker`, `saga`,\n `trigger`, `stream`, and `service` domains and correlate whole executions by id, because they\n understand the `netscript.*` attribute conventions the framework emits.\n- **Default-deny CLI gate** — `execute_command` matches commands against an ordered prefix policy;\n deny beats allow, anything unmatched is denied, and the shipped policy explicitly denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove`.\n- **One-command install** — `netscript agent init` detects your agent host, writes the MCP\n configuration, and installs the matching NetScript skills.\n- **Matched agent surface** — `netscript agent init` writes host configuration pinned to your\n installed CLI version and installs the skills that ship with that same release, so the tool\n catalog the agent sees comes from the release it runs.\n- **Zero npm MCP SDK** — a minimal newline-delimited JSON-RPC transport keeps the dependency graph\n lean and the lockfile stable.\n\n## Architecture\n\n```mermaid\nflowchart LR\n A["Agent host
(Claude Code, VS Code, ...)"] <-- "JSON-RPC / stdio" --> S["netscript agent mcp
14 tools · bounded results"]\n S --> T["Telemetry endpoint
(OTLP read model)"]\n S --> D["Docs corpus
(public Markdown)"]\n S --> P["Command policy
(default-deny allowlist)"]\n T --> R["Running NetScript app"]\n P --> C["netscript CLI"]\n C --> R\n```\n\nThe server is one third of the NetScript agent surface — the CLI is the hands, the skills are the\nplaybook, MCP is the eyes. It deliberately wraps the CLI rather than reimplementing it:\n`list_commands` reflects the live command tree, and `execute_command` shells the CLI through the\npolicy gate. MCP exists for what a shell cannot cheaply give an agent — bounded aggregation,\ncross-domain diagnostics, and documentation lookup.\n\n## Install\n\nMost users never import this package. Install the server into a project with the CLI:\n\n```bash\nnetscript agent init\n```\n\nThat detects your agent host and writes `.mcp.json` (Claude Code) and/or `.vscode/mcp.json` (VS\nCode) pointing at `netscript agent mcp`, and installs the NetScript skills shipped with your CLI\nrelease. Use `--host claude|vscode|all` to choose explicitly.\n\nTo embed the server in your own host process, add it as a library:\n\n```bash\ndeno add jsr:@netscript/mcp@\n```\n\nTo run the standalone stdio entrypoint directly when integrating another MCP host:\n\n```bash\ndeno x -A jsr:@netscript/mcp@/cli\n```\n\nPin `` to match your installed CLI; bare `jsr:@netscript/*` specifiers do not resolve on\nthe pre-release line, and `netscript agent init` writes the correct pinned form for you.\n\n## Quick example\n\n**1. Wire up an agent host.** From a NetScript project root:\n\n```bash\n$ netscript agent init\nInstalled NetScript agent integration for claude, vscode.\n```\n\nThe generated `.mcp.json` runs the server for this project — equivalent to:\n\n```json\n{\n "mcpServers": {\n "netscript": {\n "command": "deno",\n "args": [\n "run",\n "-A",\n "jsr:@netscript/cli@",\n "agent",\n "mcp",\n "--project-root",\n ""\n ]\n }\n }\n}\n```\n\n**2. Ask the agent.** With the app started, the agent turns questions into bounded tool calls:\n\n> **You:** Is the app healthy? Anything in the docs about telemetry?\n>\n> **Agent:** calls `get_app_status` →\n> `{"status": "…", "counts": {…}, "domains": [{"domain": "worker", …}, …]}` — a health verdict with\n> per-domain summaries, not a span dump. Calls `search_docs {"query": "telemetry"}` →\n> `{"count": 1, "matches": [{"slug": "mcp", "title": "@netscript/mcp", "snippet": "…", "score": 35}]}`,\n> then `get_doc` with the winning slug to read just the section it needs.\n\nWhen telemetry is unreachable, nothing crashes: `get_app_status` and the doctor\'s telemetry checks\nreport a structured `warn`/`fail` status, the list and analytics tools return their ordinary empty\nresults, and `get_run` returns a structured `run_not_found` error the agent can reason about.\n\n## Tool catalog\n\n| Tool | Required input | Bounded result |\n| ----------------------------- | ----------------- | ---------------------------------------------------------------------------- |\n| `get_app_status` | — | Health verdict, counts, per-domain summaries |\n| `list_runs` | — | Recent executions filtered by domain, status, service, time |\n| `get_run` | `id` | One correlated execution with bounded spans and logs |\n| `get_recent_errors` | — | Recent errors grouped by service and domain |\n| `get_last_job_result` | — | The latest matching job outcome |\n| `analyze_service_performance` | `service` | Duration percentiles, throughput, error rate |\n| `analyze_db_bottlenecks` | — | Ranked database and KV operations |\n| `doctor` | — | Telemetry, Aspire, wiring, and plugin checks; suggested fixes on problems |\n| `search_docs` | `query` | Ranked public-document matches with snippets |\n| `list_docs` | — | Public-document summaries |\n| `get_doc` | `slug` | One public document, or one named section of it |\n| `list_commands` | — | Live CLI command descriptors |\n| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked |\n| `record_drift` | `resource`, `summary` | Evidence-gated drift entry appended to project drift log |\n\nA top-level input/result field overview for every tool is on the\n[MCP reference](https://rickylabs.github.io/netscript/reference/mcp/); the complete Standard Schema\ncontracts are published as `TOOL_INPUT_SCHEMAS` / `TOOL_OUTPUT_SCHEMAS` and returned by the live\n`tools/list`.\n\n## Record drift\n\n`record_drift` is an evidence-gated mutating tool that records verified architecture or runtime drift into `.netscript/agent/drift.jsonl`.\n\n- **Required evidence**: Requires a fresh successful diagnostic receipt (timestamped within 15 minutes, `exitStatus: 0`) for the target resource. Receipts are automatically produced when calling `doctor`, telemetry tools, or `netscript plugin doctor --resource `.\n- **Target & Scope**: The `resource` argument targets a specific plugin, service, or `\'project\'`. Receipts live at `.netscript/agent/diagnostics/.json`.\n- **Mutation behavior**: Appends a single JSON line to `.netscript/agent/drift.jsonl` under the project root containing `timestamp`, `resource`, `summary`, optional `details`, and the attached evidence receipt.\n- **Failure modes**: If no receipt exists, if the receipt is older than 15 minutes, or if the receipt recorded a non-zero exit status, `record_drift` refuses with structured error code `diagnostic_evidence_required`.\n- **Dry-run / Preview**: Inspecting receipts or running `doctor` / telemetry tools previews current diagnostic state without mutating `drift.jsonl`.\n\n## Embedding as a library\n\nTo run the public stdio composition from your own Deno entrypoint:\n\n```ts\nimport { runMcpStdioServer } from \'@netscript/mcp/cli\';\n\nawait runMcpStdioServer({\n projectRoot: Deno.cwd(),\n // Omit docsRoot to use the package-embedded corpus, or select a filesystem corpus explicitly.\n docsRoot: Deno.env.get(\'NETSCRIPT_DOCS_ROOT\'),\n});\n```\n\n`runMcpStdioServer` owns the newline-delimited stdio transport. It shuts down when the host closes\nstdin or terminates the process; callers do not need to reach into an internal transport API.\n\n## Public surface\n\nThree entrypoints carry the package:\n\n| Entry | What it gives you |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| `.` | Tool contracts and schemas, the tool registry, protocol runner, service endpoint directory ports, and default adapters |\n| `./cli` | The executable composition plus every export from `.`, including the service endpoint directory surface |\n| `./openapi-projection` | Pure OpenAPI operation indexing and schema projections, with no discovery, filesystem, network, or runtime work |\n\nThe projection subpath accepts an already-loaded OpenAPI document. It keeps discovery and I/O at\nthe caller\'s boundary:\n\n```ts\nimport { indexOpenApiOperations } from \'@netscript/mcp/openapi-projection\';\n\nconst index = indexOpenApiOperations(openApiDocument);\n\nEvery tool flow depends on a port interface, so embedders and tests supply their own adapters and\nassert against the published schemas. The always-current symbol list is\n[`deno doc jsr:@netscript/mcp@`](https://jsr.io/@netscript/mcp/doc) (pin `` on the\npre-release line, as above).\n\n### Discover service OpenAPI endpoints\n\nEmbedders can compose the four discovery sources and bounded network probe without importing an\nOpenAPI projection layer:\n\n```ts\nimport { createServiceEndpointDirectory } from \'@netscript/mcp\';\n\nconst endpoints = createServiceEndpointDirectory({\n projectRoot: Deno.cwd(),\n});\n\nconst { entries, sources } = await endpoints.list();\n```\n\nThe effective per-service precedence is `override > aspire-cli > run-manifest > appsettings`. Every\nsource remains visible as `used`, `absent`, or `failed`; a failed Aspire CLI query or a stale\nmanifest is never rendered as healthy absence. The manifest at `.netscript/run/endpoints.json` is\neligible only when its real project root and `runId` match the supplied current run. `appHostPath`\ndefaults to `./aspire/apphost.mts`; override it when the active AppHost lives elsewhere. Supply\n`expectedRunId` only when the host owns the current AppHost run token; without that identity proof,\na present run manifest is reported as failed and does not contribute endpoints.\n\nExplicit operator endpoints and exclusions live only in the S5-owned subsection of\n`.netscript/agent-mcp.json`; sibling settings are ignored:\n\n```json\n{\n "introspection": {\n "serviceEndpoints": {\n "orders": "https://orders.example.test"\n },\n "excludeServices": ["internal-admin"]\n }\n}\n```\n\nExclusions are applied before network access. Other rows report `running`, `not_running`,\n`spec_unavailable`, or `identity_mismatch`; parsed OpenAPI is retained as opaque JSON for a later\nconsumer. Probes do not send credentials or follow redirects. A 401/403 explains how to expose only\nthe OpenAPI route anonymously or supply a reachable public spec URL. A running service must return\nJSON containing its selected service name, for example `{ "service": "orders" }`, from its selected\nbase path; this second request prevents a reused port from being mistaken for the intended service.\n\nThe default library composition needs `--allow-read` for carriers and real-path checks,\n`--allow-run` for `aspire describe`, and `--allow-net` for bounded spec/identity requests. Tests and\ncustom hosts can replace every source and the probe through `ServiceEndpointDirectoryOptions`.\n\n## Configuration at a glance\n\n- **Telemetry endpoint discovery** (tools and `doctor`): explicit `--endpoint`, then\n `NETSCRIPT_TELEMETRY_ENDPOINT`, then `ASPIRE_DASHBOARD_PORT`, then `http://localhost:18888`.\n- **Docs corpus**: by default the docs tools index the documentation shipped with the installed\n package; set `--docs-root ` (or `NETSCRIPT_DOCS_ROOT`) to serve a project or site corpus\n instead.\n- **Service endpoint discovery** (library surface): `.netscript/agent-mcp.json` override, then the\n Aspire CLI machine-readable query, then an identity-bound run manifest, then\n `aspire/appsettings.json`; lower-priority disagreements remain visible as conflicts.\n- **Command policy**: the shipped default allows the prefixes\n `db init|generate|migrate|seed|status|introspect`, `generate`, `contract`, `service list`,\n `plugin install|list|sync|doctor`, and `ui:add|ui:init|ui:list|ui:update`, and denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove` — deny beats allow, anything\n unmatched is denied. Embedders can pass their own policy.\n\nThe full flag reference, policy table, and composition options are on the docs site.\n\n## Docs\n\n- **MCP reference — the 14-tool field overview, policy, and exports**:\n [rickylabs.github.io/netscript/reference/mcp/](https://rickylabs.github.io/netscript/reference/mcp/)\n- **Agent tooling — install, flags, troubleshooting, CLI × skills × MCP**:\n [rickylabs.github.io/netscript/capabilities/agent-tooling/](https://rickylabs.github.io/netscript/capabilities/agent-tooling/)\n- **API docs on JSR**: [jsr.io/@netscript/mcp/doc](https://jsr.io/@netscript/mcp/doc)\n\n## Compatibility\n\nThe **server** requires Deno 2.9+ (both entrypoints use `Deno.*` APIs); Node.js and Bun are not\nsupported as server runtimes. The **client** side is unconstrained: any MCP-capable host — Claude\nCode, VS Code, and others — only has to spawn the process and speak JSON-RPC over stdio. The\nexecutable needs `--allow-env`, `--allow-net`, `--allow-read`, and `--allow-run`; the `netscript`\nbinary grants these at its edge. The server never returns project source, environment-variable\nvalues, credentials, or secrets.\n\n## License\n\nApache-2.0 — see [LICENSE](https://github.com/rickylabs/netscript/blob/main/LICENSE). Published to\nJSR with cryptographically verified provenance.\n';
+ '# @netscript/mcp\n\n[](https://jsr.io/@netscript/mcp)\n[](https://github.com/rickylabs/netscript/actions/workflows/ci.yml)\n[](https://rickylabs.github.io/netscript/)\n\n**The Model Context Protocol server for NetScript: 17 token-bounded tools that let a coding agent\nmonitor a running app, debug a correlated execution, read framework-semantic telemetry, run the\ndoctor, and search the docs — all over stdio.**\n\nPoint Claude Code or VS Code at a running NetScript app and the agent can ask _"is the app\nhealthy?"_, _"why did the last import job fail?"_, and _"what is slowing down `checkout`?"_ — and\nget compact, structured answers instead of raw logs. It can correlate one execution\'s spans, logs,\nand outcome by id; rank the queries hammering your database; and trigger allowlisted CLI commands\nthrough a default-deny policy. One command — `netscript agent init` — wires all of it into your\nagent host.\n\nGeneric observability tooling hands an agent raw spans and log lines and lets it burn its context\nwindow re-deriving structure the framework already knows. `@netscript/mcp` answers in NetScript\'s\nown vocabulary — jobs, sagas, triggers, streams, services — and bounds every result server-side, so\nthe agent gets percentiles, error rates, and ranked operations rather than the spans they were\ncomputed from. It reads the same OpenTelemetry data the Aspire dashboard shows you, and complements\nAspire\'s own MCP server: Aspire speaks resources and containers; this server speaks your app.\n\n## Why agents like it\n\n- **17 token-bounded tools** — every successful result is capped server-side (50 array items, 2,000\n characters per string) before it reaches the model; the analytics tools never return raw spans at\n all.\n- **Framework-semantic trace intelligence** — tools classify telemetry into `worker`, `saga`,\n `trigger`, `stream`, and `service` domains and correlate whole executions by id, because they\n understand the `netscript.*` attribute conventions the framework emits.\n- **Default-deny CLI gate** — `execute_command` matches commands against an ordered prefix policy;\n deny beats allow, anything unmatched is denied, and the shipped policy explicitly denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove`.\n- **One-command install** — `netscript agent init` detects your agent host, writes the MCP\n configuration, and installs the matching NetScript skills.\n- **Matched agent surface** — `netscript agent init` writes host configuration pinned to your\n installed CLI version and installs the skills that ship with that same release, so the tool\n catalog the agent sees comes from the release it runs.\n- **Zero npm MCP SDK** — a minimal newline-delimited JSON-RPC transport keeps the dependency graph\n lean and the lockfile stable.\n\n## Architecture\n\n```mermaid\nflowchart LR\n A["Agent host
(Claude Code, VS Code, ...)"] <-- "JSON-RPC / stdio" --> S["netscript agent mcp
17 tools · bounded results"]\n S --> T["Telemetry endpoint
(OTLP read model)"]\n S --> D["Docs corpus
(public Markdown)"]\n S --> P["Command policy
(default-deny allowlist)"]\n T --> R["Running NetScript app"]\n P --> C["netscript CLI"]\n C --> R\n```\n\nThe server is one third of the NetScript agent surface — the CLI is the hands, the skills are the\nplaybook, MCP is the eyes. It deliberately wraps the CLI rather than reimplementing it:\n`list_commands` reflects the live command tree, and `execute_command` shells the CLI through the\npolicy gate. MCP exists for what a shell cannot cheaply give an agent — bounded aggregation,\ncross-domain diagnostics, and documentation lookup.\n\n## Install\n\nMost users never import this package. Install the server into a project with the CLI:\n\n```bash\nnetscript agent init\n```\n\nThat detects your agent host and writes `.mcp.json` (Claude Code) and/or `.vscode/mcp.json` (VS\nCode) pointing at `netscript agent mcp`, and installs the NetScript skills shipped with your CLI\nrelease. Use `--host claude|vscode|all` to choose explicitly.\n\nTo embed the server in your own host process, add it as a library:\n\n```bash\ndeno add jsr:@netscript/mcp@\n```\n\nTo run the standalone stdio entrypoint directly when integrating another MCP host:\n\n```bash\ndeno x -A jsr:@netscript/mcp@/cli\n```\n\nPin `` to match your installed CLI; bare `jsr:@netscript/*` specifiers do not resolve on\nthe pre-release line, and `netscript agent init` writes the correct pinned form for you.\n\n## Quick example\n\n**1. Wire up an agent host.** From a NetScript project root:\n\n```bash\n$ netscript agent init\nInstalled NetScript agent integration for claude, vscode.\n```\n\nThe generated `.mcp.json` runs the server for this project — equivalent to:\n\n```json\n{\n "mcpServers": {\n "netscript": {\n "command": "deno",\n "args": [\n "run",\n "-A",\n "jsr:@netscript/cli@",\n "agent",\n "mcp",\n "--project-root",\n ""\n ]\n }\n }\n}\n```\n\n**2. Ask the agent.** With the app started, the agent turns questions into bounded tool calls:\n\n> **You:** Is the app healthy? Anything in the docs about telemetry?\n>\n> **Agent:** calls `get_app_status` →\n> `{"status": "…", "counts": {…}, "domains": [{"domain": "worker", …}, …]}` — a health verdict with\n> per-domain summaries, not a span dump. Calls `search_docs {"query": "telemetry"}` →\n> `{"count": 1, "matches": [{"slug": "mcp", "title": "@netscript/mcp", "snippet": "…", "score": 35}]}`,\n> then `get_doc` with the winning slug to read just the section it needs.\n\nWhen telemetry is unreachable, nothing crashes: `get_app_status` and the doctor\'s telemetry checks\nreport a structured `warn`/`fail` status, the list and analytics tools return their ordinary empty\nresults, and `get_run` returns a structured `run_not_found` error the agent can reason about.\n\n## Tool catalog\n\n| Tool | Required input | Bounded result |\n| ----------------------------- | ----------------- | ---------------------------------------------------------------------------- |\n| `get_app_status` | — | Health verdict, counts, per-domain summaries |\n| `list_runs` | — | Recent executions filtered by domain, status, service, time |\n| `get_run` | `id` | One correlated execution with bounded spans and logs |\n| `get_recent_errors` | — | Recent errors grouped by service and domain |\n| `get_last_job_result` | — | The latest matching job outcome |\n| `analyze_service_performance` | `service` | Duration percentiles, throughput, error rate |\n| `analyze_db_bottlenecks` | — | Ranked database and KV operations |\n| `doctor` | — | Telemetry, Aspire, wiring, and plugin checks; suggested fixes on problems |\n| `search_docs` | `query` | Ranked public-document matches with snippets |\n| `list_docs` | — | Public-document summaries |\n| `get_doc` | `slug` | One public document, or one named section of it |\n| `list_commands` | — | Live CLI command descriptors |\n| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked |\n| `record_drift` | `resource`, `summary` | Evidence-gated drift entry appended to project drift log |\n| `list_api_services` | — | Discovered services, live spec status, source outcomes, and operation counts |\n| `list_service_operations` | `service` | Bounded OpenAPI operation rows with honest truncation metadata |\n| `get_operation_schema` | `service`, `operation` | Request, response, and error views plus an unauthenticated curl template |\n\nA top-level input/result field overview for every tool is on the\n[MCP reference](https://rickylabs.github.io/netscript/reference/mcp/); the complete Standard Schema\ncontracts are published as `TOOL_INPUT_SCHEMAS` / `TOOL_OUTPUT_SCHEMAS` and returned by the live\n`tools/list`.\n\n## Record drift\n\n`record_drift` is an evidence-gated mutating tool that records verified architecture or runtime drift into `.netscript/agent/drift.jsonl`.\n\n- **Required evidence**: Requires a fresh successful diagnostic receipt (timestamped within 15 minutes, `exitStatus: 0`) for the target resource. Receipts are automatically produced when calling `doctor`, telemetry tools, or `netscript plugin doctor --resource `.\n- **Target & Scope**: The `resource` argument targets a specific plugin, service, or `\'project\'`. Receipts live at `.netscript/agent/diagnostics/.json`.\n- **Mutation behavior**: Appends a single JSON line to `.netscript/agent/drift.jsonl` under the project root containing `timestamp`, `resource`, `summary`, optional `details`, and the attached evidence receipt.\n- **Failure modes**: If no receipt exists, if the receipt is older than 15 minutes, or if the receipt recorded a non-zero exit status, `record_drift` refuses with structured error code `diagnostic_evidence_required`.\n- **Dry-run / Preview**: Inspecting receipts or running `doctor` / telemetry tools previews current diagnostic state without mutating `drift.jsonl`.\n\n## Embedding as a library\n\nTo run the public stdio composition from your own Deno entrypoint:\n\n```ts\nimport { runMcpStdioServer } from \'@netscript/mcp/cli\';\n\nawait runMcpStdioServer({\n projectRoot: Deno.cwd(),\n // Omit docsRoot to use the package-embedded corpus, or select a filesystem corpus explicitly.\n docsRoot: Deno.env.get(\'NETSCRIPT_DOCS_ROOT\'),\n});\n```\n\n`runMcpStdioServer` owns the newline-delimited stdio transport. It shuts down when the host closes\nstdin or terminates the process; callers do not need to reach into an internal transport API.\n\n## Public surface\n\nThree entrypoints carry the package:\n\n| Entry | What it gives you |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| `.` | Tool contracts and schemas, the tool registry, protocol runner, service endpoint directory ports, and default adapters |\n| `./cli` | The executable composition plus every export from `.`, including the service endpoint directory surface |\n| `./openapi-projection` | Pure OpenAPI operation indexing and schema projections, with no discovery, filesystem, network, or runtime work |\n\nThe projection subpath accepts an already-loaded OpenAPI document. It keeps discovery and I/O at\nthe caller\'s boundary:\n\n```ts\nimport { indexOpenApiOperations } from \'@netscript/mcp/openapi-projection\';\n\nconst index = indexOpenApiOperations(openApiDocument);\n\nEvery tool flow depends on a port interface, so embedders and tests supply their own adapters and\nassert against the published schemas. The always-current symbol list is\n[`deno doc jsr:@netscript/mcp@`](https://jsr.io/@netscript/mcp/doc) (pin `` on the\npre-release line, as above).\n\n### Discover service OpenAPI endpoints\n\nEmbedders can compose the four discovery sources and bounded network probe without importing an\nOpenAPI projection layer:\n\n```ts\nimport { createServiceEndpointDirectory } from \'@netscript/mcp\';\n\nconst endpoints = createServiceEndpointDirectory({\n projectRoot: Deno.cwd(),\n});\n\nconst { entries, sources } = await endpoints.list();\n```\n\nThe effective per-service precedence is `override > aspire-cli > run-manifest > appsettings`. Every\nsource remains visible as `used`, `absent`, or `failed`; a failed Aspire CLI query or a stale\nmanifest is never rendered as healthy absence. The manifest at `.netscript/run/endpoints.json` is\neligible only when its real project root and `runId` match the supplied current run. `appHostPath`\ndefaults to `./aspire/apphost.mts`; override it when the active AppHost lives elsewhere. Supply\n`expectedRunId` only when the host owns the current AppHost run token; without that identity proof,\na present run manifest is reported as failed and does not contribute endpoints.\n\nExplicit operator endpoints and exclusions live only in the S5-owned subsection of\n`.netscript/agent-mcp.json`; sibling settings are ignored:\n\n```json\n{\n "introspection": {\n "serviceEndpoints": {\n "orders": "https://orders.example.test"\n },\n "excludeServices": ["internal-admin"]\n }\n}\n```\n\nExclusions are applied before network access. Other rows report `running`, `not_running`,\n`spec_unavailable`, or `identity_mismatch`; parsed OpenAPI is retained as opaque JSON for a later\nconsumer. Probes do not send credentials or follow redirects. A 401/403 explains how to expose only\nthe OpenAPI route anonymously or supply a reachable public spec URL. A running service must return\nJSON containing its selected service name, for example `{ "service": "orders" }`, from its selected\nbase path; this second request prevents a reused port from being mistaken for the intended service.\n\nThe default library composition needs `--allow-read` for carriers and real-path checks,\n`--allow-run` for `aspire describe`, and `--allow-net` for bounded spec/identity requests. Tests and\ncustom hosts can replace every source and the probe through `ServiceEndpointDirectoryOptions`.\n\n## Configuration at a glance\n\n- **Telemetry endpoint discovery** (tools and `doctor`): explicit `--endpoint`, then\n `NETSCRIPT_TELEMETRY_ENDPOINT`, then `ASPIRE_DASHBOARD_PORT`, then `http://localhost:18888`.\n- **Docs corpus**: by default the docs tools index the documentation shipped with the installed\n package; set `--docs-root ` (or `NETSCRIPT_DOCS_ROOT`) to serve a project or site corpus\n instead.\n- **Service endpoint discovery** (library surface): `.netscript/agent-mcp.json` override, then the\n Aspire CLI machine-readable query, then an identity-bound run manifest, then\n `aspire/appsettings.json`; lower-priority disagreements remain visible as conflicts.\n- **Command policy**: the shipped default allows the prefixes\n `db init|generate|migrate|seed|status|introspect`, `generate`, `contract`, `service list`,\n `plugin install|list|sync|doctor`, and `ui:add|ui:init|ui:list|ui:update`, and denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove` — deny beats allow, anything\n unmatched is denied. Embedders can pass their own policy.\n\nThe full flag reference, policy table, and composition options are on the docs site.\n\n## Docs\n\n- **MCP reference — the 17-tool field overview, policy, and exports**:\n [rickylabs.github.io/netscript/reference/mcp/](https://rickylabs.github.io/netscript/reference/mcp/)\n- **Agent tooling — install, flags, troubleshooting, CLI × skills × MCP**:\n [rickylabs.github.io/netscript/capabilities/agent-tooling/](https://rickylabs.github.io/netscript/capabilities/agent-tooling/)\n- **API docs on JSR**: [jsr.io/@netscript/mcp/doc](https://jsr.io/@netscript/mcp/doc)\n\n## Compatibility\n\nThe **server** requires Deno 2.9+ (both entrypoints use `Deno.*` APIs); Node.js and Bun are not\nsupported as server runtimes. The **client** side is unconstrained: any MCP-capable host — Claude\nCode, VS Code, and others — only has to spawn the process and speak JSON-RPC over stdio. The\nexecutable needs `--allow-env`, `--allow-net`, `--allow-read`, and `--allow-run`; the `netscript`\nbinary grants these at its edge. The server never returns project source, environment-variable\nvalues, credentials, or secrets.\n\n## License\n\nApache-2.0 — see [LICENSE](https://github.com/rickylabs/netscript/blob/main/LICENSE). Published to\nJSR with cryptographically verified provenance.\n';
diff --git a/packages/mcp/tests/openapi-read-tools_test.ts b/packages/mcp/tests/openapi-read-tools_test.ts
new file mode 100644
index 0000000000..e0ca739fcc
--- /dev/null
+++ b/packages/mcp/tests/openapi-read-tools_test.ts
@@ -0,0 +1,195 @@
+import { assert, assertEquals, assertObjectMatch } from '@std/assert';
+import {
+ API_SERVICE_RESULT_LIMIT,
+ createGetOperationSchemaFlow,
+ createListApiServicesFlow,
+ createListServiceOperationsFlow,
+ SERVICE_OPERATION_RESULT_LIMIT,
+} from '../mod.ts';
+import type { ServiceEndpointDirectoryPort, ServiceEndpointDirectoryResult } from '../mod.ts';
+import { createMcpCliServer } from '../cli.ts';
+import type { DiagnosticEvidencePort, DiagnosticEvidenceReceipt } from '../mod.ts';
+
+const sources = [
+ { source: 'override', outcome: 'used', candidates: [], excludedServices: [] },
+ {
+ source: 'aspire-cli',
+ outcome: 'failed',
+ code: 'command_failed',
+ reason: 'aspire describe exited 1',
+ candidates: [],
+ excludedServices: [],
+ },
+] as const;
+
+function directory(result: ServiceEndpointDirectoryResult): ServiceEndpointDirectoryPort {
+ return { list: () => Promise.resolve(result) };
+}
+
+function spec(count: number): Record {
+ return {
+ openapi: '3.1.0',
+ paths: Object.fromEntries(Array.from({ length: count }, (_, index) => [
+ `/items/${index}`,
+ {
+ get: {
+ operationId: `items.get${index}`,
+ summary: `Get item ${index}`,
+ tags: ['items'],
+ responses: {
+ 200: {
+ description: 'OK',
+ content: { 'application/json': { schema: { type: 'object' } } },
+ },
+ 404: {
+ description: 'Missing',
+ content: { 'application/json': { schema: { type: 'object' } } },
+ },
+ },
+ },
+ },
+ ])),
+ };
+}
+
+Deno.test('list_api_services forwards sources verbatim and omits counts without a fetched spec', async () => {
+ const flow = createListApiServicesFlow(directory({
+ sources,
+ entries: [
+ {
+ name: 'running',
+ status: 'running',
+ source: 'aspire-cli',
+ conflicts: [],
+ baseUrl: 'http://127.0.0.1:4100',
+ spec: spec(2),
+ },
+ {
+ name: 'stopped',
+ status: 'not_running',
+ source: 'appsettings',
+ conflicts: [],
+ reason: 'not listening',
+ },
+ ],
+ }));
+ const result = await flow({});
+ assert(result.ok);
+ const value = result.value as { services: Array>; sources: unknown };
+ assertEquals(value.sources, sources);
+ assertEquals(value.sources === sources, true);
+ assertEquals((result.value as { truncated: boolean }).truncated, false);
+ assertEquals(value.services[0]?.operationCount, 2);
+ assertEquals('operationCount' in value.services[1]!, false);
+});
+
+Deno.test('list_api_services reports truncation when service rows are dropped', async () => {
+ const entries = Array.from({ length: API_SERVICE_RESULT_LIMIT + 1 }, (_, index) => ({
+ name: `service-${index}`,
+ status: 'not_running' as const,
+ source: 'appsettings' as const,
+ conflicts: [],
+ reason: 'not listening',
+ }));
+ const result = await createListApiServicesFlow(directory({ sources, entries }))({});
+ assert(result.ok);
+ assertEquals((result.value as { services: unknown[] }).services.length, API_SERVICE_RESULT_LIMIT);
+ assertEquals((result.value as { truncated: boolean }).truncated, true);
+});
+
+Deno.test('list_service_operations marks truncation iff at least one matching row was dropped', async () => {
+ const flow = createListServiceOperationsFlow(directory({
+ sources,
+ entries: [{
+ name: 'catalog',
+ status: 'running',
+ source: 'aspire-cli',
+ conflicts: [],
+ baseUrl: 'http://127.0.0.1:4200',
+ spec: spec(SERVICE_OPERATION_RESULT_LIMIT + 1),
+ }],
+ }));
+ const dropped = await flow({ service: 'catalog' });
+ assert(dropped.ok);
+ assertObjectMatch(dropped.value as Record, { truncated: true });
+ assertEquals(
+ (dropped.value as { operations: unknown[] }).operations.length,
+ SERVICE_OPERATION_RESULT_LIMIT,
+ );
+
+ const retained = await flow({
+ service: 'catalog',
+ limit: SERVICE_OPERATION_RESULT_LIMIT,
+ filter: 'get0',
+ });
+ assert(retained.ok);
+ assertObjectMatch(retained.value as Record, { truncated: false });
+ assertEquals((retained.value as { operations: unknown[] }).operations.length, 1);
+});
+
+Deno.test('get_operation_schema composes S4 views and an unauthenticated curl template', async () => {
+ const flow = createGetOperationSchemaFlow(directory({
+ sources,
+ entries: [{
+ name: 'catalog',
+ status: 'running',
+ source: 'aspire-cli',
+ conflicts: [],
+ baseUrl: 'http://127.0.0.1:4200',
+ spec: spec(1),
+ }],
+ }));
+ const result = await flow({ service: 'catalog', operation: 'items.get0', view: 'errors' });
+ assert(result.ok);
+ assertObjectMatch(result.value as Record, {
+ operation: 'items.get0',
+ method: 'GET',
+ path: '/items/0',
+ view: 'errors',
+ curlExample: "curl -X GET 'http://127.0.0.1:4200/items/0'",
+ });
+ assertEquals(
+ (result.value as { schema: Record }).schema['404'] !== undefined,
+ true,
+ );
+ assert((result.value as { authNote: string }).authNote.includes('Unauthenticated'));
+});
+
+Deno.test('CLI settles a successful S6 receipt through the S8 lifecycle', async () => {
+ let receipt: DiagnosticEvidenceReceipt | undefined;
+ const evidence: DiagnosticEvidencePort = {
+ read: () => Promise.resolve(undefined),
+ write: (value) => {
+ receipt = value;
+ return Promise.resolve();
+ },
+ appendDrift: () => Promise.resolve(),
+ };
+ const serviceDirectory = directory({
+ sources,
+ entries: [{
+ name: 'catalog',
+ status: 'running',
+ source: 'aspire-cli',
+ conflicts: [],
+ baseUrl: 'http://127.0.0.1:4200',
+ spec: spec(1),
+ }],
+ });
+ const server = createMcpCliServer({
+ projectRoot: '/fixture',
+ diagnosticEvidence: evidence,
+ serviceEndpointDirectory: serviceDirectory,
+ });
+ const response = await server.handle({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'tools/call',
+ params: { name: 'list_api_services', arguments: {} },
+ });
+
+ assertEquals(response?.result?.isError, false);
+ assertEquals(receipt?.command, 'mcp list_api_services');
+ assertEquals(receipt?.resource, 'project');
+ assertEquals(receipt?.exitStatus, 0);
+});
diff --git a/packages/mcp/tests/registry_test.ts b/packages/mcp/tests/registry_test.ts
index cb24e43dcf..eb7a4abb90 100644
--- a/packages/mcp/tests/registry_test.ts
+++ b/packages/mcp/tests/registry_test.ts
@@ -56,20 +56,20 @@ Deno.test('docs drift proof: documentation reflects registered tool surface and
new URL('../../../docs/site/ai/agent-tooling.md', import.meta.url),
);
- assertEquals(TOOL_NAMES.length, 14);
+ assertEquals(TOOL_NAMES.length, 17);
assertEquals(TOOL_NAMES.includes('record_drift'), true);
assert(
- readme.includes('14 token-bounded tools'),
- 'README.md does not state 14 token-bounded tools',
+ readme.includes('17 token-bounded tools'),
+ 'README.md does not state 17 token-bounded tools',
);
assert(
- refMcp.includes('14 token-bounded tools') || refMcp.includes('14 tools'),
- 'reference/mcp/index.md missing 14 tools count',
+ refMcp.includes('17 token-bounded tools') || refMcp.includes('17 tools'),
+ 'reference/mcp/index.md missing 17 tools count',
);
assert(
- agentTooling.includes('Fourteen tools') || agentTooling.includes('14 tools'),
- 'agent-tooling.md missing 14 tools count',
+ agentTooling.includes('Seventeen tools') || agentTooling.includes('17 tools'),
+ 'agent-tooling.md missing 17 tools count',
);
for (const toolName of TOOL_NAMES) {
diff --git a/packages/mcp/tests/stdio_test.ts b/packages/mcp/tests/stdio_test.ts
index aab623c275..422e9d8b3e 100644
--- a/packages/mcp/tests/stdio_test.ts
+++ b/packages/mcp/tests/stdio_test.ts
@@ -41,7 +41,7 @@ Deno.test('stdio initialize, list, and unreachable doctor round trip', async ()
);
assertEquals(responses[0].result.serverInfo.name, '@netscript/mcp');
assertEquals(responses[0].result.serverInfo.version, MCP_PACKAGE_VERSION);
- assertEquals(responses[1].result.tools.length, 14);
+ assertEquals(responses[1].result.tools.length, 17);
assert(typeof responses[0].result.instructions === 'string');
for (const name of ['doctor', 'get_app_status', 'get_recent_errors', 'record_drift']) {
assert(responses[0].result.instructions.includes(name));