Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions use-crystallize/mcp-servers/crystallize/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ src/
│ ├── analytics-tracker.ts # AnalyticsTracker / AnalyticsEvent / AnalyticsRequestContext types
│ ├── app-context.ts # AppContext type (Bindings + Variables)
│ ├── auth-context-resolver.ts # AuthContextResolver type (resolve client credentials)
│ ├── core-domain-selector.ts # CoreDomainSelector / SelectedDomain types (intent → domains)
│ ├── core-schema-domain-splitter.ts # Core schema domain-splitting types
│ ├── graphql-query-corrector.ts # Query correction types (CorrectionResult, CorrectionLog)
│ ├── graphql-schema-compacter.ts # Schema compaction types (GraphqlSchemaCompacter, options)
│ ├── jev.ts # Jev request/response types + JevClient
│ ├── mass-operation-runner.ts # MassOperationRunner types (task + status)
│ ├── mutation-executor.ts # MutationExecutor type (execute-once, no retry)
│ ├── query-executor.ts # Query execution types (QueryExecutor, options, result)
Expand All @@ -48,7 +50,7 @@ src/
│ │ ├── build-mass-operation.ts # Validate a mass-operation file (read-only)
│ │ ├── fetch-catalog-graphql-schema.ts # Fetch compacted Catalogue API schema
│ │ ├── fetch-content-model.ts # Fetch tenant shapes/content model
│ │ ├── fetch-core-graphql-schema.ts # Fetch compacted Core API schema (by domain)
│ │ ├── fetch-core-graphql-schema.ts # Fetch compacted Core API schema (by domain or intent)
│ │ ├── fetch-discovery-graphql-schema.ts # Fetch compacted Discovery API schema
│ │ ├── fetch-shop-cart-graphql-schema.ts # Fetch compacted Shop Cart API schema
│ │ ├── get-mass-operation-status.ts # Poll a mass-operation bulk task (read-only)
Expand All @@ -65,9 +67,11 @@ src/
│ └── services/
│ ├── auth-context-helpers.ts # Resolve client credentials from auth (token/session/bearer)
│ ├── compact-schema-builder.ts # GraphQL schema compaction (introspection → compact text)
│ ├── core-domain-selector.ts # Pick the Core schema domains an intent needs (Jev Nouls)
│ ├── core-schema-domain-splitter.ts # Split the Core schema into queryable domains
│ ├── execute-mutation.ts # Execute-once mutation executor (no correction, no retry)
│ ├── graphql-query-corrector.ts # Auto-correct malformed GraphQL queries (Levenshtein)
│ ├── jev-client.ts # Run TypeSafe's Jev (typesafe/jev) on the Workers AI binding
│ ├── mass-operation-runner.ts # Upload + create + start bulk tasks; read task status
│ ├── query-with-correction.ts # Execute queries with auto-correction on failure
│ └── tenant-matcher.ts # Match tenant by id/identifier from auth context
Expand Down Expand Up @@ -111,6 +115,10 @@ The app uses **Awilix** for dependency injection. The container is built once (s
- `tenantMatcher` — resolve tenant from auth context
- `graphqlSchemaCompacter` — compact introspection schemas
- `coreSchemaDomainSplitter` — split the Core schema into queryable domains
- `ai` — the Workers AI binding (`env.AI`), held by the cached container because bindings are isolate-stable
- `jevClient` — runs TypeSafe's Jev (`typesafe/jev`) on Workers AI
- `coreDomainSelector` — picks the Core schema domains an `intent` needs (one Jev Noul per domain; the tool falls
back to the domain index when it fails or picks nothing)
- `graphqlQueryCorrector` — fix malformed GraphQL queries
- `queryExecutor` — execute queries with auto-correction
- `mutationExecutor` — execute mutations exactly once (no correction, no retry)
Expand Down Expand Up @@ -192,7 +200,7 @@ Goals panel has fewer `tool:` rows than that list, the difference is what to cre
| `fetch-content-model` | `fetchContentModelToolWrapper` | Fetch tenant shapes / content model |
| `fetch-catalog-graphql-schema` | `fetchCatalogGraphqlSchemaToolWrapper` | Get compacted Catalogue schema |
| `fetch-discovery-graphql-schema` | `fetchDiscoveryGraphqlSchemaToolWrapper` | Get compacted Discovery schema |
| `fetch-core-graphql-schema` | `fetchCoreGraphqlSchemaToolWrapper` | Get compacted Core schema (by domain) |
| `fetch-core-graphql-schema` | `fetchCoreGraphqlSchemaToolWrapper` | Compacted Core schema (domain or intent) |
| `fetch-shop-cart-graphql-schema` | `fetchShopCartGraphqlSchemaToolWrapper` | Get compacted Shop Cart schema |
| `build-mass-operation` | `buildMassOperationToolWrapper` | Validate a mass-operation file |
| `get-mass-operation-status` | `getMassOperationStatusToolWrapper` | Poll a mass-operation bulk task |
Expand Down Expand Up @@ -409,8 +417,9 @@ retroactive, so create that goal before shipping.
- Visits, visit duration, bounce rate and entry/exit pages are **meaningless** for this traffic (they derive from a
rolling 30-minute window over that coarse hash). That is precisely why session config lives in the path.
- `analyticsTracker` is registered **`.scoped()`**, and `defer` / `analyticsRequestContext` are registered on the
request scope. `buildContainer` caches one container per isolate and ignores `env`, so anything request-derived
registered as a singleton would freeze at the first request.
request scope. `buildContainer` caches one container per isolate (built from the first request's `env`), so
anything request-derived registered as a singleton would freeze at the first request. Bindings like `AI` are
isolate-stable, which is why the container may hold them.
- Localhost **and `*.workers.dev`** are skipped. `PLAUSIBLE_DOMAIN` sits in top-level `vars`, so it binds to every
deploy including preview URLs; without that guard a preview would file its traffic as production.
- `PLAUSIBLE_API_ENDPOINT` drives **both** sides: the server-side POST target, and the landing page's `data-api`
Expand Down Expand Up @@ -460,6 +469,8 @@ Use `c.set()` / `c.get()` in middleware/handlers. Extend `Variables` when adding
- **Deployment**: Wrangler CLI
- **Smart placement** enabled in wrangler config
- Bindings type generated via `bun cf-typegen` → `worker-configuration.d.ts`
- **Bindings**: `AI` (Workers AI; used for `typesafe/jev`). Local dev calls the remote model, so `wrangler login` is
needed for intent selection; without it `fetch-core-graphql-schema` falls back to the domain index.
- **Vars** (plain `vars` in `wrangler.jsonc`, none are secrets): `PLAUSIBLE_DOMAIN` (site ID for server-side MCP
events; unset to disable them), `PLAUSIBLE_SCRIPT_URL` (the landing page's `pa-XXXX.js`; unset to drop the
script) and `PLAUSIBLE_API_ENDPOINT` (override for a self-hosted Plausible or a first-party proxy — it feeds
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{ "intent": "list the last 10 orders", "expected": ["order"] },
{ "intent": "create an order for an existing customer", "expected": ["registerOrder", "customer"] },
{ "intent": "find a customer by email and show their subscriptions", "expected": ["customer", "subscriptionContract"] },
{ "intent": "pause a customer's subscription until next month", "expected": ["pauseSubscriptionContract"] },
{ "intent": "add a price list with 20% off for B2B customers", "expected": ["priceList"] },
{ "intent": "move an order to the shipped stage of its pipeline", "expected": ["orderPipelineStage"] },
{ "intent": "set up a webhook that fires when an order is created", "expected": ["webhook"] },
{ "intent": "invite a new colleague to the tenant", "expected": ["inviteToken"] },
{ "intent": "give an existing user the admin role", "expected": ["userRole"] },
{ "intent": "set the stock of a product variant to 12", "expected": ["modifyProductVariantStock"] },
{ "intent": "cancel a customer's subscription", "expected": ["subscriptionContract"] },
{ "intent": "install a plugin on the tenant", "expected": ["pluginInstallation"] }
]
101 changes: 101 additions & 0 deletions use-crystallize/mcp-servers/crystallize/bin/eval-domain-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { readFileSync } from "node:fs";
import type { JevClient } from "../src/contracts/jev";
import { createJevClient, type JevBinding } from "../src/core/services/jev-client";
import { MAX_DOMAINS, pickDomains, scoreDomains } from "../src/core/services/core-domain-selector";
import { createCoreSchemaDomainSplitter } from "../src/core/services/core-schema-domain-splitter";
import { fetchIntrospection } from "../src/core/services/compact-schema-builder";
import { buildAtApiUrl } from "../src/core/security";

const env = (name: string) => {
const value = process.env[name];
if (!value) {
console.error(`Missing env var ${name}`);
process.exit(1);
}
return value;
};

// The account is pinned in wrangler.jsonc (not a secret); CLOUDFLARE_ACCOUNT_ID overrides it.
const pinnedAccountId = readFileSync(new URL("../wrangler.jsonc", import.meta.url), "utf-8").match(
/"account_id"\s*:\s*"([0-9a-f]+)"/,
)?.[1];
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID || pinnedAccountId || env("CLOUDFLARE_ACCOUNT_ID");
// Defaults to the current `wrangler login`, so a logged-in developer needs no token at all.
const wranglerToken = () => {
const out = Bun.spawnSync(["bunx", "wrangler", "auth", "token", "--json"], { stderr: "ignore" });
try {
return (JSON.parse(out.stdout.toString()) as { token?: string }).token;
} catch {
return undefined;
}
};
const apiToken = process.env.CLOUDFLARE_API_TOKEN || wranglerToken() || env("CLOUDFLARE_API_TOKEN");

// A stand-in for the AI binding over the REST API, so the eval runs outside a Worker but still goes
// through the production client (envelope unwrapping, validation). REST's `result` is what the binding returns.
const restBinding: JevBinding = {
async run(model, input) {
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run`, {
method: "POST",
headers: { Authorization: `Bearer ${apiToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ model, input }),
});
const json = (await response.json()) as { success: boolean; result: unknown; errors?: unknown };
if (!json.success) throw new Error(`Workers AI error: ${JSON.stringify(json.errors)}`);
return json.result;
},
};
const productionClient = createJevClient({ ai: restBinding }, 30_000);
const usage = { inputTokens: 0, calls: 0, ms: 0 };
const jevClient: JevClient = async (request) => {
const started = Date.now();
const response = await productionClient(request);
usage.ms += Date.now() - started;
usage.calls++;
usage.inputTokens += response.usage?.input_tokens ?? 0;
return response;
};

