norma-mcp-init
v0.5.0
Published
Single source-of-truth registry of MCP clients powering every Norma by Quality Clouds one-click install surface (Phase 1b, QSA-1278).
Maintainers
Readme
norma-mcp-init
Norma MCP one-click installer — Phase 1b (epic QSA-1277). Cursor directory listing prep: docs/cursor-directory-listing.md (QSA-1459).
Install
One command works everywhere — no account needed beforehand, you sign in on first use:
npx norma-mcp-initClaude Code — claude mcp add --transport http norma https://api.qualityclouds.ai/mcp
Claude Desktop / Lovable / v0 / claude.ai — paste the server URL in the app's connector settings: https://api.qualityclouds.ai/mcp
👉 Full per-client guide (all buttons, one-liners and manual snippets): INSTALL.md
This package is the home of the single client registry that powers every install surface (CLI, deep links, install page, docs). It is the source of truth so those surfaces can never contradict each other (functional spec §6, §40 / technical decision D1).
Scope of QSA-1278: the registry, its JSON Schema, a typed loader and CI validation. The CLI itself (detection, safe writer, reporter, probe) lands in QSA-1279/1280/1281; the release pipeline and npm publishing in QSA-1282; the generated install page and docs in QSA-1283.
The registry
- Data:
src/registry/clients.json - Schema:
src/registry/clients.schema.json(JSON Schema draft 2020-12) - Types + loader:
src/registry/index.ts
One entry per client from the functional §1 table: Cursor, VS Code, Claude Code, Claude Desktop, Windsurf (local) and the web builders Lovable, v0, claude.ai. Each entry carries: id, display name, config file locations per OS (macOS/Windows/Linux), config entry templates (OAuth and API-key variants), deep-link / one-liner / connector-URL templates where supported, and minimum version notes.
Server identity & placeholders
The server is declared once under server (name, entryKey, url).
Templates never hardcode the URL — they use render-time tokens so a single edit
propagates to every surface:
| Token | Meaning |
|---|---|
| {{SERVER_URL}} | The MCP server URL (server.url) — the only thing written to a config |
| {{SERVER_NAME}} / {{SERVER_NAME_URLENCODED}} | Server display name |
| {{SERVER_ENTRY_KEY}} | Stable key our entry is upserted under (idempotency) |
| {{CONFIG_BASE64}} / {{CONFIG_URLENCODED}} | Encoded OAuth entry JSON for deep links |
| {{API_KEY}} | User-supplied key — only ever rendered into the API-key variant, never into an OAuth template or a deep link |
No credentials by construction: OAuth surfaces (entry template, deep link, connector URL, one-liner) contain only the server name and URL. This is enforced in CI (§7, §38).
server.urlhere is production (https://api.qualityclouds.ai/mcp) — every public surface (README, INSTALL.md, install page, deep links, defaultnpx) resolves to prod. Staging and development are published only under npm dist-tags and are not advertised here; developers reach them withnpx norma-mcp-init@staging/@dev. The endpoint is baked per environment at publish time — see docs/RELEASING.md. Note: the VS Code entry carries a staticoauth.clientId(VS Code has no Dynamic Client Registration); the same id is used in all environments, so that Keycloak client must be registered in each realm — it is registered in the dev, staging and production realms.
Consuming the registry
import { clientRegistry, localClients, configLocations } from 'norma-mcp-init';
clientRegistry.server.url; // "https://api.qualityclouds.ai/mcp"
localClients().map((c) => c.id); // ["cursor","vscode","claude-code","claude-desktop","windsurf"]
configLocations('cursor', 'macos'); // ["~/.cursor/mcp.json"]The build-time docs/landing pipeline reads the raw JSON directly:
import clients from 'norma-mcp-init/clients.json'.
Development
npm install
npm run validate # schema + semantic validation (CI gate — see below)
npm run typecheck
npm test # vitest — covers the four acceptance-criteria scenarios
npm run build # tsc -> dist/ + copies the registry JSON alongsideCI gate
npm run validate fails with a non-zero exit code if the registry violates the
schema or the business rules (missing client, missing OS location, leaked
credential). It runs in the GitHub Actions release workflow (and locally via
prepublishOnly); the acceptance criterion "Invalid registry blocks release"
is satisfied by this exit code.
Adding or changing a client
Edit src/registry/clients.json (and, only if a client needs a genuinely new
config shape, add a writer variant later in the CLI). Run npm run validate.
Because npx always fetches the latest published version, the fix reaches users
with zero action on their side (§18, §34).
CLI — client detection (QSA-1279)
npx norma-mcp-init # detect installed clients, multi-select (detected pre-checked)
npx norma-mcp-init --client cursor # target a client by name (repeatable), no prompt
npx norma-mcp-init --workspace # write project-local config in the current folder
npx norma-mcp-init --api-key <key> # CI/headless: write the API-key entry variant
npx norma-mcp-init --remove # clean uninstall: remove only our entry
npx norma-mcp-init --helpScope. By default the CLI writes the per-user config (global — the server
is available in every window of that client). --workspace instead writes a
project-local file in the current folder, for clients that support it:
| Client | Workspace file |
|---|---|
| Cursor | .cursor/mcp.json |
| VS Code | .vscode/mcp.json |
| Claude Code | .mcp.json |
Commit that file to share the server with everyone who opens the project. Clients
without a project scope (Windsurf, Claude Desktop) are skipped under --workspace.
Detection is registry-driven and conservative (src/detection): a client is
detected when its config file or parent directory exists for the current OS.
Unresolvable locations (e.g. a missing Windows env var) are reported as not
detected — never guessed. When no client is detected the CLI prints the full
per-client manual instructions and the install page URL. --client exits
non-zero if a named client's config location cannot be determined.
Selected clients are configured by the safe writer (src/config): a surgical,
idempotent OAuth-mode upsert via jsonc-parser (server URL only — no credentials),
with a single rotating .qc-backup, abort-on-malformed, and per-client isolation
so one failure never aborts the run. A post-install probe checks reachability
(401 + WWW-Authenticate = success) and the CLI prints a per-client report with
the "sign in on first use" closing message.
--api-key writes the API-key entry variant embedding the user-supplied key
(the only credential the CLI ever handles — it never creates keys). --remove
deletes only our entry (desired-state: "nothing to remove" when absent), reusing
the same backup/surgical/isolation guarantees. Both are covered by a round-trip
test (§37): init → init → remove restores the prior state.
Install page & docs generation (QSA-1283)
Every published surface is generated from the registry at build time (src/surfaces,
decision D1/D4) — hand-written per-client content is forbidden, because drift is the
bug this phase kills:
npm run generate # writes dist/surfaces/index.html and dist/surfaces/install.mdThe landing/docs pipeline consumes the exported generators directly:
import { generateInstallPage, generateDocsMarkdown, deepLinkFor, badgeMarkdownFor } from 'norma-mcp-init';- Deep links (D3): static strings —
cursor://…?config=<base64 {url}>andvscode:mcp/install?<url-encoded {name,type,url}>— identical for every user, carrying only the server name and URL (no secrets, enforced by tests). - Install page: one static "Get started in your IDE" page; client-side JS detects the OS to order options but never hides any — every path is always present, and each deep link is accompanied inline by the CLI command and the manual snippet (§9).
- Docs/README parity:
generateDocsMarkdownemits the same per-client content (badges, one-liners, connector URLs, manual snippets) from the one source (§6).
Publishing to npm (multi-environment)
Distribution is publication to the public npm registry; npx fetches and runs
the package on the fly. One package, three targets selected by npm dist-tag —
the endpoint is baked into each published artifact at build time. Full runbook:
docs/RELEASING.md.
| Command | dist-tag | endpoint | git branch | version |
|---|---|---|---|---|
| npx norma-mcp-init | latest | api.qualityclouds.ai/mcp | main | X.Y.Z |
| npx norma-mcp-init@staging | staging | api-staging.qualityclouds.ai/mcp | develop | X.Y.Z-staging.N |
| npx norma-mcp-init@dev | dev | api-development.qualityclouds.ai/mcp | develop | X.Y.Z-dev.N |
Prerelease versions (-staging/-dev) are never resolved by a plain npx, so the
non-prod targets stay invisible to the default install. The OAuth clientId is the
same across all environments (see the note above).
⚠️ A published version is globally available within seconds and cannot be overwritten (only deprecated).
scripts/release.mjsrefuses to publish an environment from the wrong branch and computes the next prerelease index.
Ways to publish
- Manual (local, needs
npm whoami):npm run release:prod|release:staging|release:dev. Each runsprepublishOnly(build && validate && test), bakes the environment, and publishes under the right tag. - GitHub Actions:
.github/workflows/release.yml— push tomainpublishes prod/latest;developpublishes@stagingand@dev; needs anNPM_TOKENrepo secret.
Safe local verification (never publishes)
npm run release:staging -- --dry-run # full flow, no upload
npm pack --dry-run # list exactly what would be uploaded