@nifrajs/cli 2.2.0 → 2.4.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/README.md +25 -25
- package/dist/assure.d.ts +2 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +17 -10
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +7 -0
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +68 -16
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +61 -22
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +336 -87
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +30 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +148 -39
- package/dist/cli.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.js +7 -7
- package/dist/docs-search.d.ts +3 -3
- package/dist/docs-search.js +6 -6
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +12 -8
- package/dist/doctor.js.map +1 -1
- package/dist/examples.d.ts +5 -5
- package/dist/examples.js +5 -5
- package/dist/init-agents.d.ts +14 -14
- package/dist/init-agents.js +14 -14
- package/dist/internal/typescript-import.d.ts +24 -0
- package/dist/internal/typescript-import.d.ts.map +1 -0
- package/dist/internal/typescript-import.js +32 -0
- package/dist/internal/typescript-import.js.map +1 -0
- package/dist/introspect.d.ts +12 -12
- package/dist/introspect.js +24 -24
- package/dist/learn.d.ts +32 -0
- package/dist/learn.d.ts.map +1 -0
- package/dist/learn.js +127 -0
- package/dist/learn.js.map +1 -0
- package/dist/levels-tool.d.ts +8 -8
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -30
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +6 -6
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +15 -7
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +4 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts +14 -0
- package/dist/mcp-app-tool.d.ts.map +1 -0
- package/dist/mcp-app-tool.js +80 -0
- package/dist/mcp-app-tool.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts +1 -1
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +25 -9
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.d.ts +14 -10
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +30 -16
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-protocol.d.ts +2 -2
- package/dist/mcp-protocol.js +2 -2
- package/dist/mcp-render.d.ts +3 -3
- package/dist/mcp-render.js +6 -6
- package/dist/mcp-run.d.ts +2 -2
- package/dist/mcp-run.js +2 -2
- package/dist/mcp.d.ts +28 -14
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +263 -45
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +1 -1
- package/dist/openapi-tool.js +2 -2
- package/dist/pipeline-guard.d.ts +1 -1
- package/dist/pipeline-guard.js +1 -1
- package/dist/port.d.ts +9 -9
- package/dist/port.js +27 -27
- package/dist/recipes/1.8.0.d.ts +2 -2
- package/dist/recipes/1.8.0.js +5 -5
- package/dist/recipes/index.d.ts +2 -2
- package/dist/recipes/index.js +1 -1
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +8 -8
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.d.ts.map +1 -0
- package/dist/sync-routes.js +55 -0
- package/dist/sync-routes.js.map +1 -0
- package/dist/types-search.d.ts +5 -5
- package/dist/types-search.js +7 -7
- package/dist/upgrade.d.ts +20 -9
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +117 -19
- package/dist/upgrade.js.map +1 -1
- package/dist/verification.d.ts +51 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +80 -0
- package/dist/verification.js.map +1 -0
- package/docs/examples.json +174 -90
- package/docs/llms-full.txt +1771 -748
- package/docs/types.json +2027 -3089
- package/package.json +22 -11
- package/src/assure.ts +17 -18
- package/src/capabilities-tool.ts +82 -16
- package/src/check.ts +410 -104
- package/src/cli.ts +161 -41
- package/src/diff-tool.ts +8 -8
- package/src/docs-search.ts +6 -6
- package/src/doctor.ts +18 -10
- package/src/examples.ts +7 -7
- package/src/init-agents.ts +17 -17
- package/src/internal/typescript-import.ts +33 -0
- package/src/introspect.ts +28 -28
- package/src/learn.ts +152 -0
- package/src/levels-tool.ts +37 -31
- package/src/load.ts +21 -12
- package/src/manifest-tool.ts +4 -2
- package/src/mcp-app-tool.ts +86 -0
- package/src/mcp-docs-tools.ts +26 -9
- package/src/mcp-http.ts +31 -18
- package/src/mcp-protocol.ts +2 -2
- package/src/mcp-render.ts +6 -6
- package/src/mcp-run.ts +2 -2
- package/src/mcp.ts +319 -45
- package/src/openapi-tool.ts +2 -2
- package/src/pipeline-guard.ts +1 -1
- package/src/port.ts +31 -31
- package/src/recipes/1.8.0.ts +5 -5
- package/src/recipes/index.ts +2 -2
- package/src/scaffold.ts +8 -8
- package/src/sync-routes.ts +69 -0
- package/src/types-search.ts +8 -8
- package/src/upgrade.ts +137 -19
- package/src/verification.ts +123 -0
package/dist/mcp-protocol.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The MCP JSON-RPC dispatch now lives in `@nifrajs/mcp` (shared with nifra apps that expose their own
|
|
3
|
-
* MCP server / MCP Apps widgets). This module is a thin re-export so the CLI's existing imports
|
|
4
|
-
* the `@nifrajs/cli/mcp` self-host surface
|
|
3
|
+
* MCP server / MCP Apps widgets). This module is a thin re-export so the CLI's existing imports - and
|
|
4
|
+
* the `@nifrajs/cli/mcp` self-host surface - keep resolving here unchanged.
|
|
5
5
|
*/
|
|
6
6
|
export * from "@nifrajs/mcp/protocol";
|
|
7
7
|
//# sourceMappingURL=mcp-protocol.js.map
|
package/dist/mcp-render.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `nifra_render` engine + its child-process entry
|
|
2
|
+
* The `nifra_render` engine + its child-process entry - the page half of `nifra_run`. Where `nifra_run`
|
|
3
3
|
* runs requests through the BACKEND, this SSRs page routes through the WEB app (`createWebApp`), so an
|
|
4
4
|
* agent can verify a page renders and its loader ran after an edit. Spawned fresh per call (`bun
|
|
5
5
|
* mcp-render.ts <cwd>`) so the project's CURRENT route/loader code is loaded.
|
|
6
6
|
*
|
|
7
|
-
* No build required: a placeholder `clientEntry` is injected (the SSR HTML renders regardless
|
|
7
|
+
* No build required: a placeholder `clientEntry` is injected (the SSR HTML renders regardless - the
|
|
8
8
|
* client bundle only matters for hydration in the browser, not for "did this page render server-side").
|
|
9
|
-
* {@link renderPages} never throws
|
|
9
|
+
* {@link renderPages} never throws - every failure (no config, a loader error, a render throw) becomes
|
|
10
10
|
* `{ error }` so the agent gets the actionable message.
|
|
11
11
|
*/
|
|
12
12
|
export declare function renderPages(cwd: string, requests: unknown): Promise<{
|
package/dist/mcp-render.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `nifra_render` engine + its child-process entry
|
|
2
|
+
* The `nifra_render` engine + its child-process entry - the page half of `nifra_run`. Where `nifra_run`
|
|
3
3
|
* runs requests through the BACKEND, this SSRs page routes through the WEB app (`createWebApp`), so an
|
|
4
4
|
* agent can verify a page renders and its loader ran after an edit. Spawned fresh per call (`bun
|
|
5
5
|
* mcp-render.ts <cwd>`) so the project's CURRENT route/loader code is loaded.
|
|
6
6
|
*
|
|
7
|
-
* No build required: a placeholder `clientEntry` is injected (the SSR HTML renders regardless
|
|
7
|
+
* No build required: a placeholder `clientEntry` is injected (the SSR HTML renders regardless - the
|
|
8
8
|
* client bundle only matters for hydration in the browser, not for "did this page render server-side").
|
|
9
|
-
* {@link renderPages} never throws
|
|
9
|
+
* {@link renderPages} never throws - every failure (no config, a loader error, a render throw) becomes
|
|
10
10
|
* `{ error }` so the agent gets the actionable message.
|
|
11
11
|
*/
|
|
12
12
|
import { inProcessClient } from "@nifrajs/client";
|
|
@@ -15,7 +15,7 @@ import { createWebApp } from "@nifrajs/web";
|
|
|
15
15
|
import { discoverRoutes } from "@nifrajs/web/fs";
|
|
16
16
|
import { loadApp, resolvePlugins } from "./load.js";
|
|
17
17
|
const errString = (err) => err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
18
|
-
/** Load the project, register its SSR plugins, and build the SSR `createWebApp` once
|
|
18
|
+
/** Load the project, register its SSR plugins, and build the SSR `createWebApp` once - the cost the warm
|
|
19
19
|
* worker amortizes across calls. Never throws (every failure → `{ error }`). Factored out of
|
|
20
20
|
* {@link renderPages} so the cold path (fresh process per call) and the warm worker share one builder. */
|
|
21
21
|
async function buildWebApp(cwd) {
|
|
@@ -79,7 +79,7 @@ function redirectConsoleToStderr() {
|
|
|
79
79
|
/**
|
|
80
80
|
* Warm-worker loop (`--worker`): build the web app ONCE, then SSR each `{ id, input: { requests } }` line
|
|
81
81
|
* against that hot app and reply `{ id, output }`. The parent (`mcp.ts`'s warm-render handler) spawns this,
|
|
82
|
-
* fingerprints the source tree, and restarts the worker when a file changes
|
|
82
|
+
* fingerprints the source tree, and restarts the worker when a file changes - so warm reuse never serves a
|
|
83
83
|
* stale render. Mirrors `mcp-run.ts`'s worker (single backend entry → no per-call entry key needed here).
|
|
84
84
|
*/
|
|
85
85
|
async function runWorker(cwd) {
|
|
@@ -136,7 +136,7 @@ if (import.meta.main) {
|
|
|
136
136
|
// The parent (mcp.ts) spawns `bun mcp-render.ts <cwd>` WITHOUT setting the subprocess `cwd` option, so
|
|
137
137
|
// this process inherits the MCP server's working dir, not the app's. SSR resolves `react-dom/server`
|
|
138
138
|
// from `process.cwd()` to share ONE React core with the app's route components (the dual-React fix in
|
|
139
|
-
// @nifrajs/web-react)
|
|
139
|
+
// @nifrajs/web-react) - so we must chdir to the app root before any render. Best-effort: a bad path
|
|
140
140
|
// surfaces later as a load error rather than crashing the child here.
|
|
141
141
|
if (cwd !== process.cwd()) {
|
|
142
142
|
try {
|
package/dist/mcp-run.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `nifra_run` engine + its child-process entry. The MCP server spawns this file fresh on every
|
|
3
|
-
* `nifra_run` call (`bun mcp-run.ts <cwd>`) so the project's CURRENT backend code is loaded
|
|
3
|
+
* `nifra_run` call (`bun mcp-run.ts <cwd>`) so the project's CURRENT backend code is loaded - picking
|
|
4
4
|
* up the agent's latest edits. {@link runBackend} is the pure-ish core (resolve entry → import → run),
|
|
5
5
|
* exported so it's unit-testable in-process; the entry below wires it to stdin/stdout.
|
|
6
6
|
*/
|
|
@@ -12,7 +12,7 @@ export declare function loadBackend(cwd: string, entry?: string): Promise<{
|
|
|
12
12
|
}>;
|
|
13
13
|
/**
|
|
14
14
|
* Resolve the backend entry under `cwd`, import it, and run `requests` through its exported app. Never
|
|
15
|
-
* throws
|
|
15
|
+
* throws - every failure (missing/invalid entry, import/compile/runtime error) becomes `{ error }`, so
|
|
16
16
|
* the caller (and the agent) always gets actionable output. An import error here IS the failure the
|
|
17
17
|
* agent needs to see and fix.
|
|
18
18
|
*/
|
package/dist/mcp-run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `nifra_run` engine + its child-process entry. The MCP server spawns this file fresh on every
|
|
3
|
-
* `nifra_run` call (`bun mcp-run.ts <cwd>`) so the project's CURRENT backend code is loaded
|
|
3
|
+
* `nifra_run` call (`bun mcp-run.ts <cwd>`) so the project's CURRENT backend code is loaded - picking
|
|
4
4
|
* up the agent's latest edits. {@link runBackend} is the pure-ish core (resolve entry → import → run),
|
|
5
5
|
* exported so it's unit-testable in-process; the entry below wires it to stdin/stdout.
|
|
6
6
|
*/
|
|
@@ -47,7 +47,7 @@ export async function loadBackend(cwd, entry) {
|
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
49
|
* Resolve the backend entry under `cwd`, import it, and run `requests` through its exported app. Never
|
|
50
|
-
* throws
|
|
50
|
+
* throws - every failure (missing/invalid entry, import/compile/runtime error) becomes `{ error }`, so
|
|
51
51
|
* the caller (and the agent) always gets actionable output. An import error here IS the failure the
|
|
52
52
|
* agent needs to see and fix.
|
|
53
53
|
*/
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,35 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra mcp`
|
|
2
|
+
* `nifra mcp` - a Model Context Protocol server (stdio) that lets a coding agent (Claude, Cursor, …)
|
|
3
3
|
* act on a nifra project, not just read about it:
|
|
4
4
|
*
|
|
5
|
-
* - `nifra_context`
|
|
6
|
-
* - `nifra_run`
|
|
5
|
+
* - `nifra_context` - this project's API routes + page routes + conventions (see {@link describeProject}).
|
|
6
|
+
* - `nifra_run` - run HTTP requests through the project's backend and return structured results
|
|
7
7
|
* (status, body, errors). The write → run → see-the-failure → fix loop, powered by `@nifrajs/runner`.
|
|
8
8
|
* Each call runs the backend in a FRESH subprocess, so it reflects the agent's latest edits.
|
|
9
|
-
* - `nifra_render`
|
|
9
|
+
* - `nifra_render` - SSR a page route through the project's web app; returns the rendered HTML (the
|
|
10
10
|
* page half of `nifra_run`). Fresh subprocess per call; no build needed.
|
|
11
|
-
* - `nifra_ws`
|
|
12
|
-
* - `nifra_docs`
|
|
13
|
-
* - `nifra_example`
|
|
11
|
+
* - `nifra_ws` - verify a WebSocket route with a real Bun WebSocket round-trip.
|
|
12
|
+
* - `nifra_docs` - keyword-search the framework docs; returns only the matching sections.
|
|
13
|
+
* - `nifra_example` - a verified, copy-pasteable snippet for a task (typechecked against the live API,
|
|
14
14
|
* so it can't hallucinate a drifted nifra API).
|
|
15
|
-
* - `
|
|
16
|
-
*
|
|
17
|
-
* - `
|
|
15
|
+
* - `nifra_learn` - the guided, ordered path to build a nifra app end to end (create → deploy), each
|
|
16
|
+
* step naming the tool that emits the correct artifact and how to verify it.
|
|
17
|
+
* - `nifra_scaffold`- map a URL path to the correct `routes/` file + a contract-correct page stub.
|
|
18
|
+
* - `nifra_check` - the drift gate (typecheck + lints, each with a structured fix), for an agent to fix against.
|
|
19
|
+
* - `nifra_assure` - route classification + enforcement-evidence gate.
|
|
18
20
|
* - `nifra_levels` - the cumulative verification ladder (L0 contract → L4 invariants): what the
|
|
19
21
|
* project actually proves, and why each level it misses does not hold.
|
|
20
|
-
* - `nifra_doctor`
|
|
22
|
+
* - `nifra_doctor` - package.json dependency drift detector, with safe local-version auto-fix.
|
|
23
|
+
* - `nifra_explain` - resolve an error (pasted, or the dev server's last) into a structured
|
|
24
|
+
* diagnostic: stable code, a codeframe in the user's source, and the recognised cause + fix.
|
|
25
|
+
* - `nifra_inspect` - read the running dev server's recent request traces (method/path/status/
|
|
26
|
+
* duration/ISR) from the DevTools plugin: what your requests ACTUALLY did, not a guess.
|
|
21
27
|
*
|
|
22
28
|
* Wire it into a client (e.g. Claude Desktop / Cursor) as: command `nifra`, args `["mcp"]`, run in the
|
|
23
29
|
* project root. The protocol is hand-rolled (newline-delimited JSON-RPC 2.0 over stdio), including
|
|
24
|
-
* standard MCP progress notifications and request cancellation
|
|
30
|
+
* standard MCP progress notifications and request cancellation - no SDK dependency, the same
|
|
25
31
|
* minimal-surface choice as the rest of nifra. The pure dispatch lives in `./mcp-protocol.ts`; this
|
|
26
32
|
* module is the I/O shell (stdin loop, tool wiring, the run subprocess).
|
|
27
33
|
*/
|
|
28
34
|
import type { LoadAppOptions, LoadedApp } from "./load.js";
|
|
29
35
|
import { type McpPrompt, type McpResource, type McpServerFeatures, type McpTool } from "./mcp-protocol.js";
|
|
36
|
+
/** Local dev-tool reads are intentionally bounded: MCP runs in an agent process and must not hang on or
|
|
37
|
+
* buffer an unrelated loopback service just because a caller supplied its port. */
|
|
38
|
+
export declare const LOCAL_TOOL_FETCH_TIMEOUT_MS = 2000;
|
|
39
|
+
export declare const LOCAL_TOOL_MAX_RESPONSE_BYTES = 1048576;
|
|
40
|
+
/** Accept only a concrete TCP port. Port 0 (bind-any-free-port) is not a valid read target. */
|
|
41
|
+
export declare function validateLocalPort(port: unknown): number | undefined;
|
|
42
|
+
/** Read a dev-tool response without allowing an untrusted localhost service to allocate unbounded memory. */
|
|
43
|
+
export declare function readBoundedResponse(response: Response, maxBytes?: number): Promise<string>;
|
|
30
44
|
/** A persistent `mcp-run`/`mcp-render` `--worker` subprocess: the backend/web app is loaded ONCE and
|
|
31
45
|
* reused across newline-delimited `{ id, input }` requests, replying `{ id, output }`. The same machinery
|
|
32
|
-
* powers both `nifra_run warm` and `nifra_render warm`
|
|
46
|
+
* powers both `nifra_run warm` and `nifra_render warm` - `child` selects which engine, `label` shapes the
|
|
33
47
|
* cancellation message. The owning handler ({@link createWarmHandler}) fingerprints the source tree and
|
|
34
48
|
* replaces the worker when a file changes, so warm reuse never serves a stale result. Exported for the
|
|
35
49
|
* concurrency test that proves a single per-request cancel doesn't tear down the shared worker. */
|
|
@@ -60,7 +74,7 @@ export declare function projectResources(cwd: string, loadAppCached?: (outDirNam
|
|
|
60
74
|
export declare function projectPrompts(): McpPrompt[];
|
|
61
75
|
export declare function projectFeatures(cwd: string, loadAppCached?: (outDirName?: string) => Promise<LoadedApp>): McpServerFeatures;
|
|
62
76
|
/**
|
|
63
|
-
* Resolve an optional `dir` tool argument
|
|
77
|
+
* Resolve an optional `dir` tool argument - a subdirectory of the project root the caller wants to scope a
|
|
64
78
|
* check/test to (e.g. `nifra check` on `app/` when the MCP server's root is a monorepo). Returns the
|
|
65
79
|
* absolute target, or `null` if `dir` escapes the root (a path-traversal guard: no `..` out, no absolute
|
|
66
80
|
* path elsewhere). `undefined`/empty → the root itself.
|
package/dist/mcp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp.js
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra mcp`
|
|
2
|
+
* `nifra mcp` - a Model Context Protocol server (stdio) that lets a coding agent (Claude, Cursor, …)
|
|
3
3
|
* act on a nifra project, not just read about it:
|
|
4
4
|
*
|
|
5
|
-
* - `nifra_context`
|
|
6
|
-
* - `nifra_run`
|
|
5
|
+
* - `nifra_context` - this project's API routes + page routes + conventions (see {@link describeProject}).
|
|
6
|
+
* - `nifra_run` - run HTTP requests through the project's backend and return structured results
|
|
7
7
|
* (status, body, errors). The write → run → see-the-failure → fix loop, powered by `@nifrajs/runner`.
|
|
8
8
|
* Each call runs the backend in a FRESH subprocess, so it reflects the agent's latest edits.
|
|
9
|
-
* - `nifra_render`
|
|
9
|
+
* - `nifra_render` - SSR a page route through the project's web app; returns the rendered HTML (the
|
|
10
10
|
* page half of `nifra_run`). Fresh subprocess per call; no build needed.
|
|
11
|
-
* - `nifra_ws`
|
|
12
|
-
* - `nifra_docs`
|
|
13
|
-
* - `nifra_example`
|
|
11
|
+
* - `nifra_ws` - verify a WebSocket route with a real Bun WebSocket round-trip.
|
|
12
|
+
* - `nifra_docs` - keyword-search the framework docs; returns only the matching sections.
|
|
13
|
+
* - `nifra_example` - a verified, copy-pasteable snippet for a task (typechecked against the live API,
|
|
14
14
|
* so it can't hallucinate a drifted nifra API).
|
|
15
|
-
* - `
|
|
16
|
-
*
|
|
17
|
-
* - `
|
|
15
|
+
* - `nifra_learn` - the guided, ordered path to build a nifra app end to end (create → deploy), each
|
|
16
|
+
* step naming the tool that emits the correct artifact and how to verify it.
|
|
17
|
+
* - `nifra_scaffold`- map a URL path to the correct `routes/` file + a contract-correct page stub.
|
|
18
|
+
* - `nifra_check` - the drift gate (typecheck + lints, each with a structured fix), for an agent to fix against.
|
|
19
|
+
* - `nifra_assure` - route classification + enforcement-evidence gate.
|
|
18
20
|
* - `nifra_levels` - the cumulative verification ladder (L0 contract → L4 invariants): what the
|
|
19
21
|
* project actually proves, and why each level it misses does not hold.
|
|
20
|
-
* - `nifra_doctor`
|
|
22
|
+
* - `nifra_doctor` - package.json dependency drift detector, with safe local-version auto-fix.
|
|
23
|
+
* - `nifra_explain` - resolve an error (pasted, or the dev server's last) into a structured
|
|
24
|
+
* diagnostic: stable code, a codeframe in the user's source, and the recognised cause + fix.
|
|
25
|
+
* - `nifra_inspect` - read the running dev server's recent request traces (method/path/status/
|
|
26
|
+
* duration/ISR) from the DevTools plugin: what your requests ACTUALLY did, not a guess.
|
|
21
27
|
*
|
|
22
28
|
* Wire it into a client (e.g. Claude Desktop / Cursor) as: command `nifra`, args `["mcp"]`, run in the
|
|
23
29
|
* project root. The protocol is hand-rolled (newline-delimited JSON-RPC 2.0 over stdio), including
|
|
24
|
-
* standard MCP progress notifications and request cancellation
|
|
30
|
+
* standard MCP progress notifications and request cancellation - no SDK dependency, the same
|
|
25
31
|
* minimal-surface choice as the rest of nifra. The pure dispatch lives in `./mcp-protocol.ts`; this
|
|
26
32
|
* module is the I/O shell (stdin loop, tool wiring, the run subprocess).
|
|
27
33
|
*/
|
|
@@ -56,6 +62,59 @@ function childPath(name) {
|
|
|
56
62
|
* within one attention span rather than after minutes of polling.
|
|
57
63
|
*/
|
|
58
64
|
const CHILD_TIMEOUT_MS = 30_000;
|
|
65
|
+
/** Local dev-tool reads are intentionally bounded: MCP runs in an agent process and must not hang on or
|
|
66
|
+
* buffer an unrelated loopback service just because a caller supplied its port. */
|
|
67
|
+
export const LOCAL_TOOL_FETCH_TIMEOUT_MS = 2_000;
|
|
68
|
+
export const LOCAL_TOOL_MAX_RESPONSE_BYTES = 1_048_576;
|
|
69
|
+
/** Accept only a concrete TCP port. Port 0 (bind-any-free-port) is not a valid read target. */
|
|
70
|
+
export function validateLocalPort(port) {
|
|
71
|
+
return typeof port === "number" && Number.isInteger(port) && port >= 1 && port <= 65_535
|
|
72
|
+
? port
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Read a dev-tool response without allowing an untrusted localhost service to allocate unbounded memory. */
|
|
76
|
+
export async function readBoundedResponse(response, maxBytes = LOCAL_TOOL_MAX_RESPONSE_BYTES) {
|
|
77
|
+
const declared = response.headers.get("content-length");
|
|
78
|
+
if (declared !== null) {
|
|
79
|
+
const length = Number(declared);
|
|
80
|
+
if (Number.isFinite(length) && length > maxBytes)
|
|
81
|
+
throw new Error("response exceeded the size limit");
|
|
82
|
+
}
|
|
83
|
+
if (response.body === null)
|
|
84
|
+
return "";
|
|
85
|
+
const reader = response.body.getReader();
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let total = 0;
|
|
88
|
+
try {
|
|
89
|
+
for (;;) {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
if (done)
|
|
92
|
+
break;
|
|
93
|
+
if (value === undefined)
|
|
94
|
+
continue;
|
|
95
|
+
total += value.byteLength;
|
|
96
|
+
if (total > maxBytes) {
|
|
97
|
+
await reader.cancel();
|
|
98
|
+
throw new Error("response exceeded the size limit");
|
|
99
|
+
}
|
|
100
|
+
chunks.push(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
reader.releaseLock();
|
|
105
|
+
}
|
|
106
|
+
const bytes = new Uint8Array(total);
|
|
107
|
+
let offset = 0;
|
|
108
|
+
for (const chunk of chunks) {
|
|
109
|
+
bytes.set(chunk, offset);
|
|
110
|
+
offset += chunk.byteLength;
|
|
111
|
+
}
|
|
112
|
+
return new TextDecoder().decode(bytes);
|
|
113
|
+
}
|
|
114
|
+
const notNifraResponse = (service) => JSON.stringify({
|
|
115
|
+
code: "NIFRA_NOT_DEV_SERVER",
|
|
116
|
+
message: `The service on this port is not a Nifra ${service} endpoint.`,
|
|
117
|
+
}, null, 2);
|
|
59
118
|
const timeoutMessage = (label, ms) => `${label} timed out after ${ms / 1000}s and was terminated.\n` +
|
|
60
119
|
`The app was loaded but the process did not finish. Most often this is a module-level side effect ` +
|
|
61
120
|
`that keeps the event loop alive (a database pool, a Redis client, an interval) opened during import ` +
|
|
@@ -127,7 +186,7 @@ function boundedAppend(current, next, max = 12_000) {
|
|
|
127
186
|
}
|
|
128
187
|
/** A persistent `mcp-run`/`mcp-render` `--worker` subprocess: the backend/web app is loaded ONCE and
|
|
129
188
|
* reused across newline-delimited `{ id, input }` requests, replying `{ id, output }`. The same machinery
|
|
130
|
-
* powers both `nifra_run warm` and `nifra_render warm`
|
|
189
|
+
* powers both `nifra_run warm` and `nifra_render warm` - `child` selects which engine, `label` shapes the
|
|
131
190
|
* cancellation message. The owning handler ({@link createWarmHandler}) fingerprints the source tree and
|
|
132
191
|
* replaces the worker when a file changes, so warm reuse never serves a stale result. Exported for the
|
|
133
192
|
* concurrency test that proves a single per-request cancel doesn't tear down the shared worker. */
|
|
@@ -175,7 +234,7 @@ export class WarmWorker {
|
|
|
175
234
|
// Per-request cancel: drop just THIS id and resolve its cancellation. The worker is shared
|
|
176
235
|
// across concurrent requests (`pending` is id-keyed for exactly this reason), so killing the
|
|
177
236
|
// process here would reject every OTHER in-flight request via the `exited` handler and force a
|
|
178
|
-
// cold rebuild. Leave it hot
|
|
237
|
+
// cold rebuild. Leave it hot - `createWarmHandler` already replaces it on file change.
|
|
179
238
|
this.pending.delete(id);
|
|
180
239
|
clearTimeout(timer);
|
|
181
240
|
const reason = typeof signal?.reason === "string" ? `: ${signal.reason}` : "";
|
|
@@ -256,7 +315,7 @@ export class WarmWorker {
|
|
|
256
315
|
}
|
|
257
316
|
/** A warm handler that reuses a hot {@link WarmWorker} across calls and falls back to a one-shot fresh
|
|
258
317
|
* subprocess on any worker failure. Shared by `nifra_run` (`child: "mcp-run"`, `label: "run"`) and
|
|
259
|
-
* `nifra_render` (`child: "mcp-render"`, `label: "render"`)
|
|
318
|
+
* `nifra_render` (`child: "mcp-render"`, `label: "render"`) - the only differences are the engine and the
|
|
260
319
|
* message label, so there's a single source for the reuse + auto-restart-on-file-change logic. */
|
|
261
320
|
function createWarmHandler(child, cwd, label) {
|
|
262
321
|
let worker;
|
|
@@ -484,7 +543,7 @@ export function projectFeatures(cwd, loadAppCached = createCachedAppLoader(cwd))
|
|
|
484
543
|
return { resources: projectResources(cwd, loadAppCached), prompts: projectPrompts() };
|
|
485
544
|
}
|
|
486
545
|
/**
|
|
487
|
-
* Resolve an optional `dir` tool argument
|
|
546
|
+
* Resolve an optional `dir` tool argument - a subdirectory of the project root the caller wants to scope a
|
|
488
547
|
* check/test to (e.g. `nifra check` on `app/` when the MCP server's root is a monorepo). Returns the
|
|
489
548
|
* absolute target, or `null` if `dir` escapes the root (a path-traversal guard: no `..` out, no absolute
|
|
490
549
|
* path elsewhere). `undefined`/empty → the root itself.
|
|
@@ -497,7 +556,7 @@ export function resolveProjectDir(root, dir) {
|
|
|
497
556
|
}
|
|
498
557
|
/** Consistent error string for a `dir` that escapes the project root. */
|
|
499
558
|
function dirError(dir) {
|
|
500
|
-
return JSON.stringify({ ok: false, error: `dir must be a subdirectory of the project root
|
|
559
|
+
return JSON.stringify({ ok: false, error: `dir must be a subdirectory of the project root - "${dir}" escapes it.` }, null, 2);
|
|
501
560
|
}
|
|
502
561
|
/** Build the project-scoped tools for `cwd`. */
|
|
503
562
|
export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
@@ -506,7 +565,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
506
565
|
return [
|
|
507
566
|
{
|
|
508
567
|
name: "nifra_context",
|
|
509
|
-
description: "Get this nifra project's surface. Call it once UNFILTERED for a tight INDEX: the route list (API routes as `METHOD path`, page routes as `pattern → file`) + framework conventions + a pointer
|
|
568
|
+
description: "Get this nifra project's surface. Call it once UNFILTERED for a tight INDEX: the route list (API routes as `METHOD path`, page routes as `pattern → file`) + framework conventions + a pointer - cheap even on a big app, no per-route schema dump. Then pass `path` (a route prefix like /api/orders) and/or `kind` (api|pages) to fetch the FULL contract for that slice (body/query/response TS shapes + the exact typed-client call form).",
|
|
510
569
|
inputSchema: {
|
|
511
570
|
type: "object",
|
|
512
571
|
properties: {
|
|
@@ -527,9 +586,138 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
527
586
|
return describeProject(await loadAppCached(), filter);
|
|
528
587
|
},
|
|
529
588
|
},
|
|
589
|
+
{
|
|
590
|
+
name: "nifra_explain",
|
|
591
|
+
description: "Turn a nifra error into a STRUCTURED diagnostic instead of eyeballing a stack trace: a stable `code`, the top frame in YOUR source, a codeframe around the offending line, and - when nifra recognises the failure - the plain-language `cause` + `fix` + docs anchor. Pass `error` (and `stack` if you have it, e.g. from nifra_run/nifra_test output or a failing build) to explain a specific failure; or pass `port` to fetch the running dev server's most recent SSR failure from `/__nifra/last-error`. Returns the same JSON the dev overlay renders.",
|
|
592
|
+
inputSchema: {
|
|
593
|
+
type: "object",
|
|
594
|
+
properties: {
|
|
595
|
+
error: {
|
|
596
|
+
type: "string",
|
|
597
|
+
description: "The error message to explain (copy it from nifra_run/nifra_test output or a failing build).",
|
|
598
|
+
},
|
|
599
|
+
stack: {
|
|
600
|
+
type: "string",
|
|
601
|
+
description: "The error's stack trace, if you have it - enables the source codeframe and the top user frame.",
|
|
602
|
+
},
|
|
603
|
+
name: {
|
|
604
|
+
type: "string",
|
|
605
|
+
description: "The error's class name (e.g. TypeError, SchemaError), if known - sharpens classification.",
|
|
606
|
+
},
|
|
607
|
+
port: {
|
|
608
|
+
type: "number",
|
|
609
|
+
description: "Instead of a pasted error, fetch the running dev server's most recent SSR failure from this port.",
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
additionalProperties: false,
|
|
613
|
+
},
|
|
614
|
+
handler: async (args) => {
|
|
615
|
+
const { error, stack, name, port } = args;
|
|
616
|
+
if (error !== undefined || stack !== undefined) {
|
|
617
|
+
const { buildDiagnostic } = await import("@nifrajs/web/diagnostic");
|
|
618
|
+
const e = new Error(error ?? "");
|
|
619
|
+
if (name !== undefined)
|
|
620
|
+
e.name = name;
|
|
621
|
+
if (stack !== undefined)
|
|
622
|
+
e.stack = stack;
|
|
623
|
+
return JSON.stringify(buildDiagnostic(e, { root: cwd }), null, 2);
|
|
624
|
+
}
|
|
625
|
+
if (port !== undefined) {
|
|
626
|
+
const validPort = validateLocalPort(port);
|
|
627
|
+
if (validPort === undefined) {
|
|
628
|
+
return JSON.stringify({ code: "NIFRA_INVALID_PORT", message: "port must be an integer from 1 to 65535." }, null, 2);
|
|
629
|
+
}
|
|
630
|
+
const { LAST_ERROR_PATH } = await import("@nifrajs/web/diagnostic");
|
|
631
|
+
try {
|
|
632
|
+
const res = await fetch(`http://127.0.0.1:${validPort}${LAST_ERROR_PATH}`, {
|
|
633
|
+
signal: AbortSignal.timeout(LOCAL_TOOL_FETCH_TIMEOUT_MS),
|
|
634
|
+
});
|
|
635
|
+
if (res.headers.get("x-nifra-diagnostic") !== "true")
|
|
636
|
+
return notNifraResponse("diagnostic");
|
|
637
|
+
if (!res.ok) {
|
|
638
|
+
return JSON.stringify({
|
|
639
|
+
code: "NIFRA_NONE",
|
|
640
|
+
message: `dev server at :${validPort} returned ${res.status}`,
|
|
641
|
+
}, null, 2);
|
|
642
|
+
}
|
|
643
|
+
return await readBoundedResponse(res);
|
|
644
|
+
}
|
|
645
|
+
catch (cause) {
|
|
646
|
+
return JSON.stringify({
|
|
647
|
+
code: "NIFRA_NONE",
|
|
648
|
+
message: `could not reach a nifra dev server at :${validPort} - ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
649
|
+
}, null, 2);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return JSON.stringify({
|
|
653
|
+
code: "NIFRA_NONE",
|
|
654
|
+
message: "Pass `error` (and `stack` if available) to explain a failure, or `port` to fetch the dev server's last error.",
|
|
655
|
+
}, null, 2);
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "nifra_inspect",
|
|
660
|
+
description: "Observe what your requests ACTUALLY did on the running dev server - the recent request traces the DevTools plugin records: `{ method, path, status, durationMs, isrStatus, bodyBytes }` per request. The read no other tool gives you: after nifra_run or a real browser request, call this to SEE the outcome (which route answered, the status, how long, ISR hit/miss) instead of guessing. Pass `port` (the running dev server); narrow with `path` (a path prefix) or `limit` (most recent N). Requires the app to mount `@nifrajs/web`'s `devtools()` plugin (which auto-enables in development).",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
port: { type: "number", description: "The running dev server's port." },
|
|
665
|
+
path: { type: "string", description: "Only traces whose path starts with this prefix." },
|
|
666
|
+
limit: { type: "number", description: "Return only the most recent N traces." },
|
|
667
|
+
},
|
|
668
|
+
required: ["port"],
|
|
669
|
+
additionalProperties: false,
|
|
670
|
+
},
|
|
671
|
+
handler: async (args) => {
|
|
672
|
+
const { port, path, limit } = args;
|
|
673
|
+
if (port === undefined) {
|
|
674
|
+
return JSON.stringify({
|
|
675
|
+
events: [],
|
|
676
|
+
note: "Pass `port` - the running dev server whose request traces to read.",
|
|
677
|
+
}, null, 2);
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const validPort = validateLocalPort(port);
|
|
681
|
+
if (validPort === undefined) {
|
|
682
|
+
return JSON.stringify({
|
|
683
|
+
events: [],
|
|
684
|
+
code: "NIFRA_INVALID_PORT",
|
|
685
|
+
note: "port must be an integer from 1 to 65535.",
|
|
686
|
+
}, null, 2);
|
|
687
|
+
}
|
|
688
|
+
const url = new URL(`http://127.0.0.1:${validPort}/_nifra/devtools/state`);
|
|
689
|
+
if (path !== undefined)
|
|
690
|
+
url.searchParams.set("path", path);
|
|
691
|
+
if (limit !== undefined)
|
|
692
|
+
url.searchParams.set("limit", String(limit));
|
|
693
|
+
const res = await fetch(url, {
|
|
694
|
+
signal: AbortSignal.timeout(LOCAL_TOOL_FETCH_TIMEOUT_MS),
|
|
695
|
+
});
|
|
696
|
+
if (res.headers.get("x-nifra-devtools") !== "true")
|
|
697
|
+
return notNifraResponse("DevTools");
|
|
698
|
+
if (res.status === 404) {
|
|
699
|
+
return JSON.stringify({
|
|
700
|
+
events: [],
|
|
701
|
+
note: "No DevTools endpoint on that server. Mount `devtools()` from @nifrajs/web and run in development.",
|
|
702
|
+
}, null, 2);
|
|
703
|
+
}
|
|
704
|
+
if (!res.ok) {
|
|
705
|
+
return JSON.stringify({ events: [], note: `DevTools state returned ${res.status}.` }, null, 2);
|
|
706
|
+
}
|
|
707
|
+
return await readBoundedResponse(res);
|
|
708
|
+
}
|
|
709
|
+
catch (cause) {
|
|
710
|
+
const validPort = validateLocalPort(port);
|
|
711
|
+
return JSON.stringify({
|
|
712
|
+
events: [],
|
|
713
|
+
note: `Could not reach a dev server at :${validPort ?? String(port)} - ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
714
|
+
}, null, 2);
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
},
|
|
530
718
|
{
|
|
531
719
|
name: "nifra_routes",
|
|
532
|
-
description: "List this project's API routes as STRUCTURED JSON
|
|
720
|
+
description: "List this project's API routes as STRUCTURED JSON - each `{ method, path, call, body?, query?, response? }`, where `call` is the exact typed-client call form and the shapes are compact TS-typed contracts. For programmatic use (list_routes / get_route_schema) instead of parsing the nifra_context Markdown. No args = every route; pass `path` (a path or prefix like /api/orders) to narrow to those routes.",
|
|
533
721
|
inputSchema: {
|
|
534
722
|
type: "object",
|
|
535
723
|
properties: {
|
|
@@ -694,7 +882,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
694
882
|
...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus),
|
|
695
883
|
{
|
|
696
884
|
name: "nifra_scaffold",
|
|
697
|
-
description: 'Map a URL path to the CORRECT routes/ file and get a contract-correct page stub. Agents routinely place file routes wrong
|
|
885
|
+
description: 'Map a URL path to the CORRECT routes/ file and get a contract-correct page stub. Agents routinely place file routes wrong - this applies the convention for you: ":id"/"[id]" → [id], "*rest" → [...rest], "/" → index. Pass path (e.g. "/users/:id"). Returns the file to create + the route-module contract (loader/action/meta/default) + a stub (ready-to-write for react/preact/solid; path+contract for vue/svelte/vanilla - use nifra_example for those bodies).',
|
|
698
886
|
inputSchema: {
|
|
699
887
|
type: "object",
|
|
700
888
|
properties: {
|
|
@@ -728,7 +916,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
728
916
|
},
|
|
729
917
|
{
|
|
730
918
|
name: "nifra_check",
|
|
731
|
-
description: 'Run the project\'s drift gate and return a structured result { ok, typecheck, diagnostics[] }: typecheck (the frontend↔backend contract), plus lints for hand-rolled fetch() to your own API, untyped client("…") calls missing <typeof app>, and server-only imports in routes/. Pass lintsOnly:true for a near-instant lint pass while iterating; run the full gate (default) to confirm the work is done
|
|
919
|
+
description: 'Run the project\'s drift gate and return a structured result { ok, typecheck, diagnostics[] }: typecheck (the frontend↔backend contract), plus lints for hand-rolled fetch() to your own API, untyped client("…") calls missing <typeof app>, and server-only imports in routes/. Pass lintsOnly:true for a near-instant lint pass while iterating; run the full gate (default) to confirm the work is done - fix every diagnostic before finishing.',
|
|
732
920
|
inputSchema: {
|
|
733
921
|
type: "object",
|
|
734
922
|
properties: {
|
|
@@ -897,7 +1085,7 @@ export function projectTools(cwd, loadAppCached = createCachedAppLoader(cwd)) {
|
|
|
897
1085
|
},
|
|
898
1086
|
{
|
|
899
1087
|
name: "nifra_doctor",
|
|
900
|
-
description: "Check this project for packages imported in source but NOT declared in package.json
|
|
1088
|
+
description: "Check this project for packages imported in source but NOT declared in package.json - the Bun-workspace trap where an import resolves at runtime (hoisting/workspace) so tests pass and `bun install` says no changes, yet tsc fails and a fresh/standalone install can't resolve it. Returns { ok, ran, findings[], fixed?, skippedFixes? }. Pass autoFix:true to update package.json only when the dependency version can be inferred locally from an ancestor package.json or installed package metadata; otherwise the tool returns the exact bun add command to run.",
|
|
901
1089
|
inputSchema: {
|
|
902
1090
|
type: "object",
|
|
903
1091
|
properties: {
|
|
@@ -1028,6 +1216,51 @@ function namespaceForApp(name, tools, features) {
|
|
|
1028
1216
|
features: { resources: namespacedResources, prompts: namespacedPrompts },
|
|
1029
1217
|
};
|
|
1030
1218
|
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Merge the backend-derived resources/prompts into `base` - and keep a project that cannot be loaded
|
|
1221
|
+
* from deciding whether the server starts at all.
|
|
1222
|
+
*
|
|
1223
|
+
* Resources and prompts are OPTIONAL capabilities extracted from the app's backend, so this load is a
|
|
1224
|
+
* best-effort enrichment. It was awaited unguarded, which made it a hard boot requirement: `loadApp`
|
|
1225
|
+
* needs a web config (`adapter` + `clientModule`) and a `routes/` directory, and a backend-only
|
|
1226
|
+
* project - the shape `create-nifra`'s DEFAULT template produces - has neither. On those projects the
|
|
1227
|
+
* throw escaped before the JSON-RPC loop began, so the server wrote nothing and exited. `nifra
|
|
1228
|
+
* init-agents` would happily register an `.mcp.json` pointing at a server that could never start, and
|
|
1229
|
+
* every tool went with it, including the twelve that never touch the app (docs, examples, types,
|
|
1230
|
+
* check, doctor, levels, test).
|
|
1231
|
+
*
|
|
1232
|
+
* Degrading costs only the backend resources/prompts. Tools that genuinely need the app still fail,
|
|
1233
|
+
* but per call, with their own message - which is a diagnosis instead of a silence.
|
|
1234
|
+
*/
|
|
1235
|
+
async function backendFeatures(loader, base) {
|
|
1236
|
+
const resources = [...(base.resources ?? [])];
|
|
1237
|
+
const prompts = [...(base.prompts ?? [])];
|
|
1238
|
+
try {
|
|
1239
|
+
const app = await loader();
|
|
1240
|
+
resources.push(...extractBackendResources(app.backend));
|
|
1241
|
+
prompts.push(...extractBackendPrompts(app.backend));
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
// Not loadable here (no web config, or the config itself throws). The server still serves.
|
|
1245
|
+
}
|
|
1246
|
+
return { resources, prompts };
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* The MCP tools the app itself declares via `app.tool(...)`, or none when the app cannot be loaded.
|
|
1250
|
+
*
|
|
1251
|
+
* Same reasoning as {@link backendFeatures}, on the hotter path: this ran on EVERY JSON-RPC message,
|
|
1252
|
+
* so an unloadable project failed `initialize` itself with a `-32603` and the session never opened.
|
|
1253
|
+
* An app's own tools are an extension of the built-in set, so their absence must not withdraw the
|
|
1254
|
+
* built-ins - a backend-only project still gets docs, examples, types, check, doctor, levels and test.
|
|
1255
|
+
*/
|
|
1256
|
+
async function appDeclaredTools(loader) {
|
|
1257
|
+
try {
|
|
1258
|
+
return extractBackendTools((await loader()).backend);
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
return [];
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1031
1264
|
/** Run the stdio MCP server: read newline-delimited JSON-RPC from stdin, write responses to stdout. */
|
|
1032
1265
|
export async function runMcpServer(cwd, version) {
|
|
1033
1266
|
let features;
|
|
@@ -1038,12 +1271,7 @@ export async function runMcpServer(cwd, version) {
|
|
|
1038
1271
|
const allPrompts = [];
|
|
1039
1272
|
for (const { name, cwd: appCwd } of appEntries) {
|
|
1040
1273
|
const loader = createCachedAppLoader(appCwd);
|
|
1041
|
-
const
|
|
1042
|
-
const base = projectFeatures(appCwd, loader);
|
|
1043
|
-
const ns = namespaceForApp(name, [], {
|
|
1044
|
-
resources: [...(base.resources ?? []), ...extractBackendResources(app.backend)],
|
|
1045
|
-
prompts: [...(base.prompts ?? []), ...extractBackendPrompts(app.backend)],
|
|
1046
|
-
});
|
|
1274
|
+
const ns = namespaceForApp(name, [], await backendFeatures(loader, projectFeatures(appCwd, loader)));
|
|
1047
1275
|
allResources.push(...(ns.features.resources ?? []));
|
|
1048
1276
|
allPrompts.push(...(ns.features.prompts ?? []));
|
|
1049
1277
|
}
|
|
@@ -1051,12 +1279,7 @@ export async function runMcpServer(cwd, version) {
|
|
|
1051
1279
|
}
|
|
1052
1280
|
else {
|
|
1053
1281
|
const loadAppCached = createCachedAppLoader(cwd);
|
|
1054
|
-
|
|
1055
|
-
const app = await loadAppCached();
|
|
1056
|
-
features = {
|
|
1057
|
-
resources: [...(base.resources ?? []), ...extractBackendResources(app.backend)],
|
|
1058
|
-
prompts: [...(base.prompts ?? []), ...extractBackendPrompts(app.backend)],
|
|
1059
|
-
};
|
|
1282
|
+
features = await backendFeatures(loadAppCached, projectFeatures(cwd, loadAppCached));
|
|
1060
1283
|
}
|
|
1061
1284
|
const loadAppCached = createCachedAppLoader(cwd);
|
|
1062
1285
|
const serverInfo = { name: "nifra", version };
|
|
@@ -1071,22 +1294,17 @@ export async function runMcpServer(cwd, version) {
|
|
|
1071
1294
|
const allTools = [];
|
|
1072
1295
|
for (const { name, cwd: appCwd } of appEntries) {
|
|
1073
1296
|
const loader = createCachedAppLoader(appCwd);
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const backendTools = extractBackendTools(app.backend);
|
|
1077
|
-
const ns = namespaceForApp(name, [...baseTools, ...backendTools], {
|
|
1078
|
-
resources: [],
|
|
1079
|
-
prompts: [],
|
|
1080
|
-
});
|
|
1297
|
+
const tools = [...projectTools(appCwd, loader), ...(await appDeclaredTools(loader))];
|
|
1298
|
+
const ns = namespaceForApp(name, tools, { resources: [], prompts: [] });
|
|
1081
1299
|
allTools.push(...ns.tools);
|
|
1082
1300
|
}
|
|
1083
1301
|
activeTools = [...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus), ...allTools];
|
|
1084
1302
|
}
|
|
1085
1303
|
else {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1304
|
+
activeTools = [
|
|
1305
|
+
...projectTools(cwd, loadAppCached),
|
|
1306
|
+
...(await appDeclaredTools(loadAppCached)),
|
|
1307
|
+
];
|
|
1090
1308
|
}
|
|
1091
1309
|
const response = await handleRpc(message, activeTools, serverInfo, features, {
|
|
1092
1310
|
state,
|