const url = buildAtApiUrl("https://api.crystallize.com", process.env.CRYSTALLIZE_TENANT || "furnitut", "");
const introspection = await fetchIntrospection(url, {
"X-Crystallize-Access-Token-Id": env("CRYSTALLIZE_TOKEN_ID"),
"X-Crystallize-Access-Token-Secret": env("CRYSTALLIZE_TOKEN_SECRET"),
});
const index = createCoreSchemaDomainSplitter().listDomains(introspection);
const names = new Set(index.domains.map((d) => d.name));
console.log(`${index.domains.length} domains: ${[...names].join(", ")}\n`);

const cases = JSON.parse(readFileSync(new URL("./eval-domain-selection.cases.json", import.meta.url), "utf-8")) as {
intent: string;
expected: string[];
}[];

const THRESHOLDS = [0.2, 0.3, 0.5];
const totals = new Map(THRESHOLDS.map((t) => [t, { fullRecall: 0, returned: 0 }]));

for (const c of cases) {
const missingFromIndex = c.expected.filter((e) => !names.has(e));
if (missingFromIndex.length > 0) console.warn(`! case expects unknown domains: ${missingFromIndex.join(", ")}`);

const scored = await scoreDomains(jevClient, index, c.intent);
const top = [...scored].sort((a, b) => b.probability - a.probability).slice(0, 6);
console.log(`- ${c.intent}\n expected: ${c.expected.join(", ")}`);
console.log(` top: ${top.map((d) => `${d.name} ${d.probability.toFixed(2)}`).join(", ")}`);

for (const t of THRESHOLDS) {
const picked = pickDomains(scored, t, MAX_DOMAINS).map((d) => d.name);
const totalsForT = totals.get(t)!;
if (c.expected.every((e) => picked.includes(e))) totalsForT.fullRecall++;
totalsForT.returned += picked.length;
}
}

console.log("\nthreshold full-recall avg-domains-returned");
for (const [t, { fullRecall, returned }] of totals) {
console.log(`${t.toFixed(2)} ${fullRecall}/${cases.length} ${(returned / cases.length).toFixed(2)}`);
}

console.log(
`\nper call: ${Math.round(usage.inputTokens / usage.calls)} input tokens, ` +
`${Math.round(usage.ms / usage.calls)}ms average latency (REST, from this machine)`,
);
Loading
Loading