ProjexCloud · Architecture · v3.1 · P9 Companion

SDK Discoverability & AI-Driven Vertical App Builder

How a tenant auto-discovers ProjexCloud's ~70 SDK catalog and composes a working vertical application — either locally through Claude Code / Cursor / Windsurf in the CLI, or remotely through the hosted Cloud Builder agent. Companion architectural narrative for P9-SDK-Discoverability-AI-Builder.md.
Phase · P9 (W50–W62) · P9.1 follow-up W64
Wave · W7 productization
Gates · G13 SDK Composability · G14 AI-Native Builder
Status · Architecture Draft · Pending WG Review
Source PRD · prd/P9-SDK-Discoverability-AI-Builder.md
What this document does — Translates the P9 PRD into the architectural picture an implementing engineer needs: how the four layers (capability manifests → registry → MCP → builders) compose, how an AI client on a developer's machine discovers ProjexCloud SDKs with zero prior knowledge, how the hosted/local MCP split balances latency against authority, and how a non-developer's natural-language prompt is funneled into the same substrate. Diagrams are normative; prose explains intent.
v3.2 revision (2026-06-03) — Adds §14: the registry (L2) evolves from a build-time file artifact into an auto-populated Postgres + pgvector RAG store; the /build planner moves from a single "stuff all ~90 SDKs into one prompt" call to a retrieve-then-compose pipeline with a foundation-tier + dependency-closure resolver (so identity/AIM always surfaces); manifests gain per-endpoint payload schemas and an ingest capability dimension for ETL agents. Retrieval embeddings stay on the local bge-small model, so a generation-LLM provider change never breaks discovery. §1–§13 describe the original P9 design; §14 is the P9.2 delta that supersedes the file-dump path.

1 · TL;DR

ProjexCloud's ~70 cross-cutting SDKs (identity, tenancy, billing, audit, AI gateway, evidence chain, geo, dispatch, …) are useless if a tenant developer has to grep source to find them. P9 fixes that by turning the SDK catalog into a machine-readable, AI-consumable platform. The architecture is four cooperating layers:

Layer 1
Capability Manifests
Every SDK ships sdk-capability.json describing what it provides, consumes, and when to use it. CI rejects publishes without one.
Layer 2
Registry & Embedding Index
Build-time scanner unifies manifests into a catalog + ANN vector index. Powers semantic searchByIntent() across the SDK universe.
Layer 3
MCP Server (Split)
Hosted half handles tenant-scoped reads & all writes; local half ships in the CLI for offline reads + write proxy. Same wire protocol both sides.
Layer 4
Two Builder Entrypoints
CLI path — projex init wires Claude Code / Cursor / Windsurf to the local MCP. Cloud path — /build chat surface inside the tenant workspace, driven by the same MCP tools.
The one-sentence model: manifests make SDKs describable, the registry makes them findable, MCP makes them callable by AI, and the builders make them composable into a running tenant app — all without the developer ever cloning the monorepo.

2 · Why auto-discovery, why now

After P1–P8 the SDK stack is broad enough that the bottleneck to ProjexCloud's growth is no longer features — it is composability. A tenant who signs up today gets an empty workspace; the only way to actually build something is to clone the monorepo and learn ~70 packages. That doesn't scale to thousands of tenants.

The MCP ecosystem (Anthropic, Cursor, Windsurf, Cline) has converged on a stable protocol over the last 6 months. Building on it now means every AI coding tool, present and future, becomes a ProjexCloud entry point with no per-tool plugin. Delay means: every onboarding burns operator hours, the AI-native window closes as competitors ship their own MCP catalogs, and the P8 vertical packs (Healthcare, FinServ, Public Sector) have no surface to materialize on.

Before P9
Read-the-source discovery
Tenant dev clones monorepo · greps for an SDK that "does billing" · reads 3 READMEs · pastes example code · prays it compiles. Onboarding: 2–6 weeks.
After P9
Prompt-to-app discovery
Tenant dev runs one command · their AI tool already sees every SDK · types "I need consent receipts for GDPR" · AI calls projex_registry_search_sdks · scaffolds working code. Onboarding: minutes.

3 · The four-layer architecture

