@maravilla-labs/platform 0.11.1 → 0.13.0

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/dist/mcp.d.ts CHANGED
@@ -149,26 +149,33 @@ interface McpServerSpec {
149
149
  */
150
150
  public?: boolean;
151
151
  /**
152
- * Mini-app UI templates referenced by tools via `ui.template`. Each maps a
153
- * template `name` to `htmlPath` — a **self-contained single-file HTML widget**
154
- * (JS/CSS inlined; build it with e.g. `vite-plugin-singlefile`). Output the
155
- * build into your app's **static assets** and point `htmlPath` at that
156
- * deploy-relative path (e.g. `static/mcp-ui/reader.html`), built BEFORE the app
157
- * build so the framework ships it as a normal static asset. The runtime reads
158
- * those bytes and serves them as the `ui://<tool>/<template>` resource
159
- * (`text/html;profile=mcp-app`), which the host (Claude, ChatGPT) renders inline
160
- * — the MCP Apps standard. The widget receives its data over the MCP Apps
161
- * channel (`@modelcontextprotocol/ext-apps`, `ontoolresult` →
162
- * `structuredContent`), not via cookies.
152
+ * Mini-app UI templates referenced by tools via `ui.template`. Each renders a
153
+ * tool result as a **self-contained single-file HTML widget** the runtime
154
+ * serves inline as the `ui://<tool>/<template>` resource
155
+ * (`text/html;profile=mcp-app`) the MCP Apps standard which the host
156
+ * (Claude, ChatGPT) renders in a sandboxed iframe. The widget receives its
157
+ * data over the MCP Apps channel (`@modelcontextprotocol/ext-apps`,
158
+ * `ontoolresult` `structuredContent`), not via cookies.
163
159
  *
164
- * `csp` declares the resource's Content-Security-Policy as
165
- * {@link McpUiResourceCsp} (emitted at `_meta.ui.csp`): list the external
166
- * origins the widget may load e.g. `resourceDomains` for images/fonts the
167
- * widget references. Omit for a fully self-contained widget.
160
+ * **The build is automatic.** Put the widget *source* in `mcp-ui/<name>/`
161
+ * (or `src/mcp-ui/<name>/`) `index.html` + your component + `app.css` with
162
+ * `@import 'tailwindcss';` and the platform compiles it to a single inlined
163
+ * file during `maravilla build` (framework plugin + Tailwind + singlefile, all
164
+ * resolved for you). No second Vite config, no build script, no `@source`. See
165
+ * the `mcp-ui-mini-app` recipe.
166
+ *
167
+ * - `entry` — override the source location (a dir or an `index.html`); defaults
168
+ * to the `mcp-ui/<name>/` convention.
169
+ * - `htmlPath` — *legacy* escape hatch: point at a pre-built single-file under
170
+ * `static/` you build yourself. Prefer the convention.
171
+ * - `csp` — the resource's Content-Security-Policy ({@link McpUiResourceCsp},
172
+ * emitted at `_meta.ui.csp`): external origins the widget may load (e.g.
173
+ * `resourceDomains` for remote images). Omit for a self-contained widget.
168
174
  */
169
175
  uiTemplates?: Array<{
170
176
  name: string;
171
- htmlPath: string;
177
+ entry?: string;
178
+ htmlPath?: string;
172
179
  csp?: McpUiResourceCsp;
173
180
  }>;
174
181
  }
