nw-connector-builder¶
Advanced — most users want the nw CLI
nw-connector-builder is the standalone entry point and exposes the full flag surface.
For the normal path (spec → connector → wheels → MCP host → image) use
nw gen-all, which calls this tool for you. Read on when you need a
flag nw does not pass through, or are debugging this stage on its own.
Self-contained tool inside the node-wire repo with two subcommands:
| Command | Purpose |
|---|---|
nw-connector-builder from-openapi |
Turn a Swagger 2.0 / OpenAPI 3.x document into a node_wire_<id> connector (and optionally an MCP host) |
nw-connector-builder mcp |
Generate an MCP host from an existing connector (same as standalone nw-mcp-builder) |
Use from-openapi when the upstream API already ships an OpenAPI/Swagger spec and you want a first-class Node Wire RestConnector instead of hand-writing schemas and @nw_action methods. Use mcp (or nw-mcp-builder) for hand-written connectors. For the full happy path (codegen → Linux wheels → MCP host → wire → Docker), prefer the nw CLI. For SDK-style or non-REST adapters, follow the hand-written path in Build a connector.
What this folder is¶
| Path | Purpose |
|---|---|
nw-connector-builder/src/nw_connector_builder/ |
CLI, load/normalize, derive, codegen, gate, promote, wire, MCP hand-off |
tests/nw_connector_builder/ |
Pipeline and unit tests (run from the node-wire repo root) |
Generated output lands in the monorepo (not under nw-connector-builder/):
| Output | Location |
|---|---|
| Connector package | src/node_wire_<id>/ |
| Publishable package metadata + model tests | packages/connectors/<id>/ |
| Build report | packages/connectors/<id>/report.json (success) or ./report.json (abort) |
MCP host (unless --no-mcp) |
nw-mcp-builder/out/<name>-mcp/ |
What it does (end to end)¶
flowchart TB
spec[/"OpenAPI / Swagger spec"/] --> load["1 · Load<br/>file or URL"]
load --> normalize["2 · Normalize<br/>Swagger 2.0 → OpenAPI 3.x"]
normalize --> validate["3 · Validate<br/>resolve refs, reject remote $ref"]
validate --> derive["4 · Derive<br/>auth plan + actions"]
derive --> codegen["5 · Codegen<br/>stage schema.py + logic.py"]
codegen --> gate{"6 · Gate<br/>import smoke + pytest"}
gate -- "fail" --> abort(["exit 1 · report.json<br/>repo left untouched"])
gate -- "pass" --> promote["7 · Promote<br/>atomic, two-phase"]
promote --> mcp["8 · MCP hand-off"]
promote --> wire["9 · Wire (--wire)"]
classDef step fill:#ddf1fb,stroke:#1a88b0,stroke-width:1px,color:#0d2f3d
classDef gateC fill:#fdf2d6,stroke:#b8860b,stroke-width:1px,color:#3a2c05
classDef term fill:#eceff3,stroke:#5b7387,stroke-width:1px,color:#1c2733
classDef bad fill:#fde2ea,stroke:#b81548,stroke-width:1px,color:#3d0a1d
class load,normalize,validate,derive,codegen,promote,mcp,wire step
class gate gateC
class spec term
class abort bad
For a connector id like pet_store:
- Load — read the spec document from a local file path or
http(s)URL (--path). URL fetches go through Node Wire's SSRF guard (assert_safe_destination) and never follow redirects. The raw bytes are decoded as UTF-8 and parsed as YAML/JSON into a document, then the version is detected fromswagger: "2.0"vs.openapi: "3.x". - Normalize — Swagger 2.0 documents are translated into OpenAPI 3.x in-house; OpenAPI 3.x documents pass through unchanged.
- Validate — reject any remote (absolute-URL)
$refs outright (only local relative / in-document#/refs are allowed), resolve the remaining local$refs in place withprance, then structurally and semantically validate the fully-resolved document withopenapi-spec-validator. This step also resolves the connector's base URL (--base-urloverride, else the firstservers[]entry). - Derive — from the validated document, work out the connector's auth plan (one connector-level default scheme, plus any additional per-action schemes — see Auth mapping below) and, per operation, either a plan for a generated
@nw_actionor a soft-drop reason (unsupported/divergent-and-unpresentable/AND-multi security, unsupported parameter styles, etc. — see Soft-drop rules). - Codegen — write a temp staging tree:
schema.py(Pydantic input/output models) andlogic.py, aRestConnectorsubclass with one@nw_action-decorated async method per derived action. Each method's@nw_action(...)decorator (requires_auth=Falsefor anonymous actions) makes it discoverable bynw-mcp-builder's regex scan; the method body itself callsself.execute_rest(...), passingauth_scheme=<name>when the action uses a named, non-default scheme from step 4. Also emits the packagepyproject.tomland model tests. - Gate — import smoke +
pyteston staged model tests (must pass before promote) - Promote atomically into
src/node_wire_<id>/andpackages/connectors/<id>/ - MCP hand-off (default) — call
nw-mcp-builder, which regex-scrapes the promotedlogic.pyfor@nw_actionmethods, forout/<name>-mcp/ - Wire (optional
--wire) — updateconfig/connectors.yamlandsample.env
Promote never runs if the gate fails. MCP or --wire failures after a clean promote return exit code 1 but leave the connector in the tree.
Requirements¶
- Python 3.13+
- uv (recommended) or an editable install of the package
- Run from / against a node-wire checkout (default
--node-wire-rootis the parent ofnw-connector-builder/) - For URL specs: network access; fetches use Node Wire’s HTTP safety checks (
assert_safe_destination) and do not follow redirects - MCP hand-off needs
nw-mcp-builderimportable (declared as a path dependency of this package)
Quick start¶
1. Install / sync the tool¶
From the node-wire repo root:
2. Generate a connector¶
# Local file — connector only (skip MCP)
uv run --directory nw-connector-builder nw-connector-builder from-openapi \
--path path/to/openapi.yaml \
--id my_api \
--no-mcp
# Remote Swagger/OpenAPI — overwrite if present, wire config, build MCP host
uv run --directory nw-connector-builder nw-connector-builder from-openapi \
--path https://petstore.swagger.io/v2/swagger.json \
--id pet_store \
--force \
--wire
--id must match [a-z][a-z0-9_]* (e.g. pet_store, not PetStore).
To regenerate an MCP host from an existing connector (no OpenAPI step):
3. Run and verify¶
After a successful promote (and usually --wire):
# Ensure the connector is allowlisted (done automatically with --wire)
# NW_ALLOWED_CONNECTORS=...,pet_store
MODE=API uv run node-wire
Open http://localhost:8000/docs. Generated actions appear under the new connector.
If MCP was generated:
cd nw-mcp-builder/out/pet-store-nw-mcp
cp .env.example .env # fill secrets
uv sync
uv run python -m pet_store_nw_mcp
See nw-mcp-builder for ToolHive, Linux wheels, and Inspector.
4. Tests for the builder itself¶
From the node-wire repo root:
CLI reference¶
nw-connector-builder from-openapi --path <SPEC> --id <connector_id> [options]
nw-connector-builder mcp -c <connector_id> [options]
Legacy flat flags (nw-connector-builder --path … --id …) still map to from-openapi.
from-openapi¶
| Option | Default | Description |
|---|---|---|
--path |
— (required) | Local path or http(s) URL to an OpenAPI/Swagger document |
--id |
— (required) | Connector id ([a-z][a-z0-9_]*) |
--wire |
off | After promote, edit config/connectors.yaml + sample.env |
--force |
off | Overwrite an existing connector tree; pass --force-output to MCP hand-off |
--no-mcp |
off | Stop after a clean connector promote (skip MCP host generation) |
--base-url |
servers[0] |
Override baked-in default base URL (required if the spec has no absolute server URL) |
--node-wire-root |
Parent of nw-connector-builder/ |
Monorepo root used for promote / wire / MCP |
--report-path |
./report.json |
Where to write report.json on abort (success writes beside the package) |
-v, --verbose |
off | Debug logging |
mcp¶
Same flags as standalone nw-mcp-builder (see nw-mcp-builder): -c / --connector-id, --force-output, --force-fixture, --skip-build-wheels, -o / --output-dir, --fixtures-dir, --python, --node-wire-root, -v.
Help:
uv run --directory nw-connector-builder nw-connector-builder --help
uv run --directory nw-connector-builder nw-connector-builder from-openapi --help
uv run --directory nw-connector-builder nw-connector-builder mcp --help
Exit codes¶
| Code | Meaning |
|---|---|
0 |
Clean build (gate + promote OK; MCP/wire OK or skipped) |
1 |
Hard failure (load/derive/gate) or post-promote MCP/--wire failure |
2 |
Usage error (bad --id, destination exists without --force, etc.) |
Spec loading rules¶
| Topic | Behavior |
|---|---|
| Formats | YAML or JSON, UTF-8 |
| Versions | Swagger 2.0 (normalized to OpenAPI 3) or OpenAPI 3.x |
| Sources | Local file path, or http / https URL |
Remote $ref |
Rejected — absolute remote refs are not fetched; keep a self-contained document (local relative/#/ refs OK) |
| Validation | Resolved with prance + validated with openapi-spec-validator |
| Draft-4 repair | Constructs JSON Schema draft-4 allows but OpenAPI 3.0 forbids are rewritten before validation, not rejected (see below) |
| Base URL | From --base-url, else first servers[] entry with substitutable defaults; relative-only servers hard-fail |
Draft-4 constructs that OpenAPI 3.0 forbids (and the Swagger 2.0 examples move) are
rewritten rather than rejected — see codegen behaviour.
Auth mapping¶
The builder picks one connector-level default security scheme (document security, else the most common scheme across operations) and maps it to a Node Wire auth provider for --wire / connectors.yaml:
| OpenAPI scheme | Node Wire auth.provider |
Typical secret env |
|---|---|---|
apiKey in header |
static_token |
<ID>_API_KEY |
apiKey in query |
apikey_query |
<ID>_API_KEY |
http + bearer |
static_token |
<ID>_TOKEN |
http + basic |
static_token (prefix: Basic, base64) |
<ID>_BASIC_AUTH |
oauth2 (any flow) or openIdConnect |
static_token (prefix: Bearer, host_supplied: true) |
<ID>_ACCESS_TOKEN |
| None / unsupported only | none (anonymous) |
— |
Secret names come from the connector id and the credential kind, so a second scheme of the same
kind would otherwise reuse the first one's secret. When that happens the extra scheme is emitted
under auth_schemes: with its name folded in — <ID>_<SCHEME>_ACCESS_TOKEN,
<ID>_<SCHEME>_API_KEY, and so on. The build report's
auth.notes name the scheme and, when the OpenAPI document declares them, the
authorizationUrl / tokenUrl for the host to call — those URLs are documentation only;
the connector never POSTs to a token endpoint.
oauth2 and openIdConnect become a host-supplied bearer: Node Wire presents
<ID>_ACCESS_TOKEN but never obtains, refreshes or detects the expiry of it. Operations that need a
different but still-presentable scheme get a named entry under auth_schemes: instead of being
dropped. mutualTLS, cookie API keys, AND-combined requirements and unrecognized types are
soft-dropped. What each of these means, and why: the generator contract's
host-supplied tier and
per-action auth schemes.
How generated code is shaped¶
Tool and parameter descriptions, action naming, the flat request-body contract, dropped
credential parameters and success-flag (ok: false) envelopes are covered in
codegen behaviour.
Soft-drop rules¶
Unsupported operations are skipped and listed in the report. The build aborts only if zero operations remain, and it warns when fewer than 50% of document operations were generated. The full list of what is soft-dropped, and why, is the generator contract's Out of scope and Soft-drop vs. hard failure.
Generated layout¶
After promote:
src/node_wire_<id>/
__init__.py
schema.py # Pydantic input models + outputs (or RestResponseOutput)
logic.py # RestConnector subclass with @nw_action methods + declare_secret_shape()
README.md # actions table + the credential contract for this connector
packages/connectors/<id>/
pyproject.toml # entry point + deps
report.json # full build report
tests/
test_<id>_models.py # polyfactory round-trip (+ example parse when present)
Generated files are marked # Generated by nw-connector-builder — do not hand-edit. Prefer regenerating with --force over hand-patching; if you must customize, treat the tree as owned source and stop overwriting it.
Complex request bodies and non-object success responses use permissive typing (Any / RestResponseOutput) for robustness.
Staging gate¶
Before promote, the builder:
- Import smoke — load
node_wire_<id>.logic, find aRestConnectorwith matchingconnector_id, require at least one@nw_action - pytest — run staged
packages/connectors/<id>/testswithPYTHONPATHpointing at stagedsrc/
Gate failure writes report.json to --report-path (default cwd) and exits 1 without mutating the repo.
Promote semantics¶
Promote is a two-phase commit across both trees (src/node_wire_<id> and packages/connectors/<id>):
- Copy staging →
*.promotingsiblings - Move existing destinations aside to
*.bak(if any) - Rename
*.promoting→ final paths - On failure, restore backups and discard promoting leftovers
If either destination already exists, you must pass --force.
--force only replaces output this tool wrote. If src/node_wire_<id>/logic.py exists without the # Generated by nw-connector-builder marker, the build is refused before staging — a hand-written connector sharing the id would otherwise be deleted, and the generated package's declare_secret_shape() call would replace that connector's tenant-secret contract, so stored credentials stop validating. Pick a different --id, or move the existing package aside first. The check runs before staging because the gate imports the generated module in-process.
--wire behavior¶
When --wire is set after a clean promote:
config/connectors.yaml — upserts. The connector's entry is replaced wholesale, not merged: a previously derived auth: block (and any keys hand-added under that connector) is discarded and rewritten from the current auth plan. Regenerating a spec whose auth mapping changed therefore changes which secrets the connector reads.
connectors:
<id>:
enabled: true
exposed_via: ["rest", "grpc", "mcp"]
base_url: <derived or --base-url>
auth: # omitted when anonymous
provider: ...
secret_key: ...
auth_schemes: # only present when a divergent scheme needed its own auth_scheme=
<scheme_name>:
provider: ...
secret_key: ...
sample.env — appends the connector to NW_ALLOWED_CONNECTORS and adds empty placeholders for derived secret keys (e.g. PET_STORE_API_KEY=, and PET_STORE_ACCESS_TOKEN= for a host-supplied scheme).
Wire edits preserve YAML comments via ruamel.yaml. Failures set exit code 1 but do not roll back the promoted connector.
MCP hand-off¶
Unless --no-mcp is set, the builder calls nw-mcp-builder with:
force_fixture=True(fixture must track the newly promoted connector)force_output=<value of --force>
MCP details (wheels, ToolHive, Inspector) live in nw-mcp-builder. A failed hand-off returns exit code 1 after a successful promote — re-run either of these once the connector tree is good:
uv run --directory nw-connector-builder nw-connector-builder mcp -c <id> --force-output
# equivalent:
uv run --directory nw-mcp-builder nw-mcp-builder -c <id> --force-output
Build report¶
Stdout summary plus JSON:
| Field | Meaning |
|---|---|
summary |
id, source, version, content hash, counts, errors |
generated_actions |
method, path, action name, auth flag |
skipped |
soft-dropped ops + reasons |
auth |
chosen provider / secret key / yaml block |
gate / mcp / wire |
stage outcomes when run |
On success: packages/connectors/<id>/report.json
On abort: --report-path or ./report.json
Environment variables (generated connectors)¶
Generated RestConnectors honor the same egress controls as other REST adapters:
| Variable | Purpose |
|---|---|
NW_REST_ALLOWED_HOSTS |
Egress allowlist for outbound HTTP |
NW_REST_TRUST_ENV |
Set true to honor HTTP(S)_PROXY (default off) |
Plus connector-specific secrets from the auth plan (<ID>_API_KEY, <ID>_TOKEN, …) when using --wire / sample.env.
Storing those secrets per tenant¶
sample.env is the single-tenant path. For per-tenant/per-config credentials the values go through the config store, which keeps a registry of which logical secrets each connector owns — hand-written connectors are hard-coded in node_wire_runtime/tenant_persistence.py, and generated ones declare themselves. Each generated logic.py therefore ends with:
declare_secret_shape(
"slack_web",
required=["SLACK_WEB_ACCESS_TOKEN"],
formats={"SLACK_WEB_ACCESS_TOKEN": "opaque_secret"},
)
It runs at import time (the entry point imports logic), so the store accepts this connector's credentials even under NW_SECRET_SHAPE_POLICY=enforce, which otherwise rejects any connector it does not recognise. The connector-level scheme's secrets are required; per-action scheme secrets are format-checked but optional, so a host can store the default credential without provisioning every scheme. Connectors with no auth declare declare_secret_shape("<id>") — an explicit "no tenant secrets" rather than silence.
After generation — publishing checklist¶
nw-connector-builder creates the runtime and package skeleton, and --wire covers the Tier 1
wiring. nw gen-all also inserts the ALL_PACKAGES entry unless you pass --no-wire. The rest
(setup.py for binary wheels, the CI allowlists, and an optional MCP image) is the Tier 2 / Tier 3
checklist in Packaging. The CI allowlists are
always manual.
Related docs¶
| Doc | When to read it |
|---|---|
| nw-cli.md | Orchestrated codegen → wheels → MCP → Docker |
| Build a connector | Hand-written connectors, BaseConnector, auth patterns |
| nw-mcp-builder | Running / packaging the generated MCP host |
| packaging.md | Wheels, PyPI, CI allowlists |
| configuration.md | connectors.yaml and env vars |
| local-packages-to-images.md | Wheel → Docker image workflow |