flowchart TB subgraph DEV["Developer's machine"] direction TB AI["AI Coding Tool
(Claude Code · Cursor · Windsurf · Cline)"] LOCAL["registry-mcp-local
(stdio · CLI-bundled)"] CACHE[("~/.projex/cache
catalog.json + embeddings.bin")] CLI["@projexlight/cli
projex init/install/deploy"] AI -- "MCP stdio
tool calls" --> LOCAL LOCAL -- "reads" --> CACHE CLI -- "writes config" --> AI CLI -- "spawns" --> LOCAL end subgraph TENANT["Tenant Workspace · Browser"] BUILDER["/build chat UI
(apps/cloud-builder)"] AGENT["sdk-agent-runtime
+ sdk-ai-gateway"] BUILDER --> AGENT end subgraph HOSTED["ProjexCloud · Per-Region"] GW["api-gateway"] HMCP["registry-mcp (HOSTED)
SSE transport"] REG[("dist/registry.catalog.json
+ embeddings.bin · S3")] BP[("blueprints/
YAML + templates")] AUDIT["sdk-audit · chain"] METER["sdk-meter"] POLICY["sdk-policy
(pack guardrails)"] SCAFFOLD["scaffold + deploy
orchestrator"] GW --> HMCP HMCP --> REG HMCP --> BP HMCP --> AUDIT HMCP --> METER HMCP --> POLICY HMCP --> SCAFFOLD end subgraph POOLS["Tenant Pools"] APP[("Tenant App Pool")] EVID[("Evidence Pool")] SCAFFOLD --> APP SCAFFOLD --> EVID end LOCAL -- "daily ETag pull
read tools (cache miss)" --> GW LOCAL -- "write proxy: scaffold/deploy" --> GW AGENT -- "MCP/SSE" --> HMCP CLI -- "deploy upload" --> GW classDef dev fill:#1a2234,stroke:#6aa9ff,color:#e6ecf5; classDef ten fill:#1a2234,stroke:#b88dff,color:#e6ecf5; classDef host fill:#1a2234,stroke:#5dd39e,color:#e6ecf5; classDef pool fill:#1a2234,stroke:#c9a86a,color:#e6ecf5; class AI,LOCAL,CACHE,CLI dev; class BUILDER,AGENT ten; class GW,HMCP,REG,BP,AUDIT,METER,POLICY,SCAFFOLD host; class APP,EVID pool;
Figure 3.1 — End-to-end topology. The local MCP and hosted MCP speak the same wire protocol; the split is about where authority lives, not what the AI sees.
1
Layer 1 · Self-describing SDKs

Capability Manifests

Every SDK ships an sdk-capability.json. Without one, CI refuses to publish it. The manifest is the contract — it tells the registry, MCP, agent, and downstream tooling exactly what the SDK provides, consumes, when to use it, and what compliance posture it carries.

2
Layer 2 · Aggregation & semantic index

Registry & Embedding Index

A build-time scanner walks packages/* and services/*, validates every manifest, and emits a single normalized catalog plus an ANN vector index over the hand-written scenarios. This is what makes "search by intent" possible.

3
Layer 3 · The discovery wire

MCP Server (Hosted + Local split)

Two binaries, one wire protocol. The hosted half (per-region, behind api-gateway) owns tenant scope and writes. The local half (CLI-bundled, stdio) caches the public catalog and proxies writes. AI tools see one tool list; the split is invisible to them.

4
Layer 4 · The two builder personas

CLI Builder (developer) · Cloud Builder (non-developer)

CLI path: projex init auto-detects installed AI tools and writes their MCP configs. The developer prompts their AI tool; the AI calls registry tools and writes code. Cloud path: the tenant workspace exposes a chat surface at /build; a hosted agent calls the same MCP tools server-side and returns a working URL.

4 · Layer 1 — Capability Manifests

The sdk-capability.json file is the foundation of the whole stack. If a manifest doesn't exist, the SDK doesn't exist to the AI. If a manifest is wrong, the AI suggests it for the wrong things. The schema is versioned (schema_version: "1.0"); future bumps are backward-compatible or carry a migration.

4.1 — Manifest anatomy

{
  "schema_version": "1.0",
  "name": "sdk-consent",
  "version": "1.4.2",
  "summary": "GDPR/CCPA consent receipts with cryptographic proof, double opt-in flows, and revocation propagation.",
  "tags": ["privacy", "gdpr", "ccpa", "consent", "compliance"],
  "provides": {
    "endpoints": ["POST /v1/consent", "GET /v1/consent/:subject"],
    "events":    ["consent.granted.v1", "consent.revoked.v1"],
    "models":    ["ConsentReceipt", "ConsentScope"],
    "hooks":     ["beforeConsentGrant", "afterConsentRevoke"],
    "ui_components": ["<ConsentBanner/>", "<ConsentPreferences/>"]
  },
  "consumes": {
    "events":      ["identity.subject.created.v1"],
    "infra":       ["sdk-audit", "sdk-vault"],
    "config_keys": ["CONSENT_DEFAULT_TTL_DAYS"]
  },
  "scenarios": [
    {
      "id": "gdpr-receipt",
      "title": "Issue a GDPR-compliant consent receipt",
      "when_to_use": "Customer opts in to marketing email; you must store proof.",
      "example_code": "await consent.grant({ subject, scope: 'marketing.email' });",
      "expected_outcome": "Hash-anchored receipt; event emitted; revocation honored within 30 days."
    }
    /* … 2–4 more scenarios … */
  ],
  "compliance_posture": {
    "regimes": ["GDPR", "CCPA", "SOC2"],
    "notes":   "Receipts hash-anchored into sdk-audit chain."
  },
  "pool_placement": "app",
  "pricing_skus":   [{ "sku": "consent.receipt", "mode": "metered", "unit_description": "per receipt issued" }],
  "links": {
    "readme":      "packages/sdk-consent/README.md",
    "source":      "packages/sdk-consent",
    "prd_section": "P3#consent"
  }
}
Why scenarios matter most. The scenarios[] block is the only part of the manifest that gets embedded into the vector index. summary and tags help keyword search; scenarios are what make "I need consent receipts for GDPR" land on sdk-consent with score ≥ 0.7. Quality bar in CI: ≥ 3 scenarios, no TBD placeholders, compliance posture filled.

4.2 — Authoring economics

Hand-writing 70 manifests was the single biggest risk (R-1 in the PRD). It is mitigated by splitting the work:

Auto-generated
Boilerplate (≈60% of the file)
npx @projexlight/sdk-capability scaffold introspects package.json, route registrations, events.ts, and DB migrations to emit the provides / consumes / links blocks deterministically.
Hand-authored
Prose (≈40% of the file)
Summary, scenarios, compliance notes, when-to-use guidance. ~half-day per SDK owner. First 5 manifests pair-authored with platform team to set quality bar; CI lints reject TBD placeholders.

4.3 — CI gate (the no-exceptions rule)

# .github/workflows/registry-validate.yml (excerpt)
- name: Validate every manifest
  run: pnpm registry:validate
  # Fails the build with structured errors:
  #   MANIFEST_MISSING  packages/sdk-foo          (no sdk-capability.json)
  #   MANIFEST_INVALID  packages/sdk-bar/v1.0.0   (summary > 500 chars)
  #   SCENARIO_TBD      packages/sdk-baz#sc-2     (example_code contains "TBD")

This gate codifies Architecture Doctrine §C — Capability-First SDK Authoring: every new SDK ships its manifest at v1.0. No exceptions.

5 · Layer 2 — Registry & Embedding Index

The registry is two artifacts produced by one build job:

ArtifactFormatPurposeRefresh
dist/registry.catalog.json Normalized JSON Every manifest + derived dependency graph (who-consumes-whose-events). Drives list(), get(), findCompatibleSdks(). On any manifest change · RPO ≤ 1h via S3
dist/registry.embeddings.bin ANN vector index (HNSW) Vectors keyed by (sdk_name, scenario_id). Powers searchByIntent(query, top_k). Deterministic per content hash · CI-cached

