From f5ef89fb0f2e4f756b9efdba04448ea65066d924 Mon Sep 17 00:00:00 2001 From: vinaayakh-aot <61819385+vinaayakh-aot@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:24:38 -0700 Subject: [PATCH 1/6] Updated documentation clarity for creating connectors and mcps --- docs/connectors.md | 121 +++++++++++++++++++++++---- docs/local-packages-to-images.md | 2 +- docs/mcp-servers.md | 136 +++++++++++++++++++++++++++++-- docs/mcp.md | 2 + docs/packaging.md | 50 ++++++++++++ docs/public-api.md | 2 + sample.env | 5 +- 7 files changed, 291 insertions(+), 27 deletions(-) diff --git a/docs/connectors.md b/docs/connectors.md index 282cb27..f3242d4 100644 --- a/docs/connectors.md +++ b/docs/connectors.md @@ -29,7 +29,7 @@ Each connector is a **top-level package** under `src/` (e.g. `node_wire_fhir_epi | `registration.py` | Optional: registers connector-specific exceptions with `ErrorMapper`. | | `exceptions.py` | Optional: custom exception types. | -At startup, call **`node_wire_runtime.connector_registry.auto_register()`**: it loads entry points in group `node_wire.connectors`, imports each connector's `logic` module (triggering `BaseConnector.__init_subclass__` and registration via `get_connector_registry()`), then imports optional `registration.py` for `ErrorMapper` side effects. +At startup, call **`node_wire_runtime.connector_registry.auto_register()`**: it loads entry points in group `node_wire.connectors`, imports each connector's `logic` module (triggering `BaseConnector.__init_subclass__`, which populates the registry returned by `get_connector_registry()`), then imports optional `registration.py` for `ErrorMapper` side effects. --- @@ -112,10 +112,12 @@ GoogleDriveOperationInput = RootModel[_GoogleDriveOperationUnion] class GoogleDriveOperationOutput(BaseModel): - raw: dict + raw: dict | list description: str ``` +Use `dict | list` for `raw` when vendor APIs return arrays (e.g. list endpoints); Pydantic validates either shape. Per-action output models can use typed fields instead of a shared envelope. + When a connector only has **one** action, the `action` field is still required — the runtime always validates through the discriminated union. ### Step 2 — Map operations to the SDK (`action_spec.py`) @@ -225,12 +227,15 @@ To use authentication, call **`await self.get_auth_headers()`** (inherited from ```python # logic.py usage +# Base URL: read from the connector's own config, an env var, or a module constant. +# There is no inherited _get_base_url() helper — connectors own their URL resolution. +BASE_URL = "https://api.example.com" # or: os.environ["MY_SERVICE_URL"] + async def read_resource(self, params: In, *, trace_id: str) -> Out: - base_url = self._get_base_url() headers = await self.get_auth_headers() # Fetched/cached by provider async with httpx.AsyncClient() as client: - resp = await client.get(f"{base_url}/resource", headers=headers) + resp = await client.get(f"{BASE_URL}/resource", headers=headers) resp.raise_for_status() ... ``` @@ -239,11 +244,23 @@ async def read_resource(self, params: In, *, trace_id: str) -> Out: Choose a provider in your **`connectors.yaml`** via the `auth:` block: -| Type | Description | -|------|-------------| -| **`none`** | (Default) No auth headers added. | -| **`static_token`** | Uses a fixed token from a secret (Bearer, Basic, or custom). Supports refresh. | -| **`oauth2`** | Full Client Credentials flow. Supports `private_key_jwt` (RS384) and `client_secret_post`. Handles caching and expiry automagically. | +| Type | Description | Example connector | +|------|-------------|-------------------| +| **`none`** | (Default) No auth headers added. | `http_generic` | +| **`static_token`** | Uses a fixed token from a secret (Bearer, Basic, or custom). Supports refresh. | `stripe`, `slack` | +| **`static_credentials`** | Username + password pair (e.g. SMTP relay). | `smtp` | +| **`service_account`** | Google-style service account JSON + scopes. | `google_drive` | +| **`oauth2`** | Token exchange (`private_key_jwt`, `refresh_token`, `client_secret_post`, etc.). Handles caching and expiry. | `fhir_epic`, `salesforce` | + +#### `static_token` field reference + +| Field | Required | Default | Notes | +|-------|----------|---------|-------| +| `secret_key` | Yes | — | Env var name holding the raw token value (`EnvSecretProvider` tries the key as-is, then uppercased). | +| `header_name` | No | `Authorization` | HTTP header the token is injected into. | +| `prefix` | No | `Bearer ` (with trailing space) | String prepended to the token value. Set `prefix: ""` for APIs that expect the raw token (e.g. Stripe). Set `prefix: "token "` for older GitHub PAT style. | + +So `slack` (no `header_name`/`prefix`) produces `Authorization: Bearer `, and `stripe` (with `prefix: ""`) produces `Authorization: `. ### Configuration (`connectors.yaml`) @@ -264,14 +281,31 @@ connectors: enabled: true auth: provider: static_token - secret_key: STRIPE_API_KEY + secret_key: stripe_api_key + header_name: Authorization + prefix: "" # Stripe expects raw key; env var is STRIPE_API_KEY + + smtp: + enabled: true + auth: + provider: static_credentials + username_secret: SMTP_USERNAME + password_secret: SMTP_PASSWORD + + google_drive: + enabled: true + auth: + provider: service_account + sa_json_secret: GOOGLE_DRIVE_SA_JSON + scopes: + - https://www.googleapis.com/auth/drive ``` --- Key points: - **`connector_id`** — unique string; used for routing, config, and registry lookup. -- **`output_model`** — the Pydantic class returned by every action (Drive uses one shared envelope with `raw` + `description`). +- **`output_model`** — the Pydantic class returned by every action. Shared envelopes often use `raw: dict | list` for list-heavy vendor APIs; per-action models can use typed fields instead (see SMS example below). - **`error_map`** — maps exception types to `(ErrorCategory, error_code)`. Entries are registered with `ErrorMapper` automatically at class definition time. - **`build_client()`** — override to create the Google API client. `get_client()` caches the result in `self._client`. - **`action_specs`** — each key becomes a manifest action (e.g. `files.list`). Do **not** also add a manual `@nw_action` with the same name. @@ -295,7 +329,38 @@ connectors: ### Step 5 — Auto-registration (nothing extra needed) -`BaseConnector.__init_subclass__` registers your class (exposed via `get_connector_registry()`) as soon as `logic.py` is imported. **`node_wire_runtime.connector_registry.auto_register()`** performs those imports at startup. **No manual factory branch is required.** +`BaseConnector.__init_subclass__` registers your class in the global registry as soon as `logic.py` is imported. **`node_wire_runtime.connector_registry.auto_register()`** performs those imports at startup. **No manual factory branch is required.** + +### Connector registry API + +`get_connector_registry()` is defined in `base_connector.py` and exported from the top-level `node_wire_runtime` package — it is **not** in `node_wire_runtime.connector_registry`. Use it to read the connector-id → class map after `auto_register()` has imported your `logic` module: + +```python +from node_wire_runtime import get_connector_registry +from node_wire_runtime.connector_registry import auto_register + +auto_register() # requires NW_ALLOWED_CONNECTORS +registry = get_connector_registry() # Dict[str, Type[BaseConnector]] +connector_cls = registry["google_drive"] +``` + +For the full run pipeline (YAML config, instantiation, protocol routing), use **`ConnectorFactory`** (see [Calling a connector directly](#calling-a-connector-directly-in-process)). + +### Optional: `registration.py` for ErrorMapper + +When exceptions are raised outside the connector class (or shared across modules), register them in `registration.py` instead of inline `error_map`: + +```python +# src/node_wire_/registration.py +from node_wire_runtime import ErrorCategory, ErrorMapper + +from .exceptions import MyAuthError, MyRateLimitError + +ErrorMapper.register(MyAuthError, ErrorCategory.AUTH, code="MY_AUTH_ERROR") +ErrorMapper.register(MyRateLimitError, ErrorCategory.RETRYABLE, code="MY_RATE_LIMIT") +``` + +`auto_register()` imports `registration.py` after `logic.py`, so these registrations run at startup. Alternatively, use inline **`error_map`** on the connector class (Google Drive example above). --- @@ -342,12 +407,17 @@ class SmsConnector(BaseConnector): ## Calling a connector directly (in-process) -Use `connector.run(dict)` for the full pipeline (validation, policy, retries, error mapping): +Use `connector.run(dict)` for the full pipeline (validation, policy, retries, error mapping). + +Set **`NW_ALLOWED_CONNECTORS`** to a comma-separated list of entry-point names (e.g. `google_drive`) before calling `auto_register()` — without it, `auto_register()` loads nothing (fail-closed). ```python +import os + from node_wire_runtime.connector_registry import auto_register from bindings.factory import ConnectorFactory +os.environ["NW_ALLOWED_CONNECTORS"] = "google_drive" auto_register() factory = ConnectorFactory() factory.load() @@ -451,7 +521,7 @@ Published **`input_schema` omits the `action` property** (manifest contract v2+) ### Manifest -`build_manifest(connectors)` is the single source of truth for both bindings (by default it strips `action` from each entry’s `input_schema`). It returns one entry per `@sdk_action`: +`build_manifest(connectors)` (from `node_wire_runtime.manifest`) is the single source of truth for both bindings (by default it strips `action` from each entry’s `input_schema`). It returns one entry per `@sdk_action`: ```python [ @@ -480,7 +550,6 @@ Published **`input_schema` omits the `action` property** (manifest contract v2+) | `stripe` | `charge` | | `salesforce` | `create_lead`, `read_lead`, `update_lead`, `delete_lead`, `create_contact`, `read_contact`, `update_contact`, `delete_contact` | | `google_drive` | `files.list`, `files.upload`, … (see `action_specs`) | - | `fhir_epic` | `read_patient`, `search_patients`, `search_encounter`, `create_document_reference`, `search_document_reference` | | `fhir_cerner` | Same family as Epic with Cerner-specific schemas | | `slack` | `post_message`, `send_direct_message`, `upload_file` | @@ -491,13 +560,29 @@ MCP tool names: **`.`** (e.g. `fhir_epic.read_patient`). S ## Adding a new connector (checklist) -1. Create the package directory `src/node_wire_/` with `schema.py` (Pydantic input/output models) and register the entry point under `[project.entry-points."node_wire.connectors"]`. +### Runtime (dev) + +1. Create the package directory `src/node_wire_/` with `schema.py` (Pydantic input/output models) and register the entry point under `[project.entry-points."node_wire.connectors"]` in the root `pyproject.toml`. 2. In `logic.py`: subclass `BaseConnector`, set `connector_id` and `output_model`, then add `@nw_action` methods or wire `action_specs`. 3. **Authentication**: Delegate all header construction to **`self.get_auth_headers()`**. Do not hardcode secret lookups or IdP handshakes and ensure sensitive fields are removed from your `input_schema`. 4. For SDK-style connectors, add an `action_spec.py` (or similar) with `SdkActionSpec` entries and use **`execute_spec_in_thread`** when the vendor client is blocking. -5. Optionally add `error_map` and/or `registration.py` for custom exception handling. +5. Optionally add `error_map` and/or `registration.py` for custom exception handling (see [registration.py example](#optional-registrationpy-for-errormapper) below). 6. Add the connector to **`config/connectors.yaml`** with `enabled: true`, the desired `exposed_via` protocols, and an **`auth:`** block. -7. That's it — `auto_register()` handles the rest. No factory branch required. +7. **Environment template:** Add required secrets and connector-specific vars to [`sample.env`](../sample.env) (referenced by [configuration.md](configuration.md) and [installation.md](installation.md)). Use commented placeholders with the env var names your connector reads via `SecretProvider`. +8. `auto_register()` handles runtime registration — **no factory branch required**. + +### Publishable PyPI package (when shipping on PyPI) + +9. Add `packages/connectors//pyproject.toml` and `setup.py`; register the entry point. +10. Add the package path to **`scripts/build-packages.sh`** (`ALL_PACKAGES`) and CI allowlists (`.github/workflows/publish.yml`, `github-release.yml`, `security-pr.yml`). +11. Update the inventory table in **[packaging.md](packaging.md)**. + +### Standalone MCP server (optional — dedicated Docker/ToolHive image) + +12. Add `src/agents/_mcp.py`, a `[project.scripts]` entry in root `pyproject.toml`, `docker//Dockerfile`, and entries in **`scripts/build-mcp-images.sh`** and **`docker-compose.mcp.yml`**. +13. Add a row to the naming table in **[mcp-servers.md](mcp-servers.md)**. + +For full file lists see [packaging.md — Adding a new publishable connector](packaging.md#adding-a-new-publishable-connector). --- diff --git a/docs/local-packages-to-images.md b/docs/local-packages-to-images.md index faa3e13..ab33aba 100644 --- a/docs/local-packages-to-images.md +++ b/docs/local-packages-to-images.md @@ -14,7 +14,7 @@ The Dockerfiles in this repo install local wheel artifacts from `packages/**/dis ## Prerequisites -- Python 3.12 available in your shell +- Python 3.11+ available in your shell - Docker installed and running - Build tooling installed: diff --git a/docs/mcp-servers.md b/docs/mcp-servers.md index 55d716c..70c315f 100644 --- a/docs/mcp-servers.md +++ b/docs/mcp-servers.md @@ -65,6 +65,106 @@ flowchart TD | Salesforce | `python -m agents.salesforce_mcp` | `nw-salesforce` | `nw-salesforce` | All manifest actions for `salesforce` (e.g., `salesforce.create_lead`) | | Slack | `python -m agents.slack_mcp` | `nw-slack` | `nw-slack` | All manifest actions for `slack` (e.g. `slack.post_message`) | +### Adding a row for a new connector + +When you add a standalone MCP server for a connector, add a row to the table above and update these files: + +| Field | Convention | +|-------|--------------| +| Python entrypoint | `python -m agents._mcp` (or `uv run nw-` from root `pyproject.toml` `[project.scripts]`) | +| Docker image | `nw-` | +| ToolHive name | Same as Docker image tag | +| MCP tools | `.` for each manifest action | + +Files to update: `src/agents/_mcp.py`, root `pyproject.toml` `[project.scripts]`, `docker//Dockerfile`, `scripts/build-mcp-images.sh`, `docker-compose.mcp.yml`, this table, and [local-packages-to-images.md](local-packages-to-images.md). See [packaging.md — Adding a new publishable connector](packaging.md#adding-a-new-publishable-connector) for the full checklist. + +#### Per-connector MCP entrypoint template (`src/agents/_mcp.py`) + +Every per-connector MCP file follows the same pattern — create `src/agents/_mcp.py`: + +```python +# src/agents/_mcp.py +"""MCP Server — connector only. Usage: python -m agents._mcp""" +from __future__ import annotations + +import logging +import os + +from dotenv import load_dotenv + +load_dotenv() +load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("agents._mcp") + + +def main() -> None: + from bindings.mcp_server.server import McpServer + + logger.info("Starting nw- MCP server (stdio, manifest-driven)") + McpServer( + server_name="nw-", + connector_ids=[""], + ).run_stdio() + + +if __name__ == "__main__": + main() +``` + +`McpServer` is the shared manifest-driven server from `bindings.mcp_server.server`. The `connector_ids` list filters the full connector registry to only the connectors this image should expose. `run_stdio()` starts the MCP stdio transport; call `run_streamable_http()` instead for the HTTP transport (reads `NW_MCP_HOST`/`NW_MCP_PORT`/`NW_MCP_PATH`). + +Then register the script in root `pyproject.toml`: + +```toml +[project.scripts] +nw- = "agents._mcp:main" +``` + +#### Dockerfile template (`docker//Dockerfile`) + +All per-connector Docker images follow the same structure. The key requirements are: copy `src/` and `config/` from the repo root, install pre-built wheels from `/wheels/`, pin `NW_ALLOWED_CONNECTORS` to the single connector ID, and run as a non-root user. + +```dockerfile +FROM python:3.12-slim@sha256:3d5ed973e45820f5ba5e46bd065bd88b3a504ff0724d85980dcd05eab361fcf4 + +LABEL org.opencontainers.image.title="nw-" \ + org.opencontainers.image.description="Node Wire — MCP server" \ + org.opencontainers.image.source="https://github.com/AOT-Technologies/node-wire" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY src/ ./src/ +COPY config/ ./config/ +COPY packages/runtime/dist/*.whl /wheels/ +COPY packages/connectors//dist/*.whl /wheels/ + +ENV PYTHONPATH=/app/src \ + NW_ALLOWED_CONNECTORS= + +RUN pip install --no-cache-dir --find-links=/wheels \ + node-wire-runtime node-wire- "mcp>=1.6.0" \ + && rm -rf /wheels + +RUN groupadd --system --gid 1000 app \ + && useradd --system --uid 1000 --gid app --home /app app \ + && chown -R app:app /app + +USER app + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD \ + python -c "from agents._mcp import main; assert callable(main); print('ok')" || exit 1 + +CMD ["python", "-m", "agents._mcp"] +``` + +> **Note:** The `COPY packages/connectors//dist/*.whl` line requires the connector's publishable wheel (Tier 2) to be built first via `bash scripts/build-packages.sh packages/connectors/`. Build wheels before building Docker images. + The unified server (`python -m agents.mcp_entrypoint`) exposes **every** connector enabled for MCP in `config/connectors.yaml` (e.g. `http_generic.request`, `stripe.charge`, `stripe.create_payment_intent`, `stripe.create_subscription`, `stripe.cancel_subscription`, `stripe.issue_refund`, plus the rows above). @@ -82,6 +182,21 @@ The unified server (`python -m agents.mcp_entrypoint`) exposes **every** connect --- +## Command reference + +| Goal | Command | +|------|---------| +| Combined MCP server (all MCP-enabled connectors) | `uv run python -m agents.mcp_entrypoint` | +| Single-connector MCP | `uv run nw-` or `python -m agents._mcp` | +| REST API | `uv run node-wire` (default `MODE=API`) | +| gRPC | `MODE=GRPC uv run node-wire` | + +For MCP transport configuration (`NW_MCP_TRANSPORT`, `streamable-http`, Inspector), see **[mcp.md](mcp.md)** (canonical MCP transport doc). For REST/gRPC setup see **[installation.md](installation.md)**. + +> **Note:** `uv run node-wire` and `python -m bindings_entrypoint` start the **REST API** by default (`MODE=API`). They do **not** start an MCP transport server. `MODE=MCP` on `bindings_entrypoint` is a POC stub only — use `agents.mcp_entrypoint` for real MCP. + +--- + ## Shifting between transport modes Node Wire now supports two ways to expose tools to AI agents. By default, it uses `stdio`, but you can easily shift to the native `streamable-http` mode for web-native deployments. @@ -103,7 +218,13 @@ You can switch modes and ports instantly using environment variables. No code ch No extra variables are needed. This is the mode expected by local stdio clients and ToolHive-style stdio wrapping. ```bash +# Using uv +uv run python -m agents.mcp_entrypoint + +# Using python python -m agents.mcp_entrypoint + +# Per-connector: uv run nw-slack ``` PowerShell: @@ -111,10 +232,10 @@ PowerShell: ```powershell $env:NW_MCP_TRANSPORT="stdio" # Using uv -uv run node-wire +uv run python -m agents.mcp_entrypoint # Using python -python -m bindings_entrypoint +python -m agents.mcp_entrypoint ``` #### 2. Shifting to native HTTP mode (Port 8081) @@ -127,10 +248,10 @@ $env:NW_MCP_HOST="127.0.0.1" $env:NW_MCP_PORT="8081" $env:NW_MCP_PATH="/mcp" # Using uv -uv run node-wire +uv run python -m agents.mcp_entrypoint # Using python -python -m bindings_entrypoint +python -m agents.mcp_entrypoint ``` **Bash (Linux/macOS):** @@ -140,10 +261,10 @@ export NW_MCP_HOST="127.0.0.1" export NW_MCP_PORT="8081" export NW_MCP_PATH="/mcp" # Using uv -uv run node-wire +uv run python -m agents.mcp_entrypoint # Using python -python -m bindings_entrypoint +python -m agents.mcp_entrypoint ``` The native HTTP endpoint will be: @@ -633,7 +754,10 @@ python -m agents.toolhive --local --patient-id 12724066 --recipient-email you@ex | `fhir_epic connector not configured` | Missing Epic env vars | Ensure all `EPIC_*` variables are set and non-empty | | `fhir_cerner connector not configured` | Missing Cerner env vars | Ensure all `CERNER_*` variables are set and non-empty | | Docker build fails with `COPY src/ not found` | Wrong build context | Always run `docker build` from the **repository root**, not from `docker//` | +| Docker build fails with `COPY packages/connectors/.../dist/*.whl` | Missing wheel | Run `bash scripts/build-packages.sh packages/connectors/` to build the wheel first (Tier 2 must exist before Docker image) | | Image healthcheck fails | Import error at startup | Run `docker logs ` to see the Python traceback; usually a missing env var | +| Log shows many `Connector enabled in configuration but not registered; skipping instantiation` warnings | Expected behaviour | Per-connector images set `NW_ALLOWED_CONNECTORS` to one ID; all other connectors in `config/connectors.yaml` produce this warning and are correctly skipped. Not an error. | +| `MCP server initialized \| manifest_contract=5` — what does the number mean? | Version indicator | `manifest_contract` is the MCP input-schema contract version (currently 5), **not** the number of tools. Use `tools/list` to count exposed tools. | ## Rollout verification checklist diff --git a/docs/mcp.md b/docs/mcp.md index 731275b..d9c9be9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6,6 +6,8 @@ Node Wire integrates with the Model Context Protocol to allow AI agents (like Claude or custom LLM orchestrators) to discover and use connectors as tools. +For **per-connector Docker images and ToolHive registration**, see [mcp-servers.md](mcp-servers.md). + For **outbound OAuth** when connecting to remote authorized MCP servers over HTTP, see [mcp-client-oauth.md](mcp-client-oauth.md). ## Transport Modes diff --git a/docs/packaging.md b/docs/packaging.md index d5486fc..73c0aad 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -26,6 +26,56 @@ Node Wire ships as **nine independent PyPI packages** (the runtime plus eight co Each connector's `pyproject.toml` lives at `packages/connectors//pyproject.toml`; the runtime's is at `packages/runtime/pyproject.toml`. +**Source of truth:** Keep this table in sync with `ALL_PACKAGES` in [`scripts/build-packages.sh`](../scripts/build-packages.sh). MCP Docker images are a **separate, smaller set** (seven connectors today) — see [Docker demo images](#docker-demo-images). `http_generic` is publishable on PyPI but does not have a standalone MCP container image. + +--- + +## Adding a new publishable connector + +After implementing the connector runtime (see [connectors.md](connectors.md)), update these files to ship it on PyPI and optionally as a standalone MCP server. + +### Tier 1 — Runtime (dev, always required) + +| File / area | Purpose | +|---|---| +| `src/node_wire_/` | `schema.py`, `logic.py`, optional `registration.py`, `action_spec.py`, `README.md` | +| Root `pyproject.toml` | `[project.entry-points."node_wire.connectors"]` for editable dev install | +| `config/connectors.yaml` | `enabled`, `exposed_via`, `auth:` | +| [`sample.env`](../sample.env) | Commented placeholders for connector secrets | +| Tests | e.g. `tests/test_connectors_basic.py`, registry tests | + +`auto_register()` discovers the connector via the entry point — no factory branch required. + +### Tier 2 — Publishable PyPI package + +| File | Purpose | +|---|---| +| `packages/connectors//pyproject.toml` | Publishable package metadata, version, entry point | +| `packages/connectors//setup.py` | Cython/build glue (copy pattern from an existing connector) | +| [`scripts/build-packages.sh`](../scripts/build-packages.sh) | Add path to `ALL_PACKAGES` | +| [`.github/workflows/publish.yml`](../.github/workflows/publish.yml) | Add to `allowed` set | +| [`.github/workflows/github-release.yml`](../.github/workflows/github-release.yml) | Add to `package_paths` | +| [`.github/workflows/security-pr.yml`](../.github/workflows/security-pr.yml) | Add to matrix `package_path` | +| This doc — [Package inventory](#package-inventory) | Add row; update package count | +| Root + all package `pyproject.toml` | Version bump on release | +| `CHANGELOG.md` | Release section | + +### Tier 3 — Standalone MCP server (optional) + +> **Prerequisite:** Tier 2 (the PyPI wheel) must be completed first. The Dockerfile copies pre-built `.whl` files from `packages/connectors//dist/`; that directory does not exist until you run `bash scripts/build-packages.sh packages/connectors/`. + +Use when you need a dedicated Docker/ToolHive image for a single connector (not required for the combined `agents.mcp_entrypoint` server). For the entrypoint code template and Dockerfile template see [mcp-servers.md — Adding a row for a new connector](mcp-servers.md#adding-a-row-for-a-new-connector). + +| File | Purpose | +|---|---| +| `src/agents/_mcp.py` | Per-connector MCP agent entrypoint | +| Root `pyproject.toml` | `[project.scripts]` e.g. `nw-` | +| `docker//Dockerfile` | Demo MCP image | +| [`scripts/build-mcp-images.sh`](../scripts/build-mcp-images.sh) | `docker build` block | +| [`docker-compose.mcp.yml`](../docker-compose.mcp.yml) | Service + `NW_ALLOWED_CONNECTORS` | +| [mcp-servers.md](mcp-servers.md) | Naming conventions table row | +| [local-packages-to-images.md](local-packages-to-images.md) | Wheel → image mapping | + --- ## Python package build lifecycle diff --git a/docs/public-api.md b/docs/public-api.md index 7354ebd..219ef8a 100644 --- a/docs/public-api.md +++ b/docs/public-api.md @@ -55,6 +55,8 @@ Connector authors depend on these stable modules: Connectors register via the `node_wire.connectors` entry-point group. +**Bootstrap (not in `__all__`):** `node_wire_runtime.connector_registry.auto_register()` loads entry points at process startup (requires `NW_ALLOWED_CONNECTORS`). In-process usage typically goes through `bindings.factory.ConnectorFactory` after `auto_register()`, not direct registry access. + ## Wire contracts - **REST** — routes and request/response schemas served by the API binding diff --git a/sample.env b/sample.env index a09c535..e9cefd0 100644 --- a/sample.env +++ b/sample.env @@ -34,7 +34,7 @@ SMTP_PASSWORD=your-gmail-app-password STRIPE_API_KEY=sk_test_your_key_here # Slack -NW_SLACK_API_BASE_URL =https://slack.com/api +NW_SLACK_API_BASE_URL=https://slack.com/api NW_SLACK_SKIP_RESOLVE=true # Bot Token from https://api.slack.com/apps (Bot Token Scopes: chat:write, files:write, im:write) SLACK_BOT_TOKEN=xoxb-your-token-here @@ -82,7 +82,7 @@ NW_STREAM_BUFFER_MS=0 # NW_MCP_TRANSPORT: Selects the communication layer. # - stdio: (Default) Required for ToolHive proxying and Claude Desktop. # - streamable-http: Native HTTP/SSE transport for direct web integration. -NW_MCP_TRANSPORT=streamable-http +NW_MCP_TRANSPORT=stdio # NW_MCP_HOST defaults to 127.0.0.1 in code; set 0.0.0.0 only when exposing beyond localhost. NW_MCP_HOST=127.0.0.1 NW_MCP_PATH=/mcp @@ -144,6 +144,7 @@ NW_MCP_SCOPE_POLICY_DEFAULT=deny # REST auth for Playground demo (disable for local UI testing) # NW_REST_AUTH_DISABLED=true +# NW_REST_API_KEY=replace-with-strong-random-value NW_REST_LOAD_DOTENV=true # REST API key scopes (same format as NW_MCP_API_KEY_SCOPES). Empty = no scopes unless JWT carries scopes. # NW_REST_API_KEY_SCOPES=["mcp:smtp.send_email"] From 7103c66ddcdf6f4b0c127235ff297d3782a83238 Mon Sep 17 00:00:00 2001 From: vinaayakh-aot <61819385+vinaayakh-aot@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:35:47 -0700 Subject: [PATCH 2/6] Updated documentation for connector creation --- docs/connectors.md | 17 ++-- docs/local-packages-to-images.md | 2 +- docs/mcp-servers.md | 2 +- docs/packaging.md | 128 ++++++++++++++++++++++++++++--- 4 files changed, 128 insertions(+), 21 deletions(-) diff --git a/docs/connectors.md b/docs/connectors.md index f3242d4..b9ca836 100644 --- a/docs/connectors.md +++ b/docs/connectors.md @@ -23,6 +23,7 @@ Each connector is a **top-level package** under `src/` (e.g. `node_wire_fhir_epi | File | Role | |------|------| +| `__init__.py` | Required empty file — marks the directory as a Python package. | | `schema.py` | Pydantic input/output models. Each input model has an `action: Literal[...]` discriminator field (often combined into a discriminated union). | | `logic.py` | Connector class: `BaseConnector` subclass — either explicit `@nw_action` methods, or **`action_specs`** plus an optional `_execute_action_spec` override for SDK dispatch. | | `action_spec.py` (optional) | Declarative `SdkActionSpec` entries mapping validated models to vendor SDK calls (see Google Drive). | @@ -258,7 +259,7 @@ Choose a provider in your **`connectors.yaml`** via the `auth:` block: |-------|----------|---------|-------| | `secret_key` | Yes | — | Env var name holding the raw token value (`EnvSecretProvider` tries the key as-is, then uppercased). | | `header_name` | No | `Authorization` | HTTP header the token is injected into. | -| `prefix` | No | `Bearer ` (with trailing space) | String prepended to the token value. Set `prefix: ""` for APIs that expect the raw token (e.g. Stripe). Set `prefix: "token "` for older GitHub PAT style. | +| `prefix` | No | `Bearer ` (with trailing space) | String prepended to the token value. Set `prefix: ""` for APIs that expect the raw token (e.g. Stripe). Set `prefix: "token "` for APIs that require the `token` scheme (check your vendor's auth docs). | So `slack` (no `header_name`/`prefix`) produces `Authorization: Bearer `, and `stripe` (with `prefix: ""`) produces `Authorization: `. @@ -562,25 +563,25 @@ MCP tool names: **`.`** (e.g. `fhir_epic.read_patient`). S ### Runtime (dev) -1. Create the package directory `src/node_wire_/` with `schema.py` (Pydantic input/output models) and register the entry point under `[project.entry-points."node_wire.connectors"]` in the root `pyproject.toml`. -2. In `logic.py`: subclass `BaseConnector`, set `connector_id` and `output_model`, then add `@nw_action` methods or wire `action_specs`. +1. Create the package directory `src/node_wire_/`. The directory **must contain `__init__.py`** (empty is fine) to be importable as a Python package. Add `schema.py` with Pydantic input/output models and register the entry point under `[project.entry-points."node_wire.connectors"]` in the root `pyproject.toml`. +2. In `logic.py`: subclass `BaseConnector`, set `connector_id` and `output_model`, then add `@nw_action` methods or wire `action_specs`. If your connector makes outbound HTTP calls (e.g. using `httpx`), declare that library as a dependency in the connector's `packages/connectors//pyproject.toml`. 3. **Authentication**: Delegate all header construction to **`self.get_auth_headers()`**. Do not hardcode secret lookups or IdP handshakes and ensure sensitive fields are removed from your `input_schema`. 4. For SDK-style connectors, add an `action_spec.py` (or similar) with `SdkActionSpec` entries and use **`execute_spec_in_thread`** when the vendor client is blocking. 5. Optionally add `error_map` and/or `registration.py` for custom exception handling (see [registration.py example](#optional-registrationpy-for-errormapper) below). 6. Add the connector to **`config/connectors.yaml`** with `enabled: true`, the desired `exposed_via` protocols, and an **`auth:`** block. -7. **Environment template:** Add required secrets and connector-specific vars to [`sample.env`](../sample.env) (referenced by [configuration.md](configuration.md) and [installation.md](installation.md)). Use commented placeholders with the env var names your connector reads via `SecretProvider`. +7. **Environment template:** Add required secrets and connector-specific vars to [`sample.env`](../sample.env) (referenced by [configuration.md](configuration.md) and [installation.md](installation.md)). Use commented placeholders with the env var names your connector reads via `SecretProvider`. Also add the new connector's entry-point name to the `NW_ALLOWED_CONNECTORS` line so the template stays current. 8. `auto_register()` handles runtime registration — **no factory branch required**. ### Publishable PyPI package (when shipping on PyPI) -9. Add `packages/connectors//pyproject.toml` and `setup.py`; register the entry point. -10. Add the package path to **`scripts/build-packages.sh`** (`ALL_PACKAGES`) and CI allowlists (`.github/workflows/publish.yml`, `github-release.yml`, `security-pr.yml`). +9. Create `packages/connectors//pyproject.toml` and `packages/connectors//setup.py`. See [packaging.md — Tier 2 templates](packaging.md#tier-2-templates) for copy-paste starting points for both files. +10. Add the package path to **`scripts/build-packages.sh`** (`ALL_PACKAGES`) and to the three CI workflow allowlists — see [packaging.md — CI allowlist updates](packaging.md#ci-allowlist-updates) for the exact lines to add in each file. 11. Update the inventory table in **[packaging.md](packaging.md)**. ### Standalone MCP server (optional — dedicated Docker/ToolHive image) -12. Add `src/agents/_mcp.py`, a `[project.scripts]` entry in root `pyproject.toml`, `docker//Dockerfile`, and entries in **`scripts/build-mcp-images.sh`** and **`docker-compose.mcp.yml`**. -13. Add a row to the naming table in **[mcp-servers.md](mcp-servers.md)**. +12. Add `src/agents/_mcp.py`, a `[project.scripts]` entry in root `pyproject.toml`, `docker//Dockerfile`, and entries in **`scripts/build-mcp-images.sh`**, **`docker-compose.mcp.yml`**, and **[local-packages-to-images.md](local-packages-to-images.md)** (wheel → image mapping table). +13. Add a row to the naming table in **[mcp-servers.md](mcp-servers.md)** and update the architecture diagram in that file to include the new connector. For full file lists see [packaging.md — Adding a new publishable connector](packaging.md#adding-a-new-publishable-connector). diff --git a/docs/local-packages-to-images.md b/docs/local-packages-to-images.md index ab33aba..32871fe 100644 --- a/docs/local-packages-to-images.md +++ b/docs/local-packages-to-images.md @@ -101,7 +101,7 @@ docker build -f docker/smtp/Dockerfile -t nw-smtp:local . ## Wheel requirements by image -Each Dockerfile expects specific wheel files to exist in `dist/`: +Each Dockerfile expects specific wheel files to exist in `dist/`. Keep this table in sync with the Dockerfiles in `docker/` — add a row here whenever you add a Tier 3 standalone MCP image (see [connectors.md — Step 12](connectors.md#adding-a-new-connector-checklist)). | Image | Required wheels | |---|---| diff --git a/docs/mcp-servers.md b/docs/mcp-servers.md index 70c315f..913a320 100644 --- a/docs/mcp-servers.md +++ b/docs/mcp-servers.md @@ -76,7 +76,7 @@ When you add a standalone MCP server for a connector, add a row to the table abo | ToolHive name | Same as Docker image tag | | MCP tools | `.` for each manifest action | -Files to update: `src/agents/_mcp.py`, root `pyproject.toml` `[project.scripts]`, `docker//Dockerfile`, `scripts/build-mcp-images.sh`, `docker-compose.mcp.yml`, this table, and [local-packages-to-images.md](local-packages-to-images.md). See [packaging.md — Adding a new publishable connector](packaging.md#adding-a-new-publishable-connector) for the full checklist. +Files to update: `src/agents/_mcp.py`, root `pyproject.toml` `[project.scripts]`, `docker//Dockerfile`, `scripts/build-mcp-images.sh`, `docker-compose.mcp.yml`, this table (naming conventions + architecture diagram), and [local-packages-to-images.md](local-packages-to-images.md) (wheel requirements table). See [packaging.md — Adding a new publishable connector](packaging.md#adding-a-new-publishable-connector) for the full checklist. #### Per-connector MCP entrypoint template (`src/agents/_mcp.py`) diff --git a/docs/packaging.md b/docs/packaging.md index 73c0aad..b078542 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Packaging & Publishing -Node Wire ships as **nine independent PyPI packages** (the runtime plus eight connectors) built from a single monorepo. All wheels are binary-only (Cython-compiled `.so`/`.pyd` files) — no `.py` source is included in any published wheel. +Node Wire ships as multiple independent PyPI packages (the runtime plus one package per connector) built from a single monorepo. All wheels are binary-only (Cython-compiled `.so`/`.pyd` files) — no `.py` source is included in any published wheel. --- @@ -26,7 +26,7 @@ Node Wire ships as **nine independent PyPI packages** (the runtime plus eight co Each connector's `pyproject.toml` lives at `packages/connectors//pyproject.toml`; the runtime's is at `packages/runtime/pyproject.toml`. -**Source of truth:** Keep this table in sync with `ALL_PACKAGES` in [`scripts/build-packages.sh`](../scripts/build-packages.sh). MCP Docker images are a **separate, smaller set** (seven connectors today) — see [Docker demo images](#docker-demo-images). `http_generic` is publishable on PyPI but does not have a standalone MCP container image. +**Source of truth:** Keep this table in sync with `ALL_PACKAGES` in [`scripts/build-packages.sh`](../scripts/build-packages.sh). MCP Docker images are a **separate subset** — see [Docker demo images](#docker-demo-images). `http_generic` is publishable on PyPI but does not have a standalone MCP container image. --- @@ -51,15 +51,121 @@ After implementing the connector runtime (see [connectors.md](connectors.md)), u | File | Purpose | |---|---| | `packages/connectors//pyproject.toml` | Publishable package metadata, version, entry point | -| `packages/connectors//setup.py` | Cython/build glue (copy pattern from an existing connector) | +| `packages/connectors//setup.py` | Cython/build glue — see [Tier 2 templates](#tier-2-templates) below | | [`scripts/build-packages.sh`](../scripts/build-packages.sh) | Add path to `ALL_PACKAGES` | -| [`.github/workflows/publish.yml`](../.github/workflows/publish.yml) | Add to `allowed` set | -| [`.github/workflows/github-release.yml`](../.github/workflows/github-release.yml) | Add to `package_paths` | -| [`.github/workflows/security-pr.yml`](../.github/workflows/security-pr.yml) | Add to matrix `package_path` | -| This doc — [Package inventory](#package-inventory) | Add row; update package count | +| [`.github/workflows/publish.yml`](../.github/workflows/publish.yml) | Add to `allowed` set — see [CI allowlist updates](#ci-allowlist-updates) below | +| [`.github/workflows/github-release.yml`](../.github/workflows/github-release.yml) | Add to `package_paths` list — see [CI allowlist updates](#ci-allowlist-updates) below | +| [`.github/workflows/security-pr.yml`](../.github/workflows/security-pr.yml) | Add to matrix `package_path` — see [CI allowlist updates](#ci-allowlist-updates) below | +| This doc — [Package inventory](#package-inventory) | Add row | | Root + all package `pyproject.toml` | Version bump on release | | `CHANGELOG.md` | Release section | +### Tier 2 templates + +#### `pyproject.toml` template + +```toml +# packages/connectors//pyproject.toml +[project] +name = "node-wire-" +version = "1.0.0" +description = "Node Wire connector — " +requires-python = ">=3.11" +license = "Apache-2.0" +authors = [{ name = "AOT Technologies", email = "opensource@aot-technologies.com" }] + +dependencies = [ + "node-wire-runtime>=1.0.0", + # Add vendor SDK or HTTP client here, e.g.: + # "httpx>=0.27.0,<0.28.0", +] + +[project.entry-points."node_wire.connectors"] + = "node_wire_.logic" + +[build-system] +requires = ["setuptools>=69.0.0", "cython>=3.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["../../../src"] +include = ["node_wire_*"] +``` + +#### `setup.py` template + +```python +# packages/connectors//setup.py +import glob +import os +from Cython.Build import cythonize +from setuptools import setup +from setuptools.command.build_py import build_py as _BuildPy + + +class NoPyBuild(_BuildPy): + def find_package_modules(self, package, package_dir): + return [] + + +src_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../src/node_wire_")) +py_files = glob.glob(os.path.join(src_root, "**", "*.py"), recursive=True) + +setup( + cmdclass={"build_py": NoPyBuild}, + ext_modules=cythonize(py_files, compiler_directives={"language_level": "3"}, build_dir="build"), +) +``` + +Replace `` with the connector's snake_case name (e.g. `my_service`) and `` with the entry-point key (same string used in `config/connectors.yaml` and `NW_ALLOWED_CONNECTORS`). + +### CI allowlist updates + +Three workflow files each maintain a hardcoded list of publishable packages. Add one entry to each when shipping a new connector. + +#### `.github/workflows/publish.yml` — `allowed` set + +Inside the `validate` step, add your package path to the `allowed` Python set: + +```python +# .github/workflows/publish.yml (inside the inline Python script) +allowed = { + "packages/runtime", + "packages/connectors/http_generic", + "packages/connectors/stripe", + # ... existing entries ... + "packages/connectors/", # ← add this line +} +``` + +#### `.github/workflows/github-release.yml` — `package_paths` list + +Inside the release-manifest step, add your path to the `package_paths` Python list: + +```python +# .github/workflows/github-release.yml (inside the inline Python script) +package_paths = [ + "packages/runtime", + "packages/connectors/http_generic", + # ... existing entries ... + "packages/connectors/", # ← add this line +] +``` + +#### `.github/workflows/security-pr.yml` — matrix `package_path` + +Add a new YAML list item under `jobs..strategy.matrix.package_path`: + +```yaml +# .github/workflows/security-pr.yml +matrix: + package_path: + - packages/runtime + - packages/connectors/http_generic + # ... existing entries ... + - packages/connectors/ # ← add this line +``` + ### Tier 3 — Standalone MCP server (optional) > **Prerequisite:** Tier 2 (the PyPI wheel) must be completed first. The Dockerfile copies pre-built `.whl` files from `packages/connectors//dist/`; that directory does not exist until you run `bash scripts/build-packages.sh packages/connectors/`. @@ -88,7 +194,7 @@ Prerequisites: `pip install build cython wheel` (and a usable `python` on the ho bash scripts/build-packages.sh ``` -Default mode builds each of the **nine** known package paths (see inventory above): `python -m build --wheel` on the **host**, then again inside **Docker** (`python:3.12-slim`) so you get Linux-tagged wheels suitable for containers. **Docker must be installed and the daemon running.** After each package, the script scans every produced wheel and fails if any `.py` file appears inside the archive. +Default mode builds each package path listed in `ALL_PACKAGES` in the script (see the [Package inventory](#package-inventory) for the current set): `python -m build --wheel` on the **host**, then again inside **Docker** (`python:3.12-slim`) so you get Linux-tagged wheels suitable for containers. **Docker must be installed and the daemon running.** After each package, the script scans every produced wheel and fails if any `.py` file appears inside the archive. ### Artifact layout and safe command usage @@ -228,7 +334,7 @@ manual step per package, bound to that tag. ### Step 1 — Prepare the release -1. Bump version in the root `pyproject.toml` and all nine package `pyproject.toml` files. +1. Bump version in the root `pyproject.toml` and all connector package `pyproject.toml` files (one per entry in `ALL_PACKAGES` in `scripts/build-packages.sh`). 2. Add a dated `CHANGELOG.md` section and release link for the target version. 3. Merge to `main` and confirm required CI checks are green. @@ -249,13 +355,13 @@ The workflow: 1. Validates all package versions match the tag. 2. Verifies `CHANGELOG.md` has the matching section and release link. 3. Generates `sbom.json` (release-level SBOM). -4. Creates `release-manifest.txt` listing all nine publishable package paths. +4. Creates `release-manifest.txt` listing all publishable package paths (one per entry in `github-release.yml`'s `package_paths` list). 5. Creates the GitHub Release with changelog notes, SBOM, and manifest attached. ### Step 3 — Publish packages to PyPI After the GitHub Release exists, dispatch `.github/workflows/publish.yml` **once per -package** (nine times for a full release). +package** (once per entry in the `allowed` set in that workflow). **Required inputs:** From 0624b8dcd89d26cfeb1b18824545adea2dc9ebba Mon Sep 17 00:00:00 2001 From: vinaayakh-aot <61819385+vinaayakh-aot@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:44:03 -0700 Subject: [PATCH 3/6] Updated docs --- docs/connectors.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/connectors.md b/docs/connectors.md index b9ca836..c53f554 100644 --- a/docs/connectors.md +++ b/docs/connectors.md @@ -226,21 +226,40 @@ Node Wire provides a shared **`AuthProvider`** abstraction (`src/node_wire_runti To use authentication, call **`await self.get_auth_headers()`** (inherited from `BaseConnector`). This returns a dictionary of headers (e.g. `{"Authorization": "Bearer "}`) injected by the configured provider. +There are two patterns depending on how your connector talks to the vendor: + +**HTTP connectors** (direct REST calls via `httpx`) — create a short-lived client inside each `@nw_action` method. Do **not** override `build_client()`: + ```python -# logic.py usage +# logic.py — HTTP connector pattern (e.g. Slack, FHIR, GitHub) # Base URL: read from the connector's own config, an env var, or a module constant. # There is no inherited _get_base_url() helper — connectors own their URL resolution. BASE_URL = "https://api.example.com" # or: os.environ["MY_SERVICE_URL"] +@nw_action("read_resource") async def read_resource(self, params: In, *, trace_id: str) -> Out: headers = await self.get_auth_headers() # Fetched/cached by provider - async with httpx.AsyncClient() as client: resp = await client.get(f"{BASE_URL}/resource", headers=headers) resp.raise_for_status() ... ``` +**SDK connectors** (vendor Python SDK with a long-lived client object) — override `build_client()` so `get_client()` can cache the result across calls. Auth is handled inside `build_client()`, not via `get_auth_headers()`: + +```python +# logic.py — SDK connector pattern (e.g. Google Drive) +def build_client(self) -> Any: + # Read credential from secret provider and build the vendor client once. + raw_sa = self.secret_provider.get_secret("MY_SA_JSON") + creds = ... + return vendor_sdk.build("v1", credentials=creds) + +async def _execute_action_spec(self, action_name, params, *, trace_id, log_extra=None): + client = self.get_client() # cached; calls build_client() on first use + ... +``` + ### Supported Provider Types Choose a provider in your **`connectors.yaml`** via the `auth:` block: @@ -564,7 +583,7 @@ MCP tool names: **`.`** (e.g. `fhir_epic.read_patient`). S ### Runtime (dev) 1. Create the package directory `src/node_wire_/`. The directory **must contain `__init__.py`** (empty is fine) to be importable as a Python package. Add `schema.py` with Pydantic input/output models and register the entry point under `[project.entry-points."node_wire.connectors"]` in the root `pyproject.toml`. -2. In `logic.py`: subclass `BaseConnector`, set `connector_id` and `output_model`, then add `@nw_action` methods or wire `action_specs`. If your connector makes outbound HTTP calls (e.g. using `httpx`), declare that library as a dependency in the connector's `packages/connectors//pyproject.toml`. +2. In `logic.py`: subclass `BaseConnector`, set `connector_id` and `output_model`, then add `@nw_action` methods or wire `action_specs`. If your connector makes outbound HTTP calls (e.g. using `httpx`), declare that library as a dependency in the connector's `packages/connectors//pyproject.toml`. For HTTP-based connectors use an inline `async with httpx.AsyncClient() as client:` inside each `@nw_action` method (see [Using Auth in a Connector](#using-auth-in-a-connector)); only override `build_client()` / `get_client()` when wrapping a vendor SDK that requires a long-lived client object (e.g. `google_drive`). 3. **Authentication**: Delegate all header construction to **`self.get_auth_headers()`**. Do not hardcode secret lookups or IdP handshakes and ensure sensitive fields are removed from your `input_schema`. 4. For SDK-style connectors, add an `action_spec.py` (or similar) with `SdkActionSpec` entries and use **`execute_spec_in_thread`** when the vendor client is blocking. 5. Optionally add `error_map` and/or `registration.py` for custom exception handling (see [registration.py example](#optional-registrationpy-for-errormapper) below). @@ -580,6 +599,8 @@ MCP tool names: **`.`** (e.g. `fhir_epic.read_patient`). S ### Standalone MCP server (optional — dedicated Docker/ToolHive image) +> **Prerequisite:** Complete Steps 9–11 (Tier 2) first. The Dockerfile copies pre-built `.whl` files from `packages/connectors//dist/`; that directory does not exist until you run `bash scripts/build-packages.sh packages/connectors/`. + 12. Add `src/agents/_mcp.py`, a `[project.scripts]` entry in root `pyproject.toml`, `docker//Dockerfile`, and entries in **`scripts/build-mcp-images.sh`**, **`docker-compose.mcp.yml`**, and **[local-packages-to-images.md](local-packages-to-images.md)** (wheel → image mapping table). 13. Add a row to the naming table in **[mcp-servers.md](mcp-servers.md)** and update the architecture diagram in that file to include the new connector. From a0aa94f3b6dd812164956cba73d7766ec0bcb109 Mon Sep 17 00:00:00 2001 From: vinaayakh-aot <61819385+vinaayakh-aot@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:21:13 -0700 Subject: [PATCH 4/6] Added Mkdocs --- .github/workflows/docs.yml | 37 ++++ .gitignore | 3 + docs/architecture.md | 8 +- docs/compliance/hipaa-considerations.md | 8 +- docs/configuration.md | 8 +- docs/contributing.md | 109 ++++++++++ docs/index.md | 73 +++++++ docs/installation.md | 8 +- docs/mcp-client-oauth.md | 276 ++++++++++++------------ docs/mcp.md | 8 +- docs/privacy.md | 8 +- docs/quality-security-gates.md | 8 +- docs/salesforce_connector.md | 8 +- docs/slack_connector.md | 8 +- docs/troubleshooting.md | 8 +- mkdocs.yml | 56 +++++ pyproject.toml | 6 +- uv.lock | 190 ++++++++++++++++ 18 files changed, 662 insertions(+), 168 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/contributing.md create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..528af8b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026 AOT Technologies +# +# SPDX-License-Identifier: Apache-2.0 + +name: docs + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install docs dependencies + run: pip install mkdocs mkdocs-material + + - name: Build (PRs — validates site builds cleanly) + if: github.event_name == 'pull_request' + run: mkdocs build + + - name: Deploy to GitHub Pages + if: github.event_name == 'push' + run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index 50368c5..eca270c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ coverage.xml bandit-report.json .venv*/ +# MkDocs generated site +/site/ + # debug launch.json diff --git a/docs/architecture.md b/docs/architecture.md index b67a6a7..babddd4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Node Wire Architecture diff --git a/docs/compliance/hipaa-considerations.md b/docs/compliance/hipaa-considerations.md index 2443070..98a3a6d 100644 --- a/docs/compliance/hipaa-considerations.md +++ b/docs/compliance/hipaa-considerations.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # HIPAA Compliance Considerations diff --git a/docs/configuration.md b/docs/configuration.md index 752d454..11033b8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Configuration Guide diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..d55da99 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,109 @@ + + +# Contributing to Node Wire + +Thanks for your interest in contributing! This guide covers how to set up a +development environment, the quality checks we enforce, and the conventions for +submitting changes. By contributing you agree that your contributions are +licensed under the project's [Apache License 2.0](https://github.com/AOT-Technologies/node-wire/blob/main/LICENSE) and that you sign off +each commit under the +[Developer Certificate of Origin](#developer-certificate-of-origin-dco). + +## Prerequisites + +| Requirement | Version | Notes | +|---|---|---| +| Python | 3.11+ | Required to run the platform | +| `uv` | Latest | Recommended for dependency management and reproducible installs | +| Git | Any recent version | | +| Docker | Latest | Only needed for MCP server image builds | + +## Development Setup + +Install the project and all development dependencies from the committed +lockfile: + +```bash +git clone https://github.com/AOT-Technologies/node-wire.git +cd node-wire +uv sync --frozen --all-extras --dev +``` + +Install the pre-commit hooks so checks run automatically before each commit: + +```bash +pre-commit install +``` + +## Quality Checks + +All of the following run in CI on pull requests against `main`. Run them +locally before opening a PR: + +- **Lint:** `uv run ruff check .` +- **Auto-fix & format:** `uv run ruff check --fix . && uv run ruff format .` +- **Type-check:** `uv run mypy` +- **Security (SAST):** `uv run bandit -c pyproject.toml -r src` +- **Tests:** `uv run pytest` + +See [Code Quality](code-quality-compliance.md) and [Quality & Security Gates](quality-security-gates.md) for the full tooling reference. + +## Licensing & REUSE Compliance + +This project is [REUSE](https://reuse.software/) compliant. **Every new file +must carry an SPDX header.** For source and Markdown files use the project's +standard header, e.g. for Python: + +```python +# SPDX-FileCopyrightText: 2026 AOT Technologies +# +# SPDX-License-Identifier: Apache-2.0 +``` + +and for Markdown/HTML, an HTML comment with the same two SPDX tags. + +## Git Identity + +Configure your git identity before committing. Commits must use your real +name and a valid email so attribution is accurate: + +```bash +git config user.name "Your Name" +git config user.email "you@example.com" +``` + +## Developer Certificate of Origin (DCO) + +This project uses the [Developer Certificate of Origin](https://developercertificate.org/) +(DCO) instead of a CLA. Add a `Signed-off-by` trailer to **every commit**: + +```bash +git commit -s -m "Your commit message" +``` + +CI (`.github/workflows/dco.yml`) verifies that every commit in a pull request +carries a sign-off matching its author. If you forget: + +```bash +git rebase --signoff main +git push --force-with-lease +``` + +## Submitting Changes + +1. Fork the repository and create a feature branch from `main`. +2. Make your change, including tests and documentation updates where relevant. + Sign off every commit with `git commit -s`. +3. Ensure all quality checks above pass locally. +4. Open a pull request against `main` with a clear description and motivation. +5. A maintainer will review your PR. Address feedback by pushing additional + commits to your branch. + +## Reporting Bugs & Requesting Features + +Use the GitHub issue templates. For **security vulnerabilities**, do not open a +public issue — follow the [Security Policy](https://github.com/AOT-Technologies/node-wire/blob/main/SECURITY.md) instead. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..903fd54 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,73 @@ + + +# Node Wire + +[![CI](https://github.com/AOT-Technologies/node-wire/actions/workflows/pytest.yml/badge.svg)](https://github.com/AOT-Technologies/node-wire/actions/workflows/pytest.yml) +[![CodeQL](https://github.com/AOT-Technologies/node-wire/actions/workflows/codeql.yml/badge.svg)](https://github.com/AOT-Technologies/node-wire/actions/workflows/codeql.yml) +[![PyPI](https://img.shields.io/pypi/v/node-wire.svg)](https://pypi.org/project/node-wire/) +[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/AOT-Technologies/node-wire/blob/main/LICENSE) + +Node Wire is a three-layer Python platform that runs connector adapters (Google Drive, SMTP, Stripe, FHIR, Salesforce, Slack, and more) and exposes them over REST, gRPC, or MCP. It provides a consistent execution contract with built-in validation, resilience, and telemetry. + +## Quick Start + +```bash +git clone https://github.com/AOT-Technologies/node-wire.git +cd node-wire +uv sync --frozen --all-extras --dev +cp sample.env .env +MODE=API uv run node-wire +``` + +Open [http://localhost:8000/docs](http://localhost:8000/docs) for the Swagger UI. + +## Key Sections + +
+ +- **Getting Started** + + Set up your environment and configure connectors. + + [:octicons-arrow-right-24: Installation](installation.md) + +- **Architecture** + + Understand the three-layer design: Runtime, Connectors, and Bindings. + + [:octicons-arrow-right-24: Architecture](architecture.md) + +- **Connectors** + + Build or configure integrations with Google Drive, Salesforce, Slack, and more. + + [:octicons-arrow-right-24: Connectors Guide](connectors.md) + +- **MCP Integration** + + Deploy connectors as Model Context Protocol servers for AI agents. + + [:octicons-arrow-right-24: MCP Overview](mcp.md) + +
+ +## Available Connectors + +| Connector | Protocol | Doc | +|---|---|---| +| Google Drive | REST + OAuth | [Guide](google_drive_connector.md) | +| Salesforce | REST | [Guide](salesforce_connector.md) | +| Slack | Events API | [Guide](slack_connector.md) | +| SMTP | Email | [Connectors](connectors.md) | +| Stripe | REST | [Connectors](connectors.md) | +| FHIR Epic | SMART on FHIR | [Connectors](connectors.md) | +| FHIR Cerner | SMART on FHIR | [Connectors](connectors.md) | +| HTTP Generic | REST bridge | [Connectors](connectors.md) | + +## Contributing + +Contributions are welcome. See the [Contributing guide](contributing.md) for development setup, quality checks, and DCO requirements. diff --git a/docs/installation.md b/docs/installation.md index b9da6b4..9b94728 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Installation Guide diff --git a/docs/mcp-client-oauth.md b/docs/mcp-client-oauth.md index 7ee709c..7345cf8 100644 --- a/docs/mcp-client-oauth.md +++ b/docs/mcp-client-oauth.md @@ -1,137 +1,139 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 - -# MCP Client OAuth (Outbound) - -Node Wire can act as an **OAuth 2.1 client** when connecting to **remote HTTP MCP servers** that require authorization per the MCP Authorization specification (2025-11-25). - -This is **separate** from: - -- **Inbound MCP server auth** (`NW_MCP_API_KEY`, `NW_MCP_JWT_SECRET`) — clients calling *node-wire’s* MCP server. -- **Connector OAuth** (`OAuth2AuthProvider`) — Epic/Cerner/Stripe credentials for connector actions. - -## Package - -Implementation lives in `src/node_wire_runtime/mcp_client/`: - -| Module | Purpose | -|--------|---------| -| `config.py` | Operator settings (Section 10) | -| `discovery.py` | RFC 9728 + RFC 8414 metadata discovery | -| `dcr.py` | RFC 7591 dynamic client registration | -| `oauth_flow.py` | Authorization code + PKCE + resource parameter | -| `redirect_listener.py` | Loopback redirect callback (desktop) | -| `storage.py` | Persisted DCR registrations per issuer | - -## Configuration - -```python -from node_wire_runtime.mcp_client import McpClientConfig, McpServerConfig, AuthConfig - -config = McpClientConfig( - server=McpServerConfig(url="https://mcp.example.com/mcp"), - auth=AuthConfig( - scopes="mcp:tools", - client=AuthClientConfig(id="", secret=""), # empty → use DCR when supported - ), -) -``` - -### Section 10 settings - -| Setting | Default | Purpose | -|---------|---------|---------| -| `server.url` | (required) | Canonical MCP server URL; RFC 8707 `resource` | -| `auth.discovery.cacheTtlSeconds` | `3600` | Discovery metadata cache TTL | -| `auth.dcr.enabled` | `true` | Attempt RFC 7591 when `registration_endpoint` exists | -| `auth.client.id` / `secret` | empty | Override when AS has no DCR | -| `auth.scopes` | empty | Requested scopes | -| `auth.production` | `false` | When `true`, require `https://` MCP server URL and `configured-url` HTTPS redirect; disables HTTP loopback | -| `auth.redirect.mode` | `loopback` | `loopback` or `configured-url` (defaults to `configured-url` when `auth.production=true`) | -| `auth.redirect.url` | `http://127.0.0.1:0/callback` | HTTPS callback URL for hosted / production mode | -| `auth.token.refreshLeadSeconds` | `60` | Proactive refresh lead time | -| `auth.token.store` | `os-keychain` | Token storage backend | - -## Discovery - -```python -from node_wire_runtime.mcp_client import discover, discovery_cache_for_config - -result = await discover(config, www_authenticate=challenge_header, cache=cache) -``` - -Triggered on first MCP request without a token or after `401` with `WWW-Authenticate` `resource_metadata`. - -## Authorization - -**Loopback (desktop):** - -```python -from node_wire_runtime.mcp_client import AuthorizationCodeFlow - -flow = AuthorizationCodeFlow(config) -tokens = await flow.run_loopback_authorization(open_browser=True) -``` - -**Configured URL (hosted / production):** - -```python -url = await flow.start_authorization(open_browser=True) -# User completes login; your app receives redirect at auth.redirect.url -tokens = await flow.complete_authorization_with_callback_url(callback_url) -``` - -### Production hardening (`auth.production=true`) - -Set `NW_MCP_OAUTH_PRODUCTION=true` (or `auth.production=True` in code) for server deployments: - -- `mcp.server.url` must use `https://` -- `auth.redirect.mode` must be `configured-url` -- `auth.redirect.url` must use `https://` (HTTP loopback is rejected) -- Re-authorization cannot use `run_loopback_authorization`; the host app must expose an HTTPS callback route or inject `TokenManager(reauthorize=...)` / `McpOAuthClient(reauthorize=...)` - -Example environment (see also `sample.env`): - -```env -NW_MCP_OAUTH_ENABLED=true -NW_MCP_OAUTH_PRODUCTION=true -NW_MCP_SERVER_URL=https://mcp.example.com/mcp -NW_MCP_OAUTH_REDIRECT_MODE=configured-url -NW_MCP_OAUTH_REDIRECT_URL=https://app.example.com/oauth/mcp/callback -``` - -Desktop / local development keeps the default (`NW_MCP_OAUTH_PRODUCTION` unset or `false`) with `auth.redirect.mode=loopback`. - -## STDIO - -STDIO MCP transports use environment credentials only; this OAuth client does not apply. - -## Token manager and HTTP client (Phases 4–5) - -```python -from node_wire_runtime.mcp_client import McpOAuthClient, create_http_mcp_client - -# Factory: NW_MCP_OAUTH_ENABLED=true, or legacy TOOLHIVE_MCP_BEARER_TOKEN for static auth -client = create_http_mcp_client("https://mcp.example.com/mcp") - -# Production: pass reauthorize when tokens must be renewed without loopback -# client = create_http_mcp_client("https://mcp.example.com/mcp", reauthorize=my_reauth_fn) - -tools = await client.list_tools() -``` - -Environment variables: see `sample.env` (`NW_MCP_OAUTH_*`). - -## ToolHive / playground integration (Phase 6) - -`agents.toolhive` and `playground/scenarios.py` call `create_http_mcp_client()` for HTTP MCP URLs: - -1. `TOOLHIVE_MCP_BEARER_TOKEN` / `TOOLHIVE_MCP_API_KEY` → legacy static Bearer (no OAuth). -2. `NW_MCP_OAUTH_ENABLED=true` → `McpOAuthClient` with discovery, DCR, PKCE, token refresh. - -## Internal demo script - -```bash -uv run python scripts/demo_mcp_oauth_mock.py -``` + + +# MCP Client OAuth (Outbound) + +Node Wire can act as an **OAuth 2.1 client** when connecting to **remote HTTP MCP servers** that require authorization per the MCP Authorization specification (2025-11-25). + +This is **separate** from: + +- **Inbound MCP server auth** (`NW_MCP_API_KEY`, `NW_MCP_JWT_SECRET`) — clients calling *node-wire’s* MCP server. +- **Connector OAuth** (`OAuth2AuthProvider`) — Epic/Cerner/Stripe credentials for connector actions. + +## Package + +Implementation lives in `src/node_wire_runtime/mcp_client/`: + +| Module | Purpose | +|--------|---------| +| `config.py` | Operator settings (Section 10) | +| `discovery.py` | RFC 9728 + RFC 8414 metadata discovery | +| `dcr.py` | RFC 7591 dynamic client registration | +| `oauth_flow.py` | Authorization code + PKCE + resource parameter | +| `redirect_listener.py` | Loopback redirect callback (desktop) | +| `storage.py` | Persisted DCR registrations per issuer | + +## Configuration + +```python +from node_wire_runtime.mcp_client import McpClientConfig, McpServerConfig, AuthConfig + +config = McpClientConfig( + server=McpServerConfig(url="https://mcp.example.com/mcp"), + auth=AuthConfig( + scopes="mcp:tools", + client=AuthClientConfig(id="", secret=""), # empty → use DCR when supported + ), +) +``` + +### Section 10 settings + +| Setting | Default | Purpose | +|---------|---------|---------| +| `server.url` | (required) | Canonical MCP server URL; RFC 8707 `resource` | +| `auth.discovery.cacheTtlSeconds` | `3600` | Discovery metadata cache TTL | +| `auth.dcr.enabled` | `true` | Attempt RFC 7591 when `registration_endpoint` exists | +| `auth.client.id` / `secret` | empty | Override when AS has no DCR | +| `auth.scopes` | empty | Requested scopes | +| `auth.production` | `false` | When `true`, require `https://` MCP server URL and `configured-url` HTTPS redirect; disables HTTP loopback | +| `auth.redirect.mode` | `loopback` | `loopback` or `configured-url` (defaults to `configured-url` when `auth.production=true`) | +| `auth.redirect.url` | `http://127.0.0.1:0/callback` | HTTPS callback URL for hosted / production mode | +| `auth.token.refreshLeadSeconds` | `60` | Proactive refresh lead time | +| `auth.token.store` | `os-keychain` | Token storage backend | + +## Discovery + +```python +from node_wire_runtime.mcp_client import discover, discovery_cache_for_config + +result = await discover(config, www_authenticate=challenge_header, cache=cache) +``` + +Triggered on first MCP request without a token or after `401` with `WWW-Authenticate` `resource_metadata`. + +## Authorization + +**Loopback (desktop):** + +```python +from node_wire_runtime.mcp_client import AuthorizationCodeFlow + +flow = AuthorizationCodeFlow(config) +tokens = await flow.run_loopback_authorization(open_browser=True) +``` + +**Configured URL (hosted / production):** + +```python +url = await flow.start_authorization(open_browser=True) +# User completes login; your app receives redirect at auth.redirect.url +tokens = await flow.complete_authorization_with_callback_url(callback_url) +``` + +### Production hardening (`auth.production=true`) + +Set `NW_MCP_OAUTH_PRODUCTION=true` (or `auth.production=True` in code) for server deployments: + +- `mcp.server.url` must use `https://` +- `auth.redirect.mode` must be `configured-url` +- `auth.redirect.url` must use `https://` (HTTP loopback is rejected) +- Re-authorization cannot use `run_loopback_authorization`; the host app must expose an HTTPS callback route or inject `TokenManager(reauthorize=...)` / `McpOAuthClient(reauthorize=...)` + +Example environment (see also `sample.env`): + +```env +NW_MCP_OAUTH_ENABLED=true +NW_MCP_OAUTH_PRODUCTION=true +NW_MCP_SERVER_URL=https://mcp.example.com/mcp +NW_MCP_OAUTH_REDIRECT_MODE=configured-url +NW_MCP_OAUTH_REDIRECT_URL=https://app.example.com/oauth/mcp/callback +``` + +Desktop / local development keeps the default (`NW_MCP_OAUTH_PRODUCTION` unset or `false`) with `auth.redirect.mode=loopback`. + +## STDIO + +STDIO MCP transports use environment credentials only; this OAuth client does not apply. + +## Token manager and HTTP client (Phases 4–5) + +```python +from node_wire_runtime.mcp_client import McpOAuthClient, create_http_mcp_client + +# Factory: NW_MCP_OAUTH_ENABLED=true, or legacy TOOLHIVE_MCP_BEARER_TOKEN for static auth +client = create_http_mcp_client("https://mcp.example.com/mcp") + +# Production: pass reauthorize when tokens must be renewed without loopback +# client = create_http_mcp_client("https://mcp.example.com/mcp", reauthorize=my_reauth_fn) + +tools = await client.list_tools() +``` + +Environment variables: see `sample.env` (`NW_MCP_OAUTH_*`). + +## ToolHive / playground integration (Phase 6) + +`agents.toolhive` and `playground/scenarios.py` call `create_http_mcp_client()` for HTTP MCP URLs: + +1. `TOOLHIVE_MCP_BEARER_TOKEN` / `TOOLHIVE_MCP_API_KEY` → legacy static Bearer (no OAuth). +2. `NW_MCP_OAUTH_ENABLED=true` → `McpOAuthClient` with discovery, DCR, PKCE, token refresh. + +## Internal demo script + +```bash +uv run python scripts/demo_mcp_oauth_mock.py +``` diff --git a/docs/mcp.md b/docs/mcp.md index d9c9be9..1ee0bdc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Model Context Protocol (MCP) in Node Wire diff --git a/docs/privacy.md b/docs/privacy.md index b39f399..6da8d35 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Privacy Policy and Compliance diff --git a/docs/quality-security-gates.md b/docs/quality-security-gates.md index 8bb0dea..6cb2d06 100644 --- a/docs/quality-security-gates.md +++ b/docs/quality-security-gates.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Quality and security gates diff --git a/docs/salesforce_connector.md b/docs/salesforce_connector.md index 56db076..b00c301 100644 --- a/docs/salesforce_connector.md +++ b/docs/salesforce_connector.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Salesforce Connector (`src/node_wire_salesforce`) diff --git a/docs/slack_connector.md b/docs/slack_connector.md index de05f88..23e72fb 100644 --- a/docs/slack_connector.md +++ b/docs/slack_connector.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Slack Connector diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4693067..ce24918 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2026 AOT Technologies -# -# SPDX-License-Identifier: Apache-2.0 + # Troubleshooting Guide diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..64e3664 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2026 AOT Technologies +# +# SPDX-License-Identifier: Apache-2.0 + +site_name: Node Wire +site_url: https://aot-technologies.github.io/node-wire/ +repo_url: https://github.com/AOT-Technologies/node-wire +repo_name: AOT-Technologies/node-wire +docs_dir: docs + +theme: + name: material + palette: + scheme: default + primary: indigo + accent: indigo + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - toc.integrate + - search.highlight + +plugins: + - search + +nav: + - Home: index.md + - Getting Started: + - Installation: installation.md + - Configuration: configuration.md + - Architecture: architecture.md + - Connectors: + - Overview: connectors.md + - Google Drive: google_drive_connector.md + - Salesforce: salesforce_connector.md + - Slack: slack_connector.md + - MCP: + - Overview: mcp.md + - MCP Servers: mcp-servers.md + - Client OAuth: mcp-client-oauth.md + - Agent Scenario: toolhive_agent_scenario.md + - Development: + - Contributing: contributing.md + - Code Quality: code-quality-compliance.md + - Quality & Security Gates: quality-security-gates.md + - Versioning: versioning.md + - Packaging: packaging.md + - Public API: public-api.md + - Operations: + - Troubleshooting: troubleshooting.md + - Release & Rollback: release-rollback.md + - Local Images: local-packages-to-images.md + - Compliance: + - Privacy: privacy.md + - HIPAA Considerations: compliance/hipaa-considerations.md diff --git a/pyproject.toml b/pyproject.toml index d04a89e..77013f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ Homepage = "https://github.com/AOT-Technologies/node-wire" Repository = "https://github.com/AOT-Technologies/node-wire" Issues = "https://github.com/AOT-Technologies/node-wire/issues" -Documentation = "https://github.com/AOT-Technologies/node-wire/tree/main/docs" +Documentation = "https://aot-technologies.github.io/node-wire/" [project.scripts] node-wire = "bindings_entrypoint:main" @@ -96,6 +96,10 @@ requires = ["setuptools>=69.0.0", "wheel"] build-backend = "setuptools.build_meta" [dependency-groups] +docs = [ + "mkdocs>=1.6", + "mkdocs-material>=9.5", +] dev = [ "pytest>=9.0.2", "pytest-asyncio>=1.3.0", diff --git a/uv.lock b/uv.lock index a493fad..d903399 100644 --- a/uv.lock +++ b/uv.lock @@ -281,6 +281,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, +] + [[package]] name = "bandit" version = "1.9.4" @@ -942,6 +964,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + [[package]] name = "google-ai-generativelanguage" version = "0.6.15" @@ -1788,6 +1822,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" }, ] +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -1908,6 +1951,84 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + [[package]] name = "msgpack" version = "1.2.1" @@ -2201,6 +2322,10 @@ dev = [ { name = "reuse" }, { name = "ruff" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, +] [package.metadata] requires-dist = [ @@ -2249,6 +2374,10 @@ dev = [ { name = "reuse", specifier = ">=5.0.0" }, { name = "ruff", specifier = ">=0.3.5" }, ] +docs = [ + { name = "mkdocs", specifier = ">=1.6" }, + { name = "mkdocs-material", specifier = ">=9.5" }, +] [[package]] name = "nodeenv" @@ -3033,6 +3162,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -3528,6 +3666,19 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymdown-extensions" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -3755,6 +3906,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -4479,6 +4642,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "watchfiles" version = "1.2.0" From 285db64f285f896e99f71605638d00690c8452f1 Mon Sep 17 00:00:00 2001 From: vinaayakh-aot <61819385+vinaayakh-aot@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:00:57 -0700 Subject: [PATCH 5/6] Updated mkdocs to match the Node-wire website theme --- docs/stylesheets/extra.css | 354 +++++++++++++++++++++++++++++++++++++ mkdocs.yml | 82 ++++++++- 2 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 docs/stylesheets/extra.css diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..9af3213 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,354 @@ +/* Node Wire — Brand Stylesheet + Matched to aot-technologies.com/products/node-wire + Background: #1C2027 (dark charcoal grey) + Surface: #242930 (card / panel charcoal) + Blue: #37c4f0 (sky blue — links, nav, code borders, h1) + Amber: #ecb32e (gold — h2 accents, tips, table headers) + Pink: #e01d5a (red-pink — h3 accents, warnings, danger) + Text: #E8EDF5 (near-white) +*/ + +/* ─── Primary color (text ON the primary-colored header/nav) ─ */ +[data-md-color-primary="custom"] { + --md-primary-fg-color: #37c4f0; + --md-primary-fg-color--light: #62d2f5; + --md-primary-fg-color--dark: #1fa8d8; + --md-primary-bg-color: #ffffff; /* text on dark header — MUST be light */ + --md-primary-bg-color--light: rgba(255, 255, 255, 0.72); +} + +/* ─── Accent ───────────────────────────────────────────────── */ +[data-md-color-accent="custom"] { + --md-accent-fg-color: #37c4f0; + --md-accent-fg-color--transparent: rgba(55, 196, 240, 0.12); + --md-accent-bg-color: #1C2027; + --md-accent-bg-color--light: rgba(28, 32, 39, 0.7); +} + +/* ─── Dark scheme (slate) ──────────────────────────────────── */ +[data-md-color-scheme="slate"] { + --md-default-bg-color: #1C2027; + --md-default-bg-color--light: rgba(28, 32, 39, 0.54); + --md-default-bg-color--lighter: rgba(28, 32, 39, 0.26); + --md-default-bg-color--lightest: rgba(28, 32, 39, 0.12); + + --md-default-fg-color: #E8EDF5; + --md-default-fg-color--light: rgba(232, 237, 245, 0.72); + --md-default-fg-color--lighter: rgba(232, 237, 245, 0.44); + --md-default-fg-color--lightest: rgba(232, 237, 245, 0.12); + + --md-typeset-a-color: #37c4f0; + + --md-code-bg-color: #242930; + --md-code-fg-color: #E8EDF5; + --md-code-hl-color: rgba(55, 196, 240, 0.15); + + --md-footer-bg-color: #13171C; + --md-footer-bg-color--dark: #0D1015; + --md-footer-fg-color: #8A9BAC; + --md-footer-fg-color--light: rgba(138, 155, 172, 0.7); + --md-footer-fg-color--lighter: rgba(138, 155, 172, 0.45); +} + +/* ─── Light scheme ─────────────────────────────────────────── */ +[data-md-color-scheme="default"] { + --md-default-bg-color: #F5F7FA; + --md-typeset-a-color: #1a9fc8; + --md-code-bg-color: #EAECF0; +} + +/* ─── Header ───────────────────────────────────────────────── */ +/* Slightly lighter than page bg so it reads as a distinct bar */ +.md-header { + background: #252A32 !important; + border-bottom: 1px solid rgba(55, 196, 240, 0.25); + box-shadow: 0 2px 20px rgba(0, 0, 0, 0.5); +} + +/* Force all header text/icons to white for contrast */ +.md-header, +.md-header .md-header__title, +.md-header .md-header__button, +.md-header .md-search__input { + color: #ffffff !important; +} + +.md-header__title { + font-weight: 700; + letter-spacing: 0.02em; +} + +/* Search bar in header */ +.md-header .md-search__input { + background: rgba(255, 255, 255, 0.1) !important; + border-radius: 6px; +} +.md-header .md-search__input::placeholder { + color: rgba(255, 255, 255, 0.55) !important; +} + +/* Header icons (search, color toggle, github) */ +.md-header .md-icon svg { + fill: rgba(255, 255, 255, 0.85); +} +.md-header .md-icon:hover svg { + fill: #37c4f0; +} + +/* ─── Nav tabs ─────────────────────────────────────────────── */ +.md-tabs { + background: #1C2027 !important; + border-bottom: 1px solid rgba(55, 196, 240, 0.12); +} + +.md-tabs__link { + color: rgba(232, 237, 245, 0.55) !important; + font-weight: 500; + transition: color 0.15s, box-shadow 0.15s; +} + +.md-tabs__link:hover { + color: #37c4f0 !important; +} + +/* Active tab — class is on the
  • , not the */ +.md-tabs__item--active .md-tabs__link { + color: #37c4f0 !important; + font-weight: 700 !important; +} + +/* ─── Sidebar ──────────────────────────────────────────────── */ +[data-md-color-scheme="slate"] .md-nav { + background: #1C2027; +} + +[data-md-color-scheme="slate"] .md-nav__title { + background: #242930; + color: #E8EDF5; +} + +.md-nav__link--active { + color: #37c4f0 !important; + font-weight: 600; +} + +[data-md-color-scheme="default"] .md-nav__link--active { + color: #1a9fc8 !important; +} + +.md-nav__item--active > .md-nav__link { + border-left: 3px solid #37c4f0; + padding-left: calc(0.6rem - 3px); +} + +.md-nav__link:hover { + color: #37c4f0 !important; +} + +/* ─── Content typography ───────────────────────────────────── */ + +/* h1: blue underline */ +.md-typeset h1 { + font-weight: 800; + letter-spacing: -0.025em; + border-bottom: 2px solid #37c4f0; + padding-bottom: 0.25em; + display: inline-block; +} + +[data-md-color-scheme="slate"] .md-typeset h1 { + color: #F0F4FA; +} + +/* h2: amber left-bar + amber color */ +.md-typeset h2 { + font-weight: 700; + padding-left: 0.7rem; + border-left: 3px solid #ecb32e; + color: #ecb32e; +} + +[data-md-color-scheme="default"] .md-typeset h2 { + color: #b07d00; + border-color: #ecb32e; +} + +/* h3: pink/red accent dot via pseudo-element */ +.md-typeset h3 { + font-weight: 600; + color: #e01d5a; +} + +[data-md-color-scheme="default"] .md-typeset h3 { + color: #b8003d; +} + +.md-typeset a { + transition: color 0.15s; +} + +.md-typeset a:hover { + color: #62d2f5 !important; +} + +/* ─── Search highlight: sky blue, not yellow ───────────────── */ +.md-typeset mark, +.md-search-result mark { + background-color: rgba(55, 196, 240, 0.22) !important; + color: inherit !important; + border-radius: 2px; + padding: 0 2px; +} + +[data-md-color-scheme="slate"] .md-search__input { + background: #242930; + color: #E8EDF5; +} + +/* ─── Code blocks ──────────────────────────────────────────── */ +.md-typeset .highlight { + border-radius: 8px; + border-left: 3px solid #37c4f0; +} + +[data-md-color-scheme="slate"] .md-typeset pre > code { + background: #242930; +} + +.md-clipboard { + color: rgba(232, 237, 245, 0.35); + transition: color 0.15s; +} +.md-clipboard:hover { + color: #37c4f0 !important; +} + +/* ─── Admonitions ──────────────────────────────────────────── */ +.md-typeset .admonition, +.md-typeset details { + border-radius: 8px; +} + +[data-md-color-scheme="slate"] .md-typeset .admonition, +[data-md-color-scheme="slate"] .md-typeset details { + background: #242930; +} + +/* note / info → blue (#37c4f0) */ +.md-typeset .admonition.note, +.md-typeset .admonition.info, +.md-typeset .admonition.abstract, +.md-typeset details.note, +.md-typeset details.info { + border-color: #37c4f0; +} +.md-typeset .admonition.note > .admonition-title, +.md-typeset .admonition.info > .admonition-title, +.md-typeset .admonition.abstract > .admonition-title, +.md-typeset details.note > summary, +.md-typeset details.info > summary { + background: rgba(55, 196, 240, 0.12); + color: #37c4f0; +} + +/* tip / hint / success → amber (#ecb32e) */ +.md-typeset .admonition.tip, +.md-typeset .admonition.hint, +.md-typeset .admonition.success, +.md-typeset details.tip, +.md-typeset details.hint { + border-color: #ecb32e; +} +.md-typeset .admonition.tip > .admonition-title, +.md-typeset .admonition.hint > .admonition-title, +.md-typeset .admonition.success > .admonition-title, +.md-typeset details.tip > summary, +.md-typeset details.hint > summary { + background: rgba(236, 179, 46, 0.12); + color: #ecb32e; +} + +/* warning / danger / failure → pink (#e01d5a) */ +.md-typeset .admonition.warning, +.md-typeset .admonition.danger, +.md-typeset .admonition.failure, +.md-typeset .admonition.bug, +.md-typeset details.warning, +.md-typeset details.danger { + border-color: #e01d5a; +} +.md-typeset .admonition.warning > .admonition-title, +.md-typeset .admonition.danger > .admonition-title, +.md-typeset .admonition.failure > .admonition-title, +.md-typeset .admonition.bug > .admonition-title, +.md-typeset details.warning > summary, +.md-typeset details.danger > summary { + background: rgba(224, 29, 90, 0.12); + color: #e01d5a; +} + +/* ─── Tables ───────────────────────────────────────────────── */ +[data-md-color-scheme="slate"] .md-typeset table:not([class]) { + background: #242930; +} + +[data-md-color-scheme="slate"] .md-typeset table:not([class]) th { + background: #2D3340; + color: #ecb32e; + border-bottom: 2px solid #ecb32e; +} + +[data-md-color-scheme="slate"] .md-typeset table:not([class]) td { + border-color: rgba(255, 255, 255, 0.07); +} + +.md-typeset table:not([class]) tr:hover { + background: rgba(55, 196, 240, 0.05); +} + +[data-md-color-scheme="default"] .md-typeset table:not([class]) th { + background: #1C2027; + color: #ecb32e; + border-bottom: 2px solid #ecb32e; +} + +/* ─── TOC ──────────────────────────────────────────────────── */ +.md-nav--secondary .md-nav__link--active { + color: #37c4f0 !important; +} + +[data-md-color-scheme="slate"] .md-nav--secondary .md-nav__link { + color: rgba(232, 237, 245, 0.60); +} + +/* ─── Footer ───────────────────────────────────────────────── */ +/* Three-color brand stripe on footer top edge */ +.md-footer { + border-top: 2px solid; + border-image: linear-gradient(90deg, #37c4f0 33%, #ecb32e 33% 66%, #e01d5a 66%) 1; +} + +[data-md-color-scheme="default"] .md-footer { + background: #1C2027; +} + +[data-md-color-scheme="default"] .md-footer-meta { + background: #13171C; +} + +/* ─── Back-to-top ──────────────────────────────────────────── */ +.md-top { + background: #242930; + color: #E8EDF5; + border-radius: 8px; + border: 1px solid rgba(55, 196, 240, 0.3); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + transition: background 0.2s, border-color 0.2s, box-shadow 0.2s, color 0.2s; +} + +.md-top:hover { + background: #37c4f0; + color: #13171C; + border-color: transparent; + box-shadow: 0 4px 24px rgba(55, 196, 240, 0.4); +} diff --git a/mkdocs.yml b/mkdocs.yml index 64e3664..ac13b5b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 site_name: Node Wire +site_description: Enterprise Connectors for AI & Workflow Systems site_url: https://aot-technologies.github.io/node-wire/ repo_url: https://github.com/AOT-Technologies/node-wire repo_name: AOT-Technologies/node-wire @@ -11,18 +12,89 @@ docs_dir: docs theme: name: material palette: - scheme: default - primary: indigo - accent: indigo + - scheme: slate + primary: custom + accent: custom + toggle: + icon: material/weather-sunny + name: Switch to light mode + - scheme: default + primary: custom + accent: custom + toggle: + icon: material/weather-night + name: Switch to dark mode + font: + text: Inter + code: JetBrains Mono features: - navigation.tabs + - navigation.tabs.sticky - navigation.sections - navigation.expand - - toc.integrate + - navigation.path + - navigation.top + - navigation.footer + - toc.follow - search.highlight + - search.suggest + - search.share + - content.code.copy + - content.code.annotate + - content.tabs.link + - header.autohide + icon: + repo: fontawesome/brands/github + logo: material/lan-connect + +extra_css: + - stylesheets/extra.css + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/AOT-Technologies/node-wire + name: Node Wire on GitHub + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/company/aot-technologies + name: AOT Technologies on LinkedIn + generator: false plugins: - - search + - search: + separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' + +markdown_extensions: + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - toc: + permalink: true + permalink_title: Anchor link to this section + toc_depth: 3 + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.keys + - pymdownx.mark + - pymdownx.tasklist: + custom_checkbox: true nav: - Home: index.md From a4de4dd9d804b5bf4fe5ac4049fa5d517afe89c5 Mon Sep 17 00:00:00 2001 From: vinaayakh-aot <61819385+vinaayakh-aot@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:22:50 -0700 Subject: [PATCH 6/6] Fix for reuse lint --- docs/stylesheets/extra.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 9af3213..306a4cd 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,3 +1,6 @@ +/* SPDX-FileCopyrightText: 2026 AOT Technologies + SPDX-License-Identifier: Apache-2.0 */ + /* Node Wire — Brand Stylesheet Matched to aot-technologies.com/products/node-wire Background: #1C2027 (dark charcoal grey)