Skip to content

v0.2.0 — sandy import: URL/file-driven MCP server configuration

Choose a tag to compare

@techeretic techeretic released this 22 Sep 00:41
· 33 commits to master since this release

The first feature release beyond the 0.1.x line: sandy import — URL- and file-driven MCP server configuration (PRs #47 / #48). Point Sandy at a URL, a file, or stdin that describes an MCP server and it produces a validated, content-hash-pinned, reviewable candidate config — no more hand-copying endpoints, tool names, and auth shapes into mcp-servers.json from remote docs.

Usage

sandy import https://registry.internal/servers/jira.json   # one-shot confirmed fetch → stage
sandy import ./jira.json                                    # air-gap: no network
curl -s <url> | sandy import -                               # host-fetched path
sandy import <src> --tools jira=read_sprints,read_issues --apply   # explicit promote
  • Staged by default — nothing touches live config until --apply (or a manual edit).
  • --apply merges into mcp-servers.json + appends the computed sandbox.allowed_network lines, refuses name collisions, and re-runs the full config load as the final gate (fail-closed — never ships a broken config).

What landed

  • The import pipeline (src/import.ts) — fetch → sniff → validate → stage → (apply), one flow, one trust gate:
    • The fetch is the one bounded, human-confirmed, audited exception to zero-egress. Sandy prints the exact GET <url> and the operator confirms (default: no — a cancelled fetch dials nothing). The dial is bounded (30s timeout, 1MB body cap, 3-redirect cap; non-http(s) refused) and recorded as an import_fetch event (url, finalUrl, sha256, bytes). This is the first change to the egress surface in Sandy's history.
    • The schema is the law. Fetched bytes run through the existing mcpServersManifestSchema (fail-closed, exit 3) — nothing is legal because a URL said so. There is no new validation surface.
    • Content-hash-pinned staging under .sandy-import/<sha256>.json — the URL is a source at time T, not a live channel: a later change to the URL is a different hash, so it produces a fresh reviewable diff, never a silent mutation (the VCS-reviewed-registry model holds). The review package prints the exact allowed_network lines to add, the env-var names (never values), and the per-server allowlist — which is never auto-expanded (confirm it or pass --tools, validated against capabilities).
    • Explicit apply only. --apply is the sole path that touches live config: it requires a loadable config, refuses to overwrite an existing server name, preserves existing entries' order, and re-runs the full loadSandyConfig as the final gate.
    • Air-gap safe. The file/stdin path needs no network at all.
    • v1 is deterministic-only. Prose URLs are rejected fail-closed; the LLM-transcription --auto path is a documented follow-up (the core has no LLM client in v1 — see docs/IMPORT_DESIGN.md).
  • Audit events — import_fetch + import_staged join the forensic record (2 new types in src/audit/logger.ts).
  • CLI — the import verb with --yes / --apply / --tools <s=a,b>; exit codes follow the stable contract (usage-class 2, fail-closed 3).
  • Docs — docs/IMPORT_DESIGN.md (design) + docs/PLAN_IMPORT.md (plan); the consumer guide audited so every "zero egress" claim honestly names the exception (security threat-model note, config guide, troubleshooting, dev layout); plugin.json description qualified.
  • Community health (PR #46) — Contributor Covenant CoC, contributing guide, security policy, issue/PR templates; ASCII diagrams → Mermaid across the docs.

Verification

  • npm run typecheck && npm test && npm run build → 346/346 tests (was 323, +23), green.
  • Conformance green (in-process + the Docker/Firejail boundary × mode matrix + byte-identical-signature identity check): the import dial is a separate, confirmed operator action, not a runtime egress path — the matrix runs sandy run / sandy ask (never sandy import), so the no-egress / cross-sandbox guarantees are unaffected.
  • No new dependencies — the fetch uses global fetch + node:crypto; the install stays at 3 runtime deps.

Security note

The import dial is a documented, bounded, human-confirmed, audited exception — not a new vulnerability surface. The threat-model write-up (why the exception stays narrow, and its accepted residual risk) is in guide/security.md. No new security advisories — a feature release.