@happyvertical/smrt-template-sveltekit 0.52.0 → 0.53.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/AGENTS.md CHANGED
@@ -13,7 +13,8 @@ It is the ground-up alternative to `smrt-saas-starter`.
13
13
 
14
14
  ## Current generated-project contract
15
15
 
16
- - Node `>=24.18.0`; pnpm `11.25.0` via `packageManager` and `engines`.
16
+ - Node `>=26.0.0`; pnpm `11.25.0` via `packageManager` and `engines`. Node 25+
17
+ images ship no Corepack, so the Dockerfile installs a pinned copy.
17
18
  - `runtime.profile` is the canonical infrastructure selector. Generated apps
18
19
  expose deterministic `app:*` operations and keep runtime state outside source.
19
20
  - The production baseline uses adapter-node with separate web, task-worker, and
package/README.md CHANGED
@@ -20,9 +20,19 @@ Run the copy command from the monorepo root. The public 0.43.10 CLI advertises
20
20
  `gnode create`, but its command dispatcher currently rejects `gnode`; this
21
21
  package's tested `copyTemplate()` export is the working scaffold path.
22
22
 
23
- Generated projects require Node.js 24.18.0 or newer and pnpm 11.25.0. They pin
23
+ Generated projects require Node.js 26.0.0 or newer and pnpm 11.25.0. They pin
24
24
  all directly used `@happyvertical/smrt-*` packages to the current monorepo
25
- release (synced at publish by `scripts/sync-template-versions.mjs`).
25
+ release, including opt-in MCP Apps dependencies (synced at publish by
26
+ `scripts/sync-template-versions.mjs`; `--check` detects drift).
27
+
28
+ `smrt gnode create <name> --mcp-apps` supports installed, local, and Git
29
+ subdirectory templates. Plugin names must satisfy the pinned Agent Plugins
30
+ 1.0.0 schema before generation starts. `smrt mcp-apps validate` applies the
31
+ official closed plugin/MCP schemas offline, then s-m-r-t endpoint and credential
32
+ checks. Every existing scoped registry directive must be canonical; inline
33
+ `#` and `;` comments are supported. Hosted MCP caches its successfully created
34
+ authenticator for the process, preserving JWKS keys across requests while
35
+ principal resolution and task authorization still run for each request.
26
36
 
27
37
  For programmatic scaffolding:
28
38
 
