Skip to content

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:

  1. 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 from swagger: "2.0" vs. openapi: "3.x".
  2. Normalize — Swagger 2.0 documents are translated into OpenAPI 3.x in-house; OpenAPI 3.x documents pass through unchanged.
  3. Validate — reject any remote (absolute-URL) $refs outright (only local relative / in-document #/ refs are allowed), resolve the remaining local $refs in place with prance, then structurally and semantically validate the fully-resolved document with openapi-spec-validator. This step also resolves the connector's base URL (--base-url override, else the first servers[] entry).
  4. 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_action or a soft-drop reason (unsupported/divergent-and-unpresentable/AND-multi security, unsupported parameter styles, etc. — see Soft-drop rules).
  5. Codegen — write a temp staging tree: schema.py (Pydantic input/output models) and logic.py, a RestConnector subclass with one @nw_action-decorated async method per derived action. Each method's @nw_action(...) decorator (requires_auth=False for anonymous actions) makes it discoverable by nw-mcp-builder's regex scan; the method body itself calls self.execute_rest(...), passing auth_scheme=<name> when the action uses a named, non-default scheme from step 4. Also emits the package pyproject.toml and model tests.
  6. Gate — import smoke + pytest on staged model tests (must pass before promote)
  7. Promote atomically into src/node_wire_<id>/ and packages/connectors/<id>/
  8. MCP hand-off (default) — call nw-mcp-builder, which regex-scrapes the promoted logic.py for @nw_action methods, for out/<name>-mcp/
  9. Wire (optional --wire) — update config/connectors.yaml and sample.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-root is the parent of nw-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-builder importable (declared as a path dependency of this package)

Quick start

1. Install / sync the tool

cd nw-connector-builder
uv sync

From the node-wire repo root:

uv run --directory nw-connector-builder nw-connector-builder --help

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):

uv run --directory nw-connector-builder nw-connector-builder mcp \
  -c pet_store --force-output

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:

uv run pytest tests/nw_connector_builder -v --no-cov

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:

  1. Import smoke — load node_wire_<id>.logic, find a RestConnector with matching connector_id, require at least one @nw_action
  2. pytest — run staged packages/connectors/<id>/tests with PYTHONPATH pointing at staged src/

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>):

  1. Copy staging → *.promoting siblings
  2. Move existing destinations aside to *.bak (if any)
  3. Rename *.promoting → final paths
  4. 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.


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