@fenglimg/fabric-server 0.1.0 → 1.1.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/chunk-U3IQH5H6.js +851 -0
- package/dist/http-FL3MB4L2.js +1231 -0
- package/dist/index.d.ts +70 -1
- package/dist/index.js +261 -188
- package/dist/static/assets/index-BiK8yn_c.js +5 -0
- package/dist/static/assets/index-D45wW11O.css +1 -0
- package/dist/static/index.html +14 -0
- package/package.json +13 -8
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
1
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { AuditMode } from '@fenglimg/fabric-shared';
|
|
4
|
+
|
|
5
|
+
type DoctorStatus = "ok" | "warn" | "error";
|
|
6
|
+
type DoctorCheck = {
|
|
7
|
+
name: string;
|
|
8
|
+
status: DoctorStatus;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
type DoctorSummary = {
|
|
12
|
+
target: string;
|
|
13
|
+
framework: {
|
|
14
|
+
kind: string;
|
|
15
|
+
version: string;
|
|
16
|
+
subkind: string;
|
|
17
|
+
};
|
|
18
|
+
entryPoints: Array<{
|
|
19
|
+
path: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}>;
|
|
22
|
+
driftCount: number;
|
|
23
|
+
protectedPathCount: number;
|
|
24
|
+
protectedPathsIntact: boolean;
|
|
25
|
+
lastLedgerEntryTs: number | null;
|
|
26
|
+
lastLedgerEntryAgeMs: number | null;
|
|
27
|
+
metaRevision: string | null;
|
|
28
|
+
audit: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
mode: AuditMode;
|
|
31
|
+
checkedPathCount: number;
|
|
32
|
+
violationCount: number;
|
|
33
|
+
windowMs: number;
|
|
34
|
+
} | null;
|
|
35
|
+
};
|
|
36
|
+
type DoctorReport = {
|
|
37
|
+
status: DoctorStatus;
|
|
38
|
+
checks: DoctorCheck[];
|
|
39
|
+
summary: DoctorSummary;
|
|
40
|
+
audit: DoctorAuditReport | null;
|
|
41
|
+
};
|
|
42
|
+
type DoctorAuditViolation = {
|
|
43
|
+
editTs: number;
|
|
44
|
+
entryId: string;
|
|
45
|
+
intent: string;
|
|
46
|
+
lastGetRulesTs: number | null;
|
|
47
|
+
path: string;
|
|
48
|
+
};
|
|
49
|
+
type DoctorAuditReport = {
|
|
50
|
+
mode: AuditMode;
|
|
51
|
+
skipped: boolean;
|
|
52
|
+
windowMs: number;
|
|
53
|
+
checkedPathCount: number;
|
|
54
|
+
violationCount: number;
|
|
55
|
+
violations: DoctorAuditViolation[];
|
|
56
|
+
};
|
|
57
|
+
declare function runDoctorReport(target: string): Promise<DoctorReport>;
|
|
58
|
+
declare function runDoctorAuditReport(target: string, options?: {
|
|
59
|
+
force?: boolean;
|
|
60
|
+
mode?: AuditMode;
|
|
61
|
+
windowMs?: number;
|
|
62
|
+
}): Promise<DoctorAuditReport>;
|
|
2
63
|
|
|
3
64
|
declare function createFabricServer(): McpServer;
|
|
4
65
|
declare function startStdioServer(): Promise<void>;
|
|
66
|
+
declare function startHttpServer(options: {
|
|
67
|
+
port: number;
|
|
68
|
+
projectRoot: string;
|
|
69
|
+
host?: string;
|
|
70
|
+
authToken?: string;
|
|
71
|
+
dashboardDistPath?: string;
|
|
72
|
+
dev?: boolean;
|
|
73
|
+
}): Promise<Server>;
|
|
5
74
|
|
|
6
|
-
export { createFabricServer, startStdioServer };
|
|
75
|
+
export { type DoctorAuditReport, type DoctorReport, createFabricServer, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FABRIC_DIR,
|
|
3
|
+
appendEditIntentAuditEvents,
|
|
4
|
+
appendGetRulesAuditEvent,
|
|
5
|
+
appendLedgerEntry,
|
|
6
|
+
atomicWriteText,
|
|
7
|
+
readAgentsMeta,
|
|
8
|
+
readHumanLock,
|
|
9
|
+
resolveProjectRoot,
|
|
10
|
+
runDoctorAuditReport,
|
|
11
|
+
runDoctorReport,
|
|
12
|
+
sha256
|
|
13
|
+
} from "./chunk-U3IQH5H6.js";
|
|
14
|
+
|
|
1
15
|
// src/index.ts
|
|
2
16
|
import { resolve } from "path";
|
|
3
17
|
import { fileURLToPath } from "url";
|
|
@@ -5,74 +19,38 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
20
|
|
|
7
21
|
// src/tools/append-intent.ts
|
|
8
|
-
import {
|
|
9
|
-
import { join as join2 } from "path";
|
|
10
|
-
import { z as z2 } from "zod";
|
|
22
|
+
import { aiLedgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
11
23
|
|
|
12
|
-
// src/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
priority: z.enum(["high", "medium", "low"]),
|
|
21
|
-
hash: z.string()
|
|
22
|
-
});
|
|
23
|
-
var agentsMetaSchema = z.object({
|
|
24
|
-
revision: z.string(),
|
|
25
|
-
nodes: z.record(agentsMetaNodeSchema)
|
|
26
|
-
});
|
|
27
|
-
var AgentsMetaFileMissingError = class extends Error {
|
|
28
|
-
constructor(metaPath) {
|
|
29
|
-
super(`Fabric agents metadata file is missing: ${metaPath}`);
|
|
30
|
-
this.metaPath = metaPath;
|
|
31
|
-
this.name = "AgentsMetaFileMissingError";
|
|
32
|
-
}
|
|
33
|
-
metaPath;
|
|
34
|
-
code = "FABRIC_META_MISSING";
|
|
35
|
-
};
|
|
36
|
-
var AgentsMetaInvalidError = class extends Error {
|
|
37
|
-
constructor(metaPath, cause) {
|
|
38
|
-
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
39
|
-
super(`Fabric agents metadata file is invalid: ${metaPath}. ${detail}`);
|
|
40
|
-
this.metaPath = metaPath;
|
|
41
|
-
this.name = "AgentsMetaInvalidError";
|
|
42
|
-
}
|
|
43
|
-
metaPath;
|
|
44
|
-
code = "FABRIC_META_INVALID";
|
|
45
|
-
};
|
|
46
|
-
function getAgentsMetaPath(projectRoot) {
|
|
47
|
-
return join(projectRoot, ".fabric", "agents.meta.json");
|
|
48
|
-
}
|
|
49
|
-
function resolveProjectRoot() {
|
|
50
|
-
return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
51
|
-
}
|
|
52
|
-
function readAgentsMeta(projectRoot) {
|
|
53
|
-
const metaPath = getAgentsMetaPath(projectRoot);
|
|
54
|
-
let raw;
|
|
55
|
-
try {
|
|
56
|
-
raw = readFileSync(metaPath, "utf8");
|
|
57
|
-
} catch (error) {
|
|
58
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
59
|
-
throw new AgentsMetaFileMissingError(metaPath);
|
|
60
|
-
}
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
24
|
+
// src/services/append-intent.ts
|
|
25
|
+
async function appendIntent(projectRoot, input) {
|
|
26
|
+
const ts = Date.now();
|
|
27
|
+
const entry = await appendLedgerEntry(projectRoot, {
|
|
28
|
+
...input.entry,
|
|
29
|
+
ts,
|
|
30
|
+
source: "ai"
|
|
31
|
+
});
|
|
63
32
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
33
|
+
await appendEditIntentAuditEvents(projectRoot, {
|
|
34
|
+
affected_paths: entry.affected_paths,
|
|
35
|
+
intent: entry.intent,
|
|
36
|
+
ledger_entry_id: entry.id,
|
|
37
|
+
ts
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
67
40
|
}
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
timestamp: ts,
|
|
44
|
+
entry
|
|
45
|
+
};
|
|
68
46
|
}
|
|
69
47
|
|
|
70
48
|
// src/tools/append-intent.ts
|
|
71
49
|
var inputSchema = {
|
|
72
|
-
entry:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
50
|
+
entry: aiLedgerEntrySchema.omit({
|
|
51
|
+
id: true,
|
|
52
|
+
source: true,
|
|
53
|
+
ts: true
|
|
76
54
|
})
|
|
77
55
|
};
|
|
78
56
|
function createTextResponse(payload) {
|
|
@@ -92,43 +70,66 @@ function registerAppendIntent(server) {
|
|
|
92
70
|
inputSchema,
|
|
93
71
|
async ({ entry }) => {
|
|
94
72
|
const projectRoot = resolveProjectRoot();
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
await appendFile(ledgerPath, `${JSON.stringify({ ts, ...entry })}
|
|
98
|
-
`, "utf8");
|
|
99
|
-
return createTextResponse({
|
|
100
|
-
success: true,
|
|
101
|
-
timestamp: ts
|
|
102
|
-
});
|
|
73
|
+
const result = await appendIntent(projectRoot, { entry });
|
|
74
|
+
return createTextResponse(result);
|
|
103
75
|
}
|
|
104
76
|
);
|
|
105
77
|
}
|
|
106
78
|
|
|
107
79
|
// src/tools/get-rules.ts
|
|
80
|
+
import { z } from "zod";
|
|
81
|
+
|
|
82
|
+
// src/services/get-rules.ts
|
|
108
83
|
import { readFile } from "fs/promises";
|
|
109
|
-
import { join
|
|
84
|
+
import { join } from "path";
|
|
110
85
|
import { minimatch } from "minimatch";
|
|
111
|
-
import { z as z3 } from "zod";
|
|
112
|
-
var inputSchema2 = {
|
|
113
|
-
path: z3.string().describe("Target file path to query rules for"),
|
|
114
|
-
client_hash: z3.string().optional().describe("Revision hash from prior fab_get_rules response; enables stale detection")
|
|
115
|
-
};
|
|
116
86
|
var PRIORITY_ORDER = {
|
|
117
87
|
high: 0,
|
|
118
88
|
medium: 1,
|
|
119
89
|
low: 2
|
|
120
90
|
};
|
|
121
|
-
function
|
|
91
|
+
async function getRules(projectRoot, input) {
|
|
92
|
+
const context = await loadGetRulesContext(projectRoot);
|
|
93
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
94
|
+
const rules = await resolveRulesForPath(projectRoot, context, input.path);
|
|
95
|
+
const result = {
|
|
96
|
+
revision_hash: context.meta.revision,
|
|
97
|
+
stale,
|
|
98
|
+
rules
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
await appendGetRulesAuditEvent(projectRoot, {
|
|
102
|
+
path: input.path,
|
|
103
|
+
client_hash: input.client_hash
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
async function loadGetRulesContext(projectRoot) {
|
|
110
|
+
const meta = readAgentsMeta(projectRoot);
|
|
111
|
+
const l0Content = await readFile(join(projectRoot, "AGENTS.md"), "utf8");
|
|
112
|
+
const humanLockedNearby = (await readHumanLock(projectRoot)).map((entry) => ({
|
|
113
|
+
file: entry.file,
|
|
114
|
+
excerpt: JSON.stringify(entry)
|
|
115
|
+
}));
|
|
122
116
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
meta,
|
|
118
|
+
l0Content,
|
|
119
|
+
humanLockedNearby
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function resolveRulesForPath(projectRoot, context, path, options = {}) {
|
|
123
|
+
const loadedRules = await loadRulesForPath(projectRoot, context.meta, path);
|
|
124
|
+
const { L1, L2 } = partitionRulesByLevel(loadedRules, options.dedupeByPath ?? false);
|
|
125
|
+
return {
|
|
126
|
+
L0: context.l0Content,
|
|
127
|
+
L1,
|
|
128
|
+
L2,
|
|
129
|
+
human_locked_nearby: context.humanLockedNearby
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
|
-
function
|
|
132
|
+
function normalizeRulesPath(value) {
|
|
132
133
|
return value.replaceAll("\\", "/");
|
|
133
134
|
}
|
|
134
135
|
function classifyNode(nodeId) {
|
|
@@ -140,32 +141,66 @@ function classifyNode(nodeId) {
|
|
|
140
141
|
}
|
|
141
142
|
return null;
|
|
142
143
|
}
|
|
143
|
-
async function
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
async function loadRulesForPath(projectRoot, meta, path) {
|
|
145
|
+
const requestedPath = normalizeRulesPath(path);
|
|
146
|
+
const matchedNodes = Object.entries(meta.nodes).filter(([, node]) => minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true })).sort((left, right) => {
|
|
147
|
+
const [leftId, leftNode] = left;
|
|
148
|
+
const [rightId, rightNode] = right;
|
|
149
|
+
const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
|
|
150
|
+
return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
|
|
151
|
+
});
|
|
152
|
+
return await Promise.all(
|
|
153
|
+
matchedNodes.map(async ([nodeId, node]) => ({
|
|
154
|
+
level: classifyNode(nodeId),
|
|
155
|
+
entry: {
|
|
156
|
+
path: node.file,
|
|
157
|
+
content: await readFile(join(projectRoot, node.file), "utf8")
|
|
155
158
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
}))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
function partitionRulesByLevel(loadedRules, dedupeByPath) {
|
|
163
|
+
const l1 = [];
|
|
164
|
+
const l2 = [];
|
|
165
|
+
for (const rule of loadedRules) {
|
|
166
|
+
if (rule.level === "L1") {
|
|
167
|
+
l1.push(rule.entry);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (rule.level === "L2") {
|
|
171
|
+
l2.push(rule.entry);
|
|
166
172
|
}
|
|
167
|
-
throw error;
|
|
168
173
|
}
|
|
174
|
+
return {
|
|
175
|
+
L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
|
|
176
|
+
L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function dedupeEntriesByPath(entries) {
|
|
180
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
181
|
+
return entries.filter((entry) => {
|
|
182
|
+
if (seenPaths.has(entry.path)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
seenPaths.add(entry.path);
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/tools/get-rules.ts
|
|
191
|
+
var inputSchema2 = {
|
|
192
|
+
path: z.string().describe("Target file path to query rules for"),
|
|
193
|
+
client_hash: z.string().optional().describe("Revision hash from prior fab_get_rules response; enables stale detection")
|
|
194
|
+
};
|
|
195
|
+
function createTextResponse2(payload) {
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: JSON.stringify(payload)
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
};
|
|
169
204
|
}
|
|
170
205
|
function registerGetRules(server) {
|
|
171
206
|
server.tool(
|
|
@@ -174,68 +209,49 @@ function registerGetRules(server) {
|
|
|
174
209
|
inputSchema2,
|
|
175
210
|
async ({ path, client_hash }) => {
|
|
176
211
|
const projectRoot = resolveProjectRoot();
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const requestedPath = normalizePath(path);
|
|
180
|
-
const l0Content = await readFile(join3(projectRoot, "AGENTS.md"), "utf8");
|
|
181
|
-
const matchedNodes = Object.entries(meta.nodes).filter(([, node]) => minimatch(requestedPath, normalizePath(node.scope_glob), { dot: true })).sort((left, right) => {
|
|
182
|
-
const [leftId, leftNode] = left;
|
|
183
|
-
const [rightId, rightNode] = right;
|
|
184
|
-
const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
|
|
185
|
-
return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
|
|
186
|
-
});
|
|
187
|
-
const loadedRules = await Promise.all(
|
|
188
|
-
matchedNodes.map(async ([nodeId, node]) => ({
|
|
189
|
-
level: classifyNode(nodeId),
|
|
190
|
-
entry: {
|
|
191
|
-
path: node.file,
|
|
192
|
-
content: await readFile(join3(projectRoot, node.file), "utf8")
|
|
193
|
-
}
|
|
194
|
-
}))
|
|
195
|
-
);
|
|
196
|
-
const l1 = [];
|
|
197
|
-
const l2 = [];
|
|
198
|
-
for (const rule of loadedRules) {
|
|
199
|
-
if (rule.level === "L1") {
|
|
200
|
-
l1.push(rule.entry);
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (rule.level === "L2") {
|
|
204
|
-
l2.push(rule.entry);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
const humanLockedNearby = await readHumanLockedNearby(projectRoot);
|
|
208
|
-
return createTextResponse2({
|
|
209
|
-
revision_hash: meta.revision,
|
|
210
|
-
stale,
|
|
211
|
-
rules: {
|
|
212
|
-
L0: l0Content,
|
|
213
|
-
L1: l1,
|
|
214
|
-
L2: l2,
|
|
215
|
-
human_locked_nearby: humanLockedNearby
|
|
216
|
-
}
|
|
217
|
-
});
|
|
212
|
+
const result = await getRules(projectRoot, { path, client_hash });
|
|
213
|
+
return createTextResponse2(result);
|
|
218
214
|
}
|
|
219
215
|
);
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
// src/tools/
|
|
223
|
-
import {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
218
|
+
// src/tools/plan-context.ts
|
|
219
|
+
import { z as z2 } from "zod";
|
|
220
|
+
|
|
221
|
+
// src/services/plan-context.ts
|
|
222
|
+
async function planContext(projectRoot, input) {
|
|
223
|
+
const context = await loadGetRulesContext(projectRoot);
|
|
224
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
225
|
+
const uniquePaths = dedupePaths(input.paths);
|
|
226
|
+
const entries = await Promise.all(
|
|
227
|
+
uniquePaths.map(async (path) => ({
|
|
228
|
+
path,
|
|
229
|
+
rules: await resolveRulesForPath(projectRoot, context, path, { dedupeByPath: true })
|
|
230
|
+
}))
|
|
231
|
+
);
|
|
232
|
+
return {
|
|
233
|
+
revision_hash: context.meta.revision,
|
|
234
|
+
stale,
|
|
235
|
+
entries
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function dedupePaths(paths) {
|
|
239
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
240
|
+
return paths.flatMap((path) => {
|
|
241
|
+
const normalizedPath = normalizeRulesPath(path);
|
|
242
|
+
if (seenPaths.has(normalizedPath)) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
seenPaths.add(normalizedPath);
|
|
246
|
+
return [normalizedPath];
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/tools/plan-context.ts
|
|
227
251
|
var inputSchema3 = {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
data: z4.record(z4.unknown()).optional()
|
|
252
|
+
paths: z2.array(z2.string()).min(1).describe("Candidate file paths to query rules for during planning or architecture review"),
|
|
253
|
+
client_hash: z2.string().optional().describe("Revision hash from a prior fab_plan_context response; enables stale detection")
|
|
231
254
|
};
|
|
232
|
-
var agentsMetaNodeSchema2 = z4.object({
|
|
233
|
-
file: z4.string(),
|
|
234
|
-
scope_glob: z4.string(),
|
|
235
|
-
deps: z4.array(z4.string()),
|
|
236
|
-
priority: z4.enum(["high", "medium", "low"]),
|
|
237
|
-
hash: z4.string()
|
|
238
|
-
});
|
|
239
255
|
function createTextResponse3(payload) {
|
|
240
256
|
return {
|
|
241
257
|
content: [
|
|
@@ -246,15 +262,56 @@ function createTextResponse3(payload) {
|
|
|
246
262
|
]
|
|
247
263
|
};
|
|
248
264
|
}
|
|
265
|
+
function registerPlanContext(server) {
|
|
266
|
+
server.tool(
|
|
267
|
+
"fab_plan_context",
|
|
268
|
+
"Use during plan or architecture phases to batch-query Fabric rules for multiple candidate paths in one round-trip.",
|
|
269
|
+
inputSchema3,
|
|
270
|
+
async ({ paths, client_hash }) => {
|
|
271
|
+
const projectRoot = resolveProjectRoot();
|
|
272
|
+
const result = await planContext(projectRoot, { paths, client_hash });
|
|
273
|
+
return createTextResponse3(result);
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/tools/update-registry.ts
|
|
279
|
+
import { z as z3 } from "zod";
|
|
280
|
+
|
|
281
|
+
// src/services/update-registry.ts
|
|
282
|
+
import { agentsMetaNodeSchema } from "@fenglimg/fabric-shared";
|
|
283
|
+
import { join as join2 } from "path";
|
|
284
|
+
async function updateRegistry(projectRoot, input) {
|
|
285
|
+
const metaPath = join2(projectRoot, FABRIC_DIR, "agents.meta.json");
|
|
286
|
+
const currentMeta = readAgentsMeta(projectRoot);
|
|
287
|
+
const nextMeta = applyRegistryOperation(currentMeta, input.op, input.node_id, input.data);
|
|
288
|
+
const newRevision = computeRevision(nextMeta);
|
|
289
|
+
await atomicWriteText(
|
|
290
|
+
metaPath,
|
|
291
|
+
`${JSON.stringify(
|
|
292
|
+
{
|
|
293
|
+
...nextMeta,
|
|
294
|
+
revision: newRevision
|
|
295
|
+
},
|
|
296
|
+
null,
|
|
297
|
+
2
|
|
298
|
+
)}
|
|
299
|
+
`
|
|
300
|
+
);
|
|
301
|
+
return {
|
|
302
|
+
revision_hash: newRevision,
|
|
303
|
+
success: true
|
|
304
|
+
};
|
|
305
|
+
}
|
|
249
306
|
function computeRevision(meta) {
|
|
250
307
|
const joinedHashes = Object.entries(meta.nodes).sort(([leftId], [rightId]) => leftId.localeCompare(rightId)).map(([, node]) => node.hash).join("");
|
|
251
|
-
return
|
|
308
|
+
return sha256(joinedHashes);
|
|
252
309
|
}
|
|
253
310
|
function assertNodeData(data, message) {
|
|
254
311
|
if (data === void 0) {
|
|
255
312
|
throw new Error(message);
|
|
256
313
|
}
|
|
257
|
-
return
|
|
314
|
+
return agentsMetaNodeSchema.parse(data);
|
|
258
315
|
}
|
|
259
316
|
function applyRegistryOperation(meta, op, nodeId, data) {
|
|
260
317
|
const nextNodes = { ...meta.nodes };
|
|
@@ -276,7 +333,7 @@ function applyRegistryOperation(meta, op, nodeId, data) {
|
|
|
276
333
|
if (currentNode === void 0) {
|
|
277
334
|
throw new Error(`Cannot update missing Fabric registry node: ${nodeId}`);
|
|
278
335
|
}
|
|
279
|
-
nextNodes[nodeId] =
|
|
336
|
+
nextNodes[nodeId] = agentsMetaNodeSchema.parse({
|
|
280
337
|
...currentNode,
|
|
281
338
|
...data
|
|
282
339
|
});
|
|
@@ -285,34 +342,32 @@ function applyRegistryOperation(meta, op, nodeId, data) {
|
|
|
285
342
|
nodes: nextNodes
|
|
286
343
|
};
|
|
287
344
|
}
|
|
345
|
+
|
|
346
|
+
// src/tools/update-registry.ts
|
|
347
|
+
var inputSchema4 = {
|
|
348
|
+
op: z3.enum(["add-node", "remove-node", "update-node"]),
|
|
349
|
+
node_id: z3.string(),
|
|
350
|
+
data: z3.record(z3.unknown()).optional()
|
|
351
|
+
};
|
|
352
|
+
function createTextResponse4(payload) {
|
|
353
|
+
return {
|
|
354
|
+
content: [
|
|
355
|
+
{
|
|
356
|
+
type: "text",
|
|
357
|
+
text: JSON.stringify(payload)
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
};
|
|
361
|
+
}
|
|
288
362
|
function registerUpdateRegistry(server) {
|
|
289
363
|
server.tool(
|
|
290
364
|
"fab_update_registry",
|
|
291
365
|
"MANDATORY: Call to add, remove, or update Fabric registry nodes instead of editing registry files directly.",
|
|
292
|
-
|
|
366
|
+
inputSchema4,
|
|
293
367
|
async ({ op, node_id, data }) => {
|
|
294
368
|
const projectRoot = resolveProjectRoot();
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
const nextMeta = applyRegistryOperation(currentMeta, op, node_id, data);
|
|
298
|
-
const newRevision = computeRevision(nextMeta);
|
|
299
|
-
await writeFile(
|
|
300
|
-
metaPath,
|
|
301
|
-
`${JSON.stringify(
|
|
302
|
-
{
|
|
303
|
-
...nextMeta,
|
|
304
|
-
revision: newRevision
|
|
305
|
-
},
|
|
306
|
-
null,
|
|
307
|
-
2
|
|
308
|
-
)}
|
|
309
|
-
`,
|
|
310
|
-
"utf8"
|
|
311
|
-
);
|
|
312
|
-
return createTextResponse3({
|
|
313
|
-
revision_hash: newRevision,
|
|
314
|
-
success: true
|
|
315
|
-
});
|
|
369
|
+
const result = await updateRegistry(projectRoot, { op, node_id, data });
|
|
370
|
+
return createTextResponse4(result);
|
|
316
371
|
}
|
|
317
372
|
);
|
|
318
373
|
}
|
|
@@ -331,9 +386,10 @@ function formatError(error) {
|
|
|
331
386
|
function createFabricServer() {
|
|
332
387
|
const server = new McpServer({
|
|
333
388
|
name: "fabric-context-server",
|
|
334
|
-
version: "
|
|
389
|
+
version: "1.1.0"
|
|
335
390
|
});
|
|
336
391
|
registerGetRules(server);
|
|
392
|
+
registerPlanContext(server);
|
|
337
393
|
registerAppendIntent(server);
|
|
338
394
|
registerUpdateRegistry(server);
|
|
339
395
|
return server;
|
|
@@ -343,6 +399,20 @@ async function startStdioServer() {
|
|
|
343
399
|
const transport = new StdioServerTransport();
|
|
344
400
|
await server.connect(transport);
|
|
345
401
|
}
|
|
402
|
+
async function startHttpServer(options) {
|
|
403
|
+
const { createFabricHttpApp } = await import("./http-FL3MB4L2.js");
|
|
404
|
+
const { port, projectRoot, host = "127.0.0.1", authToken, dashboardDistPath, dev } = options;
|
|
405
|
+
const app = createFabricHttpApp({ projectRoot, host, authToken, dashboardDistPath, dev });
|
|
406
|
+
return await new Promise((resolveServer, rejectServer) => {
|
|
407
|
+
const server = app.listen(port, host);
|
|
408
|
+
server.once("listening", () => {
|
|
409
|
+
resolveServer(server);
|
|
410
|
+
});
|
|
411
|
+
server.once("error", (error) => {
|
|
412
|
+
rejectServer(error);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
346
416
|
var entrypoint = process.argv[1];
|
|
347
417
|
var currentFilePath = fileURLToPath(import.meta.url);
|
|
348
418
|
var isMainModule = entrypoint !== void 0 && resolve(entrypoint) === currentFilePath;
|
|
@@ -354,5 +424,8 @@ if (isMainModule) {
|
|
|
354
424
|
}
|
|
355
425
|
export {
|
|
356
426
|
createFabricServer,
|
|
427
|
+
runDoctorAuditReport,
|
|
428
|
+
runDoctorReport,
|
|
429
|
+
startHttpServer,
|
|
357
430
|
startStdioServer
|
|
358
431
|
};
|