@unoverse-platform/studio 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -20,7 +20,8 @@
20
20
  "react-dom": "^18.2.0",
21
21
  "react-oidc-context": "^3.1.0",
22
22
  "tailwindcss": "^4.0.0",
23
- "vite": "^6.0.0"
23
+ "vite": "^6.0.0",
24
+ "@modelcontextprotocol/sdk": "^1.25.1"
24
25
  },
25
26
  "license": "MIT",
26
27
  "bin": {
@@ -0,0 +1,106 @@
1
+ /**
2
+ * THE NATIVE HOST (2026-07-28) — the whole point of MCP apps, applied to Studio itself.
3
+ *
4
+ * Live preview does NOT render with a compiled-in SDK. Studio is a bare MCP client:
5
+ * it reads `ui://unoverse/app.html` from the connected universe (the SAME single-file
6
+ * renderer ChatGPT and Claude load), puts it in an iframe, and posts it the standard
7
+ * `unoverse:config` message (web/sdk/src/main.tsx boot contract). From that moment the
8
+ * app drives its own MCP + /stream + voice connections against the universe — Studio
9
+ * hosts it exactly like any third-party surface would. The renderer loads when it is
10
+ * required, from the thing that owns it. Nothing is vendored, nothing is bundled.
11
+ */
12
+ import { useEffect, useRef, useState } from "react";
13
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
14
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
15
+
16
+ const APP_UI_URI = "ui://unoverse/app.html";
17
+
18
+ export function NativeAppHost({
19
+ server,
20
+ token,
21
+ templateId,
22
+ userId,
23
+ conversationId,
24
+ }: {
25
+ /** Universe origin, e.g. http://localhost:4105 — the /mcp and /stream host. */
26
+ server: string;
27
+ token?: string;
28
+ templateId?: string;
29
+ userId: string;
30
+ conversationId: string;
31
+ }) {
32
+ const [html, setHtml] = useState<string | null>(null);
33
+ const [error, setError] = useState<string | null>(null);
34
+ const frame = useRef<HTMLIFrameElement>(null);
35
+
36
+ // Pull the app shell from the universe — one resources/read, then the client closes.
37
+ useEffect(() => {
38
+ let cancelled = false;
39
+ setHtml(null);
40
+ setError(null);
41
+ (async () => {
42
+ const authFetch = token
43
+ ? async (url: string | URL, init?: RequestInit): Promise<Response> => {
44
+ const headers = new Headers(init?.headers);
45
+ headers.set("Authorization", `Bearer ${token}`);
46
+ return fetch(url, { ...init, headers });
47
+ }
48
+ : undefined;
49
+ const client = new Client({ name: "unoverse-studio-host", version: "1.0.0" });
50
+ try {
51
+ const transport = new StreamableHTTPClientTransport(
52
+ new URL(`${server.replace(/\/$/, "")}/mcp`),
53
+ authFetch ? { fetch: authFetch } : undefined,
54
+ );
55
+ await client.connect(transport);
56
+ const res = await client.readResource({ uri: APP_UI_URI });
57
+ const text = (res.contents?.[0] as { text?: string } | undefined)?.text;
58
+ if (!text) throw new Error(`${APP_UI_URI} returned no content — is the webSDK built on the universe?`);
59
+ if (!cancelled) setHtml(text);
60
+ } catch (e) {
61
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
62
+ } finally {
63
+ client.close().catch(() => {});
64
+ }
65
+ })();
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [server, token]);
70
+
71
+ // The boot contract: post `unoverse:config` on iframe load, and re-post whenever the
72
+ // config changes (also the token-refresh channel — main.tsx applies updates in place).
73
+ const postConfig = () => {
74
+ frame.current?.contentWindow?.postMessage(
75
+ {
76
+ type: "unoverse:config",
77
+ config: { serverUrl: server, apiUrl: server, templateId, userId, conversationId, token },
78
+ },
79
+ "*",
80
+ );
81
+ };
82
+ // eslint-disable-next-line react-hooks/exhaustive-deps
83
+ useEffect(postConfig, [token, templateId, userId, conversationId, server, html]);
84
+
85
+ if (error)
86
+ return (
87
+ <div className="flex h-full items-center justify-center p-8 text-center text-sm text-red-500">
88
+ <div>
89
+ <div className="font-semibold">The universe did not serve the app</div>
90
+ <div className="mt-1 opacity-80">{error}</div>
91
+ </div>
92
+ </div>
93
+ );
94
+ if (!html)
95
+ return <div className="flex h-full items-center justify-center text-sm opacity-60">Loading the app from the universe…</div>;
96
+ return (
97
+ <iframe
98
+ ref={frame}
99
+ srcDoc={html}
100
+ onLoad={postConfig}
101
+ title="unoverse app"
102
+ style={{ width: "100%", height: "100%", border: 0, display: "block" }}
103
+ allow="microphone; autoplay"
104
+ />
105
+ );
106
+ }
@@ -12,6 +12,7 @@ import { useAuth } from "react-oidc-context";
12
12
  import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
13
13
  import { ComponentStore } from "@unoverse/sdk";
14
14
  import { StreamedUnoverseTemplate, useUnoverseConnection, computeAppWidth, resolveActiveLayout, type ResolvedTheme, type UseVoiceServiceConfig } from "@unoverse/sdk";
15
+ import { NativeAppHost } from "./NativeAppHost";
15
16
  import { client, authedFetch, hasAuth, UNOVERSE_BASE } from "studio-host";
16
17
 
17
18
  // Per-APP test conversation: each app chats in its own conversation id, so testing
@@ -278,10 +279,12 @@ export function TemplatesView({ theme: defaultTheme, themeName, org = "all" }: {
278
279
  [binding?.workflow, binding?.trigger, tokenSub],
279
280
  );
280
281
 
281
- // The data-plane connection. Open it only when: Live mode, authenticated (or no OIDC),
282
- // AND the selected app actually has a workflow binding (a plain template with no app
283
- // has nothing to call).
284
- const canConnect = live && Boolean(binding?.workflow) && (!hasAuth || Boolean(auth?.isAuthenticated));
282
+ // NATIVE (2026-07-28): Live mode is the NATIVE HOST an iframe running the
283
+ // universe-served ui:// app (NativeAppHost), which opens its OWN connections. The
284
+ // in-page data-plane connection below is therefore never opened; it remains only
285
+ // because Mock rendering shares this machinery (store, states, width).
286
+ const liveIsNative = live && Boolean(binding?.workflow) && (!hasAuth || Boolean(auth?.isAuthenticated));
287
+ const canConnect = false;
285
288
  const rawConn = useUnoverseConnection(config, session, store, { enabled: canConnect });
286
289
  // HARD INVARIANT (workbench doc Mode A — "Mock does not connect with server"): a Mock
287
290
  // session must NEVER reach the backend. The SDK only gates the inbound SSE stream on
@@ -874,6 +877,17 @@ export function TemplatesView({ theme: defaultTheme, themeName, org = "all" }: {
874
877
  ...((selectedDef?.fluidHeight ?? true) ? { height: "100%" } : {}),
875
878
  }}
876
879
  >
880
+ {liveIsNative ? (
881
+ /* NATIVE: the universe serves the app; Studio just hosts it. Same
882
+ ui:// shell ChatGPT loads, same boot contract, its own connections. */
883
+ <NativeAppHost
884
+ server={UNOVERSE_BASE || `http://${connCfg.server}`}
885
+ token={token}
886
+ templateId={`${selectedDef?.org ? `${selectedDef.org}/` : ""}${selected ?? ""}`}
887
+ userId={session.userId}
888
+ conversationId={session.conversationId}
889
+ />
890
+ ) : (
877
891
  <StreamedUnoverseTemplate
878
892
  client={client}
879
893
  store={store}
@@ -968,6 +982,7 @@ export function TemplatesView({ theme: defaultTheme, themeName, org = "all" }: {
968
982
  setVersion((v) => v + 1);
969
983
  }}
970
984
  />
985
+ )}
971
986
  </div>
972
987
  </div>
973
988
  )}
package/vite.config.ts CHANGED
@@ -39,14 +39,14 @@ export default defineConfig({
39
39
  "@unoverse/studio": resolve(__dirname, "screens/index.ts"),
40
40
  // The host seam. Canvas aliases this to a universe; here it is the local disk.
41
41
  "studio-host": resolve(__dirname, "src/host.ts"),
42
- // The renderer, vendored source (same convention as Canvas and the old studio).
43
- // THE VENDORED COPY WHEN THERE IS ONE. The monorepo path below exists only here;
44
- // an installed Studio has `vendor/sdk`, written by prepack (scripts/vendor-sdk.mjs).
45
- // Preferring the vendor directory is what makes the published package work at all,
46
- // and falling back keeps the monorepo editing the real source with HMR.
47
- "@unoverse/sdk": existsSync(resolve(__dirname, "vendor/sdk/lib/index.ts"))
48
- ? resolve(__dirname, "vendor/sdk/lib/index.ts")
49
- : resolve(__dirname, "../unoverse/web/sdk/src/lib/index.ts"),
42
+ // The renderer. THE REAL SOURCE WHEN IT EXISTS (the monorepo), the vendored copy
43
+ // otherwise (an installed package, where prepack wrote vendor/sdk and the monorepo
44
+ // path resolves to nothing). The old order preferred vendor so the moment a
45
+ // publish ran vendor-sdk.mjs, the monorepo's own Studio silently switched onto a
46
+ // FROZEN SDK copy and live SDK edits stopped appearing (2026-07-28).
47
+ "@unoverse/sdk": existsSync(resolve(__dirname, "../unoverse/web/sdk/src/lib/index.ts"))
48
+ ? resolve(__dirname, "../unoverse/web/sdk/src/lib/index.ts")
49
+ : resolve(__dirname, "vendor/sdk/lib/index.ts"),
50
50
  },
51
51
  },
52
52
  ssr: {