All files / src/mcp-runtime index.tsx

0% Statements 0/61
100% Branches 1/1
100% Functions 1/1
0% Lines 0/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105                                                                                                                                                                                                                 
/**
 * Grackle React runtime (#1268) — the single MCP Apps resource (`ui://grackle/runtime`)
 * that renders agent-authored React/JSX inline in the chat.
 *
 * Bundled self-contained (React + react-live + the curated Grackle component set +
 * the ext-apps guest bridge) by `vite.config.ts` into `mcp-app-runtime/runtime.js`
 * and served from the sandbox origin. The host (`McpAppWidget`) writes a tiny
 * bootstrap that loads this script, then delivers `{ source, props }` as MCP Apps
 * tool input. The runtime transpiles + evaluates the JSX via react-live against the
 * component scope and paints it. This is the load-bearing slice of the GenUX
 * component-registry vision (render-by-source; the registry comes in later phases).
 */
import { useState, type JSX } from "react";
import * as React from "react";
import { createRoot } from "react-dom/client";
import { LiveProvider, LivePreview, LiveError } from "react-live";
import { useApp, useHostStyleVariables } from "@modelcontextprotocol/ext-apps/react";
import { COMPONENT_SCOPE } from "./component-scope.js";
import { composeSource, type RenderDependency } from "./compose-source.js";
 
/** Identity reported to the host during the MCP Apps guest handshake. */
const APP_INFO: Readonly<{ name: string; version: string }> = {
  name: "GrackleReactRuntime",
  version: "0.1.0",
};
 
/** The agent-supplied render request delivered via MCP Apps tool input. */
interface RenderInput {
  /** JSX source; must call `render(<Component {...props}/>)` (react-live noInline). */
  source: string;
  /** Data bound into the component, available as `props` in the JSX scope. */
  props: Record<string, unknown>;
  /** Resolved registry dependencies in eval order (deepest first), for composition (#1270). */
  components: RenderDependency[];
}
 
/** Connects the guest bridge, receives the JSX + props, and renders via react-live. */
function Runtime(): JSX.Element {
  const [input, setInput] = useState<RenderInput | undefined>(undefined);
 
  const { app, error } = useApp({
    appInfo: APP_INFO,
    capabilities: {},
    onAppCreated: (createdApp): void => {
      createdApp.ontoolinput = (params): void => {
        const args = (params.arguments ?? {}) as {
          source?: unknown;
          props?: unknown;
          components?: unknown;
        };
        const components: RenderDependency[] = Array.isArray(args.components)
          ? (args.components as unknown[]).filter(
              (d): d is RenderDependency =>
                d !== null &&
                typeof d === "object" &&
                typeof (d as RenderDependency).name === "string" &&
                typeof (d as RenderDependency).body === "string",
            )
          : [];
        setInput({
          source: typeof args.source === "string" ? args.source : "",
          props:
            args.props !== null && typeof args.props === "object"
              ? (args.props as Record<string, unknown>)
              : {},
          components,
        });
      };
    },
  });
 
  // Mirror the host theme + style tokens onto this document (light-dark + CSS vars).
  useHostStyleVariables(app, app?.getHostContext());
 
  if (error) {
    return (
      <div style={{ color: "var(--color-text-danger, #c00)" }}>Runtime error: {error.message}</div>
    );
  }
  if (!input) {
    return <div style={{ opacity: 0.6, padding: "0.5rem" }}>Loading component…</div>;
  }
 
  // react-live transpiles the JSX (sucrase) and evaluates it with `new Function`
  // (requires `script-src 'unsafe-eval'`, set on this resource's sandbox CSP). The
  // origin-isolated sandbox + restricted connect-src keep that safe (#1268).
  const scope: Record<string, unknown> = { React, props: input.props, ...COMPONENT_SCOPE };
  return (
    <LiveProvider code={composeSource(input.source, input.components)} noInline scope={scope}>
      <LivePreview />
      <LiveError />
    </LiveProvider>
  );
}
 
/** Mount the runtime into the bootstrap's root element. */
function main(): void {
  const rootEl: HTMLElement | null = document.getElementById("grackle-root");
  if (rootEl) {
    createRoot(rootEl).render(<Runtime />);
  }
}
 
main();