5.1 — Why bge-small-en-v1.5

Open question Q-1 (locked 2026-05-25) chose bge-small-en-v1.5 loaded via @huggingface/transformers as INT8-quantized ONNX. Three properties matter:

5.2 — Public API surface

// @projexlight/sdk-registry
export interface Registry {
  list(): SdkCapabilityManifest[];
  get(sdk_name: string): SdkCapabilityManifest | null;
  searchByIntent(query: string, top_k?: number): Promise<RegistryHit[]>;
  findCompatibleSdks(sdk_name: string): string[];
  getScaffold(sdk_names: string[], app_name: string): ScaffoldTree;
}
export function loadRegistry(catalogPath?: string): Promise<Registry>;
Deterministic builds. AC-2 requires that running pnpm registry:build twice on identical inputs produces byte-identical artifacts. This rules out non-deterministic float math in the embedding pipeline (INT8 quantization is required, not optional) and lets CI cache the index across PRs.

6 · Layer 3 — MCP Server (Hosted + Local Split)

The MCP layer is where the architecture's most consequential design decision lives: split into two cooperating binaries that share one wire protocol.

The split rationale. Read tools are ~95% of MCP traffic. AI coding tools prefer local stdio MCPs for latency, trust, and offline tolerance. But writes — scaffolding into a tenant's pool, deploying, listing tenant-scoped SDKs — cannot be trusted to the client. They need server-side identity, audit, billing, and pack guardrails. Forcing one binary to do both jobs means doing both poorly. Splitting cleanly avoids that.

6.1 — Side-by-side

registry-mcp HOSTEDregistry-mcp-local LOCAL
LivesPer-region, behind api-gatewayDeveloper's machine (Docker or npx)
TransportSSE over HTTPS (Q-2 decision)stdio (the MCP default)
Catalog sourceS3 (canonical)~/.projex/cache/ (ETag-refreshed daily)
Authoritative fortenant-scoped reads + ALL writespublic reads only
Authtenant API key → SixLayer JWTsame key, stored in OS keychain
Emits audit eventsyes (every tool call)no (writes are proxied)
Distributioninternal service binaryprojexcloud/registry-mcp-local (Docker) · @projexlight/registry-mcp-local (npx)

6.2 — How the same wire protocol works both ways

sequenceDiagram autonumber participant AI as "AI Tool (Claude Code)" participant L as "registry-mcp-local (stdio)" participant C as "Local cache" participant GW as "api-gateway" participant H as "registry-mcp hosted (SSE)" participant A as "sdk-audit" participant P as "Tenant Pool" Note over AI,L: READ path — answered locally, zero network AI->>L: tools/call search_sdks (query=consent receipts GDPR) L->>C: load embeddings and catalog L-->>AI: top_k hits (offline OK) Note over AI,H: WRITE path — proxied to hosted, authority server-side AI->>L: tools/call scaffold (sdks=[sdk-consent, sdk-identity], app=my-app) L->>GW: POST /mcp/v1/sse with x-projex-api-key GW->>H: forward H->>H: validate API key, mint SixLayer JWT H->>H: sdk-policy pack guardrails check H->>P: write scaffold tree H->>A: emit registry.tool.invoked.v1 H-->>GW: stream scaffold_id and file list GW-->>L: SSE stream L-->>AI: same response shape as a local call
Figure 6.1 — One AI client view, two execution paths. The AI never sees the split: read and write responses use the same MCP envelope.

6.3 — Tool catalog (what AI tools see)

Read tools · cache-answerable
Browse + discover
projex_registry_search_sdks(query)
projex_registry_get_manifest(sdk_name)
projex_registry_get_example(sdk_name, scenario_id)
projex_registry_list_compatible_sdks(sdk_name)
projex_registry_list_blueprints()
projex_registry_get_blueprint(blueprint_id)
Write tools · hosted-only
Mutate + deploy
projex_registry_scaffold(sdks[], app_name, target_dir?)
projex_registry_deploy(scaffold_id, env)
projex_registry_list_my_sdks() tenant-scoped
projex_registry_list_my_blueprints() pack-filtered
projex_registry_request_pack_upgrade(pack_id)

6.4 — Offline behavior

When the hosted side is unreachable, the local MCP gracefully degrades per FR-MCP-L5:

AC-12 enforces this in chaos tests: network down → all reads succeed → write queues → reconnect → queue drains within 30 s with byte-identical results to an online write.

7 · Layer 4a — The CLI / IDE path (Claude Code, Cursor, Windsurf)

This is the path most tenant developers will take. It is built around zero-configuration auto-discovery: the developer never reads the SDK list, never edits MCP configs by hand, and never has to know which region their tenant lives in.

7.1 — The first-run flow

sequenceDiagram autonumber participant U as Tenant Developer participant CLI as @projexlight/cli participant KC as OS Keychain participant IDE as Claude Code / Cursor / Windsurf participant L as registry-mcp-local participant H as registry-mcp (hosted) U->>CLI: npx @projexlight/cli init my-app --blueprint revops-crm CLI->>U: open browser for OAuth device flow U->>CLI: authorize (tenant API key minted) CLI->>KC: store refresh token CLI->>CLI: detect installed AI tools
(~/.claude · ~/.cursor · ~/.windsurf) CLI->>IDE: write .claude/mcp.json, .cursor/mcp.json, .windsurf/mcp.json
(only configs needed) CLI->>L: pull catalog + embeddings (first-time) CLI->>U: scaffold revops-crm blueprint locally U->>IDE: open editor in my-app/ IDE->>L: list_tools() -- discovers projex_registry_* Note over U,L: AI now sees every SDK · no README reading needed U->>IDE: "Add a lead routing rule that sends EU leads
to our EU team queue" IDE->>L: projex_registry_search_sdks("lead routing") L-->>IDE: top 3: sdk-lead-routing, sdk-engagement, sdk-crm IDE->>L: projex_registry_get_manifest("sdk-lead-routing") IDE->>L: projex_registry_get_example("sdk-lead-routing", "geo-route") IDE->>U: writes src/leads/route.ts (working code) U->>CLI: projex deploy --env staging CLI->>H: upload + trigger migrations H-->>U: https://my-app.tenant42.projexcloud.com
Figure 7.1 — From npx init to deployed URL. The developer types two commands and one prompt; everything else is automatic.

