@nifrajs/cli 1.2.2 → 1.3.1
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/cli.js +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp.d.ts +14 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +246 -11
- package/dist/mcp.js.map +1 -1
- package/docs/llms-full.txt +2 -2
- package/docs/types.json +510 -24
- package/package.json +7 -7
- package/src/cli.ts +1 -1
- package/src/mcp-http.ts +1 -1
- package/src/mcp.ts +302 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "The nifra CLI — zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@nifrajs/client": "1.
|
|
45
|
-
"@nifrajs/mcp": "1.
|
|
46
|
-
"@nifrajs/runner": "1.
|
|
47
|
-
"@nifrajs/schema": "1.
|
|
48
|
-
"@nifrajs/web": "1.
|
|
49
|
-
"create-nifra": "1.
|
|
44
|
+
"@nifrajs/client": "1.3.1",
|
|
45
|
+
"@nifrajs/mcp": "1.3.1",
|
|
46
|
+
"@nifrajs/runner": "1.3.1",
|
|
47
|
+
"@nifrajs/schema": "1.3.1",
|
|
48
|
+
"@nifrajs/web": "1.3.1",
|
|
49
|
+
"create-nifra": "1.3.1"
|
|
50
50
|
},
|
|
51
51
|
"repository": {
|
|
52
52
|
"type": "git",
|
package/src/cli.ts
CHANGED
|
@@ -82,7 +82,7 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
|
|
|
82
82
|
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
|
|
83
83
|
|
|
84
84
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
85
|
-
const CLI_VERSION = "1.
|
|
85
|
+
const CLI_VERSION = "1.3.1"
|
|
86
86
|
|
|
87
87
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
88
88
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
package/src/mcp-http.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
|
|
|
26
26
|
export type { TypeEntry } from "./types-search.ts"
|
|
27
27
|
|
|
28
28
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
29
|
-
const VERSION = "1.
|
|
29
|
+
const VERSION = "1.3.1"
|
|
30
30
|
const SERVER_INFO = { name: "nifra-docs", version: VERSION }
|
|
31
31
|
const DOCS_HEALTH =
|
|
32
32
|
"nifra docs MCP — POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call). Tools: nifra_docs, nifra_example."
|
package/src/mcp.ts
CHANGED
|
@@ -40,10 +40,12 @@ import {
|
|
|
40
40
|
type JsonRpcRequest,
|
|
41
41
|
type JsonRpcResponse,
|
|
42
42
|
type McpPrompt,
|
|
43
|
+
type McpPromptMessage,
|
|
43
44
|
type McpResource,
|
|
44
45
|
type McpServerFeatures,
|
|
45
46
|
type McpTool,
|
|
46
47
|
type McpToolContext,
|
|
48
|
+
type McpToolResult,
|
|
47
49
|
rpcError,
|
|
48
50
|
} from "./mcp-protocol.ts"
|
|
49
51
|
import { loadTypesCorpus } from "./types-search.ts"
|
|
@@ -566,6 +568,27 @@ export function projectFeatures(
|
|
|
566
568
|
return { resources: projectResources(cwd, loadAppCached), prompts: projectPrompts() }
|
|
567
569
|
}
|
|
568
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Resolve an optional `dir` tool argument — a subdirectory of the project root the caller wants to scope a
|
|
573
|
+
* check/test to (e.g. `nifra check` on `app/` when the MCP server's root is a monorepo). Returns the
|
|
574
|
+
* absolute target, or `null` if `dir` escapes the root (a path-traversal guard: no `..` out, no absolute
|
|
575
|
+
* path elsewhere). `undefined`/empty → the root itself.
|
|
576
|
+
*/
|
|
577
|
+
export function resolveProjectDir(root: string, dir: string | undefined): string | null {
|
|
578
|
+
if (dir === undefined || dir === "") return root
|
|
579
|
+
const target = resolve(root, dir)
|
|
580
|
+
return target === root || target.startsWith(root + sep) ? target : null
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Consistent error string for a `dir` that escapes the project root. */
|
|
584
|
+
function dirError(dir: string | undefined): string {
|
|
585
|
+
return JSON.stringify(
|
|
586
|
+
{ ok: false, error: `dir must be a subdirectory of the project root — "${dir}" escapes it.` },
|
|
587
|
+
null,
|
|
588
|
+
2,
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
569
592
|
/** Build the project-scoped tools for `cwd`. */
|
|
570
593
|
export function projectTools(
|
|
571
594
|
cwd: string,
|
|
@@ -755,13 +778,21 @@ export function projectTools(
|
|
|
755
778
|
type: "number",
|
|
756
779
|
description: "Timeout in milliseconds (default 30000, max 300000).",
|
|
757
780
|
},
|
|
781
|
+
dir: {
|
|
782
|
+
type: "string",
|
|
783
|
+
description:
|
|
784
|
+
'Run the tests in this subdirectory (relative to the project root), e.g. "app". Use it when the MCP server\'s root is a monorepo but you want to test just one app. Default: the project root.',
|
|
785
|
+
},
|
|
758
786
|
},
|
|
759
787
|
additionalProperties: false,
|
|
760
788
|
},
|
|
761
789
|
handler: async (args, context) => {
|
|
762
790
|
const { collectTestResult } = await import("./test-tool.ts")
|
|
791
|
+
const opts = args as { dir?: string }
|
|
792
|
+
const target = resolveProjectDir(cwd, opts.dir)
|
|
793
|
+
if (target === null) return dirError(opts.dir)
|
|
763
794
|
return JSON.stringify(
|
|
764
|
-
await collectTestResult(
|
|
795
|
+
await collectTestResult(target, args, { signal: context.signal }),
|
|
765
796
|
null,
|
|
766
797
|
2,
|
|
767
798
|
)
|
|
@@ -817,14 +848,21 @@ export function projectTools(
|
|
|
817
848
|
type: "boolean",
|
|
818
849
|
description: "Skip tsc; run only the near-instant source lints (inner-loop mode).",
|
|
819
850
|
},
|
|
851
|
+
dir: {
|
|
852
|
+
type: "string",
|
|
853
|
+
description:
|
|
854
|
+
'Run the check in this subdirectory (relative to the project root), e.g. "app" or "packages/api". Use it when the MCP server\'s root is a monorepo but you want to check just one app. Default: the project root.',
|
|
855
|
+
},
|
|
820
856
|
},
|
|
821
857
|
additionalProperties: false,
|
|
822
858
|
},
|
|
823
859
|
handler: async (args, context) => {
|
|
824
860
|
const { collectCheckResult } = await import("./check.ts")
|
|
825
|
-
const opts = args as { lintsOnly?: boolean }
|
|
861
|
+
const opts = args as { lintsOnly?: boolean; dir?: string }
|
|
862
|
+
const target = resolveProjectDir(cwd, opts.dir)
|
|
863
|
+
if (target === null) return dirError(opts.dir)
|
|
826
864
|
return JSON.stringify(
|
|
827
|
-
await collectCheckResult(
|
|
865
|
+
await collectCheckResult(target, {
|
|
828
866
|
lintsOnly: opts.lintsOnly ?? false,
|
|
829
867
|
signal: context.signal,
|
|
830
868
|
// Bound the result so a large project can't emit an MCP message big enough to break the stdio
|
|
@@ -837,6 +875,81 @@ export function projectTools(
|
|
|
837
875
|
)
|
|
838
876
|
},
|
|
839
877
|
},
|
|
878
|
+
{
|
|
879
|
+
name: "nifra_fix",
|
|
880
|
+
description:
|
|
881
|
+
"Automatically fix diagnostic lints (such as rewriting hand-rolled fetch() calls to the typed nifra client, adding generic types to client factory calls, and resolving dependency drift in package.json). Runs diagnostics, applies all mechanical edit suggestions, applies doctor dependency fixes, and returns the remaining unresolved diagnostics.",
|
|
882
|
+
inputSchema: {
|
|
883
|
+
type: "object",
|
|
884
|
+
properties: {},
|
|
885
|
+
additionalProperties: false,
|
|
886
|
+
},
|
|
887
|
+
handler: async (_args, context) => {
|
|
888
|
+
const { collectCheckResult } = await import("./check.ts")
|
|
889
|
+
const { applyDoctorAutoFix } = await import("./doctor.ts")
|
|
890
|
+
const { writeFile, readFile } = await import("node:fs/promises")
|
|
891
|
+
const { resolve } = await import("node:path")
|
|
892
|
+
|
|
893
|
+
let doctorResult: Awaited<ReturnType<typeof applyDoctorAutoFix>> | null = null
|
|
894
|
+
try {
|
|
895
|
+
doctorResult = await applyDoctorAutoFix(cwd)
|
|
896
|
+
} catch {
|
|
897
|
+
// ignore doctor errors
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const checkResult = await collectCheckResult(cwd, {
|
|
901
|
+
lintsOnly: true,
|
|
902
|
+
signal: context.signal,
|
|
903
|
+
maxDiagnostics: 100,
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
const fixed: Array<{ file: string; line: number; title: string }> = []
|
|
907
|
+
|
|
908
|
+
for (const diag of checkResult.diagnostics) {
|
|
909
|
+
if (diag.file && diag.line && diag.suggestion?.kind === "edit" && diag.suggestion.diff) {
|
|
910
|
+
try {
|
|
911
|
+
const diffLines = diag.suggestion.diff.split("\n")
|
|
912
|
+
const beforeLine = diffLines.find((l) => l.startsWith("-"))?.slice(1)
|
|
913
|
+
const afterLine = diffLines.find((l) => l.startsWith("+"))?.slice(1)
|
|
914
|
+
if (beforeLine !== undefined && afterLine !== undefined) {
|
|
915
|
+
const filePath = resolve(cwd, diag.file)
|
|
916
|
+
const content = await readFile(filePath, "utf-8")
|
|
917
|
+
const lines = content.split("\n")
|
|
918
|
+
const idx = diag.line - 1
|
|
919
|
+
if (lines[idx] === beforeLine) {
|
|
920
|
+
lines[idx] = afterLine
|
|
921
|
+
await writeFile(filePath, lines.join("\n"), "utf-8")
|
|
922
|
+
fixed.push({
|
|
923
|
+
file: diag.file,
|
|
924
|
+
line: diag.line,
|
|
925
|
+
title: diag.suggestion.title,
|
|
926
|
+
})
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
} catch {
|
|
930
|
+
// ignore edit errors
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const finalResult = await collectCheckResult(cwd, {
|
|
936
|
+
lintsOnly: false,
|
|
937
|
+
signal: context.signal,
|
|
938
|
+
maxDiagnostics: 100,
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
return JSON.stringify(
|
|
942
|
+
{
|
|
943
|
+
ok: finalResult.ok,
|
|
944
|
+
fixed,
|
|
945
|
+
doctorFixed: doctorResult?.fixed ?? [],
|
|
946
|
+
remainingDiagnostics: finalResult.diagnostics,
|
|
947
|
+
},
|
|
948
|
+
null,
|
|
949
|
+
2,
|
|
950
|
+
)
|
|
951
|
+
},
|
|
952
|
+
},
|
|
840
953
|
{
|
|
841
954
|
name: "nifra_doctor",
|
|
842
955
|
description:
|
|
@@ -865,6 +978,151 @@ export function projectTools(
|
|
|
865
978
|
]
|
|
866
979
|
}
|
|
867
980
|
|
|
981
|
+
/** A `.tool()`-registered route as seen through the backend's `routes()` introspection. */
|
|
982
|
+
type ToolRoute = {
|
|
983
|
+
readonly schema?: { readonly body?: unknown; readonly response?: unknown }
|
|
984
|
+
readonly tool?: {
|
|
985
|
+
readonly name: string
|
|
986
|
+
readonly description: string
|
|
987
|
+
readonly annotations?: McpTool["annotations"]
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
type ToolBackend = {
|
|
991
|
+
readonly routes?: () => ToolRoute[]
|
|
992
|
+
readonly fetch: (req: Request) => Promise<Response>
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Extract tools registered via .tool() routes on the Nifra backend. Exported for the test that proves a
|
|
996
|
+
* `server().tool()` route surfaces in `tools/list` and executes through `tools/call`. */
|
|
997
|
+
export function extractBackendTools(backend: unknown): McpTool[] {
|
|
998
|
+
const b = backend as ToolBackend | null
|
|
999
|
+
if (!b || typeof b.routes !== "function") return []
|
|
1000
|
+
let routes: ToolRoute[] = []
|
|
1001
|
+
try {
|
|
1002
|
+
routes = b.routes()
|
|
1003
|
+
} catch {
|
|
1004
|
+
return []
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const jsonSchemaOf = (schema: unknown): unknown => {
|
|
1008
|
+
if (schema && typeof schema === "object" && "jsonSchema" in schema) {
|
|
1009
|
+
const js = (schema as { jsonSchema?: unknown }).jsonSchema
|
|
1010
|
+
if (js !== undefined && js !== null) return js
|
|
1011
|
+
}
|
|
1012
|
+
return undefined
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return routes
|
|
1016
|
+
.filter(
|
|
1017
|
+
(
|
|
1018
|
+
r,
|
|
1019
|
+
): r is ToolRoute & {
|
|
1020
|
+
schema: NonNullable<ToolRoute["schema"]>
|
|
1021
|
+
tool: NonNullable<ToolRoute["tool"]>
|
|
1022
|
+
} => r.schema !== undefined && r.tool !== undefined,
|
|
1023
|
+
)
|
|
1024
|
+
.map((r) => {
|
|
1025
|
+
const toolInfo = r.tool
|
|
1026
|
+
const s = r.schema
|
|
1027
|
+
return {
|
|
1028
|
+
name: toolInfo.name,
|
|
1029
|
+
description: toolInfo.description,
|
|
1030
|
+
inputSchema: (jsonSchemaOf(s.body) ?? {
|
|
1031
|
+
type: "object",
|
|
1032
|
+
properties: {},
|
|
1033
|
+
}) as McpTool["inputSchema"],
|
|
1034
|
+
...(toolInfo.annotations !== undefined ? { annotations: toolInfo.annotations } : {}),
|
|
1035
|
+
handler: async (args): Promise<string | McpToolResult> => {
|
|
1036
|
+
const res = await b.fetch(
|
|
1037
|
+
new Request(`http://localhost/_nifra/tool/${toolInfo.name}`, {
|
|
1038
|
+
method: "POST",
|
|
1039
|
+
headers: { "content-type": "application/json" },
|
|
1040
|
+
body: JSON.stringify(args),
|
|
1041
|
+
}),
|
|
1042
|
+
)
|
|
1043
|
+
if (!res.ok) {
|
|
1044
|
+
const text = await res.text()
|
|
1045
|
+
throw new Error(`Tool execution failed (${res.status}): ${text}`)
|
|
1046
|
+
}
|
|
1047
|
+
const body: unknown = await res.json()
|
|
1048
|
+
if (typeof body === "string") return body
|
|
1049
|
+
if (body && typeof body === "object") {
|
|
1050
|
+
if ("content" in body || "structuredContent" in body) {
|
|
1051
|
+
return body as McpToolResult
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
1055
|
+
structuredContent: body as Record<string, unknown>,
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return String(body)
|
|
1059
|
+
},
|
|
1060
|
+
}
|
|
1061
|
+
})
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** A resource declared on the backend via `server().resource()`, seen through `mcpResources()`. */
|
|
1065
|
+
type ResourceDescriptor = {
|
|
1066
|
+
readonly uri: string
|
|
1067
|
+
readonly name: string
|
|
1068
|
+
readonly description?: string
|
|
1069
|
+
readonly mimeType?: string
|
|
1070
|
+
readonly read: () => unknown | Promise<unknown>
|
|
1071
|
+
}
|
|
1072
|
+
type ResourceBackend = { readonly mcpResources?: () => readonly ResourceDescriptor[] }
|
|
1073
|
+
|
|
1074
|
+
/** Extract MCP resources registered via `.resource()` on the Nifra backend. Exported for the surfacing test. */
|
|
1075
|
+
export function extractBackendResources(backend: unknown): McpResource[] {
|
|
1076
|
+
const b = backend as ResourceBackend | null
|
|
1077
|
+
if (!b || typeof b.mcpResources !== "function") return []
|
|
1078
|
+
let list: readonly ResourceDescriptor[]
|
|
1079
|
+
try {
|
|
1080
|
+
list = b.mcpResources()
|
|
1081
|
+
} catch {
|
|
1082
|
+
return []
|
|
1083
|
+
}
|
|
1084
|
+
return list.map((r) => ({
|
|
1085
|
+
uri: r.uri,
|
|
1086
|
+
name: r.name,
|
|
1087
|
+
...(r.description !== undefined ? { description: r.description } : {}),
|
|
1088
|
+
...(r.mimeType !== undefined ? { mimeType: r.mimeType } : {}),
|
|
1089
|
+
read: async () => {
|
|
1090
|
+
const out = await r.read()
|
|
1091
|
+
if (typeof out === "string") return { text: out }
|
|
1092
|
+
const o = out as { text: string; mimeType?: string }
|
|
1093
|
+
return o.mimeType !== undefined ? { text: o.text, mimeType: o.mimeType } : { text: o.text }
|
|
1094
|
+
},
|
|
1095
|
+
}))
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/** A prompt declared on the backend via `server().prompt()`, seen through `mcpPrompts()`. */
|
|
1099
|
+
type PromptDescriptor = {
|
|
1100
|
+
readonly name: string
|
|
1101
|
+
readonly description: string
|
|
1102
|
+
readonly arguments?: readonly { name: string; description?: string; required?: boolean }[]
|
|
1103
|
+
readonly handler: (args: Record<string, string>) => unknown | Promise<unknown>
|
|
1104
|
+
}
|
|
1105
|
+
type PromptBackend = { readonly mcpPrompts?: () => readonly PromptDescriptor[] }
|
|
1106
|
+
|
|
1107
|
+
/** Extract MCP prompts registered via `.prompt()` on the Nifra backend. Exported for the surfacing test. */
|
|
1108
|
+
export function extractBackendPrompts(backend: unknown): McpPrompt[] {
|
|
1109
|
+
const b = backend as PromptBackend | null
|
|
1110
|
+
if (!b || typeof b.mcpPrompts !== "function") return []
|
|
1111
|
+
let list: readonly PromptDescriptor[]
|
|
1112
|
+
try {
|
|
1113
|
+
list = b.mcpPrompts()
|
|
1114
|
+
} catch {
|
|
1115
|
+
return []
|
|
1116
|
+
}
|
|
1117
|
+
return list.map((p) => ({
|
|
1118
|
+
name: p.name,
|
|
1119
|
+
description: p.description,
|
|
1120
|
+
...(p.arguments !== undefined ? { arguments: p.arguments } : {}),
|
|
1121
|
+
handler: async (args: Record<string, unknown>) =>
|
|
1122
|
+
(await p.handler(args as Record<string, string>)) as readonly McpPromptMessage[],
|
|
1123
|
+
}))
|
|
1124
|
+
}
|
|
1125
|
+
|
|
868
1126
|
/** Prefix every tool name and resource URI for a named app in a monorepo. */
|
|
869
1127
|
function namespaceForApp(
|
|
870
1128
|
name: string,
|
|
@@ -872,7 +1130,10 @@ function namespaceForApp(
|
|
|
872
1130
|
features: McpServerFeatures,
|
|
873
1131
|
): { tools: McpTool[]; features: McpServerFeatures } {
|
|
874
1132
|
const prefix = `nifra_${name}_`
|
|
875
|
-
const namespacedTools = tools.map((t) => ({
|
|
1133
|
+
const namespacedTools = tools.map((t) => ({
|
|
1134
|
+
...t,
|
|
1135
|
+
name: t.name.startsWith("nifra_") ? t.name.replace(/^nifra_/, prefix) : `${name}_${t.name}`,
|
|
1136
|
+
}))
|
|
876
1137
|
const namespacedResources = (features.resources ?? []).map((r) => ({
|
|
877
1138
|
...r,
|
|
878
1139
|
uri: r.uri.replace(/^nifra:\/\//, `nifra://${name}/`),
|
|
@@ -889,40 +1150,65 @@ function namespaceForApp(
|
|
|
889
1150
|
|
|
890
1151
|
/** Run the stdio MCP server: read newline-delimited JSON-RPC from stdin, write responses to stdout. */
|
|
891
1152
|
export async function runMcpServer(cwd: string, version: string): Promise<void> {
|
|
892
|
-
let tools: McpTool[]
|
|
893
1153
|
let features: McpServerFeatures
|
|
894
1154
|
|
|
895
1155
|
const monorepo = await detectMonorepo(cwd)
|
|
896
1156
|
if (monorepo) {
|
|
897
1157
|
const appEntries = await loadMonorepoApps(cwd, monorepo)
|
|
898
|
-
const allTools: McpTool[] = []
|
|
899
1158
|
const allResources: McpResource[] = []
|
|
900
1159
|
const allPrompts: McpPrompt[] = []
|
|
901
1160
|
for (const { name, cwd: appCwd } of appEntries) {
|
|
902
1161
|
const loader = createCachedAppLoader(appCwd)
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1162
|
+
const app = await loader()
|
|
1163
|
+
const base = projectFeatures(appCwd, loader)
|
|
1164
|
+
const ns = namespaceForApp(name, [], {
|
|
1165
|
+
resources: [...(base.resources ?? []), ...extractBackendResources(app.backend)],
|
|
1166
|
+
prompts: [...(base.prompts ?? []), ...extractBackendPrompts(app.backend)],
|
|
1167
|
+
})
|
|
909
1168
|
allResources.push(...(ns.features.resources ?? []))
|
|
910
1169
|
allPrompts.push(...(ns.features.prompts ?? []))
|
|
911
1170
|
}
|
|
912
|
-
tools = [...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus), ...allTools]
|
|
913
1171
|
features = { resources: allResources, prompts: allPrompts }
|
|
914
1172
|
} else {
|
|
915
1173
|
const loadAppCached = createCachedAppLoader(cwd)
|
|
916
|
-
|
|
917
|
-
|
|
1174
|
+
const base = projectFeatures(cwd, loadAppCached)
|
|
1175
|
+
const app = await loadAppCached()
|
|
1176
|
+
features = {
|
|
1177
|
+
resources: [...(base.resources ?? []), ...extractBackendResources(app.backend)],
|
|
1178
|
+
prompts: [...(base.prompts ?? []), ...extractBackendPrompts(app.backend)],
|
|
1179
|
+
}
|
|
918
1180
|
}
|
|
1181
|
+
const loadAppCached = createCachedAppLoader(cwd)
|
|
919
1182
|
const serverInfo = { name: "nifra", version }
|
|
920
1183
|
const state = createMcpProtocolState()
|
|
921
1184
|
const send = (message: JsonRpcResponse | JsonRpcNotification): void => {
|
|
922
1185
|
process.stdout.write(`${JSON.stringify(message)}\n`)
|
|
923
1186
|
}
|
|
924
1187
|
const dispatch = async (message: JsonRpcRequest): Promise<void> => {
|
|
925
|
-
|
|
1188
|
+
let activeTools: McpTool[]
|
|
1189
|
+
if (monorepo) {
|
|
1190
|
+
const appEntries = await loadMonorepoApps(cwd, monorepo)
|
|
1191
|
+
const allTools: McpTool[] = []
|
|
1192
|
+
for (const { name, cwd: appCwd } of appEntries) {
|
|
1193
|
+
const loader = createCachedAppLoader(appCwd)
|
|
1194
|
+
const app = await loader()
|
|
1195
|
+
const baseTools = projectTools(appCwd, loader)
|
|
1196
|
+
const backendTools = extractBackendTools(app.backend)
|
|
1197
|
+
const ns = namespaceForApp(name, [...baseTools, ...backendTools], {
|
|
1198
|
+
resources: [],
|
|
1199
|
+
prompts: [],
|
|
1200
|
+
})
|
|
1201
|
+
allTools.push(...ns.tools)
|
|
1202
|
+
}
|
|
1203
|
+
activeTools = [...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus), ...allTools]
|
|
1204
|
+
} else {
|
|
1205
|
+
const app = await loadAppCached()
|
|
1206
|
+
const baseTools = projectTools(cwd, loadAppCached)
|
|
1207
|
+
const backendTools = extractBackendTools(app.backend)
|
|
1208
|
+
activeTools = [...baseTools, ...backendTools]
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const response = await handleRpc(message, activeTools, serverInfo, features, {
|
|
926
1212
|
state,
|
|
927
1213
|
sendNotification: send,
|
|
928
1214
|
})
|