package/index.js CHANGED
@@ -80,6 +80,14 @@ export function copyTemplate(destination, options = {}) {
80
80
  if (!existsSync(destination)) {
81
81
  mkdirSync(destination, { recursive: true });
82
82
  }
83
+ if (!pathEntryExists(join(destination, '.npmrc'))) writeFileSync(join(destination, '.npmrc'), '@happyvertical:registry=https://npm.happyvertical.com/\n');
84
+ else {
85
+ const npmrc = join(destination, '.npmrc');
86
+ if (!lstatSync(npmrc).isFile()) throw new Error('Generated application .npmrc must be a regular file');
87
+ const current = readFileSync(npmrc, 'utf8'), routes = [...current.matchAll(/^\s*@happyvertical:registry\s*=\s*(\S+)(?:\s+[;#].*)?\s*$/gm)].map((match) => match[1]);
88
+ if (routes.some((route) => route !== 'https://npm.happyvertical.com/')) throw new Error('Generated application .npmrc has a conflicting @happyvertical registry');
89
+ if (!routes.length) writeFileSync(npmrc, `${current}${current.endsWith('\n') ? '' : '\n'}@happyvertical:registry=https://npm.happyvertical.com/\n`);
90
+ }
83
91
 
84
92
  // Copy all template files, skipping internal-only directories
85
93
  cpSync(templatePath, destination, {
@@ -0,0 +1,4 @@
1
+ {
2
+ "@happyvertical/smrt-app-mcp": "^0.52.0",
3
+ "@happyvertical/smrt-mcp-openai": "^0.52.0"
4
+ }
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import { requestOpenAiDisplayMode } from '@happyvertical/smrt-mcp-openai/client';
3
+ import { useMcpApp } from '@happyvertical/smrt-svelte/mcp-apps';
4
+
5
+ interface Props {
6
+ /** Exact, application-configured host origin. Never derive this from URL input. */
7
+ hostOrigin: string;
8
+ }
9
+
10
+ let { hostOrigin }: Props = $props();
11
+ let expandUnavailable = $state(false);
12
+ const app = useMcpApp(() => ({
13
+ appInfo: { name: 'smrt-app', version: '0.1.0' },
14
+ availableDisplayModes: ['inline', 'fullscreen'],
15
+ hostOrigin,
16
+ hostWindow: parent,
17
+ }));
18
+
19
+ async function expand() {
20
+ try {
21
+ if (!app.bridge) throw new Error('Host bridge unavailable');
22
+ await requestOpenAiDisplayMode(app.bridge, 'fullscreen');
23
+ } catch {
24
+ expandUnavailable = true;
25
+ }
26
+ }
27
+ </script>
28
+
29
+ <section aria-labelledby="mcp-apps-title" class="mcp-apps-bridge">
30
+ <h1 id="mcp-apps-title">Items</h1>
31
+ <p>Use this view to review the items your signed-in tenant authorizes.</p>
32
+ {#if app.error || expandUnavailable}
33
+ <p role="status">Host controls are unavailable; this view remains inline.</p>
34
+ {:else}
35
+ <button type="button" onclick={expand}>Expand view</button>
36
+ {/if}
37
+ </section>
@@ -0,0 +1,12 @@
1
+ import type { McpVerifiedIdentity, McpPrincipalMapping } from '@happyvertical/smrt-app-mcp/auth';
2
+
3
+ /**
4
+ * Deployments must replace this binding with a database-backed current-account
5
+ * and active-membership lookup. Token claims never select a tenant.
6
+ */
7
+ export type HostedMcpPrincipalResolver = (
8
+ identity: McpVerifiedIdentity,
9
+ ) => Promise<McpPrincipalMapping | null>;
10
+
11
+ export const resolveHostedMcpPrincipal: HostedMcpPrincipalResolver | undefined =
12
+ undefined;
@@ -0,0 +1,28 @@
1
+ import { createMcpResourceAuth } from '@happyvertical/smrt-app-mcp/auth';
2
+ import { applicationRuntime } from './application-runtime.js';
3
+ import { resolveHostedMcpPrincipal } from './mcp-hosted-principal.js';
4
+
5
+ function required(name: string): string {
6
+ const value = process.env[name]?.trim();
7
+ if (!value) throw new Error(`Hosted MCP requires ${name}.`);
8
+ return value;
9
+ }
10
+
11
+ let cachedAuth: ReturnType<typeof createMcpResourceAuth> | undefined;
12
+
13
+ export function hostedMcpAuth() {
14
+ if (applicationRuntime.profile === 'local') return null;
15
+ if (cachedAuth) return cachedAuth;
16
+ if (!resolveHostedMcpPrincipal)
17
+ throw new Error('Hosted MCP requires an application-owned resolveHostedMcpPrincipal binding.');
18
+ cachedAuth = createMcpResourceAuth({
19
+ profile: applicationRuntime.profile,
20
+ resource: required('SMRT_MCP_RESOURCE'),
21
+ issuer: required('SMRT_MCP_ISSUER'),
22
+ jwksUri: required('SMRT_MCP_JWKS_URI'),
23
+ scopes: required('SMRT_MCP_SCOPES').split(/\s+/u),
24
+ algorithms: ['RS256'],
25
+ resolvePrincipal: resolveHostedMcpPrincipal,
26
+ });
27
+ return cachedAuth;
28
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ createMcpAppServer,
3
+ type McpAppPrincipal,
4
+ type McpWorkflowToolDefinition,
5
+ } from '@happyvertical/smrt-app-mcp';
6
+ import { openAiDisplayMetadata, withOpenAiEntrypoints } from '@happyvertical/smrt-mcp-openai';
7
+ import { getApplicationDatabaseConfig } from './application-runtime.js';
8
+
9
+ const ITEM_RESOURCE_URI = 'ui://smrt-app/v1/items.html';
10
+ const itemResourceHtml = `<!doctype html><html lang="en"><head><title>Items</title></head><body><main tabindex="-1"><h1>Items</h1><p>Use the host controls to review your authorized items.</p></main></body></html>`;
11
+
12
+ const itemsOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
13
+ {
14
+ name: 'items_overview',
15
+ description: 'Open the authorized item overview.',
16
+ title: 'Items',
17
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
18
+ outputSchema: {
19
+ type: 'object',
20
+ properties: { state: { type: 'string' } },
21
+ required: ['state'],
22
+ additionalProperties: false,
23
+ },
24
+ effect: 'read',
25
+ idempotent: true,
26
+ openWorld: false,
27
+ ui: { resourceUri: ITEM_RESOURCE_URI },
28
+ async execute() {
29
+ return {
30
+ content: [{ type: 'text', text: 'Item overview is ready.' }],
31
+ structuredContent: { state: 'ready' },
32
+ };
33
+ },
34
+ },
35
+ ['global'],
36
+ );
37
+
38
+ /**
39
+ * The session hook resolves membership and permissions for every request. This
40
+ * adapter only projects that trusted, request-local result; headers and route
41
+ * arguments are never identity inputs.
42
+ */
43
+ export function resolveMcpPrincipal(event: {
44
+ locals?: Record<string, unknown>;
45
+ }): McpAppPrincipal | null {
46
+ const user = event.locals?.user as { id?: unknown } | undefined;
47
+ const tenantId = event.locals?.tenantId;
48
+ const permissions = event.locals?.permissions;
49
+ if (typeof user?.id !== 'string' || typeof tenantId !== 'string') return null;
50
+ if (!Array.isArray(permissions) || !permissions.every((value) => typeof value === 'string'))
51
+ return null;
52
+ return {
53
+ id: user.id,
54
+ tenantId,
55
+ kind: 'human',
56
+ scopes: [...permissions].sort(),
57
+ };
58
+ }
59
+
60
+ export const mcpServer = createMcpAppServer({
61
+ smrtOptions: () => ({ db: getApplicationDatabaseConfig() }),
62
+ serverInfo: { name: 'smrt-app', version: '0.1.0' },
63
+ allowedClassNames: ['Item'],
64
+ workflowTools: [itemsOverview],
65
+ resources: [
66
+ {
67
+ uri: ITEM_RESOURCE_URI,
68
+ version: 'v1',
69
+ name: 'Items',
70
+ html: itemResourceHtml,
71
+ csp: {},
72
+ metadata: openAiDisplayMetadata({ availableDisplayModes: ['inline'] }),
73
+ },
74
+ ],
75
+ toolPolicy: ({ principal }) =>
76
+ principal?.kind === 'human' &&
77
+ typeof principal.id === 'string' &&
78
+ typeof principal.tenantId === 'string' &&
79
+ principal.scopes?.includes('items.read') === true,
80
+ resourcePolicy: ({ principal }) =>
81
+ principal?.kind === 'human' &&
82
+ typeof principal.id === 'string' &&
83
+ typeof principal.tenantId === 'string' &&
84
+ principal.scopes?.includes('items.read') === true,
85
+ });
@@ -0,0 +1,7 @@
1
+ import { hostedMcpAuth } from '$lib/server/mcp-hosted';
2
+
3
+ /** Serve exactly the RFC 9728 metadata URL advertised by hosted bearer challenges. */
4
+ export function GET() {
5
+ const auth = hostedMcpAuth();
6
+ return auth?.metadataResponse() ?? new Response(null, { status: 404 });
7
+ }
@@ -0,0 +1,15 @@
1
+ import { mountMcpRoute } from '@happyvertical/smrt-app-mcp/sveltekit';
2
+
3
+ import { mcpServer, resolveMcpPrincipal } from '$lib/server/mcp';
4
+ import { hostedMcpAuth } from '$lib/server/mcp-hosted';
5
+
6
+ /** Stateless SDK-v2 Streamable HTTP MCP endpoint. */
7
+ const localRoute = mountMcpRoute(mcpServer, { resolvePrincipal: resolveMcpPrincipal });
8
+
9
+ export async function POST(event: Parameters<typeof localRoute>[0]) {
10
+ const auth = hostedMcpAuth();
11
+ if (!auth) return localRoute(event);
12
+ const checked = await auth.authenticate(event.request);
13
+ if (!checked.ok) return checked.response;
14
+ return mountMcpRoute(mcpServer, { resolvePrincipal: () => checked.principal })(event);
15
+ }
package/package.json CHANGED
@@ -1,19 +1,22 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-template-sveltekit",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "Minimal SvelteKit project template with s-m-r-t framework integration",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
8
8
  ".": "./index.js",
9
+ "./template.config.js": "./template.config.js",
9
10
  "./template/*": "./template/*"
10
11
  },
11
12
  "files": [
12
13
  "CLAUDE.md",
13
14
  "README.md",
14
15
  "index.js",
16
+ "template.config.js",
15
17
  "template",
16
- "AGENTS.md"
18
+ "AGENTS.md",
19
+ "mcp-apps-template"
17
20
  ],
18
21
  "keywords": [
19
22
  "smrt",
@@ -29,22 +32,22 @@
29
32
  "directory": "packages/template-sveltekit"
30
33
  },
31
34
  "peerDependencies": {
32
- "@happyvertical/smrt-core": "0.52.0"
35
+ "@happyvertical/smrt-core": "0.53.0"
33
36
  },
34
37
  "devDependencies": {
35
- "@happyvertical/smrt-app-runtime": "0.52.0",
36
- "@happyvertical/smrt-assets": "0.52.0",
37
- "@happyvertical/smrt-cli": "0.52.0",
38
- "@happyvertical/smrt-config": "0.52.0",
39
- "@happyvertical/smrt-jobs": "0.52.0",
40
- "@happyvertical/smrt-playbooks": "0.52.0",
41
- "@happyvertical/smrt-profiles": "0.52.0",
42
- "@happyvertical/smrt-scanner": "0.52.0",
43
- "@happyvertical/smrt-svelte": "0.52.0",
44
- "@happyvertical/smrt-tenancy": "0.52.0",
45
- "@happyvertical/smrt-ui": "0.52.0",
46
- "@happyvertical/smrt-users": "0.52.0",
47
- "@happyvertical/smrt-web": "0.52.0",
38
+ "@happyvertical/smrt-app-runtime": "0.53.0",
39
+ "@happyvertical/smrt-assets": "0.53.0",
40
+ "@happyvertical/smrt-cli": "0.53.0",
41
+ "@happyvertical/smrt-config": "0.53.0",
42
+ "@happyvertical/smrt-jobs": "0.53.0",
43
+ "@happyvertical/smrt-playbooks": "0.53.0",
44
+ "@happyvertical/smrt-profiles": "0.53.0",
45
+ "@happyvertical/smrt-scanner": "0.53.0",
46
+ "@happyvertical/smrt-svelte": "0.53.0",
47
+ "@happyvertical/smrt-tenancy": "0.53.0",
48
+ "@happyvertical/smrt-ui": "0.53.0",
49
+ "@happyvertical/smrt-users": "0.53.0",
50
+ "@happyvertical/smrt-web": "0.53.0",
48
51
  "@happyvertical/sql": "^0.89.12",
49
52
  "@modelcontextprotocol/server": "^2.0.0",
50
53
  "@playwright/test": "^1.63.0",
@@ -1,15 +1,17 @@
1
- FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS build
1
+ FROM node:26.9.0-alpine@sha256:dbaa92e5758cbbcf85d65d5403fdb530fe3442cbe8c6dbfb7ef23365450d5070 AS build
2
2
  WORKDIR /app
3
- RUN corepack enable
4
- COPY package.json pnpm-lock.yaml* ./
3
+ # Node 25+ images no longer bundle Corepack; a pinned copy keeps
4
+ # `packageManager` authoritative for the pnpm version.
5
+ RUN npm install --global corepack@0.36.0 && corepack enable
6
+ COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml ./
5
7
  RUN pnpm install --frozen-lockfile
6
8
  COPY . .
7
9
  RUN pnpm build
8
10
 
9
- FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS runtime
11
+ FROM node:26.9.0-alpine@sha256:dbaa92e5758cbbcf85d65d5403fdb530fe3442cbe8c6dbfb7ef23365450d5070 AS runtime
10
12
  WORKDIR /app
11
- RUN corepack enable
12
- COPY --from=build /app/package.json /app/pnpm-lock.yaml* ./
13
+ RUN npm install --global corepack@0.36.0 && corepack enable
14
+ COPY --from=build /app/package.json /app/pnpm-lock.yaml* /app/pnpm-workspace.yaml ./
13
15
  RUN pnpm install --frozen-lockfile --prod=false
14
16
  ENV NODE_ENV=production
15
17
  ENV SMRT_RUNTIME_PROFILE=self-hosted
@@ -6,7 +6,7 @@ managed-cloud environments. It does not provision external providers.
6
6
 
7
7
  ## 1. Install and run
8
8
 
9
- Requirements: Node.js 24.18.0 or newer and pnpm 11.25.0. The exact pnpm version
9
+ Requirements: Node.js 26.0.0 or newer and pnpm 11.25.0. The exact pnpm version
10
10
  is declared in `packageManager`.
11
11
 
12
12
  ```bash
@@ -222,6 +222,32 @@ After a mutation, call `invalidate('smrt:items')`. Only loads that declared
222
222
 
223
223
  ## 8. Use generated REST, MCP, WebMCP, and CLI interfaces
224
224
 
225
+ ## Optional portable MCP Apps packaging
226
+
227
+ The ordinary scaffold does not create a plugin package or add a remote endpoint.
228
+ When you create a project with `smrt gnode create <name> --mcp-apps`, it adds
229
+ `mcp-apps/plugin.json` and `mcp-apps/mcp.json`. The initial server points only
230
+ at loopback (`http://127.0.0.1:3000/api/mcp`) so it cannot silently publish a
231
+ local instance. Run `pnpm mcp-apps:validate` in the generated application after
232
+ editing package metadata. It rejects symlinks, traversal paths, missing or linked UI assets (including screenshots),
233
+ non-object or malformed manifests, credential-shaped JSON fields and credential carrier files,
234
+ URL userinfo, non-portable schemas, and non-loopback HTTP server URLs. Diagnostics never
235
+ include malformed manifest content.
236
+
237
+ The option stages `src/routes/api/mcp/+server.ts`, a session-authorized `mcp`
238
+ server, and a bounded static resource with a restrictive CSP. It also includes
239
+ `McpAppsBridge.svelte` for an application-configured trusted host origin and
240
+ the optional OpenAI display adapter; it stays inert until your UI mounts it.
241
+ The scaffold does not enable remote MCP tasks, so a deployment that adds them
242
+ must supply the durable worker's live authorization callback before publication.
243
+
244
+ Before changing that URL, mount the native SDK v2 Streamable HTTP endpoint and
245
+ configure the application gateway's verified principal, issuer, audience and
246
+ scopes. Plugin metadata is an install surface only; it neither authenticates a
247
+ request nor grants tenant or workflow authority. Register `.app.json` only
248
+ after the host creates an eligible server ID; do not copy runtime extension
249
+ metadata into the portable manifest.
250
+
225
251
  The Item configuration generates:
226
252
 
227
253
  - REST: `GET`/`POST /api/items` and
@@ -375,3 +401,16 @@ production startup must set an explicit loopback `HOST`, and `app:start` is the
375
401
  recommended entry point. Stop the app before backup/import. For deployed import, stop
376
402
  web/workers and set `SMRT_MAINTENANCE_MODE=true`. Extend
377
403
  `scripts/smrt-portability.mjs` for domain-specific transformations.
404
+
405
+ ### Hosted MCP authorization
406
+
407
+ The opt-in MCP route uses the local signed session only in the `local` profile.
408
+ For `self-hosted` and `cloud`, configure HTTPS `SMRT_MCP_RESOURCE`,
409
+ `SMRT_MCP_ISSUER`, `SMRT_MCP_JWKS_URI`, and space-separated
410
+ `SMRT_MCP_SCOPES` values. Bind `resolveHostedMcpPrincipal` in
411
+ `src/lib/server/mcp-hosted-principal.ts` to an application-owned lookup that
412
+ checks the current account and active tenant membership on every request. It
413
+ must return `null` for disabled, revoked, or unmapped identities. The route
414
+ does not derive tenant authority from JWT claims, request headers, or tool
415
+ arguments; missing configuration or a missing binding fails closed before MCP
416
+ dispatch.
@@ -5,12 +5,12 @@
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.25.0",
7
7
  "engines": {
8
- "node": ">=24.18.0",
8
+ "node": ">=26.0.0",
9
9
  "pnpm": "11.25.0"
10
10
  },
11
11
  "scripts": {
12
12
  "dev": "node --env-file-if-exists=.env scripts/smrt-vite.mjs dev",
13
- "build": "node --env-file-if-exists=.env scripts/smrt-vite.mjs build",
13
+ "build": "node scripts/smrt-mcp-apps.mjs validate-if-present && node --env-file-if-exists=.env scripts/smrt-vite.mjs build",
14
14
  "preview": "node scripts/smrt-app.mjs start",
15
15
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
16
16
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -25,6 +25,7 @@
25
25
  "app:backup": "node scripts/smrt-app.mjs backup",
26
26
  "app:export": "node scripts/smrt-app.mjs export",
27
27
  "app:import": "node scripts/smrt-app.mjs import",
28
+ "mcp-apps:validate": "pnpm exec smrt mcp-apps validate --output-dir ./mcp-apps",
28
29
  "worker": "node scripts/smrt-worker.mjs task",
29
30
  "worker:schedule": "node scripts/smrt-worker.mjs schedule"
30
31
  },
@@ -37,19 +38,19 @@
37
38
  "svelte-check": "^4.7.6",
38
39
  "typescript": "6.0.3",
39
40
  "vite": "8.3.0",
40
- "@happyvertical/smrt-cli": "^0.52.0"
41
+ "@happyvertical/smrt-cli": "^0.53.0"
41
42
  },
42
43
  "dependencies": {
43
- "@happyvertical/smrt-app-runtime": "^0.52.0",
44
- "@happyvertical/smrt-config": "^0.52.0",
45
- "@happyvertical/smrt-core": "^0.52.0",
46
- "@happyvertical/smrt-jobs": "^0.52.0",
47
- "@happyvertical/smrt-profiles": "^0.52.0",
48
- "@happyvertical/smrt-svelte": "^0.52.0",
49
- "@happyvertical/smrt-tenancy": "^0.52.0",
50
- "@happyvertical/smrt-ui": "^0.52.0",
51
- "@happyvertical/smrt-users": "^0.52.0",
52
- "@happyvertical/smrt-web": "^0.52.0",
44
+ "@happyvertical/smrt-app-runtime": "^0.53.0",
45
+ "@happyvertical/smrt-config": "^0.53.0",
46
+ "@happyvertical/smrt-core": "^0.53.0",
47
+ "@happyvertical/smrt-jobs": "^0.53.0",
48
+ "@happyvertical/smrt-profiles": "^0.53.0",
49
+ "@happyvertical/smrt-svelte": "^0.53.0",
50
+ "@happyvertical/smrt-tenancy": "^0.53.0",
51
+ "@happyvertical/smrt-ui": "^0.53.0",
52
+ "@happyvertical/smrt-users": "^0.53.0",
53
+ "@happyvertical/smrt-web": "^0.53.0",
53
54
  "@happyvertical/sql": "^0.89.12",
54
55
  "@modelcontextprotocol/server": "^2.0.0"
55
56
  }
@@ -0,0 +1,3 @@
1
+ allowBuilds:
2
+ esbuild: true
3
+ protobufjs: true
@@ -545,11 +545,11 @@ async function doctor() {
545
545
  });
546
546
  }
547
547
 
548
- if (Number(process.versions.node.split('.')[0]) < 24) {
548
+ if (Number(process.versions.node.split('.')[0]) < 26) {
549
549
  findings.push({
550
550
  code: 'unsupported-node',
551
551
  severity: 'error',
552
- message: 'Node.js 24 or newer is required.',
552
+ message: 'Node.js 26 or newer is required.',
553
553
  recovery: 'Install the Node.js version declared in package.json engines.',
554
554
  });
555
555
  }
@@ -0,0 +1,18 @@
1
+ /** Validate opt-in portable MCP Apps metadata before an application build. */
2
+ import { existsSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ if (process.argv[2] !== 'validate-if-present') {
6
+ throw new Error('Usage: smrt-mcp-apps.mjs validate-if-present');
7
+ }
8
+
9
+ if (!existsSync('mcp-apps')) process.exit(0);
10
+
11
+ const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
12
+ const result = spawnSync(
13
+ command,
14
+ ['exec', 'smrt', 'mcp-apps', 'validate', '--output-dir', './mcp-apps'],
15
+ { stdio: 'inherit' },
16
+ );
17
+ if (result.error) throw result.error;
18
+ if (result.status !== 0) process.exit(result.status ?? 1);
@@ -106,6 +106,12 @@ export function runtimeConfigurationFingerprint(
106
106
  assets: environment.SMRT_ASSETS_READINESS_MODULE || null,
107
107
  secrets: environment.SMRT_SECRETS_READINESS_MODULE || null,
108
108
  },
109
+ mcpAuthorization: {
110
+ resource: environment.SMRT_MCP_RESOURCE || null,
111
+ issuer: environment.SMRT_MCP_ISSUER || null,
112
+ jwksUri: environment.SMRT_MCP_JWKS_URI || null,
113
+ scopes: environment.SMRT_MCP_SCOPES || null,
114
+ },
109
115
  }),
110
116
  )
111
117
  .digest('hex');
@@ -0,0 +1,75 @@
1
+ /**
2
+ * SvelteKit Template Configuration
3
+ *
4
+ * This file configures the template for use with `smrt gnode create`
5
+ *
6
+ * Version pins are kept in sync with `template/package.json` and the rest of
7
+ * the s-m-r-t monorepo. Any changes here MUST be mirrored in
8
+ * `template/package.json` so generated projects match the tracked template.
9
+ */
10
+
11
+ export default {
12
+ name: 'sveltekit',
13
+ description: 'Minimal SvelteKit project with s-m-r-t integration',
14
+ framework: 'sveltekit',
15
+ version: '1.0.0',
16
+
17
+ // Template directory location (relative to this config file)
18
+ templateDir: './template',
19
+
20
+ // Dependencies to add to generated project
21
+ dependencies: {
22
+ '@happyvertical/smrt-app-runtime': '^0.53.0',
23
+ '@happyvertical/smrt-config': '^0.53.0',
24
+ '@happyvertical/smrt-core': '^0.53.0',
25
+ '@happyvertical/smrt-jobs': '^0.53.0',
26
+ '@happyvertical/smrt-profiles': '^0.53.0',
27
+ '@happyvertical/smrt-svelte': '^0.53.0',
28
+ '@happyvertical/smrt-tenancy': '^0.53.0',
29
+ '@happyvertical/smrt-ui': '^0.53.0',
30
+ '@happyvertical/smrt-users': '^0.53.0',
31
+ '@happyvertical/smrt-web': '^0.53.0',
32
+ '@happyvertical/sql': '^0.89.12',
33
+ '@modelcontextprotocol/server': '^2.0.0',
34
+ },
35
+
36
+ devDependencies: {
37
+ '@happyvertical/smrt-cli': '^0.53.0',
38
+ '@sveltejs/adapter-node': '^5.5.7',
39
+ '@sveltejs/kit': '2.70.3',
40
+ '@sveltejs/vite-plugin-svelte': '^7.3.0',
41
+ '@types/node': '^24.13.6',
42
+ 'svelte': '^5.57.1',
43
+ 'svelte-check': '^4.7.6',
44
+ 'typescript': '6.0.3',
45
+ 'vite': '8.3.0',
46
+ },
47
+
48
+ // Optional: customize files
49
+ files: {
50
+ // Rename patterns during generation
51
+ rename: {},
52
+ // Skip certain files
53
+ skip: ['.gitkeep'],
54
+ // Append/merge patterns
55
+ append: {},
56
+ },
57
+
58
+ // Placeholder substitutions
59
+ placeholders: {
60
+ '{{PROJECT_NAME}}': (ctx) => ctx.name,
61
+ '{{PACKAGE_NAME}}': (ctx) => ctx.name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
62
+ },
63
+
64
+ // Post-generation hooks
65
+ hooks: {
66
+ afterGenerate: async (ctx) => {
67
+ console.log(`\n✅ Created ${ctx.name} successfully!`);
68
+ console.log('\nNext steps:');
69
+ console.log(` cd ${ctx.name}`);
70
+ console.log(' pnpm install');
71
+ console.log(' pnpm app:install');
72
+ console.log();
73
+ },
74
+ },
75
+ };