@maravilla-labs/platform 0.8.1 → 0.9.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 ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @fileoverview MCP tool authoring helpers for Maravilla.
3
+ *
4
+ * User apps declare MCP tools in `mcp.ts` or `mcp/*.ts`:
5
+ *
6
+ * ```ts
7
+ * import { defineMcpTool, defineMcpServer } from '@maravilla-labs/platform/mcp';
8
+ *
9
+ * export const server = defineMcpServer({
10
+ * name: 'Acme Tools',
11
+ * version: '1.0.0',
12
+ * instructions: 'Tools for managing Acme orders.',
13
+ * uiTemplates: [{ name: 'order-card', route: '/_mcp/ui/order-card' }],
14
+ * });
15
+ *
16
+ * export const getOrder = defineMcpTool(
17
+ * {
18
+ * name: 'get_order',
19
+ * description: 'Look up an order by id.',
20
+ * inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
21
+ * scopes: ['acme:read'],
22
+ * ui: { template: 'order-card' },
23
+ * },
24
+ * async (args, ctx) => {
25
+ * const order = await ctx.database.findOne('orders', { _id: args.id });
26
+ * return { content: [{ type: 'text', text: JSON.stringify(order) }] };
27
+ * },
28
+ * );
29
+ * ```
30
+ *
31
+ * These helpers are pure factories. `defineMcpTool` produces a
32
+ * `RegisteredMcpTool` marker object that the build pipeline
33
+ * (`@maravilla-labs/functions` `buildMcp`) detects by its `__maravilla_mcp`
34
+ * property; `defineMcpServer` produces a `RegisteredMcpServer` detected by
35
+ * `__maravilla_mcp_server`. The generated bundle exposes
36
+ * `globalThis.handleMcpTool(toolId, args, ctx)`; the Rust MCP dispatcher
37
+ * (`crates/platform/src/mcp/dispatch.rs`) drives it via a synthetic request
38
+ * whose body is `{ tool_id, args, identity }`.
39
+ */
40
+ /**
41
+ * Context handed to every MCP tool handler. The first seven services mirror
42
+ * the events `EventCtx` (and `globalThis.platform`) exactly. `user`/`client`
43
+ * carry the authenticated identity behind the OAuth token or API key so the
44
+ * handler — and the platform ops it calls — run as the real end-user.
45
+ */
46
+ interface McpToolContext {
47
+ /** Per-tenant env vars. */
48
+ env: Record<string, string>;
49
+ /** KV store — same shape as `getPlatform().env.KV` / `platform.kv`. */
50
+ kv?: unknown;
51
+ /** MongoDB-style database — same shape as `getPlatform().env.DB`. */
52
+ database?: unknown;
53
+ /** Object storage. */
54
+ storage?: unknown;
55
+ /** Durable queue producer (`.send(name, payload, opts?)`). */
56
+ queue?: {
57
+ send: (name: string, payload: unknown, opts?: unknown) => Promise<string>;
58
+ };
59
+ /** Auth service — register/login/logout/user CRUD/etc. */
60
+ auth?: unknown;
61
+ /** Web Push service. */
62
+ push?: unknown;
63
+ /** Full platform object — escape hatch for services not surfaced above. */
64
+ platform?: unknown;
65
+ /** Tenant identifier. */
66
+ tenant: string;
67
+ /** Trace correlation id — propagate through logs. */
68
+ traceId: string;
69
+ /** The end-user behind the token/key, or `null` for a client-only call. */
70
+ user: {
71
+ id: string;
72
+ email: string;
73
+ groups: string[];
74
+ scopes: string[];
75
+ } | null;
76
+ /** The OAuth client (agent) that authenticated, when known. */
77
+ client: {
78
+ id: string;
79
+ } | null;
80
+ }
81
+ /** A single content item returned by a tool. */
82
+ type McpContentItem = {
83
+ type: 'text';
84
+ text: string;
85
+ } | {
86
+ type: 'json';
87
+ json: unknown;
88
+ } | {
89
+ type: 'resource';
90
+ resource: Record<string, unknown>;
91
+ };
92
+ /**
93
+ * What a tool handler may return:
94
+ * - `{ content }` — explicit MCP content items.
95
+ * - `{ ui }` — render an iframe mini-app template (§7); optional `content`
96
+ * is shown alongside for clients that don't support the UI.
97
+ * - any other value — wrapped as a single `text` content item.
98
+ */
99
+ type McpToolResult = {
100
+ content: McpContentItem[];
101
+ } | {
102
+ ui: {
103
+ template: string;
104
+ data?: unknown;
105
+ };
106
+ content?: McpContentItem[];
107
+ } | unknown;
108
+ declare const MCP_TOOL_SYMBOL: "__maravilla_mcp";
109
+ declare const MCP_SERVER_SYMBOL: "__maravilla_mcp_server";
110
+ /** Spec passed to {@link defineMcpTool}. */
111
+ interface McpToolSpec {
112
+ /** Tool name surfaced to the client (defaults to the export name). */
113
+ name: string;
114
+ /** Human-readable description sent in `tools/list`. */
115
+ description: string;
116
+ /** JSON Schema for the tool input, sent verbatim in `tools/list`. */
117
+ inputSchema: Record<string, unknown>;
118
+ /** Required scopes; dispatch enforces `scopes ⊆ identity.scopes`. */
119
+ scopes?: string[];
120
+ /**
121
+ * Per-tool public opt-in. When `true`, this tool is callable with an
122
+ * anonymous identity (no bearer) even on an otherwise-private server — its
123
+ * `scopes` must still be a subset of the caller's, so an anonymous caller can
124
+ * only reach it when it declares no scopes. Defaults to `false`.
125
+ */
126
+ public?: boolean;
127
+ /** Links this tool to a UI template declared on the server (§7). */
128
+ ui?: {
129
+ template: string;
130
+ };
131
+ }
132
+ interface RegisteredMcpTool {
133
+ readonly [MCP_TOOL_SYMBOL]: McpToolSpec;
134
+ readonly handler: (args: any, ctx: McpToolContext) => McpToolResult | Promise<McpToolResult>;
135
+ }
136
+ /** Spec passed to {@link defineMcpServer}. */
137
+ interface McpServerSpec {
138
+ /** Human-readable server name (e.g. "Acme Tools"). */
139
+ name: string;
140
+ /** Optional semantic version. */
141
+ version?: string;
142
+ /** Optional natural-language usage instructions for the client/model. */
143
+ instructions?: string;
144
+ /**
145
+ * Server-level public flag. When `true`, unauthenticated MCP requests build
146
+ * an anonymous identity (instead of a 401) so `initialize` / `tools/list` and
147
+ * any no-scope tool work without a bearer. Per-tool `public` opt-in still
148
+ * works on an otherwise-private server. Defaults to `false`.
149
+ */
150
+ public?: boolean;
151
+ /** Iframe mini-app templates referenced by tools via `ui.template`. */
152
+ uiTemplates?: Array<{
153
+ name: string;
154
+ route: string;
155
+ csp?: string;
156
+ }>;
157
+ }
158
+ interface RegisteredMcpServer {
159
+ readonly [MCP_SERVER_SYMBOL]: McpServerSpec;
160
+ }
161
+ /**
162
+ * Declare an MCP tool. The runtime advertises it in `tools/list` (using
163
+ * `name`, `description`, `inputSchema`) and dispatches `tools/call` to
164
+ * `handler`, enforcing `scopes` against the caller's granted scopes.
165
+ */
166
+ declare function defineMcpTool(spec: McpToolSpec, handler: (args: any, ctx: McpToolContext) => McpToolResult | Promise<McpToolResult>): RegisteredMcpTool;
167
+ /**
168
+ * Declare the MCP server identity and its iframe mini-app templates.
169
+ * Optional — at most one per app; the last one discovered wins.
170
+ */
171
+ declare function defineMcpServer(spec: McpServerSpec): RegisteredMcpServer;
172
+ /** Type guard used by the build-time discoverer and the runtime registry. */
173
+ declare function isRegisteredMcpTool(value: unknown): value is RegisteredMcpTool;
174
+ /** Type guard for a registered MCP server descriptor. */
175
+ declare function isRegisteredMcpServer(value: unknown): value is RegisteredMcpServer;
176
+
177
+ export { MCP_SERVER_SYMBOL, MCP_TOOL_SYMBOL, type McpContentItem, type McpServerSpec, type McpToolContext, type McpToolResult, type McpToolSpec, type RegisteredMcpServer, type RegisteredMcpTool, defineMcpServer, defineMcpTool, isRegisteredMcpServer, isRegisteredMcpTool };
package/dist/mcp.js ADDED
@@ -0,0 +1,24 @@
1
+ // src/mcp.ts
2
+ var MCP_TOOL_SYMBOL = "__maravilla_mcp";
3
+ var MCP_SERVER_SYMBOL = "__maravilla_mcp_server";
4
+ function defineMcpTool(spec, handler) {
5
+ return { [MCP_TOOL_SYMBOL]: spec, handler };
6
+ }
7
+ function defineMcpServer(spec) {
8
+ return { [MCP_SERVER_SYMBOL]: spec };
9
+ }
10
+ function isRegisteredMcpTool(value) {
11
+ return typeof value === "object" && value !== null && MCP_TOOL_SYMBOL in value && typeof value.handler === "function";
12
+ }
13
+ function isRegisteredMcpServer(value) {
14
+ return typeof value === "object" && value !== null && MCP_SERVER_SYMBOL in value;
15
+ }
16
+ export {
17
+ MCP_SERVER_SYMBOL,
18
+ MCP_TOOL_SYMBOL,
19
+ defineMcpServer,
20
+ defineMcpTool,
21
+ isRegisteredMcpServer,
22
+ isRegisteredMcpTool
23
+ };
24
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +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 /** Iframe mini-app templates referenced by tools via `ui.template`. */\n uiTemplates?: Array<{ name: string; route: string; csp?: 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;AA0D1B,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":[]}
@@ -45,4 +45,4 @@ declare function renFetch(input: string | URL | Request, init?: RequestInit, cli
45
45
  declare function storageUpload(path: string, file: Blob | File, clientId?: string): Promise<any>;
46
46
  declare function storageDelete(path: string, clientId?: string): Promise<any>;
47
47
 
48
- export { RenClient as R, type RenClientOptions as a, type RenEvent as b, storageUpload as c, getOrCreateClientId as g, renFetch as r, storageDelete as s };
48
+ export { type RenEvent as R, type RenClientOptions as a, RenClient as b, storageDelete as c, getOrCreateClientId as g, renFetch as r, storageUpload as s };
@@ -17,6 +17,7 @@
17
17
  * and the TS test (`packages/platform/tests/derive-key.test.ts`) to keep
18
18
  * the two in lockstep.
19
19
  */
20
+
20
21
  /** Video container targets supported by v1. */
21
22
  type VideoFormat = 'mp4' | 'webm';
22
23
  /** Image output formats supported by v1. */
@@ -297,4 +298,4 @@ interface TransformsPatternSpec {
297
298
  */
298
299
  type TransformsConfig = Record<string, TransformsPatternSpec>;
299
300
 
300
- export { type DocConvertOpts as D, type ImageFormat as I, type JobHandle as J, type MediaInfo as M, type OcrOpts as O, type QrCodeSpec as Q, type ResizeOpts as R, type ThumbnailOpts as T, type VideoFormat as V, type DocFormat as a, type DocInsertQrCodeOpts as b, type DocReplaceImagesOpts as c, type DocTemplateMergeOpts as d, type DocThumbnailOpts as e, type DocToHtmlOpts as f, type DocToMarkdownOpts as g, type DocToPdfOpts as h, type ImageRef as i, type JobStatus as j, type JobStatusResponse as k, type QrPayload as l, type TranscodeOpts as m, type TransformSpec as n, type TransformsConfig as o, type TransformsPatternSpec as p, type TransformsService as q, keyFor as r, transforms as t };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maravilla-labs/platform",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Universal platform client for Maravilla runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,19 +22,16 @@
22
22
  "./events": {
23
23
  "types": "./dist/events.d.ts",
24
24
  "import": "./dist/events.js"
25
+ },
26
+ "./mcp": {
27
+ "types": "./dist/mcp.d.ts",
28
+ "import": "./dist/mcp.js"
25
29
  }
26
30
  },
27
- "scripts": {
28
- "build": "tsup",
29
- "dev": "tsup --watch",
30
- "typecheck": "tsc --noEmit",
31
- "typecheck:test-d": "tsc --noEmit -p tsconfig.test.json",
32
- "test": "vitest run"
33
- },
34
31
  "dependencies": {
35
- "@maravilla-labs/types": "^0.6.0",
36
32
  "@noble/hashes": "^1.5.0",
37
- "livekit-client": "^2.18.1"
33
+ "livekit-client": "^2.18.1",
34
+ "@maravilla-labs/types": "^0.9.0"
38
35
  },
39
36
  "devDependencies": {
40
37
  "@types/node": "^22.10.6",
@@ -44,5 +41,12 @@
44
41
  },
45
42
  "publishConfig": {
46
43
  "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch",
48
+ "typecheck": "tsc --noEmit",
49
+ "typecheck:test-d": "tsc --noEmit -p tsconfig.test.json",
50
+ "test": "vitest run"
47
51
  }
48
- }
52
+ }
package/src/config.ts CHANGED
@@ -419,6 +419,45 @@ export interface BrandingConfig {
419
419
  custom_css?: string;
420
420
  }