7.2 — What projex init actually writes

// .claude/mcp.json — auto-generated by `projex init`
{
  "mcpServers": {
    "projex_registry": {
      "command": "npx",
      "args": ["-y", "@projexlight/registry-mcp-local"],
      "env": {
        "PROJEX_HOSTED_MCP": "https://mcp.us-east.projexcloud.com",
        "PROJEX_TENANT_ID":  "tenant_01HXY...",
        "PROJEX_KEYCHAIN_REF": "projex.tenant_01HXY"
      }
    }
    // If Projexlight is detected on this machine, projex_dev_mcp and
    // projex_test_mcp blocks are appended here too (see §10 Cohabitation).
  }
}

7.3 — The full CLI surface

CommandWhat it doesTalks to
projex loginOAuth device flow · stores refresh token in OS keychainIdentity SDK
projex init <app> [--blueprint]Skeleton repo + AI tool MCP configs + optional blueprint scaffoldLocal FS · Registry
projex install <sdk>Adds SDK to package.json · drops starter snippetLocal MCP (read)
projex blueprint list | apply <id>List blueprints or run installer (clarifying Qs → templates → migrations → seed → smoke tests)Hosted MCP (write)
projex deploy [--env]Package · upload · migrate · roll back on failure (AC-10)Hosted MCP + Tenant pool
projex logs [--tail]Stream deployed-app logsTenant pool
projex registry refreshForce-pull latest catalog into local cacheHosted (ETag GET)
projex registry drainReplay queued offline writesHosted
Why npx-only for v1 (Q-6 decision). Most P9 users already have Node 20+. Non-Node users get a one-liner installer that puts Node on the box and aliases projexnpx @projexlight/cli; Node becomes invisible. Native binaries (signed, auto-updating) deferred to P10.

8 · Layer 4b — The Cloud Builder path (non-developers)

The Cloud Builder is the same substrate aimed at a different persona: a non-developer in a tenant who wants a working application without filing an IT ticket. It lives at /build inside the tenant workspace.

8.1 — Anatomy of a build session

sequenceDiagram autonumber participant U as Non-Dev User participant UI as /build chat UI participant AG as sdk-agent-runtime participant GW as sdk-ai-gateway participant H as registry-mcp (hosted) participant P as sdk-policy participant M as sdk-meter participant POOL as Tenant App Pool participant A as sdk-audit U->>UI: "I need a claims intake workflow with photo evidence" UI->>AG: prompt + tenant context AG->>GW: completion req (with MCP tools attached) GW->>H: projex_registry_search_sdks + list_blueprints H-->>GW: top blueprints: claims-intake, field-dispatch AG->>U: "claims-intake matches best — 2 quick questions:" U->>AG: settle in-app · photos in S3 · approver = ops lead AG->>H: projex_registry_get_blueprint("claims-intake") AG->>U: "Here's what I'll install. Deploy?" U->>AG: Deploy AG->>P: pack guardrail check (Healthcare? HIPAA fields?) P-->>AG: OK AG->>M: meter builder.session AG->>H: projex_registry_scaffold(...) + deploy(env=trial) H->>POOL: write app · run migrations · seed H->>A: registry.tool.invoked.v1 (chain) H-->>AG: scaffold_id + URL AG->>U: https://claims-intake-7f3.tenant42.projexcloud.com Note over U,A: Full conversation reconstructable
from audit chain · AC-7
Figure 8.1 — Prompt-to-URL in five minutes (AC-5 target). Same MCP tools the CLI uses; the difference is the agent driving them.

8.2 — The guardrail layer (where safety actually lives)

The agent does not enforce safety. The agent is treated as untrusted prompt-pipe; real enforcement happens server-side in two places:

Pack guardrails
sdk-policy decisions
Healthcare pack refuses unencrypted PHI columns. FinServ pack refuses uncertified data egress. PublicSector pack refuses non-FedRAMP infra. Decisions cite the offending rule and return HTTP 403 from the MCP — the agent gets a refusal, the user gets a clear explanation. AC-8 validates this in CI.
Confirm-by-default (Q-7)
Human-in-loop deployment
Default: agent generates a preview · user clicks Deploy. Opt-in autonomous mode lets the tenant admin set cloud_builder.autonomous = true with a per-session $ ceiling (default $10). Destructive operations always require confirm — autonomous mode never overrides this.

8.3 — Cost shaping (Q-4 decision)

TierIncluded builds / monthOverageNotes
Trial3Hard cap — block + upgrade promptOne-hour iteration window per session — refinements don't double-charge.
Pro20$2/build OR LLM token pass-through, lower winsSame iteration rule.
EnterpriseUnlimitedSubject to abuse SLO.

9 · The auto-discovery mechanism — how it actually works

This is the part the user most cares about. Auto-discovery isn't one mechanism — it is a chain of four discrete handshakes that compose into the experience of "the AI just knew what was available." Each handshake is independently observable and independently testable.

