@unoverse-platform/studio 0.1.90 → 0.1.92

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.
Files changed (132) hide show
  1. package/local/plugin.ts +21 -65
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +153 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +114 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +234 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +138 -0
  13. package/vendor/base/src/auth/auth.ts +307 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +155 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +659 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +397 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +246 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +212 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.d.mts +28 -0
  53. package/vendor/base/src/lint/nodes/index.mjs +82 -0
  54. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  55. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  56. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  57. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  58. package/vendor/base/src/logger.ts +33 -0
  59. package/vendor/base/src/manifests/compose.ts +539 -0
  60. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  61. package/vendor/base/src/manifests/executor/context.ts +110 -0
  62. package/vendor/base/src/manifests/executor/index.ts +47 -0
  63. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  64. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  65. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  66. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  67. package/vendor/base/src/manifests/executor/send.ts +65 -0
  68. package/vendor/base/src/manifests/executor/session.ts +46 -0
  69. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  70. package/vendor/base/src/manifests/index.ts +280 -0
  71. package/vendor/base/src/manifests/integrity.ts +93 -0
  72. package/vendor/base/src/manifests/rows.ts +33 -0
  73. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  74. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  75. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  76. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  77. package/vendor/base/src/manifests/runtime/channels/service.ts +138 -0
  78. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  79. package/vendor/base/src/manifests/runtime/context.ts +156 -0
  80. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  81. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  82. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  83. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  84. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  85. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  86. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  87. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  88. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  89. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  90. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  91. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  92. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  93. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  94. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  95. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  96. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  97. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  98. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  99. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  100. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  101. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +550 -0
  102. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  103. package/vendor/base/src/manifests/source.ts +196 -0
  104. package/vendor/base/src/manifests/watch.ts +67 -0
  105. package/vendor/base/src/markdown/document.ts +141 -0
  106. package/vendor/base/src/paths.ts +180 -0
  107. package/vendor/base/src/platform/audioManager.ts +100 -0
  108. package/vendor/base/src/platform/clientTransport.ts +65 -0
  109. package/vendor/base/src/platform/executionContext.ts +157 -0
  110. package/vendor/base/src/platform/index.ts +10 -0
  111. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  112. package/vendor/base/src/platform/redis.ts +54 -0
  113. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  114. package/vendor/base/src/pluginBase.ts +248 -0
  115. package/vendor/base/src/plugins/README.md +57 -0
  116. package/vendor/base/src/plugins/discovery.ts +87 -0
  117. package/vendor/base/src/plugins/index.ts +16 -0
  118. package/vendor/base/src/plugins/loader.ts +179 -0
  119. package/vendor/base/src/plugins/redis.ts +36 -0
  120. package/vendor/base/src/plugins/state.ts +53 -0
  121. package/vendor/base/src/registry.ts +95 -0
  122. package/vendor/base/src/security/permissions.ts +93 -0
  123. package/vendor/base/src/security/rules.ts +73 -0
  124. package/vendor/base/src/template/SafeExpression.ts +556 -0
  125. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  126. package/vendor/base/src/template/index.ts +22 -0
  127. package/vendor/base/src/types.ts +50 -0
  128. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  129. package/vendor/base/src/workflow/PlatformMemoryCore.ts +449 -0
  130. package/vendor/base/src/workflow/routing.ts +122 -0
  131. package/vendor/sdk/lib/core/store.ts +26 -6
  132. package/local/mcp.ts +0 -265
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Signal routing over a frozen route table — the runtime half of the workflow contract.
3
+ *
4
+ * The types here are the WIRE CONTRACT for a compiled workflow: the engine's compiler
5
+ * (RouteTableBuilder, engine-side — it needs the node metadata registry and must never
6
+ * enter this package) produces a RouteTable; anything that runs one consumes these shapes.
7
+ * The compiler's own declarations stay structurally identical — the runtime round-trip
8
+ * tests hold the two in parity.
9
+ *
10
+ * SignalRoutingEngine is pure routing logic: given a completed node's output and the
11
+ * route table, which downstream signals fire. No signal management, no execution.
12
+ */
13
+
14
+ import { createLogger } from "../logger.js";
15
+
16
+ /** One compiled edge: source → target with its signal type and connector handles. */
17
+ export interface RouteEntry {
18
+ targetNodeId: string;
19
+ signalType: string; // "EXECUTE" | "SPAWN" | "RESET" etc.
20
+ targetHandle?: string; // Which input connector this edge connects to
21
+ sourceHandle?: string; // Which output connector this edge comes from
22
+ }
23
+
24
+ /** The frozen routing derived from a workflow's nodes + edges. */
25
+ export interface RouteTable {
26
+ routing: Map<string, RouteEntry[]>; // Source → [RouteEntries with signal types]
27
+ dependencies: Map<string, string[]>; // Target → [Source Node IDs] (for execution order)
28
+ connectorDependencies?: Map<string, Map<string, string[]>>; // Target → Connector → [Source Node IDs]
29
+ triggerNodes: string[]; // Nodes that start execution
30
+ nodeTriggerMap?: Map<string, string>; // nodeId → upstream InputTrigger ID (Focus Mode routing)
31
+ }
32
+
33
+ /** A signal the router decided to send. */
34
+ export interface SignalRoute {
35
+ targetNodeId: string;
36
+ signal: string;
37
+ sourceNodeId: string;
38
+ targetHandle?: string; // Which input connector this signal targets
39
+ }
40
+
41
+ const logger = createLogger("SignalRoutingEngine");
42
+
43
+ export class SignalRoutingEngine {
44
+ /**
45
+ * Get signals to send when a node completes execution - ROUTE TABLE VERSION
46
+ * Ultra-fast O(1) lookup using pre-computed route table
47
+ * Now with multi-output support - filters based on active output connectors
48
+ */
49
+ static getSignalsForCompletedNode(nodeId: string, nodeOutput: any, routeTable: RouteTable): SignalRoute[] {
50
+ const signals: SignalRoute[] = [];
51
+
52
+ if (!routeTable?.routing) {
53
+ logger.warn(`No route table provided for node completion`, { nodeId });
54
+ return signals;
55
+ }
56
+
57
+ const routeEntries = routeTable.routing.get(nodeId) || [];
58
+
59
+ for (const routeEntry of routeEntries) {
60
+ // For nodes using __outputs pattern, check if the specific output exists
61
+ if (nodeOutput?.__outputs) {
62
+ // If there's a sourceHandle, check if that specific output exists
63
+ if (routeEntry.sourceHandle) {
64
+ const hasOutput =
65
+ routeEntry.sourceHandle in nodeOutput.__outputs &&
66
+ nodeOutput.__outputs[routeEntry.sourceHandle] !== undefined;
67
+
68
+ if (!hasOutput) {
69
+ // Debug level - this is normal for streaming nodes that emit partial outputs
70
+ logger.debug(`No signal - output connector has no data`, {
71
+ from: nodeId,
72
+ to: routeEntry.targetNodeId,
73
+ sourceHandle: routeEntry.sourceHandle,
74
+ });
75
+ continue; // Skip this route
76
+ }
77
+ }
78
+ // If no sourceHandle specified, node should have a default output
79
+ }
80
+ // For legacy multi-output nodes (IfElse, etc), check if the specific output exists
81
+ else if (routeEntry.sourceHandle && nodeOutput && typeof nodeOutput === "object") {
82
+ const hasOutput = routeEntry.sourceHandle in nodeOutput;
83
+
84
+ if (!hasOutput) {
85
+ logger.debug(`No signal - output connector has no data`, {
86
+ from: nodeId,
87
+ to: routeEntry.targetNodeId,
88
+ sourceHandle: routeEntry.sourceHandle,
89
+ });
90
+ continue; // Skip this route
91
+ }
92
+ }
93
+ // For flat output nodes or nodes with data, always send signal
94
+
95
+ signals.push({
96
+ targetNodeId: routeEntry.targetNodeId,
97
+ signal: routeEntry.signalType, // Use pre-computed signal type!
98
+ sourceNodeId: nodeId,
99
+ targetHandle: routeEntry.targetHandle, // Include target connector
100
+ });
101
+
102
+ if (routeEntry.signalType === "SPAWN") {
103
+ logger.debug(`Routing SPAWN signal`, {
104
+ from: nodeId,
105
+ to: routeEntry.targetNodeId,
106
+ targetHandle: routeEntry.targetHandle,
107
+ sourceHandle: routeEntry.sourceHandle,
108
+ });
109
+ }
110
+
111
+ logger.debug(`Using route table to add active signal`, {
112
+ from: nodeId,
113
+ to: routeEntry.targetNodeId,
114
+ signal: routeEntry.signalType,
115
+ targetHandle: routeEntry.targetHandle,
116
+ sourceHandle: routeEntry.sourceHandle,
117
+ });
118
+ }
119
+
120
+ return signals;
121
+ }
122
+ }
@@ -215,12 +215,20 @@ export class ComponentStore {
215
215
  * lower states update silently underneath).
