@nifrajs/cli 2.14.1 → 3.2.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 +10 -0
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +6 -1
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +3 -2
- package/dist/assure.js.map +1 -1
- package/dist/check-diagnostics.d.ts +165 -0
- package/dist/check-diagnostics.d.ts.map +1 -0
- package/dist/check-diagnostics.js +1009 -0
- package/dist/check-diagnostics.js.map +1 -0
- package/dist/check-scan.d.ts +245 -0
- package/dist/check-scan.d.ts.map +1 -0
- package/dist/check-scan.js +1599 -0
- package/dist/check-scan.js.map +1 -0
- package/dist/check.d.ts +4 -324
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +8 -2346
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +285 -10
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +27 -1
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +142 -2
- package/dist/command-catalog.js.map +1 -1
- package/dist/doctor.d.ts +42 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +64 -7
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +17 -0
- package/dist/env-file.d.ts.map +1 -0
- package/dist/env-file.js +113 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +91 -15
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.d.ts +57 -0
- package/dist/frontend-guidance.d.ts.map +1 -0
- package/dist/frontend-guidance.js +195 -0
- package/dist/frontend-guidance.js.map +1 -0
- package/dist/internal/reserved-segment-codemod.d.ts +36 -0
- package/dist/internal/reserved-segment-codemod.d.ts.map +1 -0
- package/dist/internal/reserved-segment-codemod.js +134 -0
- package/dist/internal/reserved-segment-codemod.js.map +1 -0
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +62 -11
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +26 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +74 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +6 -0
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +4 -1
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +48 -0
- package/dist/mcp-context.d.ts.map +1 -0
- package/dist/mcp-context.js +215 -0
- package/dist/mcp-context.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +31 -0
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +74 -0
- package/dist/mcp-exec.d.ts.map +1 -0
- package/dist/mcp-exec.js +844 -0
- package/dist/mcp-exec.js.map +1 -0
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +26 -0
- package/dist/mcp-io.d.ts.map +1 -0
- package/dist/mcp-io.js +100 -0
- package/dist/mcp-io.js.map +1 -0
- package/dist/mcp-reflect.d.ts +16 -0
- package/dist/mcp-reflect.d.ts.map +1 -0
- package/dist/mcp-reflect.js +101 -0
- package/dist/mcp-reflect.js.map +1 -0
- package/dist/mcp-root.d.ts +7 -0
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +51 -3
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp.d.ts +10 -84
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +13 -1170
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +5 -10
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js +21 -2
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +21 -0
- package/dist/openapi-types.d.ts.map +1 -0
- package/dist/openapi-types.js +214 -0
- package/dist/openapi-types.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +2 -1
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +2 -1
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/3.0.0.d.ts +10 -0
- package/dist/recipes/3.0.0.d.ts.map +1 -0
- package/dist/recipes/3.0.0.js +23 -0
- package/dist/recipes/3.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 +4 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +6 -123
- package/dist/release-verification.js.map +1 -1
- package/dist/rules/codes.d.ts +4 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +4 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/islands.d.ts +4 -0
- package/dist/rules/islands.d.ts.map +1 -0
- package/dist/rules/islands.js +155 -0
- package/dist/rules/islands.js.map +1 -0
- package/dist/rules/nano.d.ts +6 -0
- package/dist/rules/nano.d.ts.map +1 -0
- package/dist/rules/nano.js +264 -0
- package/dist/rules/nano.js.map +1 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +17 -30
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +38 -4
- package/dist/rules/security.js.map +1 -1
- package/dist/scaffold.d.ts +12 -6
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +149 -10
- package/dist/scaffold.js.map +1 -1
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +5 -3
- package/dist/search-terms.js.map +1 -1
- package/dist/stylex-migrate.d.ts +38 -0
- package/dist/stylex-migrate.d.ts.map +1 -0
- package/dist/stylex-migrate.js +1052 -0
- package/dist/stylex-migrate.js.map +1 -0
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +2 -1
- package/dist/sync-manifest.js.map +1 -1
- package/dist/verification-plan.d.ts +24 -0
- package/dist/verification-plan.d.ts.map +1 -0
- package/dist/verification-plan.js +116 -0
- package/dist/verification-plan.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +6 -3
- package/dist/work-graph.js.map +1 -1
- package/dist/workspace-link.d.ts +37 -0
- package/dist/workspace-link.d.ts.map +1 -0
- package/dist/workspace-link.js +72 -0
- package/dist/workspace-link.js.map +1 -0
- package/docs/examples.json +37 -1
- package/docs/llms-full.txt +1598 -44
- package/docs/types.json +5157 -172
- package/package.json +9 -9
- package/src/assure-hydration.ts +6 -1
- package/src/assure.ts +4 -2
- package/src/check-diagnostics.ts +1219 -0
- package/src/check-scan.ts +1825 -0
- package/src/check.ts +47 -2731
- package/src/cli.ts +291 -10
- package/src/command-catalog.ts +177 -2
- package/src/doctor.ts +99 -8
- package/src/env-file.ts +121 -0
- package/src/fix-recipes.ts +101 -15
- package/src/frontend-guidance.ts +259 -0
- package/src/internal/reserved-segment-codemod.ts +150 -0
- package/src/internal/typescript-import.ts +61 -11
- package/src/introspect.ts +82 -21
- package/src/levels-tool.ts +6 -0
- package/src/load.ts +4 -1
- package/src/mcp-context.ts +266 -0
- package/src/mcp-docs-tools.ts +37 -0
- package/src/mcp-exec.ts +1040 -0
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +118 -0
- package/src/mcp-reflect.ts +140 -0
- package/src/mcp-root.ts +62 -4
- package/src/mcp.ts +61 -1436
- package/src/openapi-tool.ts +32 -2
- package/src/openapi-types.ts +261 -0
- package/src/port.ts +2 -1
- package/src/project-facts.ts +2 -4
- package/src/recipes/3.0.0.ts +24 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +22 -147
- package/src/rules/codes.ts +4 -0
- package/src/rules/islands.ts +171 -0
- package/src/rules/nano.ts +295 -0
- package/src/rules/routes.ts +17 -30
- package/src/rules/security.ts +40 -4
- package/src/scaffold.ts +169 -9
- package/src/search-terms.ts +7 -3
- package/src/stylex-migrate.ts +1139 -0
- package/src/sync-manifest.ts +2 -1
- package/src/verification-plan.ts +242 -0
- package/src/work-graph.ts +6 -3
- package/src/workspace-link.ts +80 -0
package/src/mcp-http.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
|
|
|
29
29
|
export type { TypeEntry } from "./types-search.ts"
|
|
30
30
|
|
|
31
31
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
32
|
-
const VERSION = "2.
|
|
32
|
+
const VERSION = "3.2.0"
|
|
33
33
|
const SERVER_INFO = { name: "nifra-docs", version: VERSION }
|
|
34
34
|
// Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
|
|
35
35
|
const docsHealth = (tools: McpTool[]): string =>
|
package/src/mcp-io.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-neutral bounded I/O for project MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* HTTP docs and verification WebSocket transport keep their own adapters; stdio/run/render share only
|
|
5
|
+
* these limits and stream readers so an untrusted local endpoint or child process cannot exhaust MCP.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CHILD_TIMEOUT_MS = 30_000
|
|
9
|
+
/** Maximum stdout or stderr retained from a project subprocess. The process is killed at the limit. */
|
|
10
|
+
export const CHILD_OUTPUT_MAX_BYTES = 1_048_576
|
|
11
|
+
|
|
12
|
+
/** Local dev-tool reads are intentionally bounded: MCP runs in an agent process and must not hang on or
|
|
13
|
+
* buffer an unrelated loopback service just because a caller supplied its port. */
|
|
14
|
+
export const LOCAL_TOOL_FETCH_TIMEOUT_MS = 2_000
|
|
15
|
+
export const LOCAL_TOOL_MAX_RESPONSE_BYTES = 1_048_576
|
|
16
|
+
|
|
17
|
+
/** Accept only a concrete TCP port. Port 0 (bind-any-free-port) is not a valid read target. */
|
|
18
|
+
export function validateLocalPort(port: unknown): number | undefined {
|
|
19
|
+
return typeof port === "number" && Number.isInteger(port) && port >= 1 && port <= 65_535
|
|
20
|
+
? port
|
|
21
|
+
: undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Read a dev-tool response without allowing an untrusted localhost service to allocate unbounded memory. */
|
|
25
|
+
export async function readBoundedResponse(
|
|
26
|
+
response: Response,
|
|
27
|
+
maxBytes = LOCAL_TOOL_MAX_RESPONSE_BYTES,
|
|
28
|
+
): Promise<string> {
|
|
29
|
+
const declared = response.headers.get("content-length")
|
|
30
|
+
if (declared !== null) {
|
|
31
|
+
const length = Number(declared)
|
|
32
|
+
if (Number.isFinite(length) && length > maxBytes)
|
|
33
|
+
throw new Error("response exceeded the size limit")
|
|
34
|
+
}
|
|
35
|
+
if (response.body === null) return ""
|
|
36
|
+
const reader = response.body.getReader()
|
|
37
|
+
const chunks: Uint8Array[] = []
|
|
38
|
+
let total = 0
|
|
39
|
+
try {
|
|
40
|
+
for (;;) {
|
|
41
|
+
const { done, value } = await reader.read()
|
|
42
|
+
if (done) break
|
|
43
|
+
if (value === undefined) continue
|
|
44
|
+
total += value.byteLength
|
|
45
|
+
if (total > maxBytes) {
|
|
46
|
+
await reader.cancel()
|
|
47
|
+
throw new Error("response exceeded the size limit")
|
|
48
|
+
}
|
|
49
|
+
chunks.push(value)
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
reader.releaseLock()
|
|
53
|
+
}
|
|
54
|
+
const bytes = new Uint8Array(total)
|
|
55
|
+
let offset = 0
|
|
56
|
+
for (const chunk of chunks) {
|
|
57
|
+
bytes.set(chunk, offset)
|
|
58
|
+
offset += chunk.byteLength
|
|
59
|
+
}
|
|
60
|
+
return new TextDecoder().decode(bytes)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const notNifraResponse = (service: string): string =>
|
|
64
|
+
JSON.stringify(
|
|
65
|
+
{
|
|
66
|
+
code: "NIFRA_NOT_DEV_SERVER",
|
|
67
|
+
message: `The service on this port is not a Nifra ${service} endpoint.`,
|
|
68
|
+
},
|
|
69
|
+
null,
|
|
70
|
+
2,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
export const timeoutMessage = (label: string, ms: number): string =>
|
|
74
|
+
`${label} timed out after ${ms / 1000}s and was terminated.\n` +
|
|
75
|
+
`The app was loaded but the process did not finish. Most often this is a module-level side effect ` +
|
|
76
|
+
`that keeps the event loop alive (a database pool, a Redis client, an interval) opened during import ` +
|
|
77
|
+
`rather than lazily. Check for top-level connections in the app entry or anything it imports.`
|
|
78
|
+
|
|
79
|
+
export interface BoundedOutput {
|
|
80
|
+
readonly text: string
|
|
81
|
+
readonly truncated: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Read child output incrementally and cancel as soon as the byte budget is crossed. */
|
|
85
|
+
export async function readBoundedStream(
|
|
86
|
+
stream: ReadableStream<Uint8Array>,
|
|
87
|
+
maxBytes = CHILD_OUTPUT_MAX_BYTES,
|
|
88
|
+
onLimit?: () => void,
|
|
89
|
+
): Promise<BoundedOutput> {
|
|
90
|
+
const reader = stream.getReader()
|
|
91
|
+
const decoder = new TextDecoder()
|
|
92
|
+
const chunks: string[] = []
|
|
93
|
+
let totalBytes = 0
|
|
94
|
+
try {
|
|
95
|
+
for (;;) {
|
|
96
|
+
const { done, value } = await reader.read()
|
|
97
|
+
if (done) {
|
|
98
|
+
chunks.push(decoder.decode())
|
|
99
|
+
return { text: chunks.join(""), truncated: false }
|
|
100
|
+
}
|
|
101
|
+
if (value === undefined) continue
|
|
102
|
+
const remaining = maxBytes - totalBytes
|
|
103
|
+
if (remaining <= 0 || value.byteLength > remaining) {
|
|
104
|
+
if (remaining > 0) {
|
|
105
|
+
chunks.push(decoder.decode(value.subarray(0, remaining), { stream: true }))
|
|
106
|
+
chunks.push(decoder.decode())
|
|
107
|
+
}
|
|
108
|
+
await reader.cancel()
|
|
109
|
+
onLimit?.()
|
|
110
|
+
return { text: chunks.join(""), truncated: true }
|
|
111
|
+
}
|
|
112
|
+
totalBytes += value.byteLength
|
|
113
|
+
chunks.push(decoder.decode(value, { stream: true }))
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
reader.releaseLock()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend reflection adapters for the project MCP surface.
|
|
3
|
+
*
|
|
4
|
+
* These functions only translate reflected Nifra declarations into MCP descriptors. They do not know
|
|
5
|
+
* about stdio, HTTP, root trust, or project loading, so the reflection seam is independently testable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type ReflectedRoute, reflectRoutes } from "@nifrajs/core/reflection"
|
|
9
|
+
import type {
|
|
10
|
+
McpPrompt,
|
|
11
|
+
McpPromptMessage,
|
|
12
|
+
McpResource,
|
|
13
|
+
McpTool,
|
|
14
|
+
McpToolResult,
|
|
15
|
+
} from "./mcp-protocol.ts"
|
|
16
|
+
|
|
17
|
+
type ToolBackend = {
|
|
18
|
+
readonly routes?: () => readonly unknown[]
|
|
19
|
+
readonly fetch: (req: Request) => Promise<Response>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Extract tools registered via .tool() routes on the Nifra backend. Exported for the test that proves a
|
|
23
|
+
* `server().tool()` route surfaces in `tools/list` and executes through `tools/call`. */
|
|
24
|
+
export function extractBackendTools(backend: unknown): McpTool[] {
|
|
25
|
+
const b = backend as ToolBackend | null
|
|
26
|
+
if (!b || typeof b.routes !== "function") return []
|
|
27
|
+
const routes = reflectRoutes(b)
|
|
28
|
+
|
|
29
|
+
return routes
|
|
30
|
+
.filter(
|
|
31
|
+
(
|
|
32
|
+
r,
|
|
33
|
+
): r is ReflectedRoute & {
|
|
34
|
+
schema: NonNullable<ReflectedRoute["schema"]>
|
|
35
|
+
tool: NonNullable<ReflectedRoute["tool"]>
|
|
36
|
+
} => r.schema !== undefined && r.tool !== undefined,
|
|
37
|
+
)
|
|
38
|
+
.map((r) => {
|
|
39
|
+
const toolInfo = r.tool
|
|
40
|
+
const s = r.schema
|
|
41
|
+
return {
|
|
42
|
+
name: toolInfo.name,
|
|
43
|
+
description: toolInfo.description,
|
|
44
|
+
inputSchema: (s.body?.jsonSchema ?? {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {},
|
|
47
|
+
}) as McpTool["inputSchema"],
|
|
48
|
+
...(toolInfo.annotations !== undefined ? { annotations: toolInfo.annotations } : {}),
|
|
49
|
+
handler: async (args): Promise<string | McpToolResult> => {
|
|
50
|
+
const res = await b.fetch(
|
|
51
|
+
new Request(`http://localhost/_nifra/tool/${toolInfo.name}`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
body: JSON.stringify(args),
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const text = await res.text()
|
|
59
|
+
throw new Error(`Tool execution failed (${res.status}): ${text}`)
|
|
60
|
+
}
|
|
61
|
+
const body: unknown = await res.json()
|
|
62
|
+
if (typeof body === "string") return body
|
|
63
|
+
if (body && typeof body === "object") {
|
|
64
|
+
if ("content" in body || "structuredContent" in body) {
|
|
65
|
+
return body as McpToolResult
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
69
|
+
structuredContent: body as Record<string, unknown>,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return String(body)
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A resource declared on the backend via `server().resource()`, seen through `mcpResources()`. */
|
|
79
|
+
type ResourceDescriptor = {
|
|
80
|
+
readonly uri: string
|
|
81
|
+
readonly name: string
|
|
82
|
+
readonly description?: string
|
|
83
|
+
readonly mimeType?: string
|
|
84
|
+
readonly read: () => unknown | Promise<unknown>
|
|
85
|
+
}
|
|
86
|
+
type ResourceBackend = { readonly mcpResources?: () => readonly ResourceDescriptor[] }
|
|
87
|
+
|
|
88
|
+
/** Extract MCP resources registered via `.resource()` on the Nifra backend. Exported for the surfacing test. */
|
|
89
|
+
export function extractBackendResources(backend: unknown): McpResource[] {
|
|
90
|
+
const b = backend as ResourceBackend | null
|
|
91
|
+
if (!b || typeof b.mcpResources !== "function") return []
|
|
92
|
+
let list: readonly ResourceDescriptor[]
|
|
93
|
+
try {
|
|
94
|
+
list = b.mcpResources()
|
|
95
|
+
} catch {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
return list.map((r) => ({
|
|
99
|
+
uri: r.uri,
|
|
100
|
+
name: r.name,
|
|
101
|
+
...(r.description !== undefined ? { description: r.description } : {}),
|
|
102
|
+
...(r.mimeType !== undefined ? { mimeType: r.mimeType } : {}),
|
|
103
|
+
read: async () => {
|
|
104
|
+
const out = await r.read()
|
|
105
|
+
if (typeof out === "string") return { text: out }
|
|
106
|
+
const o = out as { text: string; mimeType?: string }
|
|
107
|
+
return o.mimeType !== undefined ? { text: o.text, mimeType: o.mimeType } : { text: o.text }
|
|
108
|
+
},
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A prompt declared on the backend via `server().prompt()`, seen through `mcpPrompts()`. */
|
|
113
|
+
type PromptDescriptor = {
|
|
114
|
+
readonly name: string
|
|
115
|
+
readonly description: string
|
|
116
|
+
readonly arguments?: readonly { name: string; description?: string; required?: boolean }[]
|
|
117
|
+
readonly handler: (args: Record<string, string>) => unknown | Promise<unknown>
|
|
118
|
+
}
|
|
119
|
+
type PromptBackend = { readonly mcpPrompts?: () => readonly PromptDescriptor[] }
|
|
120
|
+
|
|
121
|
+
/** Extract MCP prompts registered via `.prompt()` on the Nifra backend. Exported for the surfacing test. */
|
|
122
|
+
export function extractBackendPrompts(backend: unknown): McpPrompt[] {
|
|
123
|
+
const b = backend as PromptBackend | null
|
|
124
|
+
if (!b || typeof b.mcpPrompts !== "function") return []
|
|
125
|
+
let list: readonly PromptDescriptor[]
|
|
126
|
+
try {
|
|
127
|
+
list = b.mcpPrompts()
|
|
128
|
+
} catch {
|
|
129
|
+
return []
|
|
130
|
+
}
|
|
131
|
+
return list.map((p) => ({
|
|
132
|
+
name: p.name,
|
|
133
|
+
description: p.description,
|
|
134
|
+
...(p.arguments !== undefined ? { arguments: p.arguments } : {}),
|
|
135
|
+
handler: async (args: Record<string, unknown>) =>
|
|
136
|
+
(await p.handler(args as Record<string, string>)) as readonly McpPromptMessage[],
|
|
137
|
+
}))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Prefix every tool name and resource URI for a named app in a monorepo. */
|
package/src/mcp-root.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { readFile } from "node:fs/promises"
|
|
18
|
-
import { dirname, sep } from "node:path"
|
|
18
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path"
|
|
19
19
|
import { fileURLToPath } from "node:url"
|
|
20
20
|
|
|
21
21
|
/** How the current root was chosen. `arg` = explicit `nifra mcp <dir>`; `cwd` = the spawn directory;
|
|
@@ -102,7 +102,16 @@ export function pathsFromRootsResult(result: unknown): string[] {
|
|
|
102
102
|
const uri = (entry as { uri?: unknown })?.uri
|
|
103
103
|
if (typeof uri !== "string" || !uri.startsWith("file://")) continue
|
|
104
104
|
try {
|
|
105
|
-
|
|
105
|
+
const parsed = new URL(uri)
|
|
106
|
+
// A remote file host is a UNC path on Windows, not a local workspace root. MCP roots are
|
|
107
|
+
// intentionally local-only here: accepting it would let a client silently point the server
|
|
108
|
+
// at a different machine/share. `localhost` is the only equivalent spelling we accept.
|
|
109
|
+
if (
|
|
110
|
+
parsed.protocol !== "file:" ||
|
|
111
|
+
(parsed.hostname !== "" && parsed.hostname !== "localhost")
|
|
112
|
+
)
|
|
113
|
+
continue
|
|
114
|
+
paths.push(fileURLToPath(parsed))
|
|
106
115
|
} catch {
|
|
107
116
|
// Malformed URI - skip.
|
|
108
117
|
}
|
|
@@ -110,8 +119,13 @@ export function pathsFromRootsResult(result: unknown): string[] {
|
|
|
110
119
|
return paths
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
const contains = (parent: string, child: string): boolean =>
|
|
114
|
-
|
|
122
|
+
const contains = (parent: string, child: string): boolean => {
|
|
123
|
+
const distance = relative(resolve(parent), resolve(child))
|
|
124
|
+
return (
|
|
125
|
+
distance === "" ||
|
|
126
|
+
(distance !== ".." && !distance.startsWith(`..${sep}`) && !isAbsolute(distance))
|
|
127
|
+
)
|
|
128
|
+
}
|
|
115
129
|
|
|
116
130
|
/** True when the server root and the client's workspace are DISJOINT - neither contains the other.
|
|
117
131
|
* A server rooted at a monorepo whose sub-app is the open workspace (or vice versa) is not a
|
|
@@ -269,3 +283,47 @@ export async function detectToolingDrift(
|
|
|
269
283
|
}
|
|
270
284
|
return undefined
|
|
271
285
|
}
|
|
286
|
+
|
|
287
|
+
/** Tool names served from the bundled corpus; they do not depend on the project root. */
|
|
288
|
+
const PROJECT_FREE_TOOLS = new Set(["nifra_docs", "nifra_example", "nifra_types", "nifra_learn"])
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Wrap project-scoped tools with root trust. A bad root refuses in-band; a good root stamps every
|
|
292
|
+
* result so the agent can see which project answered. Docs tools pass through untouched.
|
|
293
|
+
*/
|
|
294
|
+
export function guardTools(
|
|
295
|
+
tools: import("./mcp-protocol.ts").McpTool[],
|
|
296
|
+
verdict: McpRootVerdict,
|
|
297
|
+
): import("./mcp-protocol.ts").McpTool[] {
|
|
298
|
+
return tools.map((tool) => {
|
|
299
|
+
if (PROJECT_FREE_TOOLS.has(tool.name)) return tool
|
|
300
|
+
return {
|
|
301
|
+
...tool,
|
|
302
|
+
handler: async (
|
|
303
|
+
args: Record<string, unknown>,
|
|
304
|
+
context: import("./mcp-protocol.ts").McpToolContext,
|
|
305
|
+
): Promise<import("./mcp-protocol.ts").McpToolResult> => {
|
|
306
|
+
if (verdict.blocked !== undefined) {
|
|
307
|
+
return { content: [{ type: "text", text: verdict.blocked }], isError: true }
|
|
308
|
+
}
|
|
309
|
+
const result = await tool.handler(args, context)
|
|
310
|
+
const note: import("./mcp-protocol.ts").McpContentBlock = {
|
|
311
|
+
type: "text",
|
|
312
|
+
text: verdict.note,
|
|
313
|
+
}
|
|
314
|
+
if (typeof result === "string") {
|
|
315
|
+
return { content: [{ type: "text", text: result }, note] }
|
|
316
|
+
}
|
|
317
|
+
return { ...result, content: [...(result.content ?? []), note] }
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Whether the client's initialize params declare the roots capability. */
|
|
324
|
+
export function clientSupportsRoots(params: Record<string, unknown> | undefined): boolean {
|
|
325
|
+
const capabilities = params?.capabilities
|
|
326
|
+
if (typeof capabilities !== "object" || capabilities === null) return false
|
|
327
|
+
const roots = (capabilities as { roots?: unknown }).roots
|
|
328
|
+
return typeof roots === "object" && roots !== null
|
|
329
|
+
}
|