Metadata-Version: 2.4
Name: ndslive-mcp
Version: 0.1.3
Summary: Local MCP server for searching and exploring the NDS.Live specification
Author: NDS Association
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: mcp,navigation,nds,ndslive,zserio
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: keyring>=24
Requires-Dist: mcp>=1.2
Requires-Dist: platformdirs>=4
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# ndslive-mcp

A locally-installable MCP server giving agents — Claude Code, IDE assistants, scripts — structured search and lookup over the NDS.Live specification.

## What it gives you

Once installed and authenticated, your MCP host gains nine tools:

| Tool | What it does |
|------|--------------|
| `search_spec`     | Full-text search over symbol names, qnames, doc comments. Filter by kind / module / version. |
| `search_docs`     | Full-text search over the bundled `documentation.nds.live` and `best-practices.nds.live` markdown. |
| `get_type`        | Resolve a fully-qualified name → kind, module, version, source file, line, doc, fields. |
| `find_references` | Every place a type is referenced, by field name and source location. |
| `list_modules`    | Modules in the bundle, optionally filtered by category (common / feature / attribute / service / reference). |
| `get_module`      | Module metadata: category, deps, top-level types. |
| `get_module_versions` | Every version of a module the bundle has indexed. |
| `compare_versions` | Diff between two versions of the same module: added / removed / changed types. |
| `update_index`    | Force a refresh against Artifactory. Live-swap; no server restart. |

## Install

```bash
pipx install ndslive-mcp     # public PyPI; no NDS gate on the code itself
ndslive-mcp install          # guided setup: verify your Artifactory PAT, save it, pre-fetch the bundle
```

Then register the server with your MCP host. **You don't run the server yourself** — the host launches `ndslive-mcp` on demand over stdio (and kills it when it's done). For Claude Code:

```bash
claude mcp add ndslive -- ndslive-mcp
```

Other hosts (Codex, Gemini, IDE assistants): configure a stdio MCP server whose command is `ndslive-mcp` (no arguments).

`ndslive-mcp install` is a wizard around the lower-level commands, which you can also run individually: `ndslive-mcp auth` (save/verify PAT) and `ndslive-mcp update` (fetch or refresh the bundle).

## How auth works

The Python package is public on PyPI — anyone can install. The **bundle** (the actual NDS.Live spec content: SQLite index, raw schemas, docs) is gated behind NDS Artifactory PAT auth. On first run, the server tries to download the bundle; if no PAT is saved it logs a warning and refuses to answer queries until you run `ndslive-mcp auth`.

PATs are stored in the OS keyring (macOS Keychain / Linux Secret Service / Windows Credential Locker). They never live in plaintext on disk.

For headless / CI usage:

```bash
NDS_ARTIFACTORY_USER=u NDS_ARTIFACTORY_PAT=p ndslive-mcp serve
```

These env vars take precedence over the keyring.

## How updates work

```
   server start ──► HEAD ndslive-mcp.json on Artifactory  (~100 ms)
                              │
                       ┌──────┴──────┐
                  same version    newer version
                       │              │
                       │              ▼
                       │      GET bundle.zip
                       │      verify sha256
                       │      extract → ~/.cache/ndslive-mcp/versions/<v>/
                       │      atomic-swap `current` symlink
                       │              │
                       └──────┬───────┘
                              ▼
                   open index.sqlite (read-only)
```

- **Atomic**: a half-downloaded bundle never becomes the live one — the symlink only flips after sha256 verification.
- **Rollback-friendly**: previous version dirs stay on disk.
- **No background polling**: check on startup and on explicit `update_index` tool call only.

Manual refresh:
```python
update_index(force=true)
```

## How it's built

The bundle is built off-band by CI and published to Artifactory:

```
spec sources ──► zserio.jar + indexer-extension ──► symbols.jsonl
                                                      │
                                  + nds.live.compatibility/*.yaml (categories)
                                  + documentation.nds.live  + best-practices  (markdown)
                                                      │
                                                      ▼
                                                build_index.py
                                                      │
                                                      ▼
                                              index.sqlite (FTS5)
                                                      │
                                                      ▼
                                ndslive-mcp.zip + ndslive-mcp.json
                                                      │
                                                      ▼
                            NDS Artifactory — same folder as the spec zip (gated)
```

Java only runs at build time. Clients are pure Python — no JRE required.

See [`docs/architecture.md`](docs/architecture.md) for the full design and [`docs/jsonl-schema.md`](docs/jsonl-schema.md) for the JSONL contract.

## Build and test locally

### Prerequisites