216
216
  */
217
217
  /**
218
- * RULE 5's EMPTY DELIVERY (STATE_MODEL §5 rule 5, amended 2026-08-26): a discovery
219
- * turn that ran with cards enabled and surfaced NONE still owns the screen — the
220
- * server says so with COMPONENTS_CLEAR, and every earlier-turn component sitting in
221
- * a public state retracts to inline. `supersede` for ALL states at once: same
222
- * durable (`lifetime: "conversation"`) opt-out, same same-turn exemption. Rule 7
223
- * stands — this rides the answer's delivery, never the guest's typing.
218
+ * RULE 5's EMPTY DELIVERY (STATE_MODEL §5 rule 5; the director is the sender since
219
+ * 2026-08-30 — confirmed-empty and spent-or-kept both arrive as COMPONENTS_CLEAR):
220
+ * every earlier-turn component sitting in a public state retires. Same durable
221
+ * (`lifetime: "conversation"`) opt-out, same same-turn exemption as `supersede`.
222
+ * Rule 7 stands — this rides the answer's delivery, never the guest's typing.
223
+ *
224
+ * A CLEAR NEVER STAMPS A STATE NOBODY DREW (STATE_MODEL §5 rule 5, 2026-09-03).
225
+ * `inline` is the retirement of a CHAT-lifetime card. A v2 slice whose public menu
226
+ * has no `inline` cannot wear it — worn anyway, the renderer fell back to the
227
+ * default drawing and the chat flow painted retired GRID cards into the
228
+ * conversation (observed live: BPP course cards). Such a slice retires to its
229
+ * DECLARED base, written WITHOUT a touch: its delivery was just spent
230
+ * (`spendDelivery` runs first), and a touch would re-admit it to the delivery it
231
+ * is leaving — the mount would hold the very cards the clear retired.
224
232
  */
