stylegallery 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/DOMAINS.md +2 -6
- package/GOVERNANCE.md +10 -9
- package/README.md +29 -127
- package/consumer-reference/agent-native/v2/admission-policy.json +646 -0
- package/consumer-reference/agent-native/v2/material-registry.json +1 -0
- package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +50 -0
- package/consumer-reference/agent-native/v2/schema/material-manifest.schema.json +17 -0
- package/consumer-reference/agent-native/v2/schema/material-record.schema.json +20 -0
- package/consumer-reference/contract.md +1 -1
- package/consumer-reference/index.md +5 -3
- package/layout/index.md +124 -0
- package/log.md +57 -0
- package/package.json +183 -22
- package/scripts/agent-native/v2/experimental-extension-registry.mjs +143 -0
- package/scripts/agent-native/v2/extensions/a2a-projection.mjs +264 -0
- package/scripts/agent-native/v2/extensions/agui-projection.mjs +191 -0
- package/scripts/agent-native/v2/material-admission.mjs +295 -0
- package/scripts/agent-native/v2/material-cli-adapter.mjs +134 -0
- package/scripts/agent-native/v2/material-context.mjs +251 -0
- package/scripts/agent-native/v2/material-identity.mjs +71 -0
- package/scripts/agent-native/v2/material-mcp-adapter.mjs +253 -0
- package/scripts/agent-native/v2/material-operation-registry.mjs +123 -0
- package/scripts/agent-native/v2/material-queries.mjs +398 -0
- package/scripts/agent-native/v2/material-registry.mjs +319 -0
- package/scripts/sg-material-mcp.mjs +98 -0
- package/scripts/sg-material.mjs +27 -0
- package/consumer-reference/adapters/style-dictionary.config.mjs +0 -36
- package/consumer-reference/baselines/calibration.json +0 -260
- package/consumer-reference/baselines/manifest.json +0 -21
- package/consumer-reference/fixtures/consumer-conformance/e2e-fixture.mjs +0 -282
- package/consumer-reference/fixtures/consumer-conformance/manifest.json +0 -102
- package/consumer-reference/fixtures/consumer-conformance/runtime-proof.mjs +0 -32
- package/consumer-reference/fixtures/consumer-conformance/valid-migration-result.json +0 -18
- package/consumer-reference/fixtures/consumer-conformance/valid-migration.json +0 -142
- package/consumer-reference/fixtures/invalid-stable-ended-support.json +0 -19
- package/consumer-reference/fixtures/promotion/invalid-blocking-evidence.json +0 -26
- package/consumer-reference/fixtures/promotion/invalid-count-only.json +0 -29
- package/consumer-reference/fixtures/promotion/invalid-missing-owner-migration.json +0 -25
- package/consumer-reference/fixtures/promotion/invalid-normative-bypass-no-regression.json +0 -25
- package/consumer-reference/fixtures/promotion/invalid-related-counted-independent.json +0 -45
- package/consumer-reference/fixtures/promotion/invalid-stable-ended-support.json +0 -26
- package/consumer-reference/fixtures/promotion/manifest.json +0 -15
- package/consumer-reference/fixtures/promotion/valid-deferred-example.json +0 -67
- package/consumer-reference/fixtures/promotion/valid-normative-bypass.json +0 -67
- package/consumer-reference/fixtures/token-portability/invalid-cycle.json +0 -7
- package/consumer-reference/fixtures/token-portability/invalid-dangling-alias.json +0 -6
- package/consumer-reference/fixtures/token-portability/invalid-extends.json +0 -4
- package/consumer-reference/fixtures/token-portability/invalid-json-pointer.json +0 -7
- package/consumer-reference/fixtures/token-portability/invalid-resolver.json +0 -9
- package/consumer-reference/fixtures/token-portability/invalid-type-mismatch.json +0 -10
- package/consumer-reference/fixtures/token-portability/invalid-unknown-reserved.json +0 -6
- package/consumer-reference/fixtures/token-portability/invalid-untested-type.json +0 -6
- package/consumer-reference/fixtures/token-portability/valid-border.json +0 -14
- package/consumer-reference/fixtures/token-portability/valid-reference.json +0 -32
- package/consumer-reference/fixtures/token-portability/valid-whole-alias.json +0 -7
- package/consumer-reference/fixtures/valid-deprecated.json +0 -21
- package/consumer-reference/fixtures/valid-experimental.json +0 -19
- package/consumer-reference/generated/manifest.json +0 -40
- package/consumer-reference/generated/tokens.css +0 -12
- package/consumer-reference/policies/shared-experimental.json +0 -37
- package/consumer-reference/schema/ax-evidence.schema.json +0 -30
- package/consumer-reference/schema/baseline-manifest.schema.json +0 -55
- package/consumer-reference/schema/calibration-record.schema.json +0 -190
- package/consumer-reference/schema/capture-session.schema.json +0 -161
- package/consumer-reference/schema/component-state.schema.json +0 -150
- package/consumer-reference/schema/consumer-conformance-record.schema.json +0 -365
- package/consumer-reference/schema/dom-evidence.schema.json +0 -38
- package/consumer-reference/schema/evidence-record.schema.json +0 -81
- package/consumer-reference/schema/fixture-manifest.schema.json +0 -32
- package/consumer-reference/schema/item.schema.json +0 -188
- package/consumer-reference/schema/page-evidence-manifest.schema.json +0 -216
- package/consumer-reference/schema/page-evidence-session.schema.json +0 -119
- package/consumer-reference/schema/portable-tokens.schema.json +0 -104
- package/consumer-reference/schema/promotion-rfc.schema.json +0 -219
- package/consumer-reference/schema/runtime-evidence-manifest.schema.json +0 -33
- package/consumer-reference/schema/visual-evidence.schema.json +0 -31
- package/design-engineering/consumer-migration-readiness.md +0 -124
- package/design-engineering/interface-craft.md +0 -107
- package/design-engineering/reference-profiles/external-adaptation/index.md +0 -12
- package/design-engineering/reference-profiles/governed-local/editorial/components/button.component.json +0 -125
- package/design-engineering/reference-profiles/governed-local/editorial/evidence/button.evidence.json +0 -3504
- package/design-engineering/reference-profiles/governed-local/editorial/fixtures/button.fixture.json +0 -65
- package/design-engineering/reference-profiles/governed-local/editorial/generated/evidence-coverage.md +0 -23
- package/design-engineering/reference-profiles/governed-local/editorial/generated/keyboard-matrix.md +0 -19
- package/design-engineering/reference-profiles/governed-local/editorial/generated/state-matrix.md +0 -21
- package/design-engineering/reference-profiles/governed-local/editorial/local-foundations.json +0 -12
- package/design-engineering/reference-profiles/governed-local/editorial/states/button.states.json +0 -195
- package/design-engineering/reference-profiles/governed-local/editorial/tokens.dtcg.json +0 -17
- package/design-engineering/reference-profiles/governed-local/terminal/components/button.component.json +0 -125
- package/design-engineering/reference-profiles/governed-local/terminal/evidence/button.evidence.json +0 -3504
- package/design-engineering/reference-profiles/governed-local/terminal/fixtures/button.fixture.json +0 -65
- package/design-engineering/reference-profiles/governed-local/terminal/generated/evidence-coverage.md +0 -23
- package/design-engineering/reference-profiles/governed-local/terminal/generated/keyboard-matrix.md +0 -19
- package/design-engineering/reference-profiles/governed-local/terminal/generated/state-matrix.md +0 -21
- package/design-engineering/reference-profiles/governed-local/terminal/local-foundations.json +0 -12
- package/design-engineering/reference-profiles/governed-local/terminal/profile.json +0 -43
- package/design-engineering/reference-profiles/governed-local/terminal/states/button.states.json +0 -195
- package/design-engineering/reference-profiles/governed-local/terminal/tokens.dtcg.json +0 -17
- package/game-ui/classification.md +0 -82
- package/game-ui/reference-record.md +0 -62
- package/game-ui/screen-hierarchy.md +0 -107
- package/game-ui/unity/architecture.md +0 -166
- package/game-ui/unity/cli-loop.md +0 -84
- package/game-ui/unity/data/unity-technologies-public-repositories.json +0 -26654
- package/game-ui/unity/org-wiki.md +0 -146
- package/game-ui/unity/repository-map.md +0 -102
- package/game-ui/unity/ui-systems.md +0 -85
- package/motion/practice-reference.md +0 -145
- package/motion/review-workflow.md +0 -86
- package/motion/vocabulary.md +0 -82
- package/platform-guides/apple-interaction.md +0 -89
- package/scripts/agent-native/events.mjs +0 -113
- package/scripts/agent-native/immutable-store.mjs +0 -102
- package/scripts/artifact-metadata.mjs +0 -20
- package/scripts/audit-evidence-freshness.mjs +0 -308
- package/scripts/baseline-contract.mjs +0 -189
- package/scripts/baseline-schema-parity.mjs +0 -40
- package/scripts/build-reference-artifacts.mjs +0 -127
- package/scripts/calibration-raw-contract.mjs +0 -172
- package/scripts/capture-session-contract.mjs +0 -187
- package/scripts/component-state-artifact-fixture.mjs +0 -44
- package/scripts/component-state-contract.mjs +0 -99
- package/scripts/component-state-semantics.mjs +0 -128
- package/scripts/component-state-workflow-contract.mjs +0 -67
- package/scripts/component-state-workflow-negative-cases.mjs +0 -64
- package/scripts/consumer-conformance-contract.mjs +0 -198
- package/scripts/consumer-evidence-governance-contract.mjs +0 -210
- package/scripts/consumer-reference-case-runner.mjs +0 -93
- package/scripts/consumer-reference-schema.mjs +0 -155
- package/scripts/create-component-state-session.mjs +0 -100
- package/scripts/create-page-evidence-session.mjs +0 -139
- package/scripts/domain-document-boundaries.mjs +0 -40
- package/scripts/domain-validation-checks.mjs +0 -141
- package/scripts/evidence-artifact-contract.mjs +0 -191
- package/scripts/finalize-component-state-evidence.mjs +0 -165
- package/scripts/finalize-page-evidence.mjs +0 -197
- package/scripts/generate-consumer-reference-evidence.mjs +0 -154
- package/scripts/generate-patterns.mjs +0 -218
- package/scripts/governance-matrix-contract.mjs +0 -73
- package/scripts/governance-matrix-negative-cases.mjs +0 -114
- package/scripts/governance-policy-contract.mjs +0 -49
- package/scripts/governance-test-fixture.mjs +0 -321
- package/scripts/governance-workflow-negative-cases.mjs +0 -241
- package/scripts/governed-profile-registry.mjs +0 -125
- package/scripts/json-schema-formats.mjs +0 -31
- package/scripts/markdown-structure.mjs +0 -45
- package/scripts/page-artifact-metadata.mjs +0 -124
- package/scripts/page-evidence-contract.mjs +0 -297
- package/scripts/page-evidence-fixture.mjs +0 -271
- package/scripts/pattern-data.mjs +0 -325
- package/scripts/profile-record-contract.mjs +0 -140
- package/scripts/promotion-attestation-contract.mjs +0 -91
- package/scripts/promotion-boundary-test-contract.mjs +0 -92
- package/scripts/promotion-fixture-inventory.mjs +0 -109
- package/scripts/promotion-governance-contract.mjs +0 -28
- package/scripts/reference-artifact-path-contract.mjs +0 -28
- package/scripts/reference-profile-contract.mjs +0 -114
- package/scripts/reference-token-contract.mjs +0 -156
- package/scripts/run-consumer-page-evidence-ci.mjs +0 -170
- package/scripts/summarize-sentinel-calibration.mjs +0 -98
- package/scripts/test-agent-adapter-conformance.mjs +0 -250
- package/scripts/test-agent-conformance-receipt.mjs +0 -106
- package/scripts/test-agent-events.mjs +0 -81
- package/scripts/test-agent-execution-effects.mjs +0 -114
- package/scripts/test-agent-execution-fixtures.mjs +0 -140
- package/scripts/test-agent-execution-kernel.mjs +0 -39
- package/scripts/test-agent-execution-learning.mjs +0 -130
- package/scripts/test-agent-execution-lifecycle.mjs +0 -143
- package/scripts/test-agent-execution-safety.mjs +0 -158
- package/scripts/test-agent-execution-views.mjs +0 -117
- package/scripts/test-agent-identity-integrity-cases.mjs +0 -160
- package/scripts/test-agent-knowledge-kernel.mjs +0 -183
- package/scripts/test-agent-learning.mjs +0 -246
- package/scripts/test-agent-mcp.mjs +0 -246
- package/scripts/test-agent-native-schemas.mjs +0 -215
- package/scripts/test-agent-projections.mjs +0 -249
- package/scripts/test-agent-registry-mcp-integrity.mjs +0 -215
- package/scripts/test-agent-registry-mutations.mjs +0 -110
- package/scripts/test-audit-evidence-freshness.mjs +0 -177
- package/scripts/test-component-state-sentinel.mjs +0 -35
- package/scripts/test-component-state-source-contract.mjs +0 -83
- package/scripts/test-consumer-conformance-sentinel.mjs +0 -165
- package/scripts/test-consumer-reference-sentinel.mjs +0 -31
- package/scripts/test-generate-consumer-reference-evidence.mjs +0 -124
- package/scripts/test-json-cli-output.mjs +0 -151
- package/scripts/test-json-schema-formats.mjs +0 -40
- package/scripts/test-reference-adapters.mjs +0 -199
- package/scripts/test-renderer-purity.mjs +0 -34
- package/scripts/test-sg-cli.mjs +0 -129
- package/scripts/test-summarize-sentinel-calibration.mjs +0 -226
- package/scripts/test-validate-baseline-manifest.mjs +0 -234
- package/scripts/test-validate-component-state-artifacts.mjs +0 -243
- package/scripts/test-validate-component-state.mjs +0 -106
- package/scripts/test-validate-consumer-conformance.mjs +0 -427
- package/scripts/test-validate-consumer-reference-cli.mjs +0 -69
- package/scripts/test-validate-consumer-reference.mjs +0 -263
- package/scripts/test-validate-domains.mjs +0 -357
- package/scripts/test-validate-governance.mjs +0 -292
- package/scripts/test-validate-ia.mjs +0 -262
- package/scripts/test-validate-okf.mjs +0 -73
- package/scripts/test-validate-page-evidence.mjs +0 -302
- package/scripts/test-validate-patterns.mjs +0 -157
- package/scripts/test-validate-promotion-rfc.mjs +0 -250
- package/scripts/test-validate-reference-profiles.mjs +0 -170
- package/scripts/test-validate-unity-org-wiki.mjs +0 -57
- package/scripts/test-validate-unity-source-contracts.mjs +0 -67
- package/scripts/test-validate-vocabulary.mjs +0 -174
- package/scripts/test-validate-webpage-workflow.mjs +0 -293
- package/scripts/validate-baseline-manifest.mjs +0 -44
- package/scripts/validate-catalog.mjs +0 -65
- package/scripts/validate-component-state.mjs +0 -141
- package/scripts/validate-consumer-conformance.mjs +0 -477
- package/scripts/validate-consumer-reference.mjs +0 -288
- package/scripts/validate-domains.mjs +0 -237
- package/scripts/validate-governance.mjs +0 -248
- package/scripts/validate-ia.mjs +0 -136
- package/scripts/validate-links.mjs +0 -66
- package/scripts/validate-okf.mjs +0 -104
- package/scripts/validate-page-evidence.mjs +0 -191
- package/scripts/validate-patterns.mjs +0 -246
- package/scripts/validate-promotion-rfc.mjs +0 -224
- package/scripts/validate-reference-artifacts.mjs +0 -125
- package/scripts/validate-renderer-purity.mjs +0 -56
- package/scripts/validate-unity-org-wiki.mjs +0 -146
- package/scripts/validate-unity-source-contracts.mjs +0 -107
- package/scripts/validate-vocabulary.mjs +0 -115
- package/scripts/validate-webpage-workflow.mjs +0 -254
- package/scripts/visual-expectation-contract.mjs +0 -20
- package/scripts/workflow-action-contract.mjs +0 -163
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { hashCanonical, canonicalize, deepFreeze } from "../canonical-json.mjs";
|
|
2
|
+
import { materialAdmissionPolicy } from "./material-admission.mjs";
|
|
3
|
+
import {
|
|
4
|
+
captureMaterialQuerySnapshot,
|
|
5
|
+
MATERIAL_GET_MAX_BYTES,
|
|
6
|
+
materialGet,
|
|
7
|
+
materialSearch,
|
|
8
|
+
rethrowMaterialQuerySnapshotError,
|
|
9
|
+
verifyMaterialQuerySnapshot,
|
|
10
|
+
} from "./material-queries.mjs";
|
|
11
|
+
|
|
12
|
+
export const MATERIAL_CONTEXT_DEFAULT_TOKENS = 8192;
|
|
13
|
+
export const MATERIAL_CONTEXT_MIN_TOKENS = 256;
|
|
14
|
+
export const MATERIAL_CONTEXT_MAX_TOKENS = 32768;
|
|
15
|
+
export const MATERIAL_CONTEXT_OPERATION_VERSION = "material-context@1";
|
|
16
|
+
export const MATERIAL_CONTEXT_TOKEN_CONTRACT = "ceil(canonical UTF-8 response bytes / 4)";
|
|
17
|
+
export const MATERIAL_CONTEXT_CACHE_CONTRACT = "material-context-cache@1";
|
|
18
|
+
|
|
19
|
+
const TRUST_INPUTS = new Set([
|
|
20
|
+
"claim", "claims", "evidence", "evidence_links", "governance", "governance_decisions",
|
|
21
|
+
"merge", "merge_flags", "merge_trust", "policy_dispositions", "trust_records", "validations",
|
|
22
|
+
]);
|
|
23
|
+
const PATH_INPUTS = new Set(["path", "repository_path", "repository_paths", "repository_root"]);
|
|
24
|
+
const MANIFEST_INPUTS = new Set(["manifest", "manifest_head", "manifest_override", "manifest_version_id"]);
|
|
25
|
+
|
|
26
|
+
export class MaterialContextError extends TypeError {
|
|
27
|
+
constructor(code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "MaterialContextError";
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fail(code, message) { throw new MaterialContextError(code, message); }
|
|
35
|
+
function unsafeInput() { fail("material_context_input_unsafe", "material context input must be plain JSON data"); }
|
|
36
|
+
function snapshotOwnData(value, seen = new WeakSet()) {
|
|
37
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
38
|
+
if (typeof value === "number") {
|
|
39
|
+
if (!Number.isFinite(value)) unsafeInput();
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
if (!value || typeof value !== "object" || seen.has(value)) unsafeInput();
|
|
43
|
+
const array = Array.isArray(value);
|
|
44
|
+
const prototype = Object.getPrototypeOf(value);
|
|
45
|
+
if (prototype !== (array ? Array.prototype : Object.prototype) && prototype !== null) unsafeInput();
|
|
46
|
+
const keys = Reflect.ownKeys(value);
|
|
47
|
+
if (keys.some((key) => typeof key !== "string" || key === "__proto__" || key === "prototype" || key === "constructor")) unsafeInput();
|
|
48
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
49
|
+
if (keys.some((key) => !Object.hasOwn(descriptors[key], "value") || (descriptors[key].enumerable !== true && !(array && key === "length")))) unsafeInput();
|
|
50
|
+
if (array && (keys.some((key) => key !== "length" && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= value.length))
|
|
51
|
+
|| descriptors.length?.enumerable !== false)) unsafeInput();
|
|
52
|
+
seen.add(value);
|
|
53
|
+
const clone = array ? [] : Object.create(null);
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
if (array && key === "length") continue;
|
|
56
|
+
clone[key] = snapshotOwnData(descriptors[key].value, seen);
|
|
57
|
+
}
|
|
58
|
+
seen.delete(value);
|
|
59
|
+
return clone;
|
|
60
|
+
}
|
|
61
|
+
function assertInput(input) {
|
|
62
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) fail("material_input_invalid", "input must be an object");
|
|
63
|
+
const snapshot = snapshotOwnData(input);
|
|
64
|
+
const keys = Object.keys(snapshot);
|
|
65
|
+
if (keys.some((key) => TRUST_INPUTS.has(key) || /^(claim|evidence|governance|policy|trust|validation)(?:_|$)/.test(key) || key.includes("merge"))) {
|
|
66
|
+
fail("material_context_trust_merge_forbidden", "trust records cannot be merged into material retrieval");
|
|
67
|
+
}
|
|
68
|
+
if (keys.some((key) => PATH_INPUTS.has(key) || /^(repository|source)_paths?$/.test(key))) {
|
|
69
|
+
fail("material_context_repository_path_forbidden", "repository paths are not caller inputs");
|
|
70
|
+
}
|
|
71
|
+
if (keys.some((key) => MANIFEST_INPUTS.has(key) || key.startsWith("manifest_"))) {
|
|
72
|
+
fail("material_context_manifest_override_forbidden", "the material manifest cannot be overridden");
|
|
73
|
+
}
|
|
74
|
+
if (keys.some((key) => key !== "query" && key !== "budget_tokens")) fail("material_input_unknown", "input contains an unsupported field");
|
|
75
|
+
const budgetTokens = snapshot.budget_tokens ?? MATERIAL_CONTEXT_DEFAULT_TOKENS;
|
|
76
|
+
if (!Number.isInteger(budgetTokens) || budgetTokens < MATERIAL_CONTEXT_MIN_TOKENS || budgetTokens > MATERIAL_CONTEXT_MAX_TOKENS) {
|
|
77
|
+
fail("material_context_budget_invalid", "budget_tokens must be an integer from 256 to 32768");
|
|
78
|
+
}
|
|
79
|
+
return { budgetTokens, query: snapshot.query };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function withoutCacheKey(context) {
|
|
83
|
+
const value = structuredClone(context);
|
|
84
|
+
delete value.cache_key;
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Recompute the canonical cache key. The key binds every output field except itself. */
|
|
89
|
+
export function materialContextCacheKey(context) {
|
|
90
|
+
return hashCanonical({ contract: MATERIAL_CONTEXT_CACHE_CONTRACT, output: withoutCacheKey(context) });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Count the complete successful operation response, not only excerpt bodies. */
|
|
94
|
+
export function countMaterialContextTokens(context) {
|
|
95
|
+
const response = { ok: true, operation: "material-context", result: context };
|
|
96
|
+
return Math.ceil(Buffer.byteLength(canonicalize(response), "utf8") / 4);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function finalizeContext(value) {
|
|
100
|
+
const context = structuredClone(value);
|
|
101
|
+
context.cache_key = "sha256:" + "0".repeat(64);
|
|
102
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
103
|
+
context.cache_key = materialContextCacheKey(context);
|
|
104
|
+
const count = countMaterialContextTokens(context);
|
|
105
|
+
if (context.budget.used_tokens === count) {
|
|
106
|
+
context.cache_key = materialContextCacheKey(context);
|
|
107
|
+
return deepFreeze(context);
|
|
108
|
+
}
|
|
109
|
+
context.budget.used_tokens = count;
|
|
110
|
+
}
|
|
111
|
+
fail("material_context_accounting_failed", "material context accounting did not converge");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function contextValue({ normalizedQuery, tokens, manifestVersionId, totalMatches, budgetTokens, excerpts }) {
|
|
115
|
+
const truncated = excerpts.length < totalMatches || excerpts.some((excerpt) => excerpt.truncated);
|
|
116
|
+
return finalizeContext({
|
|
117
|
+
schema_version: "2.0",
|
|
118
|
+
record_kind: "material_context",
|
|
119
|
+
operation_version: MATERIAL_CONTEXT_OPERATION_VERSION,
|
|
120
|
+
normalized_query: normalizedQuery,
|
|
121
|
+
query_tokens: tokens,
|
|
122
|
+
heads: {
|
|
123
|
+
manifest_ref: "sg:manifest/material-admission-v2",
|
|
124
|
+
manifest_version_id: manifestVersionId,
|
|
125
|
+
policy_ref: materialAdmissionPolicy.stable_ref,
|
|
126
|
+
policy_version_id: materialAdmissionPolicy.version_id,
|
|
127
|
+
},
|
|
128
|
+
member_manifest: excerpts.map(({ source }) => ({
|
|
129
|
+
stable_ref: source.stable_ref,
|
|
130
|
+
version_id: source.version_id,
|
|
131
|
+
sha256: source.sha256,
|
|
132
|
+
})),
|
|
133
|
+
budget: {
|
|
134
|
+
limit_tokens: budgetTokens,
|
|
135
|
+
used_tokens: 0,
|
|
136
|
+
token_count_contract: MATERIAL_CONTEXT_TOKEN_CONTRACT,
|
|
137
|
+
},
|
|
138
|
+
untrusted_retrieval: {
|
|
139
|
+
classification: "untrusted_material_excerpts",
|
|
140
|
+
retrieval_only: true,
|
|
141
|
+
total_matches: totalMatches,
|
|
142
|
+
truncated,
|
|
143
|
+
excerpts,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function excerptOf(page, content) {
|
|
149
|
+
const bytes = Buffer.from(content, "utf8");
|
|
150
|
+
return {
|
|
151
|
+
material_ref: page.identity.stable_ref,
|
|
152
|
+
source: {
|
|
153
|
+
stable_ref: page.source.stable_ref,
|
|
154
|
+
version_id: page.source.version_id,
|
|
155
|
+
sha256: page.source.source_sha256,
|
|
156
|
+
byte_offset: page.offset,
|
|
157
|
+
byte_length: bytes.byteLength,
|
|
158
|
+
},
|
|
159
|
+
truncated: page.offset + bytes.byteLength < page.source.byte_length,
|
|
160
|
+
content,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function codePointPrefixes(content) {
|
|
165
|
+
const prefixes = [""];
|
|
166
|
+
let current = "";
|
|
167
|
+
for (const point of content) {
|
|
168
|
+
current += point;
|
|
169
|
+
prefixes.push(current);
|
|
170
|
+
}
|
|
171
|
+
return prefixes;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Build a closed, read-only v2 material retrieval package. No source body cache is retained. */
|
|
175
|
+
export function materialContext({ repositoryRoot, input, fileSystem, sourceReader, gitRunner }) {
|
|
176
|
+
const { budgetTokens, query } = assertInput(input);
|
|
177
|
+
const transaction = captureMaterialQuerySnapshot({ repositoryRoot, fileSystem, gitRunner });
|
|
178
|
+
let search;
|
|
179
|
+
try {
|
|
180
|
+
search = materialSearch({
|
|
181
|
+
repositoryRoot,
|
|
182
|
+
input: { query, limit: 100 },
|
|
183
|
+
fileSystem,
|
|
184
|
+
sourceReader,
|
|
185
|
+
gitRunner,
|
|
186
|
+
snapshot: transaction,
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
rethrowMaterialQuerySnapshotError({ snapshot: transaction, error, fileSystem });
|
|
190
|
+
}
|
|
191
|
+
const normalizedQuery = search.scoring_tokens.join(" ");
|
|
192
|
+
const pages = [];
|
|
193
|
+
let excerpts = [];
|
|
194
|
+
|
|
195
|
+
// Membership is a maximal rank-ordered prefix at one code point per source.
|
|
196
|
+
// It is therefore deterministic and monotonic as the caller budget grows.
|
|
197
|
+
for (const result of search.results) {
|
|
198
|
+
const current = contextValue({ normalizedQuery, tokens: search.scoring_tokens, manifestVersionId: search.manifest_version_id, totalMatches: search.total_matches, budgetTokens, excerpts });
|
|
199
|
+
const remainingBytes = Math.max(4, (budgetTokens - current.budget.used_tokens) * 4);
|
|
200
|
+
let page;
|
|
201
|
+
try {
|
|
202
|
+
page = materialGet({
|
|
203
|
+
repositoryRoot,
|
|
204
|
+
input: { reference: result.source.stable_ref, offset: 0, length: Math.min(result.source.byte_length, MATERIAL_GET_MAX_BYTES, remainingBytes) },
|
|
205
|
+
fileSystem,
|
|
206
|
+
sourceReader,
|
|
207
|
+
gitRunner,
|
|
208
|
+
snapshot: transaction,
|
|
209
|
+
contextUtf8SafeEnd: true,
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
rethrowMaterialQuerySnapshotError({ snapshot: transaction, error, fileSystem });
|
|
213
|
+
}
|
|
214
|
+
const prefixes = codePointPrefixes(page.content);
|
|
215
|
+
const minimum = prefixes[Math.min(1, prefixes.length - 1)];
|
|
216
|
+
const candidate = [...excerpts, excerptOf(page, minimum)];
|
|
217
|
+
const packaged = contextValue({ normalizedQuery, tokens: search.scoring_tokens, manifestVersionId: search.manifest_version_id, totalMatches: search.total_matches, budgetTokens, excerpts: candidate });
|
|
218
|
+
if (packaged.budget.used_tokens > budgetTokens) break;
|
|
219
|
+
excerpts = candidate;
|
|
220
|
+
pages.push({ result, page, prefixes });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Spend remaining budget in rank order without changing membership.
|
|
224
|
+
for (let index = 0; index < pages.length; index += 1) {
|
|
225
|
+
const { page, prefixes } = pages[index];
|
|
226
|
+
let low = 1;
|
|
227
|
+
let high = prefixes.length - 1;
|
|
228
|
+
let best = excerpts[index].content;
|
|
229
|
+
while (low <= high) {
|
|
230
|
+
const middle = Math.floor((low + high) / 2);
|
|
231
|
+
const candidateExcerpts = excerpts.slice();
|
|
232
|
+
candidateExcerpts[index] = excerptOf(page, prefixes[middle]);
|
|
233
|
+
const packaged = contextValue({ normalizedQuery, tokens: search.scoring_tokens, manifestVersionId: search.manifest_version_id, totalMatches: search.total_matches, budgetTokens, excerpts: candidateExcerpts });
|
|
234
|
+
if (packaged.budget.used_tokens <= budgetTokens) { best = prefixes[middle]; low = middle + 1; }
|
|
235
|
+
else high = middle - 1;
|
|
236
|
+
}
|
|
237
|
+
excerpts[index] = excerptOf(page, best);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const context = contextValue({ normalizedQuery, tokens: search.scoring_tokens, manifestVersionId: search.manifest_version_id, totalMatches: search.total_matches, budgetTokens, excerpts });
|
|
241
|
+
if (context.budget.used_tokens > budgetTokens) fail("material_context_budget_too_small", "budget cannot contain the material context envelope");
|
|
242
|
+
verifyMaterialQuerySnapshot({
|
|
243
|
+
repositoryRoot,
|
|
244
|
+
snapshot: transaction,
|
|
245
|
+
selectedSourceRefs: excerpts.map((excerpt) => excerpt.source.stable_ref),
|
|
246
|
+
fileSystem,
|
|
247
|
+
sourceReader,
|
|
248
|
+
gitRunner,
|
|
249
|
+
});
|
|
250
|
+
return context;
|
|
251
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { deepFreeze } from "../canonical-json.mjs";
|
|
4
|
+
|
|
5
|
+
export const MATERIAL_STABLE_REF_KINDS = Object.freeze(["domain", "page", "pattern", "material"]);
|
|
6
|
+
const KIND_SET = new Set(MATERIAL_STABLE_REF_KINDS);
|
|
7
|
+
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8
|
+
const PATH_ID = /^path-sha256-[a-f0-9]{64}$/;
|
|
9
|
+
const DOMAIN_IDS = new Set(["layout", "motion", "design-engineering", "game-ui", "platform-guides"]);
|
|
10
|
+
const MATERIAL_ID = PATH_ID;
|
|
11
|
+
|
|
12
|
+
export class MaterialIdentityError extends TypeError {
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "MaterialIdentityError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fail(code, message) { throw new MaterialIdentityError(code, message); }
|
|
21
|
+
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
22
|
+
function referenceOf(input) {
|
|
23
|
+
if (typeof input === "string") return input;
|
|
24
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
|
|
25
|
+
return input.stable_ref ?? input.reference;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Parse only the closed v2 material identity grammar. This is deliberately not part of v1 identity.mjs. */
|
|
29
|
+
export function parseMaterialStableRef(input) {
|
|
30
|
+
if (input && typeof input === "object" && Object.hasOwn(input, "schema_version") && input.schema_version !== "2.0") {
|
|
31
|
+
fail("material_identity_version_invalid", "material identities require schema_version 2.0");
|
|
32
|
+
}
|
|
33
|
+
const reference = referenceOf(input);
|
|
34
|
+
if (typeof reference !== "string") fail("material_stable_ref_required", "v2 StableRef must be a string");
|
|
35
|
+
const match = /^sg:(domain|page|pattern|material)\/([a-z0-9]+(?:-[a-z0-9]+)*)$/.exec(reference);
|
|
36
|
+
if (!match || !KIND_SET.has(match[1])) fail("material_stable_ref_invalid", "reference is not a v2 material StableRef");
|
|
37
|
+
const [, kind, id] = match;
|
|
38
|
+
if ((kind === "domain" && !DOMAIN_IDS.has(id))
|
|
39
|
+
|| ((kind === "page" || kind === "pattern" || kind === "material") && !PATH_ID.test(id))) {
|
|
40
|
+
fail("material_stable_ref_invalid", "reference is outside the closed v2 material identity grammar");
|
|
41
|
+
}
|
|
42
|
+
return deepFreeze({ schema_version: "2.0", stable_ref: reference, kind, id });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createMaterialStableRef({ kind, id }) {
|
|
46
|
+
if (!KIND_SET.has(kind)) fail("material_stable_ref_kind_invalid", "unsupported v2 material StableRef kind");
|
|
47
|
+
if (typeof id !== "string" || !KEBAB.test(id)) fail("material_stable_ref_id_invalid", "v2 material StableRef id must be kebab-case");
|
|
48
|
+
return parseMaterialStableRef(`sg:${kind}/${id}`).stable_ref;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function materialIdentityForRecord(record) {
|
|
52
|
+
if (!record || typeof record !== "object" || !MATERIAL_ID.test(record.stable_ref?.replace(/^sg:material\//, "") ?? "")) {
|
|
53
|
+
fail("material_record_identity_invalid", "record does not carry a v2 material source identity");
|
|
54
|
+
}
|
|
55
|
+
const repositoryPath = record.repository_path;
|
|
56
|
+
if (typeof repositoryPath !== "string") fail("material_record_identity_invalid", "record has no bound source path");
|
|
57
|
+
let kind = "page";
|
|
58
|
+
let id = `path-sha256-${sha256(Buffer.from(repositoryPath, "utf8"))}`;
|
|
59
|
+
const domainMatch = /^(layout|motion|design-engineering|game-ui|platform-guides)\/index\.md$/.exec(repositoryPath);
|
|
60
|
+
if (domainMatch) { kind = "domain"; id = domainMatch[1]; }
|
|
61
|
+
else if (/^patterns\/[^/]+\/[^/]+\.md$/.test(repositoryPath)) kind = "pattern";
|
|
62
|
+
return deepFreeze({
|
|
63
|
+
schema_version: "2.0",
|
|
64
|
+
stable_ref: createMaterialStableRef({ kind, id }),
|
|
65
|
+
kind,
|
|
66
|
+
source_ref: record.stable_ref,
|
|
67
|
+
source_version_id: record.version_id,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const parseV2StableRef = parseMaterialStableRef;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { types as utilTypes } from "node:util";
|
|
5
|
+
|
|
6
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { CallToolRequestSchema, ErrorCode, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { canonicalize } from "../canonical-json.mjs";
|
|
11
|
+
import { parseMaterialStableRef } from "./material-identity.mjs";
|
|
12
|
+
import { createMaterialOperationRegistry, materialOperationSpecs } from "./material-operation-registry.mjs";
|
|
13
|
+
|
|
14
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
15
|
+
const manifestPath = path.join(repositoryRoot, "consumer-reference", "agent-native", "v2", "material-registry.json");
|
|
16
|
+
const exposedNames = new Set(["material-context", "material-discover", "material-get", "material-search"]);
|
|
17
|
+
const descriptions = Object.freeze({
|
|
18
|
+
"material-context": "Build a deterministic, provenance-bound material context within a token budget.",
|
|
19
|
+
"material-discover": "Discover the five governed StyleGallery domain entries.",
|
|
20
|
+
"material-get": "Read bounded UTF-8 bytes for one canonical v2 material StableRef.",
|
|
21
|
+
"material-search": "Search admitted material deterministically using normalized Unicode terms.",
|
|
22
|
+
});
|
|
23
|
+
const inputSchemas = Object.freeze({
|
|
24
|
+
"material-context": z.strictObject({ query: z.string(), budget_tokens: z.number().int().min(256).max(32768).default(8192) }),
|
|
25
|
+
"material-discover": z.strictObject({}),
|
|
26
|
+
"material-get": z.strictObject({ reference: z.string(), offset: z.number().int().min(0).optional(), length: z.number().int().min(1).max(65536).optional() }),
|
|
27
|
+
"material-search": z.strictObject({ query: z.string(), limit: z.number().int().min(1).max(100).optional() }),
|
|
28
|
+
});
|
|
29
|
+
const outputSchema = z.strictObject({
|
|
30
|
+
ok: z.boolean(),
|
|
31
|
+
operation: z.string().nullable(),
|
|
32
|
+
result: z.looseObject({}).optional(),
|
|
33
|
+
failures: z.array(z.looseObject({})).optional(),
|
|
34
|
+
});
|
|
35
|
+
const safeCallToolRequestSchema = CallToolRequestSchema.extend({
|
|
36
|
+
params: CallToolRequestSchema.shape.params.extend({ arguments: z.unknown().optional() }),
|
|
37
|
+
});
|
|
38
|
+
const unsafeInputMarker = Object.freeze({ __stylegallery_unsafe_material_input__: true });
|
|
39
|
+
|
|
40
|
+
function packagedInventoryRunner(root) {
|
|
41
|
+
return () => {
|
|
42
|
+
try {
|
|
43
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, "consumer-reference", "agent-native", "v2", "material-registry.json"), "utf8"));
|
|
44
|
+
return {
|
|
45
|
+
error: undefined,
|
|
46
|
+
status: 0,
|
|
47
|
+
stderr: Buffer.alloc(0),
|
|
48
|
+
stdout: Buffer.from(manifest.materials.map((record) => `100644 ${record.source_sha256} 0\t${record.repository_path}\0`).join(""), "utf8"),
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
return { error: undefined, status: 1, stderr: Buffer.alloc(0), stdout: Buffer.alloc(0) };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runtimeRegistry() {
|
|
57
|
+
return createMaterialOperationRegistry({
|
|
58
|
+
repositoryRoot,
|
|
59
|
+
...(fs.existsSync(path.join(repositoryRoot, ".git")) ? {} : { gitRunner: packagedInventoryRunner(repositoryRoot) }),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadManifest() {
|
|
64
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertRegistry(registry) {
|
|
68
|
+
if (!registry || typeof registry.invoke !== "function" || !Array.isArray(registry.operations)) {
|
|
69
|
+
throw new TypeError("material MCP registry must expose invoke and operations");
|
|
70
|
+
}
|
|
71
|
+
const names = registry.operations.map(({ name }) => name).sort();
|
|
72
|
+
if (names.length !== exposedNames.size || names.some((name) => !exposedNames.has(name))) {
|
|
73
|
+
throw new TypeError("material MCP registry must contain exactly four v2 read operations");
|
|
74
|
+
}
|
|
75
|
+
for (const operation of registry.operations) {
|
|
76
|
+
if (operation.read_only !== true || operation.effect_class !== "NONE") {
|
|
77
|
+
throw new TypeError("material MCP operations must be read-only with NONE effects");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function normalizeMaterialMcpInput(input) {
|
|
83
|
+
if (input === undefined) return {};
|
|
84
|
+
if (input === null || typeof input !== "object" || utilTypes.isProxy(input) || Array.isArray(input)) {
|
|
85
|
+
throw new McpError(ErrorCode.InvalidParams, "operation input must contain only own plain data");
|
|
86
|
+
}
|
|
87
|
+
let prototype;
|
|
88
|
+
let keys;
|
|
89
|
+
try {
|
|
90
|
+
prototype = Object.getPrototypeOf(input);
|
|
91
|
+
keys = Reflect.ownKeys(input);
|
|
92
|
+
} catch {
|
|
93
|
+
throw new McpError(ErrorCode.InvalidParams, "operation input must contain only own plain data");
|
|
94
|
+
}
|
|
95
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
96
|
+
throw new McpError(ErrorCode.InvalidParams, "operation input must contain only own plain data");
|
|
97
|
+
}
|
|
98
|
+
const normalized = {};
|
|
99
|
+
for (const key of keys) {
|
|
100
|
+
if (typeof key !== "string") throw new McpError(ErrorCode.InvalidParams, "operation input must contain only own plain data");
|
|
101
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
102
|
+
if (!descriptor || descriptor.enumerable !== true || !("value" in descriptor)) {
|
|
103
|
+
throw new McpError(ErrorCode.InvalidParams, "operation input must contain only own plain data");
|
|
104
|
+
}
|
|
105
|
+
const value = descriptor.value;
|
|
106
|
+
if (!["string", "number", "boolean"].includes(typeof value) && value !== null) {
|
|
107
|
+
throw new McpError(ErrorCode.InvalidParams, "operation input must contain only own scalar data");
|
|
108
|
+
}
|
|
109
|
+
normalized[key] = value;
|
|
110
|
+
}
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sanitizedToolCallMessage(message) {
|
|
115
|
+
if (!message || typeof message !== "object" || message.method !== "tools/call" || !message.params || typeof message.params !== "object") return message;
|
|
116
|
+
const descriptor = Object.getOwnPropertyDescriptor(message.params, "arguments");
|
|
117
|
+
let normalized;
|
|
118
|
+
try {
|
|
119
|
+
normalized = descriptor && "value" in descriptor ? normalizeMaterialMcpInput(descriptor.value) : {};
|
|
120
|
+
} catch {
|
|
121
|
+
normalized = unsafeInputMarker;
|
|
122
|
+
}
|
|
123
|
+
const params = Object.create(Object.getPrototypeOf(message.params), Object.getOwnPropertyDescriptors(message.params));
|
|
124
|
+
Object.defineProperty(params, "arguments", { configurable: true, enumerable: true, value: normalized, writable: true });
|
|
125
|
+
const sanitized = Object.create(Object.getPrototypeOf(message), Object.getOwnPropertyDescriptors(message));
|
|
126
|
+
Object.defineProperty(sanitized, "params", { configurable: true, enumerable: true, value: params, writable: true });
|
|
127
|
+
return sanitized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class OwnDataTransport {
|
|
131
|
+
onclose;
|
|
132
|
+
onerror;
|
|
133
|
+
onmessage;
|
|
134
|
+
|
|
135
|
+
constructor(transport) { this.transport = transport; }
|
|
136
|
+
get sessionId() { return this.transport.sessionId; }
|
|
137
|
+
async start() {
|
|
138
|
+
this.transport.onmessage = (message, extra) => this.onmessage?.(sanitizedToolCallMessage(message), extra);
|
|
139
|
+
this.transport.onerror = (error) => this.onerror?.(error);
|
|
140
|
+
this.transport.onclose = () => this.onclose?.();
|
|
141
|
+
await this.transport.start();
|
|
142
|
+
}
|
|
143
|
+
close() { return this.transport.close(); }
|
|
144
|
+
send(message, options) { return this.transport.send(message, options); }
|
|
145
|
+
setProtocolVersion(version) { return this.transport.setProtocolVersion?.(version); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toolResponse(value) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: canonicalize(value) }],
|
|
151
|
+
...(value.ok ? {} : { isError: true }),
|
|
152
|
+
structuredContent: value,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resourceContents(uri, value) {
|
|
157
|
+
return { contents: [{ uri, mimeType: "application/json", text: canonicalize(value) }] };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function materialResourceUri(stableRef) {
|
|
161
|
+
return `sg://v2/material/${encodeURIComponent(parseMaterialStableRef(stableRef).stable_ref)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function materialReference(encoded) {
|
|
165
|
+
if (typeof encoded !== "string" || encoded.length === 0 || encoded.length > 256) {
|
|
166
|
+
throw new McpError(ErrorCode.InvalidParams, "material resource reference must be one canonical encoded StableRef");
|
|
167
|
+
}
|
|
168
|
+
let decoded;
|
|
169
|
+
try { decoded = decodeURIComponent(encoded); } catch {
|
|
170
|
+
throw new McpError(ErrorCode.InvalidParams, "material resource reference has invalid percent encoding");
|
|
171
|
+
}
|
|
172
|
+
if (encodeURIComponent(decoded) !== encoded) {
|
|
173
|
+
throw new McpError(ErrorCode.InvalidParams, "material resource reference is not canonically encoded once");
|
|
174
|
+
}
|
|
175
|
+
try { return parseMaterialStableRef(decoded).stable_ref; } catch {
|
|
176
|
+
throw new McpError(ErrorCode.InvalidParams, "material resource reference must decode to one canonical v2 StableRef");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resourceEnvelope(registry, encoded) {
|
|
181
|
+
const result = registry.invoke("material-get", { reference: materialReference(encoded) });
|
|
182
|
+
if (result.ok) return result;
|
|
183
|
+
const failure = result.failures?.[0] ?? { code: "material_operation_failed", message: "v2 material operation failed" };
|
|
184
|
+
throw new McpError(ErrorCode.InvalidParams, `${failure.code}: ${failure.message}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function materialReferenceFromUri(uri) {
|
|
188
|
+
const prefix = "sg://v2/material/";
|
|
189
|
+
if (typeof uri !== "string" || !uri.startsWith(prefix) || uri.length > prefix.length + 256) {
|
|
190
|
+
throw new McpError(ErrorCode.InvalidParams, "material resource URI must use the canonical v2 authority and path");
|
|
191
|
+
}
|
|
192
|
+
const encoded = uri.slice(prefix.length);
|
|
193
|
+
const reference = materialReference(encoded);
|
|
194
|
+
if (materialResourceUri(reference) !== uri) {
|
|
195
|
+
throw new McpError(ErrorCode.InvalidParams, "material resource URI is not canonical");
|
|
196
|
+
}
|
|
197
|
+
return reference;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function createMaterialMcpServer({ registry = runtimeRegistry(), manifest = loadManifest() } = {}) {
|
|
201
|
+
assertRegistry(registry);
|
|
202
|
+
if (!manifest || !Array.isArray(manifest.materials)) throw new TypeError("material MCP manifest is unavailable");
|
|
203
|
+
const server = new McpServer({ name: "StyleGallery Material", version: "2.0.0" });
|
|
204
|
+
const operations = materialOperationSpecs.filter(({ name }) => exposedNames.has(name));
|
|
205
|
+
const operationByName = new Map(operations.map((operation) => [operation.name, operation]));
|
|
206
|
+
|
|
207
|
+
for (const operation of operations) {
|
|
208
|
+
server.registerTool(operation.name, {
|
|
209
|
+
description: descriptions[operation.name],
|
|
210
|
+
inputSchema: inputSchemas[operation.name],
|
|
211
|
+
outputSchema,
|
|
212
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false, readOnlyHint: true },
|
|
213
|
+
_meta: { effect_class: "NONE" },
|
|
214
|
+
}, async (input) => toolResponse(registry.invoke(operation.name, normalizeMaterialMcpInput(input))));
|
|
215
|
+
}
|
|
216
|
+
server.server.setRequestHandler(safeCallToolRequestSchema, async (request) => {
|
|
217
|
+
const operation = operationByName.get(request.params.name);
|
|
218
|
+
if (!operation) throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
|
|
219
|
+
if (request.params.arguments?.__stylegallery_unsafe_material_input__ === true) {
|
|
220
|
+
throw new McpError(ErrorCode.InvalidParams, "operation input must contain only own plain data");
|
|
221
|
+
}
|
|
222
|
+
const normalized = normalizeMaterialMcpInput(request.params.arguments);
|
|
223
|
+
const parsed = await inputSchemas[operation.name].safeParseAsync(normalized);
|
|
224
|
+
if (!parsed.success) throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for tool ${operation.name}`);
|
|
225
|
+
return toolResponse(registry.invoke(operation.name, parsed.data));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
server.registerResource(
|
|
229
|
+
"stylegallery-v2-material",
|
|
230
|
+
new ResourceTemplate("sg://v2/material/{reference}", {
|
|
231
|
+
list: async () => ({
|
|
232
|
+
resources: manifest.materials.map((record) => ({
|
|
233
|
+
name: record.stable_ref,
|
|
234
|
+
description: "Canonical admitted StyleGallery v2 material.",
|
|
235
|
+
mimeType: "application/json",
|
|
236
|
+
uri: materialResourceUri(record.stable_ref),
|
|
237
|
+
})),
|
|
238
|
+
}),
|
|
239
|
+
}),
|
|
240
|
+
{ description: "Read one canonically encoded v2 material StableRef.", mimeType: "application/json" },
|
|
241
|
+
async (uri, variables) => resourceContents(uri.toString(), resourceEnvelope(registry, variables.reference)),
|
|
242
|
+
);
|
|
243
|
+
server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
244
|
+
const uri = request.params.uri;
|
|
245
|
+
const reference = materialReferenceFromUri(uri);
|
|
246
|
+
return resourceContents(uri, resourceEnvelope(registry, encodeURIComponent(reference)));
|
|
247
|
+
});
|
|
248
|
+
const connect = server.connect.bind(server);
|
|
249
|
+
server.connect = (transport) => connect(new OwnDataTransport(transport));
|
|
250
|
+
return server;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const createMcpServer = createMaterialMcpServer;
|