Summary
Every scaffolded service already serves a machine-readable description of its own API. We already
ship an MCP server for agents. The two have never been connected, so agents fall back to
hand-rolled curl against endpoints whose schema is sitting one HTTP call away.
packages/service/src/presets/define-service.ts:227-228 calls .withOpenAPI().withDocs() on the
standard preset — so /api/openapi.json and /api/docs exist on every scaffolded service by
default.
packages/mcp has zero OpenAPI awareness (grep returns nothing). Its entire agent-facing tool
surface is read / mutate / meta.
Measured cost
Three wave-four agents each built a product against their own scaffolded services. All three
debugged those services with blind curl. One lost roughly 25 minutes to a publish endpoint that
hung with no error, and wrote in its self-review:
"the free Scalar docs I never opened even while debugging the RPC envelope they would have
explained instantly."
The spec was being served the whole time. Nothing made it reachable as a tool.
Note this is not a documentation gap — docs/site/services-sdk/how-to/expose-openapi-scalar.md
documents the routes and is cross-linked from the services hub. It is an activation gap of the
same shape as #1071/#1072: the capability exists, is documented, and is not present at the moment of
need.
Proposal — local-first, no hosted dependency
Expose the scaffolded OpenAPI surface as MCP tools, served locally, so an agent can ask "what
endpoints exist, what does this one accept, what does it return" instead of guessing an envelope.
Sketch: an Aspire-registered resource (or an extension of the existing MCP server) that reads the
running service's /api/openapi.json and projects it into MCP tools — endpoint listing, per-endpoint
schema lookup, and request-shape validation.
Deliberately local. A prior proposal to adopt Scalar's hosted Agent MCP was rejected, correctly:
it is a hosted Dashboard/Registry installation behind an installation ID and PAT/OAuth, and would add
cloud provisioning, credentials, offline failure modes, a third-party release dependency, and an
upload/synchronisation step. Doing the OpenAPI→MCP projection ourselves avoids all four
objections — an OpenAPI document is a standard input and the transformation is well understood.
Scalar tooling then becomes an optional implementation detail rather than a prerequisite.
Open questions — settle before implementing
-
Build or borrow. Is there a self-hostable component that serves MCP over a local OpenAPI
document, or do we do the projection ourselves inside packages/mcp? Prefer whichever adds no
credentials and no network dependency.
Two candidates already assessed and ruled out:
- Scalar Agent MCP/SDK — a hosted Dashboard/Registry installation behind an installation ID
and PAT/OAuth. Adds cloud provisioning, credentials, offline failure modes, a third-party
release dependency and an upload/sync step, and its SDKs wrap that hosted MCP rather than
exposing a local spec.
openapi.com/mcp — despite the name, this is a marketplace bridge, not a spec-to-MCP
generator. It fronts Openapi.com's own catalogue of 400+ commercial API services (company
verification, SMS, certified email) and is billed per API call consumed. There is no indication
it can be pointed at a local or arbitrary OpenAPI document. Not applicable.
A viable candidate: awslabs.openapi-mcp-server (https://awslabs.github.io/mcp/servers/openapi-mcp-server).
It creates MCP tools from OpenAPI endpoints automatically, accepts a local spec
(--spec-path ./openapi.json) and localhost targets (--allow-private-networks), is entirely
self-hostable with no cloud account, configures via env vars (API_BASE_URL, AUTH_TYPE) or CLI
flags, and ships a Docker deployment. It requires Python, and it executes API calls rather
than only introspecting.
This collapses most of the work: Aspire already runs container resources (postgres, redis and
garnet are wired exactly this way in the generated AppHost), and API_BASE_URL is an env var
Aspire can inject. The remaining work is wiring, not building a projector.
Two things to check before adopting: its licence (not stated on that page) and whether we want
the execution half at all — see question 3.
-
Dynamic ports. Aspire assigns service ports at run time. getServiceUrl(serviceName, protocol)
already exists in the SDK (packages/sdk/src/discovery/service-url.ts:97) and is the mechanism the
generated telemetry example uses. Aspire can inject the resolved URL as API_BASE_URL on the
resource, so this is largely solved rather than open.
-
Scope, and this is the real decision. Read-only introspection — list endpoints, fetch a
schema — kills blind curl on its own and carries almost no risk. Execution (an agent
invoking endpoints through MCP) is more useful and considerably more dangerous, especially against
a service with a live database. Recommendation: ship introspection first; treat execution as a
separate, deliberate decision. Also decide whether this covers all AppHost services or is opt-in
per service.
Acceptance
Provenance
Wave-four orchestration, 2026-08-03. The hosted-Scalar rejection is recorded in the docs deep-dive
report (.audit/DEEP-DIVE.md); the owner then asked whether an Aspire resource could serve an MCP
against the local API docs, which the rejection had not considered. It can — and the pieces are
already in the repo, unconnected.
Related: #1102 (intent-aware capability discovery), #1072 (harness must gate, not suggest),
#1071 (app-scoped conventions).
Sizing (added after evaluating awslabs.openapi-mcp-server)
With an existing projector that reads a local spec and ships as a container, this is wiring an
Aspire resource, not building a capability:
- the spec is already served on every service (
/api/openapi.json)
- service discovery already exists (
getServiceUrl)
- Aspire container resources are already the scaffold's standard pattern
That makes a read-only introspection version plausible inside 0.0.4. The execution half, richer
intent-aware discovery (#1102), and any bespoke projector work belong in 0.0.5.
Non-code costs to weigh against that: it adds a Python/container dependency to the dev
environment, and 0.0.4 is a stability release already carrying a large queue.
Revised direction (owner, 2026-08-03): build our own, as a NetScript package — possibly a plugin
Do not adopt a third-party server wholesale. Use the existing projects as reference and possible
source, and ship a tailored NetScript surface. Moved to 0.0.5 accordingly.
TypeScript references worth reading before writing anything
The AWS Labs server (awslabs.openapi-mcp-server) is Python — useful as a behavioural reference,
poor as a source for a Deno/TypeScript package. These are the TS options:
| Project |
Shape |
Why it matters here |
harsha-iiiv/openapi-mcp-generator |
Exposes getToolsFromOpenApi(spec, opts) — a programmatic projection function returning MCP tool definitions, with baseUrl, dereference, excludeOperationIds, and a filterFn |
The most surgical option. We already run an MCP server; we may only need the spec→tool-definition projection, not another server process. |
ivo-toby/mcp-openapi-server (@ivotoby/openapi-mcp-server) |
Usable as a library: new OpenAPIServer({ apiBaseUrl, openApiSpec, specInputMethod: 'url', transportType, toolsMode, extraTools }) |
Takes the spec by URL — exactly our /api/openapi.json case — and supports extraTools, so NetScript-specific tools can sit beside generated ones. |
nihal1294/openapi-to-mcp, beshkenadze/openapi-mcp-generator, EvilFreelancer/openapi-to-mcp |
Generators / standalone servers |
Secondary references for tool-naming, description quality and filtering strategy. |
Check the licence of anything we source from, and prefer vendoring a small projection function
over taking a runtime dependency on a whole server.
Shape — constrained by the plugin thinness law, not a free choice
.llm/harness/archetypes/ARCHETYPE-5-plugin.md settles more of this than it first appears:
"Convention-bearing primitives — contracts, base services, schema/runtime conventions, event/kind
vocabularies — live in @netscript/* core. A first-party plugins/* package is thin userland
glue… it does not redefine contracts, re-implement a core convention, or own what core should
own."
"A plugin that 'owns' a contribution axis is a smell, not the target."
So this is not a binary plugin-versus-core decision. The RFC must answer three questions:
- Is the projection a convention? Mapping an OpenAPI document to MCP tool definitions — naming,
schema handling, filtering, the agent-facing vocabulary — looks convention-bearing, which on the
thinness law argues for core ownership, most plausibly in or beside packages/mcp.
- What is left for a plugin to wire? Per-service opt-in, Aspire resource registration, spec-URL
discovery, execution policy. That is composition, and legitimately plugin-owned.
- Is the split worth two packages at all? A single core extension may be the honest answer.
Reference shape named by the archetype: auth-core plus its thin adapters. The design must also be
checked against the archetype's named anti-patterns (AP-1, AP-3, AP-8…) and fitness functions
(F-1, F-3, F-5…).
Whatever shape wins, this would be the first first-party plugin outside
workers/sagas/streams/triggers if it goes that way — real evidence about whether the contribution
model works for something new. #1093 records that plugin discovery currently hardcodes official
plugins' factory functions in the core SDK; the RFC must say plainly whether that blocks this and
what must change.
Why this is a genuine package rather than glue
Once the projection exists, the interesting NetScript-specific work is what a generic generator does
not do:
- resolve the spec URL through
getServiceUrl() for Aspire-assigned dynamic ports
- cover every service in the AppHost, not one configured base URL
- decide introspection-only versus execution, per service
- name and describe tools using the contract metadata we already have, so the agent-facing surface
reads like NetScript rather than like a REST dump
Status: RFC pipeline (owner decision, 2026-08-03)
This is large enough to need its own RFC before implementation, following the pattern of the open
RFCs #890 / #891 / #822:
- Generator — Claude Fable 5, effort medium, seed run
.llm/runs/plan-openapi-mcp-plugin--seed/ on branch plan/openapi-mcp-plugin
- Adversarial — Codex GPT-5.6 Sol, effort xhigh, findings only; the generator integrates
- Then an RFC PR carrying
rfc.md, labelled rfc + status:plan, for owner ratification
No implementation until the RFC is ratified.
Summary
Every scaffolded service already serves a machine-readable description of its own API. We already
ship an MCP server for agents. The two have never been connected, so agents fall back to
hand-rolled
curlagainst endpoints whose schema is sitting one HTTP call away.packages/service/src/presets/define-service.ts:227-228calls.withOpenAPI().withDocs()on thestandard preset — so
/api/openapi.jsonand/api/docsexist on every scaffolded service bydefault.
packages/mcphas zero OpenAPI awareness (grep returns nothing). Its entire agent-facing toolsurface is
read/mutate/meta.Measured cost
Three wave-four agents each built a product against their own scaffolded services. All three
debugged those services with blind
curl. One lost roughly 25 minutes to a publish endpoint thathung with no error, and wrote in its self-review:
The spec was being served the whole time. Nothing made it reachable as a tool.
Note this is not a documentation gap —
docs/site/services-sdk/how-to/expose-openapi-scalar.mddocuments the routes and is cross-linked from the services hub. It is an activation gap of the
same shape as #1071/#1072: the capability exists, is documented, and is not present at the moment of
need.
Proposal — local-first, no hosted dependency
Expose the scaffolded OpenAPI surface as MCP tools, served locally, so an agent can ask "what
endpoints exist, what does this one accept, what does it return" instead of guessing an envelope.
Sketch: an Aspire-registered resource (or an extension of the existing MCP server) that reads the
running service's
/api/openapi.jsonand projects it into MCP tools — endpoint listing, per-endpointschema lookup, and request-shape validation.
Deliberately local. A prior proposal to adopt Scalar's hosted Agent MCP was rejected, correctly:
it is a hosted Dashboard/Registry installation behind an installation ID and PAT/OAuth, and would add
cloud provisioning, credentials, offline failure modes, a third-party release dependency, and an
upload/synchronisation step. Doing the OpenAPI→MCP projection ourselves avoids all four
objections — an OpenAPI document is a standard input and the transformation is well understood.
Scalar tooling then becomes an optional implementation detail rather than a prerequisite.
Open questions — settle before implementing
Build or borrow. Is there a self-hostable component that serves MCP over a local OpenAPI
document, or do we do the projection ourselves inside
packages/mcp? Prefer whichever adds nocredentials and no network dependency.
Two candidates already assessed and ruled out:
and PAT/OAuth. Adds cloud provisioning, credentials, offline failure modes, a third-party
release dependency and an upload/sync step, and its SDKs wrap that hosted MCP rather than
exposing a local spec.
openapi.com/mcp— despite the name, this is a marketplace bridge, not a spec-to-MCPgenerator. It fronts Openapi.com's own catalogue of 400+ commercial API services (company
verification, SMS, certified email) and is billed per API call consumed. There is no indication
it can be pointed at a local or arbitrary OpenAPI document. Not applicable.
A viable candidate:
awslabs.openapi-mcp-server(https://awslabs.github.io/mcp/servers/openapi-mcp-server).It creates MCP tools from OpenAPI endpoints automatically, accepts a local spec
(
--spec-path ./openapi.json) and localhost targets (--allow-private-networks), is entirelyself-hostable with no cloud account, configures via env vars (
API_BASE_URL,AUTH_TYPE) or CLIflags, and ships a Docker deployment. It requires Python, and it executes API calls rather
than only introspecting.
This collapses most of the work: Aspire already runs container resources (postgres, redis and
garnet are wired exactly this way in the generated AppHost), and
API_BASE_URLis an env varAspire can inject. The remaining work is wiring, not building a projector.
Two things to check before adopting: its licence (not stated on that page) and whether we want
the execution half at all — see question 3.
Dynamic ports. Aspire assigns service ports at run time.
getServiceUrl(serviceName, protocol)already exists in the SDK (
packages/sdk/src/discovery/service-url.ts:97) and is the mechanism thegenerated telemetry example uses. Aspire can inject the resolved URL as
API_BASE_URLon theresource, so this is largely solved rather than open.
Scope, and this is the real decision. Read-only introspection — list endpoints, fetch a
schema — kills blind
curlon its own and carries almost no risk. Execution (an agentinvoking endpoints through MCP) is more useful and considerably more dangerous, especially against
a service with a live database. Recommendation: ship introspection first; treat execution as a
separate, deliberate decision. Also decide whether this covers all AppHost services or is opt-in
per service.
Acceptance
curlagent initsurface and the app-scoped conventions file (scaffold: generate an app-scoped AGENTS.md + WEB-LAYER.md naming the canonical web-layer examples #1071), not only a how-to page
curl(observational —route to verify(wave-five): does the shipped agent surface actually change agent behaviour? #1090 if it cannot be closed by a PR)
Provenance
Wave-four orchestration, 2026-08-03. The hosted-Scalar rejection is recorded in the docs deep-dive
report (
.audit/DEEP-DIVE.md); the owner then asked whether an Aspire resource could serve an MCPagainst the local API docs, which the rejection had not considered. It can — and the pieces are
already in the repo, unconnected.
Related: #1102 (intent-aware capability discovery), #1072 (harness must gate, not suggest),
#1071 (app-scoped conventions).
Sizing (added after evaluating
awslabs.openapi-mcp-server)With an existing projector that reads a local spec and ships as a container, this is wiring an
Aspire resource, not building a capability:
/api/openapi.json)getServiceUrl)That makes a read-only introspection version plausible inside 0.0.4. The execution half, richer
intent-aware discovery (#1102), and any bespoke projector work belong in 0.0.5.
Non-code costs to weigh against that: it adds a Python/container dependency to the dev
environment, and 0.0.4 is a stability release already carrying a large queue.
Revised direction (owner, 2026-08-03): build our own, as a NetScript package — possibly a plugin
Do not adopt a third-party server wholesale. Use the existing projects as reference and possible
source, and ship a tailored NetScript surface. Moved to 0.0.5 accordingly.
TypeScript references worth reading before writing anything
The AWS Labs server (
awslabs.openapi-mcp-server) is Python — useful as a behavioural reference,poor as a source for a Deno/TypeScript package. These are the TS options:
harsha-iiiv/openapi-mcp-generatorgetToolsFromOpenApi(spec, opts)— a programmatic projection function returning MCP tool definitions, withbaseUrl,dereference,excludeOperationIds, and afilterFnivo-toby/mcp-openapi-server(@ivotoby/openapi-mcp-server)new OpenAPIServer({ apiBaseUrl, openApiSpec, specInputMethod: 'url', transportType, toolsMode, extraTools })/api/openapi.jsoncase — and supportsextraTools, so NetScript-specific tools can sit beside generated ones.nihal1294/openapi-to-mcp,beshkenadze/openapi-mcp-generator,EvilFreelancer/openapi-to-mcpCheck the licence of anything we source from, and prefer vendoring a small projection function
over taking a runtime dependency on a whole server.
Shape — constrained by the plugin thinness law, not a free choice
.llm/harness/archetypes/ARCHETYPE-5-plugin.mdsettles more of this than it first appears:So this is not a binary plugin-versus-core decision. The RFC must answer three questions:
schema handling, filtering, the agent-facing vocabulary — looks convention-bearing, which on the
thinness law argues for core ownership, most plausibly in or beside
packages/mcp.discovery, execution policy. That is composition, and legitimately plugin-owned.
Reference shape named by the archetype:
auth-coreplus its thin adapters. The design must also bechecked against the archetype's named anti-patterns (AP-1, AP-3, AP-8…) and fitness functions
(F-1, F-3, F-5…).
Whatever shape wins, this would be the first first-party plugin outside
workers/sagas/streams/triggers if it goes that way — real evidence about whether the contribution
model works for something new. #1093 records that plugin discovery currently hardcodes official
plugins' factory functions in the core SDK; the RFC must say plainly whether that blocks this and
what must change.
Why this is a genuine package rather than glue
Once the projection exists, the interesting NetScript-specific work is what a generic generator does
not do:
getServiceUrl()for Aspire-assigned dynamic portsreads like NetScript rather than like a REST dump
Status: RFC pipeline (owner decision, 2026-08-03)
This is large enough to need its own RFC before implementation, following the pattern of the open
RFCs #890 / #891 / #822:
.llm/runs/plan-openapi-mcp-plugin--seed/on branchplan/openapi-mcp-pluginrfc.md, labelledrfc+status:plan, for owner ratificationNo implementation until the RFC is ratified.