- **Python 3.10+** — for the server itself, the test suite, and the index build step.
- **Java 11+** — only needed if you want to build a fresh bundle end-to-end (the indexer extension runs the zserio compiler). The installed server does not need a JRE.
- **A checkout of [`nds-live-indexer-extension`](https://github.com/ndsev/nds-live-indexer-extension)** alongside this repo, if you want to rebuild the indexer.
- **The zserio compiler jar** via `pip install zserio==2.18.1` (the prod spec zip does not bundle it). The jar lands at `…/site-packages/zserio/compiler/zserio.jar`.
- **A spec bundle zip** for the indexer to consume (e.g. `ndslive.zip` from the NDS compatibility-build pipeline; unpacks to `ndslive/` with `all.zs` at its root).

### Install and run tests

```bash
pip install -e '.[dev]'
pytest                          # hermetic — no Java, no network, no Artifactory
ruff check .
```

The test suite uses synthesized JSONL fixtures and `httpx.MockTransport` so it has no external dependencies. CI runs the same commands across Python 3.10 / 3.11 / 3.12.

### Run the server against an existing bundle

If you already have a `ndslive-mcp.zip` on disk (e.g. from CI or a colleague), point the cache at it and serve in `--offline` mode so it skips the startup Artifactory check:

```bash
# Extract the bundle into a versioned cache dir
mkdir -p ~/.cache/ndslive-mcp/versions/local
unzip -q <path-to-bundle>.zip -d ~/.cache/ndslive-mcp/versions/local/

# Point `current` at it
ln -sfn ~/.cache/ndslive-mcp/versions/local ~/.cache/ndslive-mcp/current

# Serve — no auth required, no network call
ndslive-mcp serve --offline
```

You can iterate on tool definitions in `src/ndslive_mcp/tools/`, restart, and the new code picks up the existing index.

### Build a bundle end-to-end

Useful for testing the full pipeline before pushing. Requires Java 11+ and a built indexer JAR.

```bash
# 0. Get the zserio compiler jar (shared by the indexer build and the index run)
pip install zserio==2.18.1
ZSERIO_JAR=$(python -c 'import zserio, os; print(os.path.join(os.path.dirname(zserio.__file__), "compiler", "zserio.jar"))')

# 1. Build the indexer JAR once (or after extension changes)
cd ../nds-live-indexer-extension
mkdir -p libs && cp "$ZSERIO_JAR" "libs/zserio-2.18.1.jar"   # satisfies compileOnly fileTree('libs')
gradle shadowJar

# 2. Build a bundle in this repo using a local spec zip
cd ../ndslive-mcp
ZSERIO_JAR=$ZSERIO_JAR \
INDEXER_JAR=../nds-live-indexer-extension/build/libs/nds-live-indexer-extension-*-all.jar \
LOCAL_SPEC_ZIP=../_ext/ndslive.zip \
SKIP_DOCS=1 \
bash scripts/build_bundle.sh
# → .work/ndslive-mcp.zip
# → .work/ndslive-mcp.json
```

Then run `ndslive-mcp serve --offline` against the produced bundle (see previous section).

Env-var overrides that short-circuit external fetches for offline iteration:

| Variable | Effect |
|----------|--------|
| `LOCAL_SPEC_ZIP` | Use a local spec zip instead of fetching from Artifactory. |
| `SKIP_DOCS` | Skip cloning `documentation.nds.live` + `best-practices.nds.live` + `nds.live.compatibility` (faster, but no doc FTS and no module categories). |
| `BUNDLE_VERSION` | Override the version string in `ndslive-mcp.json`; defaults to today UTC. |
| `WORK` | Work directory; defaults to `./.work`. |

Without these overrides, `build_bundle.sh` needs `NDS_ARTIFACTORY_USER` / `NDS_ARTIFACTORY_PAT` to download the spec zip from the NDS compatibility-build pipeline.

### Smoke-check what landed in the bundle

The SQLite index inside the bundle is queryable directly. A quick sanity check after a build:

```bash
python -c "
from pathlib import Path
from ndslive_mcp.store import Store
s = Store(Path('.work/out/index.sqlite'))
print('modules:', len(s.list_modules()))
print('lane versions:', s.get_module_versions('lane'))
print('sample search:', [r.qname for r in s.search('LaneGroup', limit=3)])
"
```

## Deploy

CI runs `scripts/deploy_bundle.sh` automatically as the final step of the release workflow. To deploy by hand (emergency push, or to test the deploy path without merging to main):

```bash
# 1. Build the bundle, embedding the production publish URL
NDS_BUNDLE_PUBLISH_URL=https://artifactory.nds-association.org/artifactory/<repo>/<path> \
INDEXER_JAR=../nds-live-indexer-extension/build/libs/nds-live-indexer-extension-*-all.jar \
NDS_ARTIFACTORY_USER=$USER \
NDS_ARTIFACTORY_PAT=$ART_PAT \
bash scripts/build_bundle.sh

# 2. Dry-run to confirm the upload destinations
DRY_RUN=1 bash scripts/deploy_bundle.sh

# 3. Real upload — bundle first, then ndslive-mcp.json (order matters: keeps clients consistent)
NDS_ARTIFACTORY_USER=$USER NDS_ARTIFACTORY_PAT=$ART_PAT \
NDS_BUNDLE_PUBLISH_URL=https://artifactory.nds-association.org/artifactory/<repo>/<path> \
bash scripts/deploy_bundle.sh
```

The deploy script refuses to run if `ndslive-mcp.json`'s embedded `url` doesn't match `NDS_BUNDLE_PUBLISH_URL` — that mismatch means the build was done with a stale URL and clients would 404.

## License

The `ndslive-mcp` software is licensed under [BSD-3-Clause](LICENSE) — the same as the [`ndslive-setup`](https://pypi.org/project/ndslive-setup/) installer. The license covers this software (the "hull") only. The NDS.Live specification content delivered as the bundle is **NDS Protected Material**, not part of this package, and remains gated behind NDS Artifactory authentication and governed by your NDS Member Agreement or NDS.Live Evaluation License.