225
233
  supersedeAll(arrivingChatId?: string): void {
226
234
  let changed = false;
@@ -229,6 +237,18 @@ export class ComponentStore {
229
237
  if (slice.lifetime === "conversation") continue;
230
238
  const v = (slice.state ?? slice.view ?? slice.defaultState) as unknown;
231
239
  if (typeof v !== "string" || !v || v === "inline") continue;
240
+ const menu = Array.isArray(slice.publicStates) ? (slice.publicStates as unknown[]) : undefined;
241
+ if (menu && !menu.includes("inline")) {
242
+ const base = typeof slice.initialView === "string" ? slice.initialView : undefined;
243
+ if (base && v !== base) {
244
+ const next: Record<string, unknown> = { ...slice };
245
+ next[this.faceKey(next)] = base;
246
+ if ("defaultState" in next) next.defaultState = base;
247
+ this.data.set(key, next);
248
+ changed = true;
249
+ }
250
+ continue;
251
+ }
232
252
  const next: Record<string, unknown> = { ...slice };
233
253
  next[this.faceKey(next)] = "inline";
234
254
  if ("defaultState" in next) next.defaultState = "inline";
package/local/mcp.ts DELETED
@@ -1,265 +0,0 @@
1
- /**
2
- * THE STUDIO MCP: a developer's Claude, authoring against their own folder.
3
- *
4
- * Same flow as Canvas. The developer clicks "Connect agent", Studio mints a short
5
- * single-use code, they paste it into Claude, and `bindProject` redeems it. Until a session
6
- * is bound, every other tool refuses.
7
- *
8
- * NO REDIS, and none needed: Canvas stores codes there because a universe is several
9
- * processes serving many users. Studio is one process serving one developer, so the code
10
- * lives in memory and dies with the server, which is the correct lifetime for it.
11
- *
12
- * THIS SERVER OWNS NO RULES. Saving goes through the same lint-gated chokepoint the editor
13
- * uses (source.ts), linting is the shared library, and completion data is the same
14
- * vocabulary. That is why it stays small: the guardrails already exist below it, and an
15
- * agent gets exactly the ones the human gets.
16
- *
17
- * See docs/architecture/LOCAL_STUDIO.md §The connected agent.
18
- */
19
- import { randomBytes } from "node:crypto";
20
- import { resolve } from "node:path";
21
- import { existsSync, readFileSync } from "node:fs";
22
- import type { IncomingMessage, ServerResponse } from "node:http";
23
-
24
- /** Human-typeable, unambiguous alphabet. The same one Canvas uses (no 0/O/1/I/L). */
25
- const CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
26
- const CODE_TTL_MS = 10 * 60 * 1000;
27
-
28
- let pending: { code: string; expires: number } | null = null;
29
-
30
- export function mintPairingCode(): { code: string; expiresInSeconds: number } {
31
- const b = randomBytes(8);
32
- let s = "";
33
- for (let i = 0; i < 8; i++) s += CODE_ALPHABET[b[i] % CODE_ALPHABET.length];
34
- const code = `${s.slice(0, 4)}-${s.slice(4)}`;
35
- pending = { code, expires: Date.now() + CODE_TTL_MS };
36
- return { code, expiresInSeconds: CODE_TTL_MS / 1000 };
37
- }
38
-
39
- /** Redeem and burn. A lingering copy in a transcript is inert. */
40
- function redeem(raw: string): boolean {
41
- const code = raw.trim().toUpperCase();
42
- if (!pending || pending.code !== code || Date.now() > pending.expires) return false;
43
- pending = null;
44
- return true;
45
- }
46
-
47
- export interface McpDeps {
48
- projectRoot: string;
49
- /** The catalog loaders, so the agent reads definitions the same way the screens do. */
50
- catalog: () => Promise<{
51
- listDefinitions: (kind: string, org?: string) => unknown[];
52
- loadDefinition: (ref: string, kind: string) => unknown;
53
- listOrgs: () => string[];
54
- }>;
55
- /** Files behind a definition, and their writability. */
56
- sourceFiles: (uri: string) => { path: string; rel: string; writable: boolean }[];
57
- readSource: (path: string) => { text: string; writable: boolean } | null;
58
- /** The lint-gated write chokepoint. Identical to the one the editor presses. */
59
- saveSource: (path: string, text: string) => Promise<{ saved: boolean; error?: string; findings?: unknown[] }>;
60
- lintProject: () => Promise<{ problems: unknown[] }>;
61
- /** What the developer last picked in the preview. */
62
- selection: () => Record<string, unknown> | null;
63
- vocabulary: () => Promise<unknown>;
64
- }
65
-
66
- const SKILL_DIRS = [
67
- "vendor/skills/unoverse-create",
68
- "../../.claude/skills/unoverse-create", // running from source: the authoring home
69
- "../../prompts/skills/unoverse-create",
70
- ];
71
-
72
- /** The vendored authoring skill, which is how an agent learns the rules before writing. */
73
- function authoringSkill(appRoot: string): string {
74
- for (const d of SKILL_DIRS) {
75
- const p = resolve(appRoot, d, "SKILL.md");
76
- if (existsSync(p)) return readFileSync(p, "utf8");
77
- }
78
- return "No authoring skill is vendored with this Studio build.";
79
- }
80
-
81
- type Tool = {
82
- description: string;
83
- inputSchema: { type: "object"; properties: Record<string, unknown>; required?: string[] };
84
- run: (args: Record<string, any>) => Promise<unknown>;
85
- };
86
-
87
- /**
88
- * The tool surface, deliberately short.
89
- *
90
- * Everything an agent needs to author and nothing that duplicates a rule: read the project,
91
- * learn the rules, read a definition and its files, save through the gate, lint, and ask
92
- * what the developer is pointing at.
93
- */
94
- export function buildTools(deps: McpDeps, appRoot: string): Record<string, Tool> {
95
- let bound = false;
96
- const needsBinding = () => {
97
- if (!bound) throw new Error("Not bound. Call bindProject with the code from Studio's 'Connect agent' button.");
98
- };
99
-
100
- return {
101
- bindProject: {
102
- description:
103
- "Bind this session to the developer's Studio project using the pairing code shown by Studio's 'Connect agent' button. Call this first; nothing else works until you do.",
104
- inputSchema: { type: "object", properties: { code: { type: "string" } }, required: ["code"] },
105
- run: async ({ code }) => {
106
- if (!redeem(String(code ?? ""))) throw new Error("Invalid or expired pairing code. Generate a fresh one in Studio.");
107
- bound = true;
108
- const c = await deps.catalog();
109
- return {
110
- bound: true,
111
- projectRoot: deps.projectRoot,
112
- projects: c.listOrgs(),
113
- note: "Saves are lint-gated and installed assets are read-only. Read the authoring skill before writing.",
114
- };
115
- },
116
- },
117
-
118
- readAuthoringSkill: {
119
- description: "The rules for authoring unoverse definitions. Read this before writing or editing any definition.",
120
- inputSchema: { type: "object", properties: {} },
121
- run: async () => {
122
- needsBinding();
123
- return { skill: authoringSkill(appRoot) };
124
- },
125
- },
126
-
127
- getVocabulary: {
128
- description:
129
- "The closed vocabulary: the primitive set, the style keys, and THIS project's own token names from its resolved theme. A value outside these will fail lint.",
130
- inputSchema: { type: "object", properties: {} },
131
- run: async () => {
132
- needsBinding();
133
- return deps.vocabulary();
134
- },
135
- },
136
-
137
- listDefinitions: {
138
- description: "The project's components or templates.",
139
- inputSchema: {
140
- type: "object",
141
- properties: { kind: { type: "string", enum: ["component", "app", "atom", "template"] } },
142
- required: ["kind"],
143
- },
144
- run: async ({ kind }) => {
145
- needsBinding();
146
- const c = await deps.catalog();
147
- return { definitions: c.listDefinitions(String(kind)) };
148
- },
149
- },
150
-
151
- readDefinition: {
152
- description:
153
- "One definition, composed, plus the source files behind it and whether each may be written. Installed assets read but never write.",
154
- inputSchema: { type: "object", properties: { uri: { type: "string" } }, required: ["uri"] },
155
- run: async ({ uri }) => {
156
- needsBinding();
157
- const c = await deps.catalog();
158
- // `.../definition` is a suffix on the app's URI, not part of its ref.
159
- const ref = String(uri).replace(/^unoverse:\/\/(components|atoms|apps|templates)\//, "").replace(/\/definition$/, "");
160
- const kind = /\/apps\//.test(String(uri)) ? "app" : /\/atoms\//.test(String(uri)) ? "atom" : /^unoverse:\/\/templates\//.test(String(uri)) ? "template" : "component";
161
- return { definition: c.loadDefinition(ref, kind), files: deps.sourceFiles(String(uri)) };
162
- },
163
- },
164
-
165
- readSource: {
166
- description: "One source file's text.",
167
- inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
168
- run: async ({ path }) => {
169
- needsBinding();
170
- const found = deps.readSource(String(path));
171
- if (!found) throw new Error(`Not readable: ${path}`);
172
- return found;
173
- },
174
- },
175
-
176
- saveDefinition: {
177
- description:
178
- "Write a source file. The linter runs and a failing write is ROLLED BACK, so a refusal means nothing changed on disk. Installed assets are refused outright.",
179
- inputSchema: {
180
- type: "object",
181
- properties: { path: { type: "string" }, text: { type: "string" } },
182
- required: ["path", "text"],
183
- },
184
- run: async ({ path, text }) => {
185
- needsBinding();
186
- return deps.saveSource(String(path), String(text));
187
- },
188
- },
189
-
190
- lintProject: {
191
- description: "Lint every definition. The same rules that gate a save, a publish and CI.",
192
- inputSchema: { type: "object", properties: {} },
193
- run: async () => {
194
- needsBinding();
195
- return deps.lintProject();
196
- },
197
- },
198
-
199
- getSelection: {
200
- description:
201
- "What the developer is pointing at: the element they picked in Studio's preview, as a file, an offset and the node's path. Call this when an instruction says 'this' or 'that'.",
202
- inputSchema: { type: "object", properties: {} },
203
- run: async () => {
204
- needsBinding();
205
- return deps.selection() ?? { empty: true, note: "Nothing picked. Ask the developer to use Studio's pick tool." };
206
- },
207
- },
208
- };
209
- }
210
-
211
- /**
212
- * JSON-RPC over a single POST, which is all a local MCP client needs.
213
- *
214
- * The SDK's transport exists for sessions, SSE and resumability; none of that applies to a
215
- * loopback tool talking to one editor, so this handles `initialize`, `tools/list` and
216
- * `tools/call` directly rather than carrying that machinery.
217
- */
218
- export async function handleMcp(
219
- req: IncomingMessage,
220
- res: ServerResponse,
221
- body: unknown,
222
- tools: Record<string, Tool>,
223
- ): Promise<void> {
224
- const send = (payload: unknown, status = 200) => {
225
- res.statusCode = status;
226
- res.setHeader("content-type", "application/json");
227
- res.end(JSON.stringify(payload));
228
- };
229
- const msg = body as { id?: unknown; method?: string; params?: any };
230
- const reply = (result: unknown) => send({ jsonrpc: "2.0", id: msg?.id ?? null, result });
231
- const fail = (code: number, message: string) => send({ jsonrpc: "2.0", id: msg?.id ?? null, error: { code, message } });
232
-
233
- switch (msg?.method) {
234
- case "initialize":
235
- return reply({
236
- protocolVersion: "2024-11-05",
237
- capabilities: { tools: {} },
238
- serverInfo: { name: "unoverse-studio", version: "1" },
239
- });
240
- case "notifications/initialized":
241
- res.statusCode = 202;
242
- return void res.end();
243
- case "tools/list":
244
- return reply({
245
- tools: Object.entries(tools).map(([name, t]) => ({
246
- name,
247
- description: t.description,
248
- inputSchema: t.inputSchema,
249
- })),
250
- });
251
- case "tools/call": {
252
- const tool = tools[msg.params?.name];
253
- if (!tool) return fail(-32601, `Unknown tool: ${msg.params?.name}`);
254
- try {
255
- const out = await tool.run(msg.params?.arguments ?? {});
256
- return reply({ content: [{ type: "text", text: JSON.stringify(out, null, 2) }] });
257
- } catch (e) {
258
- // A refusal is a RESULT, not a protocol error: the agent should read why and adjust.
259
- return reply({ content: [{ type: "text", text: `Error: ${(e as Error).message}` }], isError: true });
260
- }
261
- }
262
- default:
263
- return fail(-32601, `Unsupported method: ${msg?.method}`);
264
- }
265
- }