Skip to content

mcp: expose the scaffolded OpenAPI surface as MCP tools so agents stop hand-rolling curl #1117

Description

@rickylabs

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

  1. 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.

  2. 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.

  3. 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:

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions