@fenglimg/fabric-server 0.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/index.d.ts +6 -0
- package/dist/index.js +358 -0
- package/package.json +24 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/tools/append-intent.ts
|
|
8
|
+
import { appendFile } from "fs/promises";
|
|
9
|
+
import { join as join2 } from "path";
|
|
10
|
+
import { z as z2 } from "zod";
|
|
11
|
+
|
|
12
|
+
// src/meta-reader.ts
|
|
13
|
+
import { readFileSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var agentsMetaNodeSchema = z.object({
|
|
17
|
+
file: z.string(),
|
|
18
|
+
scope_glob: z.string(),
|
|
19
|
+
deps: z.array(z.string()),
|
|
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
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return agentsMetaSchema.parse(JSON.parse(raw));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new AgentsMetaInvalidError(metaPath, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/tools/append-intent.ts
|
|
71
|
+
var inputSchema = {
|
|
72
|
+
entry: z2.object({
|
|
73
|
+
commit_sha: z2.string().optional(),
|
|
74
|
+
intent: z2.string(),
|
|
75
|
+
affected_paths: z2.array(z2.string())
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
function createTextResponse(payload) {
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: JSON.stringify(payload)
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function registerAppendIntent(server) {
|
|
89
|
+
server.tool(
|
|
90
|
+
"fab_append_intent",
|
|
91
|
+
"MANDATORY: Call after a completed task to append an intent ledger entry for Fabric.",
|
|
92
|
+
inputSchema,
|
|
93
|
+
async ({ entry }) => {
|
|
94
|
+
const projectRoot = resolveProjectRoot();
|
|
95
|
+
const ledgerPath = join2(projectRoot, ".intent-ledger.jsonl");
|
|
96
|
+
const ts = Date.now();
|
|
97
|
+
await appendFile(ledgerPath, `${JSON.stringify({ ts, ...entry })}
|
|
98
|
+
`, "utf8");
|
|
99
|
+
return createTextResponse({
|
|
100
|
+
success: true,
|
|
101
|
+
timestamp: ts
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/tools/get-rules.ts
|
|
108
|
+
import { readFile } from "fs/promises";
|
|
109
|
+
import { join as join3 } from "path";
|
|
110
|
+
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
|
+
var PRIORITY_ORDER = {
|
|
117
|
+
high: 0,
|
|
118
|
+
medium: 1,
|
|
119
|
+
low: 2
|
|
120
|
+
};
|
|
121
|
+
function createTextResponse2(payload) {
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: JSON.stringify(payload)
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function normalizePath(value) {
|
|
132
|
+
return value.replaceAll("\\", "/");
|
|
133
|
+
}
|
|
134
|
+
function classifyNode(nodeId) {
|
|
135
|
+
if (nodeId.startsWith("L1/")) {
|
|
136
|
+
return "L1";
|
|
137
|
+
}
|
|
138
|
+
if (nodeId.startsWith("L2/")) {
|
|
139
|
+
return "L2";
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
async function readHumanLockedNearby(projectRoot) {
|
|
144
|
+
const humanLockPath = join3(projectRoot, ".fabric", "human-lock.json");
|
|
145
|
+
try {
|
|
146
|
+
const raw = await readFile(humanLockPath, "utf8");
|
|
147
|
+
const parsed = JSON.parse(raw);
|
|
148
|
+
const entries = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && "human_locked" in parsed && Array.isArray(parsed.human_locked) ? parsed.human_locked : [];
|
|
149
|
+
return entries.map((entry) => {
|
|
150
|
+
if (!entry || typeof entry !== "object") {
|
|
151
|
+
return {
|
|
152
|
+
file: "(unknown)",
|
|
153
|
+
excerpt: JSON.stringify(entry)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const file = typeof entry.file === "string" ? entry.file : "(unknown)";
|
|
157
|
+
const excerptCandidate = typeof entry.excerpt === "string" ? entry.excerpt : typeof entry.locked_text === "string" ? entry.locked_text : typeof entry.text === "string" ? entry.text : typeof entry.content === "string" ? entry.content : JSON.stringify(entry);
|
|
158
|
+
return {
|
|
159
|
+
file,
|
|
160
|
+
excerpt: excerptCandidate
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function registerGetRules(server) {
|
|
171
|
+
server.tool(
|
|
172
|
+
"fab_get_rules",
|
|
173
|
+
"MANDATORY: Call before modifying any file to retrieve Fabric rules for a target path.",
|
|
174
|
+
inputSchema2,
|
|
175
|
+
async ({ path, client_hash }) => {
|
|
176
|
+
const projectRoot = resolveProjectRoot();
|
|
177
|
+
const meta = readAgentsMeta(projectRoot);
|
|
178
|
+
const stale = client_hash !== void 0 && client_hash !== meta.revision;
|
|
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
|
+
});
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/tools/update-registry.ts
|
|
223
|
+
import { createHash } from "crypto";
|
|
224
|
+
import { writeFile } from "fs/promises";
|
|
225
|
+
import { join as join4 } from "path";
|
|
226
|
+
import { z as z4 } from "zod";
|
|
227
|
+
var inputSchema3 = {
|
|
228
|
+
op: z4.enum(["add-node", "remove-node", "update-node"]),
|
|
229
|
+
node_id: z4.string(),
|
|
230
|
+
data: z4.record(z4.unknown()).optional()
|
|
231
|
+
};
|
|
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
|
+
function createTextResponse3(payload) {
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "text",
|
|
244
|
+
text: JSON.stringify(payload)
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function computeRevision(meta) {
|
|
250
|
+
const joinedHashes = Object.entries(meta.nodes).sort(([leftId], [rightId]) => leftId.localeCompare(rightId)).map(([, node]) => node.hash).join("");
|
|
251
|
+
return `sha256:${createHash("sha256").update(joinedHashes).digest("hex")}`;
|
|
252
|
+
}
|
|
253
|
+
function assertNodeData(data, message) {
|
|
254
|
+
if (data === void 0) {
|
|
255
|
+
throw new Error(message);
|
|
256
|
+
}
|
|
257
|
+
return agentsMetaNodeSchema2.parse(data);
|
|
258
|
+
}
|
|
259
|
+
function applyRegistryOperation(meta, op, nodeId, data) {
|
|
260
|
+
const nextNodes = { ...meta.nodes };
|
|
261
|
+
if (op === "remove-node") {
|
|
262
|
+
delete nextNodes[nodeId];
|
|
263
|
+
return {
|
|
264
|
+
...meta,
|
|
265
|
+
nodes: nextNodes
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (op === "add-node") {
|
|
269
|
+
nextNodes[nodeId] = assertNodeData(data, `fab_update_registry requires data for ${op}`);
|
|
270
|
+
return {
|
|
271
|
+
...meta,
|
|
272
|
+
nodes: nextNodes
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const currentNode = nextNodes[nodeId];
|
|
276
|
+
if (currentNode === void 0) {
|
|
277
|
+
throw new Error(`Cannot update missing Fabric registry node: ${nodeId}`);
|
|
278
|
+
}
|
|
279
|
+
nextNodes[nodeId] = agentsMetaNodeSchema2.parse({
|
|
280
|
+
...currentNode,
|
|
281
|
+
...data
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
...meta,
|
|
285
|
+
nodes: nextNodes
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function registerUpdateRegistry(server) {
|
|
289
|
+
server.tool(
|
|
290
|
+
"fab_update_registry",
|
|
291
|
+
"MANDATORY: Call to add, remove, or update Fabric registry nodes instead of editing registry files directly.",
|
|
292
|
+
inputSchema3,
|
|
293
|
+
async ({ op, node_id, data }) => {
|
|
294
|
+
const projectRoot = resolveProjectRoot();
|
|
295
|
+
const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
|
|
296
|
+
const currentMeta = readAgentsMeta(projectRoot);
|
|
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
|
+
});
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/index.ts
|
|
321
|
+
function writeStderr(message) {
|
|
322
|
+
process.stderr.write(`${message}
|
|
323
|
+
`);
|
|
324
|
+
}
|
|
325
|
+
function formatError(error) {
|
|
326
|
+
if (error instanceof Error) {
|
|
327
|
+
return error.stack ?? `${error.name}: ${error.message}`;
|
|
328
|
+
}
|
|
329
|
+
return `Unknown error: ${String(error)}`;
|
|
330
|
+
}
|
|
331
|
+
function createFabricServer() {
|
|
332
|
+
const server = new McpServer({
|
|
333
|
+
name: "fabric-context-server",
|
|
334
|
+
version: "0.0.0"
|
|
335
|
+
});
|
|
336
|
+
registerGetRules(server);
|
|
337
|
+
registerAppendIntent(server);
|
|
338
|
+
registerUpdateRegistry(server);
|
|
339
|
+
return server;
|
|
340
|
+
}
|
|
341
|
+
async function startStdioServer() {
|
|
342
|
+
const server = createFabricServer();
|
|
343
|
+
const transport = new StdioServerTransport();
|
|
344
|
+
await server.connect(transport);
|
|
345
|
+
}
|
|
346
|
+
var entrypoint = process.argv[1];
|
|
347
|
+
var currentFilePath = fileURLToPath(import.meta.url);
|
|
348
|
+
var isMainModule = entrypoint !== void 0 && resolve(entrypoint) === currentFilePath;
|
|
349
|
+
if (isMainModule) {
|
|
350
|
+
void startStdioServer().catch((error) => {
|
|
351
|
+
writeStderr(formatError(error));
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
export {
|
|
356
|
+
createFabricServer,
|
|
357
|
+
startStdioServer
|
|
358
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fenglimg/fabric-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
12
|
+
"dev": "tsup src/index.ts --format esm --dts --watch"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
16
|
+
"minimatch": "^10.0.1",
|
|
17
|
+
"zod": "^3.25.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.15.0",
|
|
21
|
+
"tsup": "^8.5.0",
|
|
22
|
+
"typescript": "^5.8.3"
|
|
23
|
+
}
|
|
24
|
+
}
|