421
421
 
422
+ // ── Email ──
423
+
424
+ /**
425
+ * Transactional email settings for the hosted `_auth` flows (currently the
426
+ * password-reset email). Reconciled server-side on deploy.
427
+ *
428
+ * The visible `From` *address* is always the platform's verified sender
429
+ * (Maravilla Cloud) and is not configurable. The `From` display *name* is safe
430
+ * to customise via `from_name`; surface your own address via `reply_to`.
431
+ */
432
+ export interface EmailConfig {
433
+ /** Master switch. When `false`, the reset flow still mints a token but no email is sent. Defaults to `true`. */
434
+ enabled?: boolean;
435
+ /** `From` display name (e.g. `"Acme Support"`) → `Acme Support <noreply@maravilla.cloud>`. The address is fixed; only the name is yours. */
436
+ from_name?: string;
437
+ /** Reply-To address. The visible `From` is always the platform sender; replies go here. */
438
+ reply_to?: string;
439
+ /** Override the password-reset email subject line. */
440
+ password_reset_subject?: string;
441
+ /** Full custom HTML body. Use the literal `{{reset_url}}` placeholder for the link. */
442
+ password_reset_html?: string;
443
+ /** Full custom plain-text body (same `{{reset_url}}` placeholder). */
444
+ password_reset_text?: string;
445
+ /** Enable passwordless "magic link" email login on the hosted `/_auth/login` page. Off by default; requires `enabled` to send. */
446
+ magic_link_enabled?: boolean;
447
+ /** Override the magic-link email subject line. */
448
+ magic_link_subject?: string;
449
+ /** Full custom HTML body for the magic-link email. Use the literal `{{magic_link_url}}` placeholder. */
450
+ magic_link_html?: string;
451
+ /** Full custom plain-text body for the magic-link email (same `{{magic_link_url}}` placeholder). */
452
+ magic_link_text?: string;
453
+ /** Override the email-verification email subject line. */
454
+ verification_subject?: string;
455
+ /** Full custom HTML body for the verification email. Use the literal `{{verify_url}}` placeholder. */
456
+ verification_html?: string;
457
+ /** Full custom plain-text body for the verification email (same `{{verify_url}}` placeholder). */
458
+ verification_text?: string;
459
+ }
460
+
422
461
  // ── Top-level shape ──
