@nifrajs/cli 3.5.0 → 4.0.2
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/README.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +62 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +275 -35
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +52 -13
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +48 -22
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +67 -17
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.js +1 -1
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-render.js +2 -2
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +110 -23
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +82 -9
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +1155 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +36 -0
- package/dist/recipes/4.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +71 -38
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +132 -87
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +44 -19
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +4 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +389 -48
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +53 -32
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- package/src/migrate-layout.ts +1338 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +5 -0
- package/src/recipes/4.0.0.ts +37 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +13 -8
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +71 -39
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +147 -95
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +50 -19
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where `nifra mcp` keeps project code: in a fresh subprocess per call (`mcp-project-child.ts`), never
|
|
3
|
+
* in the server. The server lists the tools, resources and prompts and forwards each call; the child,
|
|
4
|
+
* started in the project's directory, loads `.env` there, imports the config and the backend, answers
|
|
5
|
+
* and exits. So the server holds neither the project's environment nor its modules, and every call
|
|
6
|
+
* sees the project's current code.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from "node:crypto"
|
|
10
|
+
import { existsSync } from "node:fs"
|
|
11
|
+
import { resolve } from "node:path"
|
|
12
|
+
import { fileURLToPath } from "node:url"
|
|
13
|
+
import { CONFIG_FILE } from "./app-files.ts"
|
|
14
|
+
import type { NifraMonorepoConfig } from "./load.ts"
|
|
15
|
+
import { appFingerprint } from "./mcp-exec.ts"
|
|
16
|
+
import { CHILD_TIMEOUT_MS, readBoundedLines, readBoundedStream } from "./mcp-io.ts"
|
|
17
|
+
import type {
|
|
18
|
+
AppSurface,
|
|
19
|
+
ProjectChildAnswer,
|
|
20
|
+
ProjectChildMessage,
|
|
21
|
+
ProjectChildRequest,
|
|
22
|
+
} from "./mcp-project-child.ts"
|
|
23
|
+
import type {
|
|
24
|
+
McpPrompt,
|
|
25
|
+
McpPromptMessage,
|
|
26
|
+
McpResource,
|
|
27
|
+
McpTool,
|
|
28
|
+
McpToolContext,
|
|
29
|
+
McpToolResult,
|
|
30
|
+
} from "./mcp-protocol.ts"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Project tools that stay in the server, because none runs project code in this process: each starts
|
|
34
|
+
* its own subprocess (run, render, ws, hydrate, test, the database tools), or reads only the
|
|
35
|
+
* framework's docs or the dev server's feed. `mcp-project-isolation.test.ts` calls each one and fails
|
|
36
|
+
* if the server evaluated project code, or if a name here has no probe there.
|
|
37
|
+
*/
|
|
38
|
+
export const IN_PROCESS_TOOLS: ReadonlySet<string> = new Set([
|
|
39
|
+
"nifra_run",
|
|
40
|
+
"nifra_render",
|
|
41
|
+
"nifra_ws",
|
|
42
|
+
"nifra_hydrate",
|
|
43
|
+
"nifra_test",
|
|
44
|
+
"nifra_db_schema",
|
|
45
|
+
"nifra_db_query",
|
|
46
|
+
"nifra_db_role",
|
|
47
|
+
"nifra_errors",
|
|
48
|
+
"nifra_logs",
|
|
49
|
+
"nifra_inspect",
|
|
50
|
+
"nifra_explain",
|
|
51
|
+
"nifra_docs",
|
|
52
|
+
"nifra_example",
|
|
53
|
+
"nifra_types",
|
|
54
|
+
"nifra_learn",
|
|
55
|
+
"nifra_frontend",
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
/** A tool result is bounded again by the stdio message limit when the server sends it. */
|
|
59
|
+
const ANSWER_MAX_BYTES = 8 * 1024 * 1024
|
|
60
|
+
|
|
61
|
+
function childPath(): string {
|
|
62
|
+
return fileURLToPath(new URL(import.meta.url)).replace(
|
|
63
|
+
/mcp-isolate\.(ts|js)$/,
|
|
64
|
+
"mcp-project-child.$1",
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
69
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
70
|
+
|
|
71
|
+
const isAnswer = (value: unknown): value is ProjectChildAnswer =>
|
|
72
|
+
isRecord(value) &&
|
|
73
|
+
(value.ok === true || (value.ok === false && typeof value.message === "string"))
|
|
74
|
+
|
|
75
|
+
function parseMessage(text: string): ProjectChildMessage | undefined {
|
|
76
|
+
let value: unknown
|
|
77
|
+
try {
|
|
78
|
+
value = JSON.parse(text)
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
if (!isRecord(value)) return undefined
|
|
83
|
+
if (value.type === "answer" && isAnswer(value.answer)) {
|
|
84
|
+
return { type: "answer", answer: value.answer }
|
|
85
|
+
}
|
|
86
|
+
if (value.type === "progress" && typeof value.progress === "number") {
|
|
87
|
+
return typeof value.total === "number"
|
|
88
|
+
? { type: "progress", progress: value.progress, total: value.total }
|
|
89
|
+
: { type: "progress", progress: value.progress }
|
|
90
|
+
}
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const failed = (message: string): ProjectChildAnswer => ({
|
|
95
|
+
ok: false,
|
|
96
|
+
message: `[nifra] ${message}`,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
export interface ProjectChildOptions {
|
|
100
|
+
/** Progress is forwarded to it, and its cancellation kills the subprocess. */
|
|
101
|
+
readonly context?: McpToolContext
|
|
102
|
+
/** Kill the subprocess after this long. Tool calls have none: a client cancels them instead. */
|
|
103
|
+
readonly timeoutMs?: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Answer `request` in a fresh subprocess in `cwd`; a crash, a timeout or no answer is `ok: false`. */
|
|
107
|
+
export async function askProjectChild(
|
|
108
|
+
cwd: string,
|
|
109
|
+
request: ProjectChildRequest,
|
|
110
|
+
options: ProjectChildOptions = {},
|
|
111
|
+
): Promise<ProjectChildAnswer> {
|
|
112
|
+
const { context, timeoutMs } = options
|
|
113
|
+
if (context?.signal.aborted) return failed("the call was cancelled before it started")
|
|
114
|
+
const token = randomUUID()
|
|
115
|
+
let proc: Bun.Subprocess<"pipe", "pipe", "pipe">
|
|
116
|
+
try {
|
|
117
|
+
proc = Bun.spawn([process.execPath, childPath(), cwd], {
|
|
118
|
+
cwd,
|
|
119
|
+
// Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
|
|
120
|
+
env: process.env,
|
|
121
|
+
stdin: "pipe",
|
|
122
|
+
stdout: "pipe",
|
|
123
|
+
stderr: "pipe",
|
|
124
|
+
})
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return failed(
|
|
127
|
+
`cannot start a process in ${cwd}: ${error instanceof Error ? error.message : String(error)}`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
let stopped: "timeout" | "cancel" | undefined
|
|
131
|
+
const stop = (reason: "timeout" | "cancel"): void => {
|
|
132
|
+
if (stopped !== undefined) return
|
|
133
|
+
stopped = reason
|
|
134
|
+
proc.kill("SIGKILL")
|
|
135
|
+
}
|
|
136
|
+
const timer = timeoutMs === undefined ? undefined : setTimeout(() => stop("timeout"), timeoutMs)
|
|
137
|
+
const onAbort = (): void => stop("cancel")
|
|
138
|
+
context?.signal.addEventListener("abort", onAbort, { once: true })
|
|
139
|
+
try {
|
|
140
|
+
proc.stdin.write(JSON.stringify({ ...request, token }))
|
|
141
|
+
await proc.stdin.end()
|
|
142
|
+
const stderr = readBoundedStream(proc.stderr, 16 * 1024)
|
|
143
|
+
let answer: ProjectChildAnswer | undefined
|
|
144
|
+
let tooLarge = false
|
|
145
|
+
for await (const item of readBoundedLines(proc.stdout, ANSWER_MAX_BYTES)) {
|
|
146
|
+
if (item.kind === "too-large") {
|
|
147
|
+
tooLarge = true
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (!item.text.startsWith(`${token} `)) continue
|
|
151
|
+
const message = parseMessage(item.text.slice(token.length + 1))
|
|
152
|
+
if (message?.type === "progress") context?.reportProgress(message.progress, message.total)
|
|
153
|
+
else if (message?.type === "answer") answer = message.answer
|
|
154
|
+
}
|
|
155
|
+
const [errors] = await Promise.all([stderr, proc.exited])
|
|
156
|
+
if (answer !== undefined) return answer
|
|
157
|
+
if (stopped === "timeout") {
|
|
158
|
+
return failed(`the project process did not answer within ${timeoutMs}ms and was killed`)
|
|
159
|
+
}
|
|
160
|
+
if (stopped === "cancel") return failed("the call was cancelled")
|
|
161
|
+
if (tooLarge) return failed(`the answer exceeded ${ANSWER_MAX_BYTES} bytes`)
|
|
162
|
+
const tail = errors.text.trim().slice(-600)
|
|
163
|
+
const how = proc.signalCode === null ? `with code ${proc.exitCode}` : `on ${proc.signalCode}`
|
|
164
|
+
return failed(
|
|
165
|
+
`the project process exited ${how} before answering${tail === "" ? "" : `: ${tail}`}`,
|
|
166
|
+
)
|
|
167
|
+
} finally {
|
|
168
|
+
clearTimeout(timer)
|
|
169
|
+
context?.signal.removeEventListener("abort", onAbort)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function callTool(
|
|
174
|
+
cwd: string,
|
|
175
|
+
name: string,
|
|
176
|
+
args: Record<string, unknown>,
|
|
177
|
+
context: McpToolContext,
|
|
178
|
+
): Promise<string | McpToolResult> {
|
|
179
|
+
const answer = await askProjectChild(cwd, { op: "tool", name, args }, { context })
|
|
180
|
+
if (!answer.ok) throw new Error(answer.message)
|
|
181
|
+
if ("result" in answer) return answer.result
|
|
182
|
+
throw new Error(`[nifra] the project process answered ${name} without a result`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function readResource(
|
|
186
|
+
cwd: string,
|
|
187
|
+
uri: string,
|
|
188
|
+
): Promise<{ readonly text: string; readonly mimeType?: string }> {
|
|
189
|
+
const answer = await askProjectChild(
|
|
190
|
+
cwd,
|
|
191
|
+
{ op: "resource", uri },
|
|
192
|
+
{ timeoutMs: CHILD_TIMEOUT_MS },
|
|
193
|
+
)
|
|
194
|
+
if (!answer.ok) throw new Error(answer.message)
|
|
195
|
+
if ("resource" in answer) return answer.resource
|
|
196
|
+
throw new Error(`[nifra] the project process answered ${uri} without its text`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function getPrompt(
|
|
200
|
+
cwd: string,
|
|
201
|
+
name: string,
|
|
202
|
+
args: Record<string, unknown>,
|
|
203
|
+
): Promise<readonly McpPromptMessage[]> {
|
|
204
|
+
const answer = await askProjectChild(
|
|
205
|
+
cwd,
|
|
206
|
+
{ op: "prompt", name, args },
|
|
207
|
+
{ timeoutMs: CHILD_TIMEOUT_MS },
|
|
208
|
+
)
|
|
209
|
+
if (!answer.ok) throw new Error(answer.message)
|
|
210
|
+
if ("messages" in answer) return answer.messages
|
|
211
|
+
throw new Error(`[nifra] the project process answered ${name} without messages`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Each tool runs in a fresh project subprocess, unless it is one that runs no project code here. */
|
|
215
|
+
export function isolateTools(cwd: string, tools: readonly McpTool[]): McpTool[] {
|
|
216
|
+
return tools.map((tool) =>
|
|
217
|
+
IN_PROCESS_TOOLS.has(tool.name)
|
|
218
|
+
? tool
|
|
219
|
+
: { ...tool, handler: (args, context) => callTool(cwd, tool.name, args, context) },
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Each resource is read in a fresh project subprocess. */
|
|
224
|
+
export function isolateResources(cwd: string, resources: readonly McpResource[]): McpResource[] {
|
|
225
|
+
return resources.map((resource) => ({ ...resource, read: () => readResource(cwd, resource.uri) }))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The app's own tools, resources and prompts, each forwarded to a project subprocess. */
|
|
229
|
+
export interface IsolatedSurface {
|
|
230
|
+
readonly tools: readonly McpTool[]
|
|
231
|
+
readonly resources: readonly McpResource[]
|
|
232
|
+
readonly prompts: readonly McpPrompt[]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const EMPTY_SURFACE: AppSurface = { tools: [], resources: [], prompts: [] }
|
|
236
|
+
|
|
237
|
+
async function readSurface(cwd: string): Promise<IsolatedSurface> {
|
|
238
|
+
const answer = await askProjectChild(cwd, { op: "surface" }, { timeoutMs: CHILD_TIMEOUT_MS })
|
|
239
|
+
const surface = answer.ok && "surface" in answer ? answer.surface : EMPTY_SURFACE
|
|
240
|
+
return {
|
|
241
|
+
tools: surface.tools.map((tool) => ({
|
|
242
|
+
...tool,
|
|
243
|
+
handler: (args, context) => callTool(cwd, tool.name, args, context),
|
|
244
|
+
})),
|
|
245
|
+
resources: surface.resources.map((resource) => ({
|
|
246
|
+
...resource,
|
|
247
|
+
read: () => readResource(cwd, resource.uri),
|
|
248
|
+
})),
|
|
249
|
+
prompts: surface.prompts.map((prompt) => ({
|
|
250
|
+
...prompt,
|
|
251
|
+
handler: (args) => getPrompt(cwd, prompt.name, args),
|
|
252
|
+
})),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* What the app declares on its backend (`app.tool(...)` and its resources and prompts), read in a
|
|
258
|
+
* project subprocess and kept until `nifra.config.ts`, `backend/framework.ts` or `backend/app.ts`
|
|
259
|
+
* changes. An app that cannot be loaded declares nothing: its absence must not withdraw the built-in
|
|
260
|
+
* tools, and a broken app must not start a subprocess on every message.
|
|
261
|
+
*/
|
|
262
|
+
export function createAppSurface(cwd: string): () => Promise<IsolatedSurface> {
|
|
263
|
+
let cached:
|
|
264
|
+
| { readonly fingerprint: string; readonly surface: Promise<IsolatedSurface> }
|
|
265
|
+
| undefined
|
|
266
|
+
return async () => {
|
|
267
|
+
const fingerprint = await appFingerprint(cwd).catch(() => "unreadable")
|
|
268
|
+
if (cached?.fingerprint !== fingerprint) {
|
|
269
|
+
cached = { fingerprint, surface: readSurface(cwd) }
|
|
270
|
+
}
|
|
271
|
+
return cached.surface
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* `detectMonorepo`, with the root config evaluated in a project subprocess. Only a directory with a
|
|
277
|
+
* `nifra.config.ts` and no `routes/` can be a monorepo root, so no other directory starts one.
|
|
278
|
+
*/
|
|
279
|
+
export async function detectMonorepoIsolated(cwd: string): Promise<NifraMonorepoConfig | null> {
|
|
280
|
+
if (!existsSync(resolve(cwd, CONFIG_FILE)) || existsSync(resolve(cwd, "routes"))) return null
|
|
281
|
+
const answer = await askProjectChild(cwd, { op: "monorepo" }, { timeoutMs: CHILD_TIMEOUT_MS })
|
|
282
|
+
if (!answer.ok || !("apps" in answer) || answer.apps === null) return null
|
|
283
|
+
const apps = Object.entries(answer.apps).filter(
|
|
284
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
285
|
+
)
|
|
286
|
+
return { apps: Object.fromEntries(apps) }
|
|
287
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subprocess `nifra mcp` runs project code in. The server (`mcp-isolate.ts`) spawns a fresh one
|
|
3
|
+
* per call in the project's directory, so Bun loads that directory's `.env` files here, and the
|
|
4
|
+
* config, the backend and everything they import run here and exit with the process. The tools,
|
|
5
|
+
* resources and prompts answered here are the same ones the server lists: the project tools, and the
|
|
6
|
+
* ones the app declares on its backend.
|
|
7
|
+
*
|
|
8
|
+
* Protocol: one JSON request on stdin; on stdout, progress lines and then one answer line, each
|
|
9
|
+
* prefixed with the request's token, so anything project code prints cannot be read as an answer.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { detectMonorepo } from "./load.ts"
|
|
13
|
+
import { projectResources } from "./mcp-context.ts"
|
|
14
|
+
import { createCachedAppLoader, projectTools } from "./mcp-exec.ts"
|
|
15
|
+
import { CHILD_INPUT_MAX_BYTES, readBoundedStream } from "./mcp-io.ts"
|
|
16
|
+
import type {
|
|
17
|
+
McpPrompt,
|
|
18
|
+
McpPromptMessage,
|
|
19
|
+
McpResource,
|
|
20
|
+
McpTool,
|
|
21
|
+
McpToolResult,
|
|
22
|
+
} from "./mcp-protocol.ts"
|
|
23
|
+
import {
|
|
24
|
+
extractBackendPrompts,
|
|
25
|
+
extractBackendResources,
|
|
26
|
+
extractBackendTools,
|
|
27
|
+
} from "./mcp-reflect.ts"
|
|
28
|
+
|
|
29
|
+
export type ProjectChildRequest =
|
|
30
|
+
| { readonly op: "monorepo" }
|
|
31
|
+
| { readonly op: "surface" }
|
|
32
|
+
| { readonly op: "tool"; readonly name: string; readonly args: Record<string, unknown> }
|
|
33
|
+
| { readonly op: "resource"; readonly uri: string }
|
|
34
|
+
| { readonly op: "prompt"; readonly name: string; readonly args: Record<string, unknown> }
|
|
35
|
+
|
|
36
|
+
/** What the server lists for the tools, resources and prompts the app declares on its backend. */
|
|
37
|
+
export interface AppSurface {
|
|
38
|
+
readonly tools: readonly Omit<McpTool, "handler">[]
|
|
39
|
+
readonly resources: readonly Omit<McpResource, "read">[]
|
|
40
|
+
readonly prompts: readonly Omit<McpPrompt, "handler">[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ProjectChildAnswer =
|
|
44
|
+
| { readonly ok: true; readonly apps: Readonly<Record<string, string>> | null }
|
|
45
|
+
| { readonly ok: true; readonly surface: AppSurface }
|
|
46
|
+
| { readonly ok: true; readonly result: string | McpToolResult }
|
|
47
|
+
| { readonly ok: true; readonly resource: { readonly text: string; readonly mimeType?: string } }
|
|
48
|
+
| { readonly ok: true; readonly messages: readonly McpPromptMessage[] }
|
|
49
|
+
| { readonly ok: false; readonly message: string }
|
|
50
|
+
|
|
51
|
+
export type ProjectChildMessage =
|
|
52
|
+
| { readonly type: "progress"; readonly progress: number; readonly total?: number }
|
|
53
|
+
| { readonly type: "answer"; readonly answer: ProjectChildAnswer }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Answer `request` for the project at `cwd`. A tool, resource or prompt that throws answers
|
|
57
|
+
* `ok: false` with its message; an app that cannot be loaded declares nothing, as in the server.
|
|
58
|
+
*/
|
|
59
|
+
export async function answerProjectRequest(
|
|
60
|
+
cwd: string,
|
|
61
|
+
request: ProjectChildRequest,
|
|
62
|
+
reportProgress: (progress: number, total?: number) => void = () => {},
|
|
63
|
+
): Promise<ProjectChildAnswer> {
|
|
64
|
+
try {
|
|
65
|
+
if (request.op === "monorepo") {
|
|
66
|
+
return { ok: true, apps: (await detectMonorepo(cwd))?.apps ?? null }
|
|
67
|
+
}
|
|
68
|
+
const loadAppCached = createCachedAppLoader(cwd)
|
|
69
|
+
const backend = (): Promise<unknown> =>
|
|
70
|
+
loadAppCached().then(
|
|
71
|
+
(app) => app.backend,
|
|
72
|
+
() => undefined,
|
|
73
|
+
)
|
|
74
|
+
if (request.op === "surface") {
|
|
75
|
+
const declared = await backend()
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
surface: {
|
|
79
|
+
tools: extractBackendTools(declared),
|
|
80
|
+
resources: extractBackendResources(declared),
|
|
81
|
+
prompts: extractBackendPrompts(declared),
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (request.op === "tool") {
|
|
86
|
+
const tool =
|
|
87
|
+
projectTools(cwd, loadAppCached).find((t) => t.name === request.name) ??
|
|
88
|
+
extractBackendTools(await backend()).find((t) => t.name === request.name)
|
|
89
|
+
if (tool === undefined) return { ok: false, message: `unknown tool: ${request.name}` }
|
|
90
|
+
const result = await tool.handler(request.args, {
|
|
91
|
+
signal: new AbortController().signal,
|
|
92
|
+
requestId: null,
|
|
93
|
+
reportProgress,
|
|
94
|
+
})
|
|
95
|
+
return { ok: true, result }
|
|
96
|
+
}
|
|
97
|
+
if (request.op === "resource") {
|
|
98
|
+
const resource =
|
|
99
|
+
projectResources(cwd, loadAppCached).find((r) => r.uri === request.uri) ??
|
|
100
|
+
extractBackendResources(await backend()).find((r) => r.uri === request.uri)
|
|
101
|
+
if (resource === undefined) return { ok: false, message: `unknown resource: ${request.uri}` }
|
|
102
|
+
return { ok: true, resource: await resource.read() }
|
|
103
|
+
}
|
|
104
|
+
const prompt = extractBackendPrompts(await backend()).find((p) => p.name === request.name)
|
|
105
|
+
if (prompt === undefined) return { ok: false, message: `unknown prompt: ${request.name}` }
|
|
106
|
+
return { ok: true, messages: await prompt.handler(request.args) }
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isArgs = (value: unknown): value is Record<string, unknown> =>
|
|
113
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
114
|
+
|
|
115
|
+
function parseRequest(text: string): { token: string; request: ProjectChildRequest } | undefined {
|
|
116
|
+
let value: unknown
|
|
117
|
+
try {
|
|
118
|
+
value = JSON.parse(text)
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined
|
|
121
|
+
}
|
|
122
|
+
if (!isArgs(value)) return undefined
|
|
123
|
+
const { token, op, name, uri, args } = value
|
|
124
|
+
if (typeof token !== "string" || !/^[0-9a-f-]{16,64}$/.test(token)) return undefined
|
|
125
|
+
if (op === "monorepo" || op === "surface") return { token, request: { op } }
|
|
126
|
+
if (op === "resource" && typeof uri === "string") return { token, request: { op, uri } }
|
|
127
|
+
if ((op === "tool" || op === "prompt") && typeof name === "string" && isArgs(args)) {
|
|
128
|
+
return { token, request: { op, name, args } }
|
|
129
|
+
}
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Serve one request: read it from `input`, answer it and write each message through `write`,
|
|
135
|
+
* prefixed with the request's token. Returns false (writing nothing) for a malformed request.
|
|
136
|
+
*/
|
|
137
|
+
export async function serveProjectChild(
|
|
138
|
+
cwd: string,
|
|
139
|
+
input: ReadableStream<Uint8Array>,
|
|
140
|
+
write: (line: string) => Promise<void>,
|
|
141
|
+
): Promise<boolean> {
|
|
142
|
+
const text = await readBoundedStream(input, CHILD_INPUT_MAX_BYTES)
|
|
143
|
+
const parsed = text.truncated ? undefined : parseRequest(text.text)
|
|
144
|
+
if (parsed === undefined) return false
|
|
145
|
+
const send = (message: ProjectChildMessage): Promise<void> =>
|
|
146
|
+
write(`${parsed.token} ${JSON.stringify(message)}\n`)
|
|
147
|
+
const answer = await answerProjectRequest(cwd, parsed.request, (progress, total) => {
|
|
148
|
+
void send(
|
|
149
|
+
total === undefined ? { type: "progress", progress } : { type: "progress", progress, total },
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
let line: string
|
|
153
|
+
try {
|
|
154
|
+
line = JSON.stringify({ type: "answer", answer } satisfies ProjectChildMessage)
|
|
155
|
+
} catch {
|
|
156
|
+
line = JSON.stringify({
|
|
157
|
+
type: "answer",
|
|
158
|
+
answer: { ok: false, message: "the answer is not serializable" },
|
|
159
|
+
} satisfies ProjectChildMessage)
|
|
160
|
+
}
|
|
161
|
+
await write(`${parsed.token} ${line}\n`)
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (import.meta.main) {
|
|
166
|
+
// Bound before project code loads, so nothing it patches can reach the protocol channel.
|
|
167
|
+
const stdout = process.stdout.write.bind(process.stdout)
|
|
168
|
+
const toStderr = (...args: unknown[]): void => {
|
|
169
|
+
process.stderr.write(`${args.map((arg) => String(arg)).join(" ")}\n`)
|
|
170
|
+
}
|
|
171
|
+
console.log = toStderr
|
|
172
|
+
console.info = toStderr
|
|
173
|
+
console.debug = toStderr
|
|
174
|
+
const served = await serveProjectChild(
|
|
175
|
+
process.argv[2] ?? process.cwd(),
|
|
176
|
+
Bun.stdin.stream(),
|
|
177
|
+
(line) => new Promise((done) => stdout(line, () => done())),
|
|
178
|
+
)
|
|
179
|
+
if (!served) process.stderr.write("nifra mcp: invalid project request\n")
|
|
180
|
+
// Project code may leave a handle open (a pool, an interval); the answer is written, so stop here.
|
|
181
|
+
process.exit(served ? 0 : 2)
|
|
182
|
+
}
|
package/src/mcp-render.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* `{ error }` so the agent gets the actionable message.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { inProcessClient } from "@nifrajs/client"
|
|
14
13
|
import { type AppLike, runApp } from "@nifrajs/runner"
|
|
15
14
|
import { createWebApp, type RenderAdapter } from "@nifrajs/web"
|
|
16
15
|
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
readBoundedStream,
|
|
23
22
|
serializeBoundedJson,
|
|
24
23
|
} from "./mcp-io.ts"
|
|
24
|
+
import { frameworkWebAppOptions } from "./web-app-options.ts"
|
|
25
25
|
|
|
26
26
|
const errString = (err: unknown): string =>
|
|
27
27
|
err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
|
@@ -54,7 +54,7 @@ async function buildWebApp(cwd: string): Promise<{ app: AppLike } | { error: str
|
|
|
54
54
|
adapter: app.framework.adapter as RenderAdapter,
|
|
55
55
|
manifest: discoverRoutes(app.routesDir),
|
|
56
56
|
clientEntry: "/_nifra-render-only.js", // placeholder; SSR HTML does not need the built client
|
|
57
|
-
...(app.
|
|
57
|
+
...frameworkWebAppOptions(app.framework, app.backend),
|
|
58
58
|
}) as unknown as AppLike
|
|
59
59
|
return { app: webApp }
|
|
60
60
|
} catch (err) {
|