flowchart LR D1["Handshake 1

CLI detects
installed AI tools"]:::hs D2["Handshake 2

CLI writes
per-tool MCP config"]:::hs D3["Handshake 3

AI tool launches
local MCP & lists tools"]:::hs D4["Handshake 4

AI tool queries
by intent → registry"]:::hs D1 --> D2 --> D3 --> D4 classDef hs fill:#1a2234,stroke:#c9a86a,color:#e6ecf5,padding:14px;
Figure 9.1 — Four handshakes that compose into "auto-discovery." Each is visible in CLI/MCP logs and is independently chaos-testable.

9.1 — Handshake 1: CLI detects installed AI tools

When projex init runs, the CLI looks for marker files in the user's home directory:

// @projexlight/cli — detection logic (sketch)
const DETECTORS = {
  "claude-code": () => existsSync(path.join(os.homedir(), ".claude")),
  "cursor":      () => existsSync(path.join(os.homedir(), ".cursor")),
  "windsurf":    () => existsSync(path.join(os.homedir(), ".windsurf")),
  "cline":       () => existsSync(path.join(os.homedir(), ".cline")),
  // Projexlight cohabitation marker — see §10
  "projexlight": () => existsSync(path.join(os.homedir(), ".projexlight", "config.json")),
};
const installed = Object.entries(DETECTORS).filter(([_, fn]) => fn()).map(([k]) => k);

The CLI writes configs only for the tools actually installed. Order matters: detection is per-machine, but every AI client ends up seeing the same MCP tool catalog.

9.2 — Handshake 2: CLI writes per-tool MCP config

Each AI tool has its own config path; the CLI knows them all and writes the matching block. Critical property: the CLI is the only sanctioned writer of these files. If a user hand-edits, the next projex init warns rather than overwrites. This avoids the "two MCPs in Claude Code config confuse non-technical users" failure mode (R-11).

AI ToolConfig pathFormat
Claude Code~/.claude/mcp.json + ./.claude/mcp.jsonJSON · mcpServers
Cursor~/.cursor/mcp.jsonJSON · mcpServers
Windsurf~/.codeium/windsurf/mcp_config.jsonJSON · servers
Cline~/.cline/mcp_settings.jsonJSON · mcpServers

9.3 — Handshake 3: AI tool launches local MCP and lists tools

This handshake is standard MCP, no ProjexCloud customization. When the IDE starts, it spawns npx @projexlight/registry-mcp-local as a stdio child process, exchanges the MCP initialize handshake, then calls tools/list. The local MCP responds with the tool catalog defined in §6.3 — read tools backed by the local cache, write tools that proxy to the hosted side.

// MCP initialize handshake (excerpt of stdio JSON-RPC)
> { "jsonrpc": "2.0", "id": 1, "method": "initialize",
>   "params": { "protocolVersion": "2025-03-26",
>               "capabilities": { "tools": {} },
>               "clientInfo": { "name": "claude-code", "version": "1.x" } } }
< { "jsonrpc": "2.0", "id": 1, "result":
<   { "protocolVersion": "2025-03-26",
<     "serverInfo": { "name": "projex-registry-local", "version": "1.0.0" },
<     "capabilities": { "tools": { "listChanged": true } } } }

> { "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
< { "jsonrpc": "2.0", "id": 2, "result":
<   { "tools": [
<     { "name": "projex_registry_search_sdks",      "description": "...", "inputSchema": { ... } },
<     { "name": "projex_registry_get_manifest",     "description": "...", "inputSchema": { ... } },
<     { "name": "projex_registry_scaffold",         "description": "...", "inputSchema": { ... } },
<     ...
<   ] } }

9.4 — Handshake 4: AI tool queries by intent → registry returns relevant SDKs

This is the handshake users feel. When the developer types "I need consent receipts for GDPR", the AI tool decides (based on the tool descriptions in §9.3) to call projex_registry_search_sdks(query: "consent receipts for GDPR"). The local MCP:

  1. Embeds the query with bge-small-en-v1.5 in-process (~5 ms).
  2. Searches the HNSW index over (sdk_name, scenario_id) vectors (~2–10 ms).
  3. Returns top-K hits with score, scenario excerpt, and a "why this matched" snippet.
// Sample response (top 3 of K=5)
{
  "hits": [
    { "sdk_name": "sdk-consent", "scenario_id": "gdpr-receipt", "score": 0.91,
      "summary": "GDPR/CCPA consent receipts with cryptographic proof...",
      "why_match": "scenario 'gdpr-receipt' matches 'consent receipts for GDPR'" },
    { "sdk_name": "sdk-privacy",  "scenario_id": "dsar-flow",     "score": 0.74, "...": "..." },
    { "sdk_name": "sdk-audit",    "scenario_id": "evidence-anchor","score": 0.68, "...": "..." }
  ]
}

AC-3 enforces this works: a 50-query test suite must hit precision@3 ≥ 0.9 with the target SDK in the top 3. Quarterly catalog-quality audits widen the suite to 200 queries and trigger manifest rewrite PRs when precision regresses.

The closed loop. If precision drops, the fix is almost always in the manifest, not the model. Better scenarios → better embeddings → better discovery. The platform team owns this feedback loop via the quarterly audit (Q-5 decision).

10 · End-to-end build flows

10.1 — Building a vertical app from scratch (CLI path)

flowchart TB S1["1. Tenant admin signs up
workspace + API key issued"] --> S2 S2["2. Tenant dev runs
projex login + projex init my-app --blueprint revops-crm"] --> S3 S3["3. CLI detects Claude Code/Cursor
writes MCP configs · pulls catalog"] --> S4 S4["4. CLI runs blueprint installer:
clarifying Qs → templates → migrations → seed"] --> S5 S5["5. Dev opens IDE · AI tool now sees
~70 ProjexCloud SDKs via local MCP"] --> S6 S6["6. Dev prompts: 'add EU-lead routing rule'
AI calls search_sdks → get_manifest → writes code"] --> S7 S7["7. Dev runs projex deploy --env trial
hosted MCP scaffolds, migrates, returns URL"] --> S8 S8["8. Working app at
my-app.tenant42.projexcloud.com
audit chain captures every step"] classDef step fill:#1a2234,stroke:#6aa9ff,color:#e6ecf5; class S1,S2,S3,S4,S5,S6,S7,S8 step;
Figure 10.1 — Eight steps, two human commands. AC-4 requires this end-to-end in ≤ 45 min for a developer who has never read a ProjexCloud README.

10.2 — Building a vertical app from a prompt (Cloud path)

flowchart TB C1["1. Non-dev opens /build
in tenant workspace"] --> C2 C2["2. Types: 'I need claims intake
with photo evidence'"] --> C3 C3["3. Agent calls hosted MCP:
search_sdks + list_blueprints"] --> C4 C4["4. Agent presents top 2-3 blueprints
asks blueprint's clarifying questions"] --> C5 C5["5. User answers → agent shows preview
(files, tables, billing impact)"] --> C6 C6["6. User clicks Deploy
(or auto-deploy if autonomous mode + under $ ceiling)"] --> C7 C7["7. sdk-policy guardrail check
(pack-aware: HIPAA/FinServ/PubSec)"] --> C8 C8["8. Hosted MCP scaffold + deploy
sdk-meter charges builder.session"] --> C9 C9["9. Working URL returned
in ≤ 5 minutes (AC-5)"] classDef step fill:#1a2234,stroke:#b88dff,color:#e6ecf5; class C1,C2,C3,C4,C5,C6,C7,C8,C9 step;
Figure 10.2 — Same substrate, different driver. The agent is a thin shim over the MCP tools; safety lives in sdk-policy, not in the prompt.

10.3 — The unified picture

Both paths end up in the same place: a scaffolded application running in the tenant's app pool, with every step in the audit chain, every tool call metered, and every guardrail enforced server-side. The difference is the orchestrator (developer-with-IDE vs. agent-with-prompt), not the substrate.

11 · Cohabitation with existing Projexlight MCPs

ProjexCloud's new registry-mcp-local is a peer, not a replacement, of the existing Projexlight MCPs at projex_mcp/projex_dev_mcp and projex_mcp/projex_test_mcp. They expose disjoint tool catalogs and zero-collision names.

MCPTool prefixCatalogLivesAuth
projex_dev_mcp projexlight_* Projexlight PRD/feature/scenario/task ops · LLM review · autofix · embedding Docker, local Projexlight project API key
projex_test_mcp projexlight_* UI test recorder · API functional-test runner Docker, local Projexlight project API key
registry-mcp-local NEW projex_registry_* ProjexCloud SDK catalog: search · manifest · scaffold · deploy Docker or npx, local ProjexCloud tenant API key
registry-mcp NEW projex_registry_* Same wire surface · tenant-scoped + write-authoritative Hosted, per-region ProjexCloud tenant API key

11.1 — One config, all MCPs

When projex init detects ~/.projexlight/config.json, it writes a single .claude/mcp.json (and equivalents) that registers both Projexlight MCPs and the new registry-mcp-local. The AI client sees all three in one tool list and routes calls by name.

// .claude/mcp.json — Projexlight + ProjexCloud cohabiting
{
  "mcpServers": {
    "projex_registry": {
      "command": "npx", "args": ["-y", "@projexlight/registry-mcp-local"],
      "env": { "PROJEX_HOSTED_MCP": "https://mcp.us-east.projexcloud.com" }
    },
    "projex_dev": {
      "command": "docker", "args": ["run", "--rm", "-i", "projexlight/projex_dev_mcp:latest"]
    },
    "projex_test": {
      "command": "docker", "args": ["run", "--rm", "-i", "projexlight/projex_test_mcp:latest"]
    }
  }
}

AC-13 enforces this works: load all three in one Claude Code session, invoke projex_registry_search_sdks and projexlight_create_feature in the same conversation, both succeed.

11.2 — Projexlight-on-ProjexCloud — the dogfood proof

The prd-management blueprint (FR-DF-1..5) rebuilds Projexlight's PRD/feature/ scenario/task surface as a vertical app composed from ProjexCloud SDKs. Projexlight retains its proprietary AI-review IP (as a private SDK), but every cross-cutting concern (multi-tenancy, identity, billing, audit, webhooks, approvals, RAG search, AI gateway) is delegated to ProjexCloud SDKs.

Why this matters architecturally. Projexlight becomes the worked example for every customer building a vertical app: "here is a real product that ships its own domain MCP alongside its app, runs on ProjexCloud's substrate, and still keeps its differentiating AI IP private." AC-11 (deferred to P9.1) is the verification that this is real, not just a roadmap claim.

12 · Security, guardrails, compliance

Identity
Tenant API key → SixLayer JWT
CLI stores refresh token in OS keychain. Hosted MCP validates the API key per request and mints a short-lived SixLayer JWT for downstream calls. No long-lived bearer tokens in transit.
Transport
TLS 1.3 + SSE session tokens
SSE channel auth uses a per-key short-lived session token (rotated hourly). Stdio local MCP never exposes a network surface.
Audit
Every tool call chained
registry.tool.invoked.v1 with full args + outcome. AC-7 requires that an operator can reconstruct an entire build session from this stream. Cloud builder transcripts retained 30 days.
Pack guardrails
sdk-policy server-side decisions
Healthcare / FinServ / PublicSector packs each carry sdk-policy rules. Violation = HTTP 403 before any scaffold/deploy returns. AC-8 fixture: PHI bound to unencrypted column must be refused with citation.
Rate & cost limits
sdk-meter soft + hard caps
100 RPM/key default · 1000 RPS aggregate · cloud builder $ ceiling per Q-4. Cap exceeded → structured error + upgrade path link.
Deploy safety
Snapshot + transactional rollback
Every projex deploy snapshots the tenant pool before migration; failure auto-rolls-back to that snapshot (AC-10 chaos test). No orphaned tables, no half-written rows.

12.1 — The five non-negotiables

  1. Writes never happen client-side. Local MCP can queue, never commit.
  2. Every write is audited before it succeeds. Audit failure = write failure.
  3. Pack guardrails are policy, not prompt. Agent can't talk its way out.
  4. Confirm-by-default for non-dev users. Autonomous mode is opt-in and $ -capped.
  5. Catalog is single-source-of-truth across hosted + local. Cache mismatches are detected at install time and refuse to scaffold removed SDKs (R-10 mitigation).

13 · Component glossary

ComponentLayerLivesOwns
@projexlight/sdk-capabilityL1npm packageManifest schema · validator · scaffold CLI
sdk-capability.jsonL1Every SDK's package rootThe contract — what an SDK is, in JSON
@projexlight/sdk-registryL2Build-time + runtime npm packageCatalog builder · embedding index · search API
dist/registry.catalog.jsonL2S3 (canonical) + ~/.projex/cache/ (mirror)Normalized manifests + dependency graph
dist/registry.embeddings.binL2S3 + local cacheHNSW vector index over scenarios
services/registry-mcpL3Per-region hosted serviceTenant-scoped reads + ALL writes · SSE
@projexlight/registry-mcp-localL3Developer machine · stdioCached reads · write proxy · offline mode
blueprints/L3Build-time YAML + templatesDeclarative SDK compositions
@projexlight/cliL4 (CLI)Developer machineprojex init/install/blueprint/deploy/logs
apps/cloud-builderL4 (Cloud)Tenant App Pool/build chat surface · agent driver
sdk-agent-runtimeP6A depTenant poolDrives cloud builder agent loop
sdk-ai-gatewayP6A depPer-regionLLM call abstraction + per-tenant token caps
sdk-policyP3 depPer-regionPack guardrail decisions (HIPAA/FinServ/PubSec)
sdk-auditP1 depEvidence poolHash-anchored event chain
sdk-meterP4 depPer-regionSKU metering · soft+hard caps

13.1 — Key NFRs at a glance

DimensionTarget
Latency · search_sdks p99300 ms cold · 80 ms warm
Latency · blueprint apply (5-SDK)≤ 60 s end-to-end
Latency · projex deploy (≤50 files)≤ 90 s p99
Throughput · MCP tool calls1000 RPS aggregate · 100 RPM/key
Availability · hosted registry MCP99.9% monthly
Durability · registry catalogRPO ≤ 1h · rebuildable from source ≤ 10 min
Time-to-URL (cloud builder, non-dev)≤ 5 min (AC-5)
Time-to-working-app (CLI, dev)≤ 45 min (AC-4, blueprint + 1 prompt)

13.2 — Related companion docs

14 · v3.2 (P9.2) — RAG Catalog Store, Payload Contracts & Planner v2

The original L2 registry (§5) is a build-time file artifact: a normalized registry.catalog.json plus an embeddings.bin, scanned once and shipped. The first tenant-facing planner at /build took the cheapest possible shortcut on top of it — it loaded all ~90 manifests, trimmed them, and pasted the entire catalog into one LLM prompt, then trusted the model to pick. Three problems surfaced in production:

Problem 1 · Recall
"Always 3 SDKs"
No retrieval, no ranking, no minimum. A small model facing 90 flat options returns the 3 most obvious and stops. Foundation concerns the user never typed — identity, AIM, tenancy — silently drop into "you'll need to build."
Problem 2 · Cost
~18k tokens / plan
The whole catalog ships on every request. Token spend scales with the SDK count, not with the query — and grows every time a new SDK is published.
Problem 3 · Fragility
Provider-coupled
Discovery quality rode on whichever frontier model was wired in. A provider deprecation or model swap silently changed which SDKs surfaced.
The v3.2 principle — separate the two model jobs. Retrieval (which SDK/endpoint is relevant) runs entirely on the local, in-process bge-small-en-v1.5 model and is stored as vector(384) rows in Postgres — it is the stable contract and never calls an external API. Generation (compose the prose plan, explain trade-offs) is a swappable LLM that only ever sees the already-retrieved top-K. Swap the generation provider any day; retrieval is untouched. This is what makes discovery durable.

14.1 — L2 evolves into a Postgres + pgvector RAG store

The catalog stops being only a file and becomes an auto-populated, auto-refreshed table in the global-catalog pool (SDK manifests are identical for every tenant, so this is global state — not per-tenant rag.* corpus). The existing migration-runner auto-creates the schema on boot (no manual SQL), and a sync job reuses the sdk-registry scanner to keep it fresh.

flowchart TB subgraph BUILD["Deploy / boot time — auto-populate & keep fresh"] MAN["packages/*/sdk-capability.json
+ payload schemas (NEW)
+ endpoint.kind (NEW)"] SYNC["catalog-sync job
(reuses sdk-registry scanner)"] MIG["migration-runner
auto-creates catalog.* on boot"] EMB["bge-small-en-v1.5
(local ONNX, in-process)"] MAN -- "content-hash diff:
only changed SDKs" --> SYNC MIG --> DB SYNC -- "upsert rows" --> DB SYNC -- "embed NL cards" --> EMB EMB -- "vector(384)" --> DB end subgraph DB["catalog schema · global-catalog pool"] T1[("catalog.sdk")] T2[("catalog.endpoint
request_schema · kind · auth_scopes")] T3[("catalog.embedding
vector(384) + HNSW")] end classDef b fill:#1a2234,stroke:#5dd39e,color:#e6ecf5; classDef d fill:#1a2234,stroke:#c9a86a,color:#e6ecf5; class MAN,SYNC,MIG,EMB b; class T1,T2,T3 d;
Figure 14.1 — The catalog as a self-refreshing RAG store. The file artifact (§5) remains the offline/CLI source; Postgres becomes the hosted source-of-truth + freshness signal. Embeddings are computed by the local model, never an external API.
-- catalog schema · global-catalog pool · auto-created by migration-runner
CREATE TABLE catalog.sdk (
  name           text PRIMARY KEY,          -- @projexlight/sdk-billing
  summary        text NOT NULL,
  tags           text[] NOT NULL DEFAULT '{}',
  tier           text,                       -- 'foundation' | 'domain'  (drives §14.3 injection)
  content_hash   text NOT NULL,              -- sha256(manifest) -> skip unchanged on resync
  updated_at     timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE catalog.endpoint (
  id             uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  sdk_name       text NOT NULL REFERENCES catalog.sdk(name) ON DELETE CASCADE,
  method         text NOT NULL,
  path           text NOT NULL,
  kind           text NOT NULL DEFAULT 'query',  -- NEW: ingest|bulk|query|mutation|webhook
  request_schema  jsonb,                          -- NEW: JSON Schema of the body
  response_schema jsonb,                          -- NEW
  auth_scopes    text[] DEFAULT '{}',             -- NEW: e.g. {billing:write}
  UNIQUE (sdk_name, method, path)
);
CREATE TABLE catalog.embedding (
  ref_kind   text NOT NULL,                  -- 'sdk' | 'endpoint' | 'scenario' | 'ingest'
  ref_id     text NOT NULL,
  card       text NOT NULL,                  -- the NL text that was embedded
  embedding  vector(384) NOT NULL            -- bge-small space; SEPARATE from the 1536 template
);
CREATE INDEX ON catalog.embedding USING hnsw (embedding vector_cosine_ops);
Hybrid, not "everything in the vector DB." A natural-language card per SDK / endpoint / scenario is embedded for discovery; the exact JSON Schema is fetched relationally by (sdk, path) for correctness. Semantic search finds the endpoint; the relational row hands over the precise payload contract. An agent never reconstructs a body from a fuzzy hit. Reuses the already-built VectorBackend interface in sdk-knowledge-rag via a new PgvectorBackend.

14.2 — Closing the two gaps: payload contracts & the ingest dimension

Gap A · Payloads
request / response schemas
Manifest endpoints were {method, path, description} — an agent saw the endpoint existed but had to guess the body. A build step now emits request_schema/response_schema from each SDK's existing Zod/TS types into the manifest and the catalog.endpoint row. New MCP tool get_endpoint(sdk, path) returns method + path + payload + auth scope.
Gap B · Ingest
endpoint.kind + ETL discovery
Connectors did event-driven sync, never bulk import; nothing tagged "external data lands here." Endpoints now carry kind: ingest|bulk|…; a first-class sdk-ingest exposes POST /api/ingest/:entity/batch (idempotent upsert envelope) backed by sdk-lineage + sdk-audit. New tools search_sdks(kind=ingest) and get_ingest_targets(entity) answer an ETL agent's "where do I push customer records, with what payload?"

14.3 — Planner v2: retrieve-then-compose + foundation-tier + dependency closure

The /build planner is rebuilt as a pipeline. Crucially, semantic retrieval alone is insufficient for cross-cutting concerns: a user who types "a financial accounting system" never says "login," so the embedding of the intent is nowhere near the embedding of sdk-identity. Two deterministic resolvers run between retrieve and compose to guarantee the auth/AIM foundation always surfaces.

flowchart LR Q["intent:
'financial accounting system'"] --> E["embed locally
bge-small ~5ms"] E --> R["pgvector HNSW
top-K domain SDKs (~5ms)"] R --> F["Foundation-tier inject
identity · persona/AIM · tenant · rebac
(tier='foundation', regardless of score)"] F --> D["Dependency-closure expand
walk manifest consumes/events
(billing -> needs identity/tenant)"] D --> C["Generation LLM composes
from K only · ~3-4k tokens
'include every candidate; justify omissions'"] C --> O["Plan:
Recommended SDKs (8-12) +
Custom work (login PAGE, admin PAGE)"] classDef s fill:#1a2234,stroke:#6aa9ff,color:#e6ecf5; classDef g fill:#1a2234,stroke:#b88dff,color:#e6ecf5; class Q,E,R,C,O s; class F,D g;
Figure 14.2 — Retrieve → inject foundation → expand dependencies → compose. The two purple stages are why a plan for "accounting" reliably includes identity/AIM rather than burying it under custom work.
Resolver 1
Foundation-tier injection
Every multi-user app needs the identity baseline. SDKs tagged tier='foundation' (sdk-identity, sdk-persona = AIM, sdk-tenant, sdk-rebac) are merged into the candidate set regardless of semantic score, then deduped. The data already exists — these SDKs already carry the foundation tag.
Resolver 2
Dependency closure
After retrieval picks domain SDKs, walk the manifest consumes/event graph (e.g. tenant.created.v1 is consumed by 10+ SDKs; persona.role.assigned.v1sdk-rebac) to pull prerequisites. Billing in → identity/persona/tenant dragged in with it.
UI honesty. The auth SDKs ship no prebuilt login/admin UI (every ui_components is empty). Planner v2 splits the output: it recommends the auth SDKs (wire to /api/auth/login, /api/personas/resolve, rebac.check) and lists the login page and admin page UI under custom work — so the agent uses the platform's AIM instead of rebuilding identity, badly.

14.4 — The agent hot path: no-latency tiers

An interactive coding agent must never round-trip to Postgres per query. The store is the durable source-of-truth and freshness signal; the hot path is an in-process index.

TierServesLatencyBacked by
Tier 0 · in-process indexEvery search the agent issues (local MCP / hosted MCP keep catalog + 384-dim vectors resident)sub-ms, zero networkcache artifact + in-memory HNSW
Tier 1 · Postgres sourceDurability, multi-instance freshness; MCP instances reload their index on a catalog.sdk version bump / LISTENoff the hot pathpgvector store (§14.1)
Exact fetchget_manifest / get_endpoint — payload schema + auth scopekeyed lookuprelational rows

Vectors for "which," relational for "exact payload," in-memory for "fast," Postgres for "fresh + shared."

14.5 — Build phases (maps to the P9.2 delivery epics)

EpicDeliverableReusesNew
A · RAG Catalog Storecatalog.* schema + pgvector + PgvectorBackend + auto-sync job + shared bge-small embedder migration-runner · sdk-registry scanner · sdk-knowledge-rag VectorBackend schema, sync job, 1 backend class
B · Manifest Contractsendpoint request_schema/ response_schema + kind classifier + sdk-ingest batch endpointsdk-capability schema · sdk-lineage · sdk-audit schema fields, Zod→JSONSchema build step, ingest SDK
C · Planner v2retrieve-then-compose /api/build/plan + foundation-tier & dependency-closure resolver + provider-agnostic generation adapter + /build UI splitthe new store · foundation tags pipeline, 2 resolvers, adapter, UI
D · Agent Discovery SurfaceMCP tools get_endpoint, search(kind=ingest), get_ingest_targets + in-memory hot index reload + ETL agent flowregistry-mcp-local3 tools, reload hook, example
What this preserves. §1–§13 stand: manifests, the four handshakes, hosted/local MCP split, blueprints, CLI, guardrails. v3.2 changes where the catalog lives (file → file + pg store), how the planner reasons (dump → retrieve-then-compose), and what a manifest carries (adds payloads + ingest). Phases A–D are tracked in Projexlight as the P9.2 epics.