423
462
 
424
463
  export interface AuthConfigBlock {
@@ -429,6 +468,8 @@ export interface AuthConfigBlock {
429
468
  oauth?: OAuthProvidersConfig;
430
469
  security?: SecurityConfig;
431
470
  branding?: BrandingConfig;
471
+ /** Transactional email settings for the hosted `_auth` flows. */
472
+ email?: EmailConfig;
432
473
  /**
433
474
  * Named, reusable policy fragments. Reference a fragment from any
434
475
  * resource policy with `fragment('name')`; {@link defineConfig} expands
@@ -460,6 +501,11 @@ export interface MaravillaConfig {
460
501
  * matching upload. See {@link TransformsConfig}.
461
502
  */
462
503
  transforms?: TransformsConfig;
504
+ /**
505
+ * MCP server block — turns the app's `mcp/` folder into an MCP server at
506
+ * `<app-url>/_mcp`. See {@link McpConfigBlock}.
507
+ */
508
+ mcp?: McpConfigBlock;
463
509
  }
464
510
 
465
511
  // ── Database block ──
@@ -517,6 +563,56 @@ export interface DatabaseConfigBlock {
517
563
  vectorIndexes?: VectorIndexDeclaration[];
518
564
  }
519
565
 
566
+ // ── MCP block ──
567
+ //
568
+ // The `mcp` block turns the app's `mcp/` folder into an MCP server hosted at
569
+ // `<app-url>/_mcp`. It is the master switch (`enabled`) plus the scope set
570
+ // agents are offered at login — the runtime is the OAuth Authorization Server,
571
+ // so agents authenticate with the app's own accounts (or a headless `mk_…`
572
+ // key). Tools, server identity, and UI templates themselves are authored in
573
+ // `mcp/*.ts` via `defineMcpServer` / `defineMcpTool`. Mirrors the Rust
574
+ // `platform::mcp::McpConfig`.
575
+
576
+ /** Server identity advertised to agents in the MCP `initialize` handshake. */
577
+ export interface McpServerConfig {
578
+ /** Human-readable server name (e.g. "Notes"). */
579
+ name: string;
580
+ /** Optional semantic version. */
581
+ version?: string;
582
+ /** Optional natural-language usage instructions for the client/model. */
583
+ instructions?: string;
584
+ }
585
+
586
+ /** Allow-list of routes that may be framed as MCP UI mini-apps. */
587
+ export interface McpUiConfig {
588
+ /** Route globs allowed to render as iframe mini-apps (e.g. `/_mcp/ui/*`). */
589
+ routes: string[];
590
+ }
591
+
592
+ /** Whether to persist a user's scope grant so they aren't re-prompted. */
593
+ export interface McpConsentConfig {
594
+ remember?: boolean;
595
+ }
596
+
597
+ export interface McpConfigBlock {
598
+ /** Master switch. With `false` (the default) the `/_mcp` endpoint is off. */
599
+ enabled: boolean;
600
+ /** Server identity advertised to agents. */
601
+ server?: McpServerConfig;
602
+ /**
603
+ * Every scope any tool can require, advertised to agents at login / Dynamic
604
+ * Client Registration. A tool whose `scopes` aren't listed here can never be
605
+ * granted. A tool with no scopes is public (callable without a login).
606
+ */
607
+ scopes?: string[];
608
+ /** Allow-list of UI mini-app routes (frame-ancestors / loopback gate). */
609
+ ui?: McpUiConfig;
610
+ /** Public origin agents connect to, when it differs from the request host. */
611
+ publicUrl?: string;
612
+ /** Consent persistence toggle. */
613
+ consent?: McpConsentConfig;
614
+ }
615
+
520
616
  /**
521
617
  * Recursively expand `fragment(name)` sentinels in `expr` against the
522
618
  * declared `fragments` map. Each expansion is wrapped in parens so it
package/src/mcp.ts ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @fileoverview MCP tool authoring helpers for Maravilla.
3
+ *
4
+ * User apps declare MCP tools in `mcp.ts` or `mcp/*.ts`:
5
+ *
6
+ * ```ts
7
+ * import { defineMcpTool, defineMcpServer } from '@maravilla-labs/platform/mcp';
8
+ *
9
+ * export const server = defineMcpServer({
10
+ * name: 'Acme Tools',
11
+ * version: '1.0.0',
12
+ * instructions: 'Tools for managing Acme orders.',
13
+ * uiTemplates: [{ name: 'order-card', route: '/_mcp/ui/order-card' }],
14
+ * });
15
+ *
16
+ * export const getOrder = defineMcpTool(
17
+ * {
18
+ * name: 'get_order',
19
+ * description: 'Look up an order by id.',
20
+ * inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
21
+ * scopes: ['acme:read'],
22
+ * ui: { template: 'order-card' },
23
+ * },
24
+ * async (args, ctx) => {
25
+ * const order = await ctx.database.findOne('orders', { _id: args.id });
26
+ * return { content: [{ type: 'text', text: JSON.stringify(order) }] };
27
+ * },
28
+ * );
29
+ * ```
30
+ *
31
+ * These helpers are pure factories. `defineMcpTool` produces a
32
+ * `RegisteredMcpTool` marker object that the build pipeline
33
+ * (`@maravilla-labs/functions` `buildMcp`) detects by its `__maravilla_mcp`
34
+ * property; `defineMcpServer` produces a `RegisteredMcpServer` detected by
35
+ * `__maravilla_mcp_server`. The generated bundle exposes
36
+ * `globalThis.handleMcpTool(toolId, args, ctx)`; the Rust MCP dispatcher
37
+ * (`crates/platform/src/mcp/dispatch.rs`) drives it via a synthetic request
38
+ * whose body is `{ tool_id, args, identity }`.
39
+ */
40
+
41
+ // ============ Tool handler context ============
42
+
43
+ /**
44
+ * Context handed to every MCP tool handler. The first seven services mirror
45
+ * the events `EventCtx` (and `globalThis.platform`) exactly. `user`/`client`
46
+ * carry the authenticated identity behind the OAuth token or API key so the
47
+ * handler — and the platform ops it calls — run as the real end-user.
48
+ */
49
+ export interface McpToolContext {
50
+ /** Per-tenant env vars. */
51
+ env: Record<string, string>;
52
+ /** KV store — same shape as `getPlatform().env.KV` / `platform.kv`. */
53
+ kv?: unknown;
54
+ /** MongoDB-style database — same shape as `getPlatform().env.DB`. */
55
+ database?: unknown;
56
+ /** Object storage. */
57
+ storage?: unknown;
58
+ /** Durable queue producer (`.send(name, payload, opts?)`). */
59
+ queue?: { send: (name: string, payload: unknown, opts?: unknown) => Promise<string> };
60
+ /** Auth service — register/login/logout/user CRUD/etc. */
61
+ auth?: unknown;
62
+ /** Web Push service. */
63
+ push?: unknown;
64
+ /** Full platform object — escape hatch for services not surfaced above. */
65
+ platform?: unknown;
66
+ /** Tenant identifier. */
67
+ tenant: string;
68
+ /** Trace correlation id — propagate through logs. */
69
+ traceId: string;
70
+ /** The end-user behind the token/key, or `null` for a client-only call. */
71
+ user: { id: string; email: string; groups: string[]; scopes: string[] } | null;
72
+ /** The OAuth client (agent) that authenticated, when known. */
73
+ client: { id: string } | null;
74
+ }
75
+
76
+ // ============ Tool result shapes ============
77
+
78
+ /** A single content item returned by a tool. */
79
+ export type McpContentItem =
80
+ | { type: 'text'; text: string }
81
+ | { type: 'json'; json: unknown }
82
+ | { type: 'resource'; resource: Record<string, unknown> };
83
+
84
+ /**
85
+ * What a tool handler may return:
86
+ * - `{ content }` — explicit MCP content items.
87
+ * - `{ ui }` — render an iframe mini-app template (§7); optional `content`
88
+ * is shown alongside for clients that don't support the UI.
89
+ * - any other value — wrapped as a single `text` content item.
90
+ */
91
+ export type McpToolResult =
92
+ | { content: McpContentItem[] }
93
+ | { ui: { template: string; data?: unknown }; content?: McpContentItem[] }
94
+ | unknown;
95
+
96
+ // ============ Registered markers ============
97
+
98
+ export const MCP_TOOL_SYMBOL = '__maravilla_mcp' as const;
99
+ export const MCP_SERVER_SYMBOL = '__maravilla_mcp_server' as const;
100
+
101
+ /** Spec passed to {@link defineMcpTool}. */
102
+ export interface McpToolSpec {
103
+ /** Tool name surfaced to the client (defaults to the export name). */
104
+ name: string;
105
+ /** Human-readable description sent in `tools/list`. */
106
+ description: string;
107
+ /** JSON Schema for the tool input, sent verbatim in `tools/list`. */
108
+ inputSchema: Record<string, unknown>;
109
+ /** Required scopes; dispatch enforces `scopes ⊆ identity.scopes`. */
110
+ scopes?: string[];
111
+ /**
112
+ * Per-tool public opt-in. When `true`, this tool is callable with an
113
+ * anonymous identity (no bearer) even on an otherwise-private server — its
114
+ * `scopes` must still be a subset of the caller's, so an anonymous caller can
115
+ * only reach it when it declares no scopes. Defaults to `false`.
116
+ */
117
+ public?: boolean;
118
+ /** Links this tool to a UI template declared on the server (§7). */
119
+ ui?: { template: string };
120
+ }
121
+
122
+ export interface RegisteredMcpTool {
123
+ readonly [MCP_TOOL_SYMBOL]: McpToolSpec;
124
+ readonly handler: (args: any, ctx: McpToolContext) => McpToolResult | Promise<McpToolResult>;
125
+ }
126
+
127
+ /** Spec passed to {@link defineMcpServer}. */
128
+ export interface McpServerSpec {
129
+ /** Human-readable server name (e.g. "Acme Tools"). */
130
+ name: string;
131
+ /** Optional semantic version. */
132
+ version?: string;
133
+ /** Optional natural-language usage instructions for the client/model. */
134
+ instructions?: string;
135
+ /**
136
+ * Server-level public flag. When `true`, unauthenticated MCP requests build
137
+ * an anonymous identity (instead of a 401) so `initialize` / `tools/list` and
138
+ * any no-scope tool work without a bearer. Per-tool `public` opt-in still
139
+ * works on an otherwise-private server. Defaults to `false`.
140
+ */
141
+ public?: boolean;
142
+ /** Iframe mini-app templates referenced by tools via `ui.template`. */
143
+ uiTemplates?: Array<{ name: string; route: string; csp?: string }>;
144
+ }
145
+
146
+ export interface RegisteredMcpServer {
147
+ readonly [MCP_SERVER_SYMBOL]: McpServerSpec;
148
+ }
149
+
150
+ // ============ Public factory helpers ============
151
+
152
+ /**
153
+ * Declare an MCP tool. The runtime advertises it in `tools/list` (using
154
+ * `name`, `description`, `inputSchema`) and dispatches `tools/call` to
155
+ * `handler`, enforcing `scopes` against the caller's granted scopes.
156
+ */
157
+ export function defineMcpTool(
158
+ spec: McpToolSpec,
159
+ handler: (args: any, ctx: McpToolContext) => McpToolResult | Promise<McpToolResult>,
160
+ ): RegisteredMcpTool {
161
+ return { [MCP_TOOL_SYMBOL]: spec, handler };
162
+ }
163
+
164
+ /**
165
+ * Declare the MCP server identity and its iframe mini-app templates.
166
+ * Optional — at most one per app; the last one discovered wins.
167
+ */
168
+ export function defineMcpServer(spec: McpServerSpec): RegisteredMcpServer {
169
+ return { [MCP_SERVER_SYMBOL]: spec };
170
+ }
171
+
172
+ /** Type guard used by the build-time discoverer and the runtime registry. */
173
+ export function isRegisteredMcpTool(value: unknown): value is RegisteredMcpTool {
174
+ return (
175
+ typeof value === 'object' &&
176
+ value !== null &&
177
+ MCP_TOOL_SYMBOL in value &&
178
+ typeof (value as Record<string, unknown>).handler === 'function'
179
+ );
180
+ }
181
+
182
+ /** Type guard for a registered MCP server descriptor. */
183
+ export function isRegisteredMcpServer(value: unknown): value is RegisteredMcpServer {
184
+ return (
185
+ typeof value === 'object' &&
186
+ value !== null &&
187
+ MCP_SERVER_SYMBOL in value
188
+ );
189
+ }