package/dist/mcp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["/**\n * @fileoverview MCP tool authoring helpers for Maravilla.\n *\n * User apps declare MCP tools in `mcp.ts` or `mcp/*.ts`:\n *\n * ```ts\n * import { defineMcpTool, defineMcpServer } from '@maravilla-labs/platform/mcp';\n *\n * export const server = defineMcpServer({\n * name: 'Acme Tools',\n * version: '1.0.0',\n * instructions: 'Tools for managing Acme orders.',\n * uiTemplates: [{ name: 'order-card', route: '/_mcp/ui/order-card' }],\n * });\n *\n * export const getOrder = defineMcpTool(\n * {\n * name: 'get_order',\n * description: 'Look up an order by id.',\n * inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },\n * scopes: ['acme:read'],\n * ui: { template: 'order-card' },\n * },\n * async (args, ctx) => {\n * const order = await ctx.database.findOne('orders', { _id: args.id });\n * return { content: [{ type: 'text', text: JSON.stringify(order) }] };\n * },\n * );\n * ```\n *\n * These helpers are pure factories. `defineMcpTool` produces a\n * `RegisteredMcpTool` marker object that the build pipeline\n * (`@maravilla-labs/functions` `buildMcp`) detects by its `__maravilla_mcp`\n * property; `defineMcpServer` produces a `RegisteredMcpServer` detected by\n * `__maravilla_mcp_server`. The generated bundle exposes\n * `globalThis.handleMcpTool(toolId, args, ctx)`; the Rust MCP dispatcher\n * (`crates/platform/src/mcp/dispatch.rs`) drives it via a synthetic request\n * whose body is `{ tool_id, args, identity }`.\n */\n\n// ============ Tool handler context ============\n\n/**\n * Context handed to every MCP tool handler. The first seven services mirror\n * the events `EventCtx` (and `globalThis.platform`) exactly. `user`/`client`\n * carry the authenticated identity behind the OAuth token or API key so the\n * handler — and the platform ops it calls — run as the real end-user.\n */\nexport interface McpToolContext {\n /** Per-tenant env vars. */\n env: Record<string, string>;\n /** KV store — same shape as `getPlatform().env.KV` / `platform.kv`. */\n kv?: unknown;\n /** MongoDB-style database — same shape as `getPlatform().env.DB`. */\n database?: unknown;\n /** Object storage. */\n storage?: unknown;\n /** Durable queue producer (`.send(name, payload, opts?)`). */\n queue?: { send: (name: string, payload: unknown, opts?: unknown) => Promise<string> };\n /** Auth service — register/login/logout/user CRUD/etc. */\n auth?: unknown;\n /** Web Push service. */\n push?: unknown;\n /** Full platform object — escape hatch for services not surfaced above. */\n platform?: unknown;\n /** Tenant identifier. */\n tenant: string;\n /** Trace correlation id — propagate through logs. */\n traceId: string;\n /** The end-user behind the token/key, or `null` for a client-only call. */\n user: { id: string; email: string; groups: string[]; scopes: string[] } | null;\n /** The OAuth client (agent) that authenticated, when known. */\n client: { id: string } | null;\n}\n\n// ============ Tool result shapes ============\n\n/** A single content item returned by a tool. */\nexport type McpContentItem =\n | { type: 'text'; text: string }\n | { type: 'json'; json: unknown }\n | { type: 'resource'; resource: Record<string, unknown> };\n\n/**\n * What a tool handler may return:\n * - `{ content }` — explicit MCP content items.\n * - `{ ui }` — render an iframe mini-app template (§7); optional `content`\n * is shown alongside for clients that don't support the UI.\n * - any other value — wrapped as a single `text` content item.\n */\nexport type McpToolResult =\n | { content: McpContentItem[] }\n | { ui: { template: string; data?: unknown }; content?: McpContentItem[] }\n | unknown;\n\n// ============ Registered markers ============\n\nexport const MCP_TOOL_SYMBOL = '__maravilla_mcp' as const;\nexport const MCP_SERVER_SYMBOL = '__maravilla_mcp_server' as const;\n\n/** Spec passed to {@link defineMcpTool}. */\nexport interface McpToolSpec {\n /** Tool name surfaced to the client (defaults to the export name). */\n name: string;\n /** Human-readable description sent in `tools/list`. */\n description: string;\n /** JSON Schema for the tool input, sent verbatim in `tools/list`. */\n inputSchema: Record<string, unknown>;\n /** Required scopes; dispatch enforces `scopes ⊆ identity.scopes`. */\n scopes?: string[];\n /**\n * Per-tool public opt-in. When `true`, this tool is callable with an\n * anonymous identity (no bearer) even on an otherwise-private server — its\n * `scopes` must still be a subset of the caller's, so an anonymous caller can\n * only reach it when it declares no scopes. Defaults to `false`.\n */\n public?: boolean;\n /** Links this tool to a UI template declared on the server (§7). */\n ui?: { template: string };\n}\n\nexport interface RegisteredMcpTool {\n readonly [MCP_TOOL_SYMBOL]: McpToolSpec;\n readonly handler: (args: any, ctx: McpToolContext) => McpToolResult | Promise<McpToolResult>;\n}\n\n/** Spec passed to {@link defineMcpServer}. */\nexport interface McpServerSpec {\n /** Human-readable server name (e.g. \"Acme Tools\"). */\n name: string;\n /** Optional semantic version. */\n version?: string;\n /** Optional natural-language usage instructions for the client/model. */\n instructions?: string;\n /**\n * Server-level public flag. When `true`, unauthenticated MCP requests build\n * an anonymous identity (instead of a 401) so `initialize` / `tools/list` and\n * any no-scope tool work without a bearer. Per-tool `public` opt-in still\n * works on an otherwise-private server. Defaults to `false`.\n */\n public?: boolean;\n /**\n * Mini-app UI templates referenced by tools via `ui.template`. Each maps a\n * template `name` to `htmlPath` — a **self-contained single-file HTML widget**\n * (JS/CSS inlined; build it with e.g. `vite-plugin-singlefile`). Output the\n * build into your app's **static assets** and point `htmlPath` at that\n * deploy-relative path (e.g. `static/mcp-ui/reader.html`), built BEFORE the app\n * buildso the framework ships it as a normal static asset. The runtime reads\n * those bytes and serves them as the `ui://<tool>/<template>` resource\n * (`text/html;profile=mcp-app`), which the host (Claude, ChatGPT) renders inline\n * the MCP Apps standard. The widget receives its data over the MCP Apps\n * channel (`@modelcontextprotocol/ext-apps`, `ontoolresult` →\n * `structuredContent`), not via cookies.\n *\n * `csp` declares the resource's Content-Security-Policy as\n * {@link McpUiResourceCsp} (emitted at `_meta.ui.csp`): list the external\n * origins the widget may load e.g. `resourceDomains` for images/fonts the\n * widget references. Omit for a fully self-contained widget.\n */\n uiTemplates?: Array<{ name: string; htmlPath: string; csp?: McpUiResourceCsp }>;\n}\n\n/**\n * Content-Security-Policy for a UI template resource, emitted at the resource's\n * `_meta.ui.csp` (the MCP Apps contract). The host derives CSP directives from\n * these origin lists; omitting a list blocks that class of request.\n */\nexport interface McpUiResourceCsp {\n /** `connect-src` — fetch / XHR / WebSocket origins. */\n connectDomains?: string[];\n /** `img-src` / `script-src` / `style-src` / `font-src` / `media-src` — static\n * resource origins. Supports wildcard subdomains, e.g. `https://*.example.com`. */\n resourceDomains?: string[];\n /** `frame-src` — origins allowed in nested iframes. */\n frameDomains?: string[];\n /** `base-uri` — allowed document base URIs. */\n baseUriDomains?: string[];\n}\n\nexport interface RegisteredMcpServer {\n readonly [MCP_SERVER_SYMBOL]: McpServerSpec;\n}\n\n// ============ Public factory helpers ============\n\n/**\n * Declare an MCP tool. The runtime advertises it in `tools/list` (using\n * `name`, `description`, `inputSchema`) and dispatches `tools/call` to\n * `handler`, enforcing `scopes` against the caller's granted scopes.\n */\nexport function defineMcpTool(\n spec: McpToolSpec,\n handler: (args: any, ctx: McpToolContext) => McpToolResult | Promise<McpToolResult>,\n): RegisteredMcpTool {\n return { [MCP_TOOL_SYMBOL]: spec, handler };\n}\n\n/**\n * Declare the MCP server identity and its iframe mini-app templates.\n * Optional — at most one per app; the last one discovered wins.\n */\nexport function defineMcpServer(spec: McpServerSpec): RegisteredMcpServer {\n return { [MCP_SERVER_SYMBOL]: spec };\n}\n\n/** Type guard used by the build-time discoverer and the runtime registry. */\nexport function isRegisteredMcpTool(value: unknown): value is RegisteredMcpTool {\n return (\n typeof value === 'object' &&\n value !== null &&\n MCP_TOOL_SYMBOL in value &&\n typeof (value as Record<string, unknown>).handler === 'function'\n );\n}\n\n/** Type guard for a registered MCP server descriptor. */\nexport function isRegisteredMcpServer(value: unknown): value is RegisteredMcpServer {\n return (\n typeof value === 'object' &&\n value !== null &&\n MCP_SERVER_SYMBOL in value\n );\n}\n"],"mappings":";AAiGO,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AA4F1B,SAAS,cACd,MACA,SACmB;AACnB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,QAAQ;AAC5C;AAMO,SAAS,gBAAgB,MAA0C;AACxE,SAAO,EAAE,CAAC,iBAAiB,GAAG,KAAK;AACrC;AAGO,SAAS,oBAAoB,OAA4C;AAC9E,SACE,OAAO,UAAU,YACjB,UAAU,QACV,mBAAmB,SACnB,OAAQ,MAAkC,YAAY;AAE1D;AAGO,SAAS,sBAAsB,OAA8C;AAClF,SACE,OAAO,UAAU,YACjB,UAAU,QACV,qBAAqB;AAEzB;","names":[]}
1
+ {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["/**\n * @fileoverview MCP tool authoring helpers for Maravilla.\n *\n * User apps declare MCP tools in `mcp.ts` or `mcp/*.ts`:\n *\n * ```ts\n * import { defineMcpTool, defineMcpServer } from '@maravilla-labs/platform/mcp';\n *\n * export const server = defineMcpServer({\n * name: 'Acme Tools',\n * version: '1.0.0',\n * instructions: 'Tools for managing Acme orders.',\n * uiTemplates: [{ name: 'order-card', route: '/_mcp/ui/order-card' }],\n * });\n *\n * export const getOrder = defineMcpTool(\n * {\n * name: 'get_order',\n * description: 'Look up an order by id.',\n * inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },\n * scopes: ['acme:read'],\n * ui: { template: 'order-card' },\n * },\n * async (args, ctx) => {\n * const order = await ctx.database.findOne('orders', { _id: args.id });\n * return { content: [{ type: 'text', text: JSON.stringify(order) }] };\n * },\n * );\n * ```\n *\n * These helpers are pure factories. `defineMcpTool` produces a\n * `RegisteredMcpTool` marker object that the build pipeline\n * (`@maravilla-labs/functions` `buildMcp`) detects by its `__maravilla_mcp`\n * property; `defineMcpServer` produces a `RegisteredMcpServer` detected by\n * `__maravilla_mcp_server`. The generated bundle exposes\n * `globalThis.handleMcpTool(toolId, args, ctx)`; the Rust MCP dispatcher\n * (`crates/platform/src/mcp/dispatch.rs`) drives it via a synthetic request\n * whose body is `{ tool_id, args, identity }`.\n */\n\n// ============ Tool handler context ============\n\n/**\n * Context handed to every MCP tool handler. The first seven services mirror\n * the events `EventCtx` (and `globalThis.platform`) exactly. `user`/`client`\n * carry the authenticated identity behind the OAuth token or API key so the\n * handler — and the platform ops it calls — run as the real end-user.\n */\nexport interface McpToolContext {\n /** Per-tenant env vars. */\n env: Record<string, string>;\n /** KV store — same shape as `getPlatform().env.KV` / `platform.kv`. */\n kv?: unknown;\n /** MongoDB-style database — same shape as `getPlatform().env.DB`. */\n database?: unknown;\n /** Object storage. */\n storage?: unknown;\n /** Durable queue producer (`.send(name, payload, opts?)`). */\n queue?: { send: (name: string, payload: unknown, opts?: unknown) => Promise<string> };\n /** Auth service — register/login/logout/user CRUD/etc. */\n auth?: unknown;\n /** Web Push service. */\n push?: unknown;\n /** Full platform object — escape hatch for services not surfaced above. */\n platform?: unknown;\n /** Tenant identifier. */\n tenant: string;\n /** Trace correlation id — propagate through logs. */\n traceId: string;\n /** The end-user behind the token/key, or `null` for a client-only call. */\n user: { id: string; email: string; groups: string[]; scopes: string[] } | null;\n /** The OAuth client (agent) that authenticated, when known. */\n client: { id: string } | null;\n}\n\n// ============ Tool result shapes ============\n\n/** A single content item returned by a tool. */\nexport type McpContentItem =\n | { type: 'text'; text: string }\n | { type: 'json'; json: unknown }\n | { type: 'resource'; resource: Record<string, unknown> };\n\n/**\n * What a tool handler may return:\n * - `{ content }` — explicit MCP content items.\n * - `{ ui }` — render an iframe mini-app template (§7); optional `content`\n * is shown alongside for clients that don't support the UI.\n * - any other value — wrapped as a single `text` content item.\n */\nexport type McpToolResult =\n | { content: McpContentItem[] }\n | { ui: { template: string; data?: unknown }; content?: McpContentItem[] }\n | unknown;\n\n// ============ Registered markers ============\n\nexport const MCP_TOOL_SYMBOL = '__maravilla_mcp' as const;\nexport const MCP_SERVER_SYMBOL = '__maravilla_mcp_server' as const;\n\n/** Spec passed to {@link defineMcpTool}. */\nexport interface McpToolSpec {\n /** Tool name surfaced to the client (defaults to the export name). */\n name: string;\n /** Human-readable description sent in `tools/list`. */\n description: string;\n /** JSON Schema for the tool input, sent verbatim in `tools/list`. */\n inputSchema: Record<string, unknown>;\n /** Required scopes; dispatch enforces `scopes ⊆ identity.scopes`. */\n scopes?: string[];\n /**\n * Per-tool public opt-in. When `true`, this tool is callable with an\n * anonymous identity (no bearer) even on an otherwise-private server — its\n * `scopes` must still be a subset of the caller's, so an anonymous caller can\n * only reach it when it declares no scopes. Defaults to `false`.\n */\n public?: boolean;\n /** Links this tool to a UI template declared on the server (§7). */\n ui?: { template: string };\n}\n\nexport interface RegisteredMcpTool {\n readonly [MCP_TOOL_SYMBOL]: McpToolSpec;\n readonly handler: (args: any, ctx: McpToolContext) => McpToolResult | Promise<McpToolResult>;\n}\n\n/** Spec passed to {@link defineMcpServer}. */\nexport interface McpServerSpec {\n /** Human-readable server name (e.g. \"Acme Tools\"). */\n name: string;\n /** Optional semantic version. */\n version?: string;\n /** Optional natural-language usage instructions for the client/model. */\n instructions?: string;\n /**\n * Server-level public flag. When `true`, unauthenticated MCP requests build\n * an anonymous identity (instead of a 401) so `initialize` / `tools/list` and\n * any no-scope tool work without a bearer. Per-tool `public` opt-in still\n * works on an otherwise-private server. Defaults to `false`.\n */\n public?: boolean;\n /**\n * Mini-app UI templates referenced by tools via `ui.template`. Each renders a\n * tool result as a **self-contained single-file HTML widget** the runtime\n * serves inline as the `ui://<tool>/<template>` resource\n * (`text/html;profile=mcp-app`) the MCP Apps standard — which the host\n * (Claude, ChatGPT) renders in a sandboxed iframe. The widget receives its\n * data over the MCP Apps channel (`@modelcontextprotocol/ext-apps`,\n * `ontoolresult` `structuredContent`), not via cookies.\n *\n * **The build is automatic.** Put the widget *source* in `mcp-ui/<name>/`\n * (or `src/mcp-ui/<name>/`) — `index.html` + your component + `app.css` with\n * `@import 'tailwindcss';` and the platform compiles it to a single inlined\n * file during `maravilla build` (framework plugin + Tailwind + singlefile, all\n * resolved for you). No second Vite config, no build script, no `@source`. See\n * the `mcp-ui-mini-app` recipe.\n *\n * - `entry` — override the source location (a dir or an `index.html`); defaults\n * to the `mcp-ui/<name>/` convention.\n * - `htmlPath` *legacy* escape hatch: point at a pre-built single-file under\n * `static/` you build yourself. Prefer the convention.\n * - `csp` the resource's Content-Security-Policy ({@link McpUiResourceCsp},\n * emitted at `_meta.ui.csp`): external origins the widget may load (e.g.\n * `resourceDomains` for remote images). Omit for a self-contained widget.\n */\n uiTemplates?: Array<{ name: string; entry?: string; htmlPath?: string; csp?: McpUiResourceCsp }>;\n}\n\n/**\n * Content-Security-Policy for a UI template resource, emitted at the resource's\n * `_meta.ui.csp` (the MCP Apps contract). The host derives CSP directives from\n * these origin lists; omitting a list blocks that class of request.\n */\nexport interface McpUiResourceCsp {\n /** `connect-src` — fetch / XHR / WebSocket origins. */\n connectDomains?: string[];\n /** `img-src` / `script-src` / `style-src` / `font-src` / `media-src` — static\n * resource origins. Supports wildcard subdomains, e.g. `https://*.example.com`. */\n resourceDomains?: string[];\n /** `frame-src` — origins allowed in nested iframes. */\n frameDomains?: string[];\n /** `base-uri` — allowed document base URIs. */\n baseUriDomains?: string[];\n}\n\nexport interface RegisteredMcpServer {\n readonly [MCP_SERVER_SYMBOL]: McpServerSpec;\n}\n\n// ============ Public factory helpers ============\n\n/**\n * Declare an MCP tool. The runtime advertises it in `tools/list` (using\n * `name`, `description`, `inputSchema`) and dispatches `tools/call` to\n * `handler`, enforcing `scopes` against the caller's granted scopes.\n */\nexport function defineMcpTool(\n spec: McpToolSpec,\n handler: (args: any, ctx: McpToolContext) => McpToolResult | Promise<McpToolResult>,\n): RegisteredMcpTool {\n return { [MCP_TOOL_SYMBOL]: spec, handler };\n}\n\n/**\n * Declare the MCP server identity and its iframe mini-app templates.\n * Optional — at most one per app; the last one discovered wins.\n */\nexport function defineMcpServer(spec: McpServerSpec): RegisteredMcpServer {\n return { [MCP_SERVER_SYMBOL]: spec };\n}\n\n/** Type guard used by the build-time discoverer and the runtime registry. */\nexport function isRegisteredMcpTool(value: unknown): value is RegisteredMcpTool {\n return (\n typeof value === 'object' &&\n value !== null &&\n MCP_TOOL_SYMBOL in value &&\n typeof (value as Record<string, unknown>).handler === 'function'\n );\n}\n\n/** Type guard for a registered MCP server descriptor. */\nexport function isRegisteredMcpServer(value: unknown): value is RegisteredMcpServer {\n return (\n typeof value === 'object' &&\n value !== null &&\n MCP_SERVER_SYMBOL in value\n );\n}\n"],"mappings":";AAiGO,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAkG1B,SAAS,cACd,MACA,SACmB;AACnB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,QAAQ;AAC5C;AAMO,SAAS,gBAAgB,MAA0C;AACxE,SAAO,EAAE,CAAC,iBAAiB,GAAG,KAAK;AACrC;AAGO,SAAS,oBAAoB,OAA4C;AAC9E,SACE,OAAO,UAAU,YACjB,UAAU,QACV,mBAAmB,SACnB,OAAQ,MAAkC,YAAY;AAE1D;AAGO,SAAS,sBAAsB,OAA8C;AAClF,SACE,OAAO,UAAU,YACjB,UAAU,QACV,qBAAqB;AAEzB;","names":[]}
@@ -43,6 +43,43 @@ interface ThumbnailOpts {
43
43
  format?: ImageFormat;
44
44
  quality?: number;
45
45
  }
