@nifrajs/cli 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/check.d.ts +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +28 -2
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -4
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +158 -15
- package/dist/doctor.js.map +1 -1
- 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/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 +17 -1
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.d.ts +10 -6
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +30 -12
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp.d.ts +14 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +188 -0
- package/dist/mcp.js.map +1 -1
- package/dist/upgrade.d.ts +13 -2
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +105 -7
- package/dist/upgrade.js.map +1 -1
- package/docs/llms-full.txt +104 -2
- package/docs/types.json +202 -26
- package/package.json +9 -9
- package/src/check.ts +30 -2
- package/src/cli.ts +21 -4
- package/src/doctor.ts +186 -3
- package/src/learn.ts +152 -0
- package/src/mcp-app-tool.ts +86 -0
- package/src/mcp-docs-tools.ts +18 -1
- package/src/mcp-http.ts +32 -14
- package/src/mcp.ts +237 -0
- package/src/upgrade.ts +125 -7
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `nifra_examples_app` MCP tool: nifra's verified code examples as an MCP Apps widget. It returns the
|
|
3
|
+
* examples as `structuredContent`, rendered by the {@link examplesWidget} `ui://` widget in MCP Apps hosts
|
|
4
|
+
* (ChatGPT Apps, MCPJam, Goose); text-only hosts still get the example names as plain text. Part of the
|
|
5
|
+
* public docs-MCP tool set (served by `handleMcpHttp`), so every self-host gets it from one definition.
|
|
6
|
+
* `loadExamples` is injected, so the same tool reads the corpus from disk (CLI) or a cached fetch (edge).
|
|
7
|
+
*/
|
|
8
|
+
import { defineMcpTool, defineMcpWidget } from "@nifrajs/mcp";
|
|
9
|
+
/** The widget: renders `structuredContent.examples` as a filterable list of example cards. */
|
|
10
|
+
export const examplesWidget = defineMcpWidget({
|
|
11
|
+
uri: "ui://nifra/examples",
|
|
12
|
+
name: "nifra examples",
|
|
13
|
+
description: "Interactive browser of verified, copy-pasteable nifra code examples.",
|
|
14
|
+
head: `<style>
|
|
15
|
+
:root { color-scheme: light dark; font: 14px/1.5 system-ui, sans-serif; }
|
|
16
|
+
body { margin: 0; padding: 16px; }
|
|
17
|
+
h1 { font-size: 16px; margin: 0 0 4px; }
|
|
18
|
+
#q { width: 100%; box-sizing: border-box; padding: 6px 10px; margin: 8px 0 14px; border-radius: 6px; border: 1px solid color-mix(in srgb, currentColor 25%, transparent); background: transparent; color: inherit; font: inherit; }
|
|
19
|
+
.card { border: 1px solid color-mix(in srgb, currentColor 15%, transparent); border-radius: 10px; padding: 10px 12px; margin-bottom: 10px; }
|
|
20
|
+
.topic { font-size: 12px; opacity: .6; }
|
|
21
|
+
.name { font-weight: 600; margin: 2px 0 6px; }
|
|
22
|
+
pre { margin: 0; padding: 8px 10px; border-radius: 6px; background: color-mix(in srgb, currentColor 8%, transparent); overflow-x: auto; font: 12px/1.5 ui-monospace, monospace; }
|
|
23
|
+
</style>`,
|
|
24
|
+
html: `
|
|
25
|
+
<h1>nifra examples</h1>
|
|
26
|
+
<input id="q" placeholder="Filter examples…" />
|
|
27
|
+
<div id="list"></div>
|
|
28
|
+
<script>
|
|
29
|
+
var ALL = [];
|
|
30
|
+
function esc(s){ return String(s).replace(/[&<>]/g, function(c){ return c==="&"?"&":c==="<"?"<":">"; }); }
|
|
31
|
+
function paint(items){
|
|
32
|
+
document.getElementById("list").innerHTML = items.map(function(e){
|
|
33
|
+
var code = e.code.length > 600 ? e.code.slice(0, 600) + "\\n…" : e.code;
|
|
34
|
+
return "<div class='card'><div class='topic'>" + esc(e.topic) + "</div><div class='name'>" + esc(e.name) +
|
|
35
|
+
"</div><pre>" + esc(code) + "</pre></div>";
|
|
36
|
+
}).join("") || "<p style='opacity:.6'>No matching examples.</p>";
|
|
37
|
+
}
|
|
38
|
+
function render(data){ ALL = (data && data.examples) || []; paint(ALL); }
|
|
39
|
+
mcpApp.onData(render);
|
|
40
|
+
document.getElementById("q").addEventListener("input", function(ev){
|
|
41
|
+
var q = ev.target.value.toLowerCase();
|
|
42
|
+
paint(ALL.filter(function(e){ return (e.topic + e.name + e.slug).toLowerCase().indexOf(q) !== -1; }));
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
`,
|
|
46
|
+
});
|
|
47
|
+
/** The widget-backed tool. `loadExamples` is injected (disk on the CLI, cached fetch on the edge). */
|
|
48
|
+
export function examplesAppTool(loadExamples) {
|
|
49
|
+
return defineMcpTool({
|
|
50
|
+
name: "nifra_examples_app",
|
|
51
|
+
description: "Browse nifra's verified code examples as an interactive, filterable list (MCP Apps widget). Pass query to pre-filter; the widget also filters client-side.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: { query: { type: "string", description: "Pre-filter examples by keyword." } },
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
},
|
|
57
|
+
widget: examplesWidget,
|
|
58
|
+
handler: async (args) => {
|
|
59
|
+
const all = (await loadExamples()) ?? [];
|
|
60
|
+
const q = (args.query ?? "").toLowerCase();
|
|
61
|
+
const matched = (q
|
|
62
|
+
? all.filter((e) => `${e.topic} ${e.name} ${e.slug} ${e.code}`.toLowerCase().includes(q))
|
|
63
|
+
: all).slice(0, 30);
|
|
64
|
+
return {
|
|
65
|
+
text: matched.length === 0
|
|
66
|
+
? "No matching examples."
|
|
67
|
+
: `${matched.length} example(s): ${matched.map((e) => e.name).join(", ")}.`,
|
|
68
|
+
structuredContent: {
|
|
69
|
+
examples: matched.map((e) => ({
|
|
70
|
+
name: e.name,
|
|
71
|
+
topic: e.topic,
|
|
72
|
+
slug: e.slug,
|
|
73
|
+
code: e.code,
|
|
74
|
+
})),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=mcp-app-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-app-tool.js","sourceRoot":"","sources":["../src/mcp-app-tool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,aAAa,EAAE,eAAe,EAAgB,MAAM,cAAc,CAAA;AAG3E,8FAA8F;AAC9F,MAAM,CAAC,MAAM,cAAc,GAAG,eAAe,CAAC;IAC5C,GAAG,EAAE,qBAAqB;IAC1B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,sEAAsE;IACnF,IAAI,EAAE;;;;;;;;;WASG;IACT,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;GAqBL;CACF,CAAC,CAAA;AAEF,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,YAAkD;IAChF,OAAO,aAAa,CAAC;QACnB,IAAI,EAAE,oBAAoB;QAC1B,WAAW,EACT,4JAA4J;QAC9J,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,iCAAiC,EAAE,EAAE;YACzF,oBAAoB,EAAE,KAAK;SAC5B;QACD,MAAM,EAAE,cAAc;QACtB,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACtB,MAAM,GAAG,GAAG,CAAC,MAAM,YAAY,EAAE,CAAC,IAAI,EAAE,CAAA;YACxC,MAAM,CAAC,GAAG,CAAE,IAAI,CAAC,KAAgB,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;YACtD,MAAM,OAAO,GAAG,CACd,CAAC;gBACC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBACzF,CAAC,CAAC,GAAG,CACR,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;YACd,OAAO;gBACL,IAAI,EACF,OAAO,CAAC,MAAM,KAAK,CAAC;oBAClB,CAAC,CAAC,uBAAuB;oBACzB,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,gBAAgB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAC/E,iBAAiB,EAAE;oBACjB,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;wBAC5B,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,KAAK,EAAE,CAAC,CAAC,KAAK;wBACd,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,IAAI,EAAE,CAAC,CAAC,IAAI;qBACb,CAAC,CAAC;iBACJ;aACF,CAAA;QACH,CAAC;KACF,CAAC,CAAA;AACJ,CAAC"}
|
package/dist/mcp-docs-tools.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The project-INDEPENDENT MCP tools - `nifra_docs`, `nifra_example`, `nifra_types`, `nifra_learn` - as a single factory shared
|
|
3
3
|
* by every transport: the project stdio server ({@link projectTools}), the CLI HTTP server
|
|
4
4
|
* (`nifra docs-mcp`), and the site's Cloudflare-Pages worker (`/mcp`). The corpus source is injected as
|
|
5
5
|
* loaders, so the SAME tool definitions work whether the corpus comes from disk (`Bun.file`, on the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-docs-tools.d.ts","sourceRoot":"","sources":["../src/mcp-docs-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,KAAK,OAAO,EAAwB,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"mcp-docs-tools.d.ts","sourceRoot":"","sources":["../src/mcp-docs-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,KAAK,OAAO,EAAwB,MAAM,eAAe,CAAA;AAElE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAKrE,yFAAyF;AACzF,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,EAC3C,YAAY,EAAE,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,EAClD,SAAS,EAAE,MAAM,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC,GAChD,OAAO,EAAE,CA8GX"}
|
package/dist/mcp-docs-tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The project-INDEPENDENT MCP tools - `nifra_docs`, `nifra_example`, `nifra_types`, `nifra_learn` - as a single factory shared
|
|
3
3
|
* by every transport: the project stdio server ({@link projectTools}), the CLI HTTP server
|
|
4
4
|
* (`nifra docs-mcp`), and the site's Cloudflare-Pages worker (`/mcp`). The corpus source is injected as
|
|
5
5
|
* loaders, so the SAME tool definitions work whether the corpus comes from disk (`Bun.file`, on the
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { renderDocsResult } from "./docs-search.js";
|
|
10
10
|
import { renderExamplesResult } from "./examples.js";
|
|
11
|
+
import { renderLearnResult } from "./learn.js";
|
|
11
12
|
import { renderTypesResult } from "./types-search.js";
|
|
12
13
|
const clamp = (n, lo, hi, dflt) => Math.min(Math.max(typeof n === "number" ? n : dflt, lo), hi);
|
|
13
14
|
/** Build `nifra_docs` + `nifra_example` + `nifra_types` over injected corpus loaders. */
|
|
@@ -93,6 +94,21 @@ export function docsTools(loadDocs, loadExamples, loadTypes) {
|
|
|
93
94
|
return renderTypesResult(corpus, name, query, clamp(limit, 1, 8, 5), full === true);
|
|
94
95
|
},
|
|
95
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: "nifra_learn",
|
|
99
|
+
description: "Walk the guided path to build a nifra app end to end - an ORDERED sequence (create → page route → loader → typed API → typed client → auth → background jobs → deploy), not the random-access search of nifra_docs/nifra_example. Call with no args for the step index; pass `step: N` for that step's goal, how to do it (which tool emits the correct artifact), and how to verify it. Use it when scaffolding a new app or learning nifra's flow - each step composes the other nifra_* tools.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
step: {
|
|
104
|
+
type: "number",
|
|
105
|
+
description: "1-based step number to expand (omit for the index).",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
},
|
|
110
|
+
handler: async (args) => renderLearnResult(args.step),
|
|
111
|
+
},
|
|
96
112
|
];
|
|
97
113
|
}
|
|
98
114
|
//# sourceMappingURL=mcp-docs-tools.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-docs-tools.js","sourceRoot":"","sources":["../src/mcp-docs-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAgB,oBAAoB,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"mcp-docs-tools.js","sourceRoot":"","sources":["../src/mcp-docs-tools.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp-http.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The PUBLIC nifra docs MCP - Streamable-HTTP transport exposing the project-INDEPENDENT tools
|
|
3
|
-
* (`nifra_docs`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* (`nifra_docs`, `nifra_example`, `nifra_types`, `nifra_learn` + the `nifra_examples_app` MCP Apps widget)
|
|
4
|
+
* so any remote AI agent can learn nifra without a local checkout. They read the bundled corpus
|
|
5
|
+
* (llms-full.txt / examples.json / types.json shipped in this package) and ignore `cwd`, so they reuse the
|
|
6
|
+
* exact definitions from {@link projectTools} - one source, no drift.
|
|
6
7
|
*
|
|
7
8
|
* The transport itself (body cap, CORS, JSON-RPC dispatch) lives in `@nifrajs/mcp/http`; this module is
|
|
8
9
|
* the docs-specific layer over it: it supplies the bundled corpus tools and the `nifra-docs` server info.
|
|
@@ -11,12 +12,14 @@
|
|
|
11
12
|
* filesystem). `export default { port, fetch }` also lets `bun run mcp-http.ts` serve it directly.
|
|
12
13
|
*/
|
|
13
14
|
import { type McpHttpOptions } from "@nifrajs/mcp/http";
|
|
14
|
-
import type
|
|
15
|
+
import { type McpTool } from "@nifrajs/mcp/protocol";
|
|
15
16
|
export type { McpHttpOptions } from "@nifrajs/mcp/http";
|
|
16
17
|
export type { Example } from "./examples.js";
|
|
18
|
+
export { examplesAppTool, examplesWidget } from "./mcp-app-tool.js";
|
|
17
19
|
export { docsTools } from "./mcp-docs-tools.js";
|
|
18
20
|
export type { TypeEntry } from "./types-search.js";
|
|
19
|
-
/** The
|
|
21
|
+
/** The project-independent tools, reading the package's bundled corpus from disk (CLI use): the text
|
|
22
|
+
* docs tools plus the `nifra_examples_app` MCP Apps widget tool. */
|
|
20
23
|
export declare function publicDocsTools(): McpTool[];
|
|
21
24
|
/**
|
|
22
25
|
* Handle one MCP request against the given `tools` with the docs server identity. A thin docs-flavored
|
|
@@ -24,7 +27,8 @@ export declare function publicDocsTools(): McpTool[];
|
|
|
24
27
|
* `(request, tools, options?)` shape (the site's edge worker calls it with two args).
|
|
25
28
|
*/
|
|
26
29
|
export declare function respondMcpHttp(request: Request, tools: McpTool[], options?: McpHttpOptions): Promise<Response>;
|
|
27
|
-
/** The CLI HTTP handler: serves the disk-backed corpus tools
|
|
30
|
+
/** The CLI HTTP handler: serves the disk-backed corpus tools + registers the examples widget's `ui://`
|
|
31
|
+
* resource so MCP Apps hosts can render `nifra_examples_app`. (`nifra docs-mcp` / `bun run` this file.) */
|
|
28
32
|
export declare function handleMcpHttp(request: Request): Promise<Response>;
|
|
29
33
|
/**
|
|
30
34
|
* Worker/edge + local entry. `export default { fetch }` is the universal server shape: Cloudflare /
|
package/dist/mcp-http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-http.d.ts","sourceRoot":"","sources":["../src/mcp-http.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mcp-http.d.ts","sourceRoot":"","sources":["../src/mcp-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,cAAc,EAAwC,MAAM,mBAAmB,CAAA;AAC7F,OAAO,EAAE,KAAK,OAAO,EAAW,MAAM,uBAAuB,CAAA;AAO7D,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAG5C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAWlD;oEACoE;AACpE,wBAAgB,eAAe,IAAI,OAAO,EAAE,CAK3C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,OAAO,EAAE,EAChB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAEnB;AAED;2GAC2G;AAC3G,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CASjE;AAED;;;;;GAKG;;;;;AACH,wBAGC"}
|
package/dist/mcp-http.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The PUBLIC nifra docs MCP - Streamable-HTTP transport exposing the project-INDEPENDENT tools
|
|
3
|
-
* (`nifra_docs`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* (`nifra_docs`, `nifra_example`, `nifra_types`, `nifra_learn` + the `nifra_examples_app` MCP Apps widget)
|
|
4
|
+
* so any remote AI agent can learn nifra without a local checkout. They read the bundled corpus
|
|
5
|
+
* (llms-full.txt / examples.json / types.json shipped in this package) and ignore `cwd`, so they reuse the
|
|
6
|
+
* exact definitions from {@link projectTools} - one source, no drift.
|
|
6
7
|
*
|
|
7
8
|
* The transport itself (body cap, CORS, JSON-RPC dispatch) lives in `@nifrajs/mcp/http`; this module is
|
|
8
9
|
* the docs-specific layer over it: it supplies the bundled corpus tools and the `nifra-docs` server info.
|
|
@@ -11,20 +12,30 @@
|
|
|
11
12
|
* filesystem). `export default { port, fetch }` also lets `bun run mcp-http.ts` serve it directly.
|
|
12
13
|
*/
|
|
13
14
|
import { respondMcpHttp as respondMcpHttpCore } from "@nifrajs/mcp/http";
|
|
15
|
+
import { UI_MIME } from "@nifrajs/mcp/protocol";
|
|
14
16
|
import { loadDocsCorpus } from "./docs-search.js";
|
|
15
17
|
import { loadExamplesCorpus } from "./examples.js";
|
|
18
|
+
import { examplesAppTool, examplesWidget } from "./mcp-app-tool.js";
|
|
16
19
|
import { docsTools } from "./mcp-docs-tools.js";
|
|
17
20
|
import { loadTypesCorpus } from "./types-search.js";
|
|
18
|
-
// Re-exported so a self-host
|
|
19
|
-
//
|
|
21
|
+
// Re-exported so a self-host gets the corpus-injectable tool factories + the transport core from one
|
|
22
|
+
// entry: `import { respondMcpHttp, docsTools, examplesAppTool } from "@nifrajs/cli/mcp"`.
|
|
23
|
+
export { examplesAppTool, examplesWidget } from "./mcp-app-tool.js";
|
|
20
24
|
export { docsTools } from "./mcp-docs-tools.js";
|
|
21
25
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
22
|
-
const VERSION = "2.
|
|
26
|
+
const VERSION = "2.5.0";
|
|
23
27
|
const SERVER_INFO = { name: "nifra-docs", version: VERSION };
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
// Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
|
|
29
|
+
const docsHealth = (tools) => `nifra docs MCP - POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call). Tools: ${tools
|
|
30
|
+
.map((tool) => tool.name)
|
|
31
|
+
.join(", ")}.`;
|
|
32
|
+
/** The project-independent tools, reading the package's bundled corpus from disk (CLI use): the text
|
|
33
|
+
* docs tools plus the `nifra_examples_app` MCP Apps widget tool. */
|
|
26
34
|
export function publicDocsTools() {
|
|
27
|
-
return
|
|
35
|
+
return [
|
|
36
|
+
...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus),
|
|
37
|
+
examplesAppTool(loadExamplesCorpus),
|
|
38
|
+
];
|
|
28
39
|
}
|
|
29
40
|
/**
|
|
30
41
|
* Handle one MCP request against the given `tools` with the docs server identity. A thin docs-flavored
|
|
@@ -32,11 +43,18 @@ export function publicDocsTools() {
|
|
|
32
43
|
* `(request, tools, options?)` shape (the site's edge worker calls it with two args).
|
|
33
44
|
*/
|
|
34
45
|
export function respondMcpHttp(request, tools, options = {}) {
|
|
35
|
-
return respondMcpHttpCore(request, tools, SERVER_INFO, { health:
|
|
46
|
+
return respondMcpHttpCore(request, tools, SERVER_INFO, { health: docsHealth(tools), ...options });
|
|
36
47
|
}
|
|
37
|
-
/** The CLI HTTP handler: serves the disk-backed corpus tools
|
|
48
|
+
/** The CLI HTTP handler: serves the disk-backed corpus tools + registers the examples widget's `ui://`
|
|
49
|
+
* resource so MCP Apps hosts can render `nifra_examples_app`. (`nifra docs-mcp` / `bun run` this file.) */
|
|
38
50
|
export function handleMcpHttp(request) {
|
|
39
|
-
return respondMcpHttp(request, publicDocsTools()
|
|
51
|
+
return respondMcpHttp(request, publicDocsTools(), {
|
|
52
|
+
features: {
|
|
53
|
+
resources: [examplesWidget.resource],
|
|
54
|
+
ui: { mimeTypes: [UI_MIME] },
|
|
55
|
+
instructions: "nifra's own documentation, runnable examples, and API types - for building with the nifra framework. Call nifra_docs / nifra_example / nifra_types / nifra_learn to learn it without a local checkout.",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
40
58
|
}
|
|
41
59
|
/**
|
|
42
60
|
* Worker/edge + local entry. `export default { fetch }` is the universal server shape: Cloudflare /
|
package/dist/mcp-http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-http.js","sourceRoot":"","sources":["../src/mcp-http.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mcp-http.js","sourceRoot":"","sources":["../src/mcp-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAuB,cAAc,IAAI,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC7F,OAAO,EAAgB,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAInD,qGAAqG;AACrG,0FAA0F;AAC1F,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAG/C,+FAA+F;AAC/F,MAAM,OAAO,GAAG,OAAO,CAAA;AACvB,MAAM,WAAW,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;AAC5D,yGAAyG;AACzG,MAAM,UAAU,GAAG,CAAC,KAAgB,EAAU,EAAE,CAC9C,iGAAiG,KAAK;KACnG,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;KACxB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAA;AAElB;oEACoE;AACpE,MAAM,UAAU,eAAe;IAC7B,OAAO;QACL,GAAG,SAAS,CAAC,cAAc,EAAE,kBAAkB,EAAE,eAAe,CAAC;QACjE,eAAe,CAAC,kBAAkB,CAAC;KACpC,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAgB,EAChB,KAAgB,EAChB,UAA0B,EAAE;IAE5B,OAAO,kBAAkB,CAAC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;AACnG,CAAC;AAED;2GAC2G;AAC3G,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,OAAO,cAAc,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE;QAChD,QAAQ,EAAE;YACR,SAAS,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC;YACpC,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE;YAC5B,YAAY,EACV,wMAAwM;SAC3M;KACF,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,eAAe;IACb,IAAI,EAAE,OAAO,GAAG,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;IACtE,KAAK,EAAE,aAAa;CACrB,CAAA"}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -12,12 +12,18 @@
|
|
|
12
12
|
* - `nifra_docs` - keyword-search the framework docs; returns only the matching sections.
|
|
13
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
|
+
* - `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.
|
|
15
17
|
* - `nifra_scaffold`- map a URL path to the correct `routes/` file + a contract-correct page stub.
|
|
16
18
|
* - `nifra_check` - the drift gate (typecheck + lints, each with a structured fix), for an agent to fix against.
|
|
17
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
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
|
|
@@ -27,6 +33,14 @@
|
|
|
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
46
|
* powers both `nifra_run warm` and `nifra_render warm` - `child` selects which engine, `label` shapes the
|
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
|
@@ -12,12 +12,18 @@
|
|
|
12
12
|
* - `nifra_docs` - keyword-search the framework docs; returns only the matching sections.
|
|
13
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
|
+
* - `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.
|
|
15
17
|
* - `nifra_scaffold`- map a URL path to the correct `routes/` file + a contract-correct page stub.
|
|
16
18
|
* - `nifra_check` - the drift gate (typecheck + lints, each with a structured fix), for an agent to fix against.
|
|
17
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
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
|
|
@@ -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 ` +
|
|
@@ -527,6 +586,135 @@ 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
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.",
|