nw-connector-builder — generator contract¶
Companion pages: running the generator · codegen behaviour.
nw-connector-builder targets a specific, common shape of REST API (single connector-level
auth scheme, JSON-first bodies, no pagination) and soft-drops anything outside that shape
rather than trying to support every corner of OpenAPI/Swagger. This page is the contract: what
the generator handles today, what it deliberately does not, and what is skipped but flagged per
build. It is the owner of those rules; other pages link here. For running the generator (flags,
the pipeline, the build report), see the how-to, nw-connector-builder.
In scope¶
Spec ingestion¶
The generator accepts the following specs and sources:
- Swagger 2.0 and OpenAPI 3.x, YAML or JSON, UTF-8
- Local file path or
http(s)URL (remote fetch goes through Node Wire's SSRF guard,assert_safe_destination, no redirects followed) - In-house Swagger 2.0 → OpenAPI 3.0 normalization at the front door
- Local relative
$refs and in-document#/refs - Swagger 2.0, OpenAPI 3.0, 3.1 and 3.2. Every field of each version, and how it is handled, is listed in OpenAPI coverage
- Recursive schemas (
$refcycles): the edge that re-enters a schema becomes a permissive object, and everything else is inlined as usual (see codegen behaviour) x-specification extensions are removed before$refresolution: the generator reads none of them, and a dangling$refinside an unused extension no longer fails the build- Structural + semantic validation via
prance+openapi-spec-validator; an operation or component that fails on its own is soft-dropped (or removed) instead of failing the whole build — see codegen behaviour
Auth¶
The generator handles auth as follows:
- One connector-level default scheme, chosen from the document's top-level
security(or the most common scheme across operations if none is declared) apiKeyinheaderorquery,httpbearer,httpbasic— mapped to Node Wire'sstatic_token/apikey_queryauth providers (self-managed presentation)- Every
oauth2flow (clientCredentials,authorizationCode,implicit,password, or none declared) andopenIdConnect— mapped to a host-supplied bearer (static_token,host_supplied: true): Node Wire presents<ID>_ACCESS_TOKENverbatim as aBearerheader but never acquires, refreshes, or detects the expiry of it — see Host-supplied auth tier. The host owns minting and rotation; the connector only attaches the token. - Operations needing a different, still-presentable scheme than the connector default are not
dropped — the generator emits it as an additional, named entry in
auth_schemes:and routes only those actions to it (auth_scheme=<name>per@nw_action, resolved at runtime byresolve_auth_provider, which fails closed on an unknown name). A connector can therefore use more than one scheme; see Per-action auth schemes. - Anonymous connectors (no scheme, or only unsupported schemes present) build as
auth: none - A spec that declares no auth can be given one with
--auth bearer|header:<name>|query:<name>(refused when the spec declares any scheme) - Fixed non-credential headers an API needs on every call (Twitch's
Client-Id) via--header NAME=VALUE, written toheaders:inconnectors.yamland sent by the runtime (node-wire-runtime1.1.0 or later, which the generated package then requires)
Host-supplied auth tier¶
Generated connectors follow the hand-written Slack ownership rule: the host obtains and rotates
the credential; the connector only attaches it. Every oauth2 scheme and openIdConnect map to
a static_token provider marked host_supplied: true that presents <ID>_ACCESS_TOKEN as a
Bearer header with no acquisition, refresh, or expiry detection. The host application owns
obtaining and rotating that token entirely out-of-band; a stale token surfaces as a plain 401
from the upstream API. The build report's auth.notes name the triggering scheme, the secret
key to set, and (when present in the spec) the authorizationUrl / tokenUrl for the host to
call — those URLs are documentation only.
This is not OAuth2 (or OIDC) client support — it's a static bearer, full stop. At runtime,
host_supplied: true configures StaticTokenAuthProvider(cache=False), which re-reads the secret
on every call instead of caching the header. Whether a rotation is actually picked up is a
property of the configured SecretProvider, not of this flag: env and overlay providers resolve
live (so the new value is seen on the next call), while AwsSecretsManagerProvider,
GcpSecretManagerProvider and HashiCorpVaultProvider load their bundle at init and keep serving
it until recreated. AzureKeyVaultProvider does resolve live but makes a blocking network call per
read, so a Key Vault deployment should stay on the cached path and call refresh() on rotation.
Either way Node Wire never calls a token endpoint, never performs a grant exchange, never
negotiates scope, never authenticates as a client. Hand-written connectors that already use
OAuth2AuthProvider (fhir_epic, salesforce, …) are unchanged — this policy applies to
generated connectors only.
One credential per connector, not per caller. <ID>_ACCESS_TOKEN resolves once per
tenant/config scope, so every caller of a generated connector reaches the upstream API as the same
identity. That is the right shape for a service credential the host owns and rotates. It is not
per-user auth: if each caller must reach the vendor API as themselves, the mechanism is the
runtime's provider: upstream_bearer relay, which forwards the inbound request's own bearer token
per call and fails closed unless the connector is also listed in NW_UPSTREAM_BEARER_CONNECTORS
(see Build a connector and
google_drive_connector.md). The generator never
emits upstream_bearer: nothing in an OpenAPI document says the caller's own token is the right
credential to relay downstream, and relaying one to the wrong audience leaks it — so that stays a
deliberate hand-wiring step.
Per-action auth schemes¶
A connector can serve more than one OpenAPI security scheme from a single instance. The
connector-level default (auth: in connectors.yaml) still covers most operations; any operation
whose declared security is a different scheme — but one that's still presentable (self-managed or
host-supplied, that is, not mutualTLS, cookie apiKey, AND-multi, or unrecognized) — gets its own
named entry under the additive auth_schemes: config block, and the generated action passes
auth_scheme=<name> so the runtime resolves the right AuthProvider per call instead of per
connector. This is how petstore-style specs (an apiKey default alongside operations gated by
an oauth2 implicit scheme) keep those operations instead of soft-dropping them. Secret names are derived from
the connector id and the credential kind, so two schemes of the same kind would otherwise share one
secret; when an extra scheme's secret name is already taken, it is qualified with the scheme name
(<ID>_<SCHEME>_ACCESS_TOKEN, <ID>_<SCHEME>_API_KEY, …) and the first claimant keeps the short
name.
Requests¶
- Every HTTP method, including OpenAPI 3.2
queryandadditionalOperations - Every parameter location and style: path
simple/label/matrix; queryform/spaceDelimited/pipeDelimited/deepObjectandallowReserved; headersimple;cookie; 3.2querystring; parameters declared withcontent(sent as JSON text). Path-level parameters are overridden by operation parameters with the same name and location - Request bodies by media type: JSON, form and multipart object bodies become one argument per
property; text, XML and other media take one string (binary content base64-encoded); multipart
per-part
contentTypefrom the Encoding Object - Responses: the lowest 2xx, else
2XX, elsedefaultwhen no success response is declared - Operation- and path-level
servers: that operation is called at its own server's first entry (execute_rest(server_url=…)) instead of the spec's base URL. Abase_urloverride inconnectors.yamlstill wins, so every operation goes to the operator's host
Codegen¶
The generated package provides the following:
- Actions discoverable via
@nw_actionon a generatedRestConnectorsubclass (regex-scrapable bynw-mcp-builder, matching the hand-written-connector convention);deprecatedoperations are marked deprecated - A shared, hardened REST executor (
node-wire-runtime) owns base URL, path templating, parameter placement and serialization, auth injection, SSRF checks, and error mapping - Hand-rolled input models: one flat, typed field per parameter and top-level body property, with
an
action: Literaldiscriminator. Schema constraints (minimum/maximum/exclusive*,multipleOf,minLength/maxLength,minItems/maxItems,enum,const) are enforced;patternandformatare advertised in the tool schema, not enforced. Nested objects stay free-form (dict) - Output: a permissive model for a JSON object success response, else a generic response envelope (status, headers, body)
- A
declare_secret_shape()call per generated package, so the config store accepts the connector's tenant secrets underNW_SECRET_SHAPE_POLICY=enforcewithout anyone editing the runtime's hard-coded registry - A package
README.mdrecording the actions table and the credential contract (which secret to set, and that nothing refreshes it) - Success-flag envelopes: when a success schema declares
okas a required boolean, a 2xx body withok: falseis raised as aBUSINESSerror instead of being returned as a successful call. Narrow by design — optional or non-booleanokfields are left alone
Testing & build gate¶
The build gate runs the following checks:
- Offline schema/contract tests only: parse the spec's
exampleinto the generated model when present and valid against its own schema, else synthesize one withpolyfactory - Generated tests ship with the connector (
packages/connectors/<id>/tests/) - Import smoke test +
pyteston staged output is a hard gate — promote only happens on green - Atomic two-phase promote into
src/node_wire_<id>/andpackages/connectors/<id>/; abort leaves the repo untouched
Wiring & hand-off¶
The hand-off covers the following:
- Optional
--wire: upsertsconfig/connectors.yaml(comment-preserving viaruamel.yaml) and appends secret placeholders / allowlist entries tosample.env - Automatic hand-off to
nw-mcp-builderafter a clean promote (unless--no-mcp), producing an MCP host undernw-mcp-builder/out/
Out of scope¶
Auth schemes¶
The generator does not support the following auth schemes:
- OAuth2 / OpenID Connect acquisition for generated connectors — Node Wire never acquires
tokens for generated connectors, deliberately. Every
oauth2flow andopenIdConnectmap to the host-supplied bearer tier. Hand-written connectors that already useOAuth2AuthProviderare unchanged. - mutualTLS — genuinely unpresentable (a transport-layer client certificate, not a header/param); operations secured only by it are soft-dropped
- Cookie-based API keys — no cookie auth provider yet; soft-dropped
- AND-combined security (
security: [{a: [], b: []}]) is sent when each scheme puts its credential in a different place (a bearer token plus an API-key header). Two schemes that both needAuthorization, or one unsupported scheme, soft-drop the operation
(Host-supplied oauth2 / openIdConnect, and multi-scheme connectors via per-action
auth_schemes:, are in scope; see Auth.)
Spec features¶
The generator does not support the following spec features:
- Remote
$refs (absolute URLs) — rejected outright; the input document must be self-contained aside from local relative/#/refs - Extra
serversentries — only the first entry of eachserverslist is used. An operation or path item with its ownserversis called at its first entry (GitHub's release asset uploads go touploads.github.com), noted in the build report - Webhooks and callbacks — requests the API sends to you, not calls a connector makes; noted in the build report
- Swagger 2.0
collectionFormat: tsv(no OpenAPI 3 equivalent) — the parameter is left out when optional, the operation soft-dropped when required - Sequential and streaming media types (3.2
itemSchema,itemEncoding,prefixEncoding)
The full list, field by field, is OpenAPI coverage.
Behavior not generated¶
The generator does not produce the following behavior:
- Pagination — no auto-pagination in v1, even when the spec documents cursor/offset patterns; generated actions return one page as-is
- Rate-limit / observability metadata — not derived from the spec (for example,
x-ratelimit-*extensions are ignored); deferred, not designed - Complex/ambiguous request bodies and non-object success responses fall back to permissive
typing (
Any/RestResponseOutput) rather than a fully modeled schema
Testing depth¶
The generated tests do not cover the following:
- Mock-server tests and live/integration tests against the real API are explicitly out of scope — only offline schema/contract tests against generated models are produced
- No contract testing against the live upstream service as part of the generator's gate
Registration & deployment¶
The generator does not do the following registration or deployment steps:
--wirenever editsconnector_registry.pyor the rootpyproject.toml— entry-point registration for editable monorepo installs is a manual follow-up step- Publishing (CI allowlists, standalone MCP Docker image rows, and the
scripts/build-packages.shentry unlessnw gen-alladds it) is a manual Tier 2/3 checklist in packaging.md — the builder produces the runtime + package skeleton only - No deployment step: the builder stops at a promoted connector (+ optional MCP host); running
thv/ToolHive deploy or verify is manual — see nw-mcp-builder and toolhive_agent_scenario.md. This was explicitly dropped from the companionnw-cliorchestrator's scope too; see nw-cli.md.
Editing generated output¶
The following limits apply to editing generated output:
- Generated files are marked "do not hand-edit" — there is no supported workflow for
incrementally patching a generated connector; the only supported update path is regenerating
with
--force(full overwrite, not a diff/merge)
Soft-drop vs. hard failure¶
Everything this page calls "soft-dropped" means: the operation is skipped and listed in
report.json, but the build continues. The only way an out-of-scope feature aborts the build
is if it leaves zero usable operations — that's a hard failure (DeriveError), since a
connector with no actions isn't useful. A coverage warning is also printed when fewer than 50%
of the document's operations survive derivation, even if the build otherwise succeeds.
This soft-fail-and-report design is deliberate: the generator is meant to get you most of the way there for typical REST APIs, with the report telling you exactly what to hand-write or follow up on for the rest — not to silently produce a broken or partial connector.