46
+ /**
47
+ * Options for `transforms.extractFrames` — decode a video into a sequence of
48
+ * still images at a fixed fps. General capability (any MP4 → frames); the
49
+ * frames renderer consumes the result to swap `<video>`→`<img>` per frame, and
50
+ * it's reusable for thumbnails/scrubbing.
51
+ */
52
+ interface ExtractFramesOpts {
53
+ /** Frames per second to sample the source at. */
54
+ fps: number;
55
+ /** Optional max width (height auto, aspect preserved, no upscaling). */
56
+ width?: number;
57
+ /** Output image format. Defaults to `"jpg"` server-side when omitted. */
58
+ format?: ImageFormat;
59
+ /** JPEG/WebP quality 1..=100 (higher = better). Ignored for PNG. */
60
+ quality?: number;
61
+ /** Start offset into the clip, ms (default 0). */
62
+ seek_ms?: number;
63
+ /** How much of the clip to extract from `seek_ms`, ms (default: to end). */
64
+ duration_ms?: number;
65
+ }
66
+ /**
67
+ * Result of an `extractFrames` job: a JSON manifest stored at the job's
68
+ * `output_key` describing the frame set. Frame `i` (0-based) lives at
69
+ * `${prefix}/frame_${i padded to 6}.${ext}`.
70
+ */
71
+ interface FramesetManifest {
72
+ /** Storage key prefix the frame images live under (no trailing slash). */
73
+ prefix: string;
74
+ /** Number of frames written. */
75
+ count: number;
76
+ /** Sampling fps the frames were extracted at. */
77
+ fps: number;
78
+ /** Frame image format. */
79
+ format: ImageFormat;
80
+ width?: number;
81
+ height?: number;
82
+ }
46
83
  /** Options for `transforms.resize`. */
47
84
  interface ResizeOpts {
48
85
  width?: number;
@@ -174,6 +211,8 @@ type TransformSpec = ({
174
211
  } & TranscodeOpts) | ({
175
212
  kind: 'thumbnail';
176
213
  } & ThumbnailOpts) | ({
214
+ kind: 'extract_frames';
215
+ } & ExtractFramesOpts) | ({
177
216
  kind: 'resize';
178
217
  } & ResizeOpts) | ({
179
218
  kind: 'ocr';
@@ -219,6 +258,12 @@ interface JobStatusResponse {
219
258
  interface TransformsService {
220
259
  transcode(srcKey: string, opts: TranscodeOpts): Promise<JobHandle>;
221
260
  thumbnail(srcKey: string, opts: ThumbnailOpts): Promise<JobHandle>;
261
+ /**
262
+ * Decode a video into a sequence of frame images at a fixed fps. Resolves to
263
+ * a `JobHandle` whose `output_key` holds a `FramesetManifest` (JSON) once
264
+ * complete; frames live under `${manifest.prefix}/frame_NNNNNN.<ext>`.
265
+ */
266
+ extractFrames(srcKey: string, opts: ExtractFramesOpts): Promise<JobHandle>;
222
267
  resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
223
268
  probe(srcKey: string): Promise<MediaInfo>;
224
269
  ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
@@ -298,4 +343,4 @@ interface TransformsPatternSpec {
298
343
  */
299
344
  type TransformsConfig = Record<string, TransformsPatternSpec>;
300
345
 
301
- export { type DocFormat as D, type ImageFormat as I, type JobStatus as J, type MediaInfo as M, type OcrOpts as O, type QrCodeSpec as Q, type ResizeOpts as R, type TransformsConfig as T, type VideoFormat as V, type TransformsPatternSpec as a, type TranscodeOpts as b, type ThumbnailOpts as c, type DocToPdfOpts as d, type DocThumbnailOpts as e, type DocConvertOpts as f, type DocToMarkdownOpts as g, type DocToHtmlOpts as h, type ImageRef as i, type DocReplaceImagesOpts as j, type DocInsertQrCodeOpts as k, type QrPayload as l, type DocTemplateMergeOpts as m, type TransformSpec as n, type JobHandle as o, type JobStatusResponse as p, type TransformsService as q, keyFor as r, transforms as t };
346
+ export { type DocFormat as D, type ExtractFramesOpts as E, type FramesetManifest as F, type ImageFormat as I, type JobStatus as J, type MediaInfo as M, type OcrOpts as O, type QrCodeSpec as Q, type ResizeOpts as R, type TransformsConfig as T, type VideoFormat as V, type TransformsPatternSpec as a, type TranscodeOpts as b, type ThumbnailOpts as c, type DocToPdfOpts as d, type DocThumbnailOpts as e, type DocConvertOpts as f, type DocToMarkdownOpts as g, type DocToHtmlOpts as h, type ImageRef as i, type DocReplaceImagesOpts as j, type DocInsertQrCodeOpts as k, type QrPayload as l, type DocTemplateMergeOpts as m, type TransformSpec as n, type JobHandle as o, type JobStatusResponse as p, type TransformsService as q, keyFor as r, transforms as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maravilla-labs/platform",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "description": "Universal platform client for Maravilla runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/mcp.ts CHANGED
@@ -140,24 +140,30 @@ export interface McpServerSpec {
140
140
  */
141
141
  public?: boolean;
142
142
  /**
143
- * Mini-app UI templates referenced by tools via `ui.template`. Each maps a
144
- * template `name` to `htmlPath` — a **self-contained single-file HTML widget**
145
- * (JS/CSS inlined; build it with e.g. `vite-plugin-singlefile`). Output the
146
- * build into your app's **static assets** and point `htmlPath` at that
147
- * deploy-relative path (e.g. `static/mcp-ui/reader.html`), built BEFORE the app
148
- * build so the framework ships it as a normal static asset. The runtime reads
149
- * those bytes and serves them as the `ui://<tool>/<template>` resource
150
- * (`text/html;profile=mcp-app`), which the host (Claude, ChatGPT) renders inline
151
- * — the MCP Apps standard. The widget receives its data over the MCP Apps
152
- * channel (`@modelcontextprotocol/ext-apps`, `ontoolresult` →
153
- * `structuredContent`), not via cookies.
143
+ * Mini-app UI templates referenced by tools via `ui.template`. Each renders a
144
+ * tool result as a **self-contained single-file HTML widget** the runtime
145
+ * serves inline as the `ui://<tool>/<template>` resource
146
+ * (`text/html;profile=mcp-app`) the MCP Apps standard which the host
147
+ * (Claude, ChatGPT) renders in a sandboxed iframe. The widget receives its
148
+ * data over the MCP Apps channel (`@modelcontextprotocol/ext-apps`,
149
+ * `ontoolresult` `structuredContent`), not via cookies.
154
150
  *
155
- * `csp` declares the resource's Content-Security-Policy as
156
- * {@link McpUiResourceCsp} (emitted at `_meta.ui.csp`): list the external
157
- * origins the widget may load e.g. `resourceDomains` for images/fonts the
158
- * widget references. Omit for a fully self-contained widget.
151
+ * **The build is automatic.** Put the widget *source* in `mcp-ui/<name>/`
152
+ * (or `src/mcp-ui/<name>/`) `index.html` + your component + `app.css` with
153
+ * `@import 'tailwindcss';` and the platform compiles it to a single inlined
154
+ * file during `maravilla build` (framework plugin + Tailwind + singlefile, all
155
+ * resolved for you). No second Vite config, no build script, no `@source`. See
156
+ * the `mcp-ui-mini-app` recipe.
157
+ *
158
+ * - `entry` — override the source location (a dir or an `index.html`); defaults
159
+ * to the `mcp-ui/<name>/` convention.
160
+ * - `htmlPath` — *legacy* escape hatch: point at a pre-built single-file under
161
+ * `static/` you build yourself. Prefer the convention.
162
+ * - `csp` — the resource's Content-Security-Policy ({@link McpUiResourceCsp},
163
+ * emitted at `_meta.ui.csp`): external origins the widget may load (e.g.
164
+ * `resourceDomains` for remote images). Omit for a self-contained widget.
159
165
  */
160
- uiTemplates?: Array<{ name: string; htmlPath: string; csp?: McpUiResourceCsp }>;
166
+ uiTemplates?: Array<{ name: string; entry?: string; htmlPath?: string; csp?: McpUiResourceCsp }>;
161
167
  }
162
168
 
163
169
  /**
package/src/transforms.ts CHANGED
@@ -93,6 +93,45 @@ export interface ThumbnailOpts {
93
93
  quality?: number;
94
94
  }
95
95
 
96
+ /**
97
+ * Options for `transforms.extractFrames` — decode a video into a sequence of
98
+ * still images at a fixed fps. General capability (any MP4 → frames); the
99
+ * frames renderer consumes the result to swap `<video>`→`<img>` per frame, and
100
+ * it's reusable for thumbnails/scrubbing.
101
+ */
102
+ export interface ExtractFramesOpts {
103
+ /** Frames per second to sample the source at. */
104
+ fps: number;
105
+ /** Optional max width (height auto, aspect preserved, no upscaling). */
106
+ width?: number;
107
+ /** Output image format. Defaults to `"jpg"` server-side when omitted. */
108
+ format?: ImageFormat;
109
+ /** JPEG/WebP quality 1..=100 (higher = better). Ignored for PNG. */
110
+ quality?: number;
111
+ /** Start offset into the clip, ms (default 0). */
112
+ seek_ms?: number;
113
+ /** How much of the clip to extract from `seek_ms`, ms (default: to end). */
114
+ duration_ms?: number;
115
+ }
116
+
117
+ /**
118
+ * Result of an `extractFrames` job: a JSON manifest stored at the job's
119
+ * `output_key` describing the frame set. Frame `i` (0-based) lives at
120
+ * `${prefix}/frame_${i padded to 6}.${ext}`.
121
+ */
122
+ export interface FramesetManifest {
123
+ /** Storage key prefix the frame images live under (no trailing slash). */
124
+ prefix: string;
125
+ /** Number of frames written. */
126
+ count: number;
127
+ /** Sampling fps the frames were extracted at. */
128
+ fps: number;
129
+ /** Frame image format. */
130
+ format: ImageFormat;
131
+ width?: number;
132
+ height?: number;
133
+ }
134
+
96
135
  /** Options for `transforms.resize`. */
97
136
  export interface ResizeOpts {
98
137
  width?: number;
@@ -236,6 +275,7 @@ export interface DocTemplateMergeOpts {
236
275
  export type TransformSpec =
237
276
  | ({ kind: 'transcode' } & TranscodeOpts)
238
277
  | ({ kind: 'thumbnail' } & ThumbnailOpts)
278
+ | ({ kind: 'extract_frames' } & ExtractFramesOpts)
239
279
  | ({ kind: 'resize' } & ResizeOpts)
240
280
  | ({ kind: 'ocr' } & OcrOpts)
241
281
  | ({ kind: 'doc_to_pdf' } & DocToPdfOpts)
@@ -277,6 +317,12 @@ export interface JobStatusResponse {
277
317
  export interface TransformsService {
278
318
  transcode(srcKey: string, opts: TranscodeOpts): Promise<JobHandle>;
279
319
  thumbnail(srcKey: string, opts: ThumbnailOpts): Promise<JobHandle>;
320
+ /**
321
+ * Decode a video into a sequence of frame images at a fixed fps. Resolves to
322
+ * a `JobHandle` whose `output_key` holds a `FramesetManifest` (JSON) once
323
+ * complete; frames live under `${manifest.prefix}/frame_NNNNNN.<ext>`.
324
+ */
325
+ extractFrames(srcKey: string, opts: ExtractFramesOpts): Promise<JobHandle>;
280
326
  resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
281
327
  probe(srcKey: string): Promise<MediaInfo>;
282
328
  ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
@@ -340,6 +386,9 @@ function outputExtension(spec: TransformSpec): string {
340
386
  switch (spec.kind) {
341
387
  case 'transcode':
342
388
  return spec.format; // 'mp4' | 'webm'
389
+ case 'extract_frames':
390
+ // Output key is the JSON frameset manifest; frames live under its base.
391
+ return 'json';
343
392
  case 'thumbnail':
344
393
  case 'resize': {
345
394
  // thumbnail's `format` is optional in TS (defaults to jpg server-side);