@fenglimg/fabric-server 1.1.0 → 1.3.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.
@@ -1,13 +1,97 @@
1
+ // src/constants.ts
2
+ var AGENTS_MD_RESOURCE_URI = "fabric://agents-md";
3
+
4
+ // src/cache.ts
5
+ var ContextCache = class {
6
+ constructor(defaultTtlMs = 5e3) {
7
+ this.defaultTtlMs = defaultTtlMs;
8
+ }
9
+ defaultTtlMs;
10
+ // Slot 1: raw AgentsMeta keyed by projectRoot
11
+ metaSlot = /* @__PURE__ */ new Map();
12
+ // Slot 2: GetRulesContext keyed by projectRoot
13
+ contextSlot = /* @__PURE__ */ new Map();
14
+ // Slot 3: audit sliding-window cursor keyed by projectRoot
15
+ auditSlot = /* @__PURE__ */ new Map();
16
+ // ---------------------------------------------------------------------------
17
+ // Generic get / set / invalidate
18
+ // ---------------------------------------------------------------------------
19
+ get(slot, key) {
20
+ const store = this.slotStore(slot);
21
+ const entry = store.get(key);
22
+ if (entry === void 0) {
23
+ return void 0;
24
+ }
25
+ if (entry.expiresAt !== 0 && Date.now() > entry.expiresAt) {
26
+ store.delete(key);
27
+ return void 0;
28
+ }
29
+ return entry.value;
30
+ }
31
+ set(slot, key, value, ttlMs) {
32
+ const store = this.slotStore(slot);
33
+ const resolvedTtl = ttlMs ?? this.defaultTtlMs;
34
+ const expiresAt = resolvedTtl > 0 ? Date.now() + resolvedTtl : 0;
35
+ store.set(key, { value, expiresAt });
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // Audit cursor (separate API — not TTL-based)
39
+ // ---------------------------------------------------------------------------
40
+ getAuditCursor(projectRoot) {
41
+ return this.auditSlot.get(projectRoot);
42
+ }
43
+ setAuditCursor(projectRoot, cursor) {
44
+ this.auditSlot.set(projectRoot, cursor);
45
+ }
46
+ resetAuditCursor(projectRoot) {
47
+ this.auditSlot.delete(projectRoot);
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Invalidation
51
+ // ---------------------------------------------------------------------------
52
+ /**
53
+ * Invalidate cache slots based on what changed.
54
+ *
55
+ * @param reason "meta_write" — only the meta slot for this projectRoot
56
+ * "file_watch" — meta + context slots (AGENTS.md may have changed)
57
+ * @param projectRoot Optional; if omitted, clears ALL keys in affected slots.
58
+ */
59
+ invalidate(reason, projectRoot) {
60
+ if (reason === "meta_write") {
61
+ if (projectRoot !== void 0) {
62
+ this.metaSlot.delete(projectRoot);
63
+ } else {
64
+ this.metaSlot.clear();
65
+ }
66
+ return;
67
+ }
68
+ if (projectRoot !== void 0) {
69
+ this.metaSlot.delete(projectRoot);
70
+ this.contextSlot.delete(projectRoot);
71
+ } else {
72
+ this.metaSlot.clear();
73
+ this.contextSlot.clear();
74
+ }
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Helpers
78
+ // ---------------------------------------------------------------------------
79
+ slotStore(slot) {
80
+ return slot === "meta" ? this.metaSlot : this.contextSlot;
81
+ }
82
+ };
83
+ var contextCache = new ContextCache(5e3);
84
+
1
85
  // src/services/doctor.ts
2
86
  import { createHash as createHash2 } from "crypto";
3
- import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
87
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
4
88
  import { readFile as readFile4 } from "fs/promises";
5
89
  import { isAbsolute as isAbsolute2, join as join5, posix as posix2, resolve as resolve3 } from "path";
6
90
  import { forensicReportSchema } from "@fenglimg/fabric-shared";
7
91
  import { detectFramework } from "@fenglimg/fabric-shared/node";
8
92
 
9
93
  // src/meta-reader.ts
10
- import { readFileSync } from "fs";
94
+ import { readFile } from "fs/promises";
11
95
  import { join } from "path";
12
96
  import { agentsMetaSchema } from "@fenglimg/fabric-shared";
13
97
  import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
@@ -36,26 +120,33 @@ function getAgentsMetaPath(projectRoot) {
36
120
  function resolveProjectRoot() {
37
121
  return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
38
122
  }
39
- function readAgentsMeta(projectRoot) {
123
+ async function readAgentsMeta(projectRoot) {
124
+ const cached = contextCache.get("meta", projectRoot);
125
+ if (cached !== void 0) {
126
+ return cached;
127
+ }
40
128
  const metaPath = getAgentsMetaPath(projectRoot);
41
129
  let raw;
42
130
  try {
43
- raw = readFileSync(metaPath, "utf8");
131
+ raw = await readFile(metaPath, "utf8");
44
132
  } catch (error) {
45
133
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
46
134
  throw new AgentsMetaFileMissingError(metaPath);
47
135
  }
48
136
  throw error;
49
137
  }
138
+ let parsed;
50
139
  try {
51
- return agentsMetaSchema.parse(JSON.parse(raw));
140
+ parsed = agentsMetaSchema.parse(JSON.parse(raw));
52
141
  } catch (error) {
53
142
  throw new AgentsMetaInvalidError(metaPath, error);
54
143
  }
144
+ contextCache.set("meta", projectRoot, parsed);
145
+ return parsed;
55
146
  }
56
147
 
57
148
  // src/services/audit-log.ts
58
- import { appendFile, mkdir, readFile } from "fs/promises";
149
+ import { appendFile, mkdir, open, stat } from "fs/promises";
59
150
  import { isAbsolute, join as join2, posix, relative, resolve as resolve2 } from "path";
60
151
 
61
152
  // src/services/_shared.ts
@@ -103,7 +194,9 @@ async function appendGetRulesAuditEvent(projectRoot, input) {
103
194
  async function appendEditIntentAuditEvents(projectRoot, input) {
104
195
  const ts = input.ts ?? Date.now();
105
196
  const windowMs = input.window_ms ?? DEFAULT_AUDIT_WINDOW_MS;
106
- const getRulesEntries = (await readAuditLog(projectRoot)).filter(isGetRulesAuditEntry);
197
+ const getRulesEntries = (await readAuditLog(projectRoot, { windowMs, ts })).filter(
198
+ isGetRulesAuditEntry
199
+ );
107
200
  const entries = input.affected_paths.map((affectedPath) => {
108
201
  const path = normalizeAuditPath(projectRoot, affectedPath);
109
202
  const matchedGetRules = findPrecedingGetRulesEvent(getRulesEntries, path, ts, windowMs);
@@ -119,23 +212,93 @@ async function appendEditIntentAuditEvents(projectRoot, input) {
119
212
  window_ms: windowMs
120
213
  };
121
214
  });
215
+ const compliance = {
216
+ compliant: entries.length === 0 || entries.every((e) => e.compliant),
217
+ matched_get_rules_ts: entries.length > 0 && entries[0].matched_get_rules_ts !== null ? new Date(entries[0].matched_get_rules_ts).toISOString() : null,
218
+ window_ms: windowMs
219
+ };
122
220
  if (entries.length === 0) {
123
- return [];
221
+ return { entries, compliance };
124
222
  }
125
223
  await appendAuditLogEntries(projectRoot, entries);
126
- return entries;
224
+ return { entries, compliance };
127
225
  }
128
- async function readAuditLog(projectRoot) {
226
+ async function readAuditLog(projectRoot, opts) {
227
+ if (opts === void 0) {
228
+ return readAuditLogFull(projectRoot);
229
+ }
230
+ return readAuditLogWindowed(projectRoot, opts.ts, opts.windowMs);
231
+ }
232
+ async function readAuditLogFull(projectRoot) {
129
233
  const auditPath = join2(projectRoot, AUDIT_LOG_FILE);
130
234
  let raw;
131
235
  try {
132
- raw = await readFile(auditPath, "utf8");
236
+ const fileStat = await stat(auditPath);
237
+ const handle = await open(auditPath, "r");
238
+ try {
239
+ const buffer = Buffer.alloc(fileStat.size);
240
+ await handle.read(buffer, 0, fileStat.size, 0);
241
+ raw = buffer.toString("utf8");
242
+ } finally {
243
+ await handle.close();
244
+ }
245
+ } catch (error) {
246
+ if (isNodeError(error) && error.code === "ENOENT") {
247
+ return [];
248
+ }
249
+ throw error;
250
+ }
251
+ return parseAuditLogText(raw);
252
+ }
253
+ async function readAuditLogWindowed(projectRoot, ts, windowMs) {
254
+ const auditPath = join2(projectRoot, AUDIT_LOG_FILE);
255
+ let fileSize;
256
+ try {
257
+ const fileStat = await stat(auditPath);
258
+ fileSize = fileStat.size;
133
259
  } catch (error) {
134
260
  if (isNodeError(error) && error.code === "ENOENT") {
135
261
  return [];
136
262
  }
137
263
  throw error;
138
264
  }
265
+ const cursor = contextCache.getAuditCursor(projectRoot);
266
+ const startOffset = cursor !== void 0 && cursor.offset <= fileSize ? cursor.offset : 0;
267
+ const priorRemainder = startOffset > 0 && cursor !== void 0 ? cursor.remainder : "";
268
+ const effectiveStart = cursor !== void 0 && cursor.offset > fileSize ? 0 : startOffset;
269
+ let newEntries = [];
270
+ if (fileSize > effectiveStart) {
271
+ const length = fileSize - effectiveStart;
272
+ let chunk;
273
+ try {
274
+ const handle = await open(auditPath, "r");
275
+ try {
276
+ const buffer = Buffer.alloc(length);
277
+ await handle.read(buffer, 0, length, effectiveStart);
278
+ chunk = `${priorRemainder}${buffer.toString("utf8")}`;
279
+ } finally {
280
+ await handle.close();
281
+ }
282
+ } catch (error) {
283
+ contextCache.resetAuditCursor(projectRoot);
284
+ return readAuditLogFull(projectRoot);
285
+ }
286
+ const lines = chunk.split(/\r?\n/);
287
+ const remainder = chunk.endsWith("\n") ? "" : lines.pop() ?? "";
288
+ contextCache.setAuditCursor(projectRoot, { offset: fileSize, remainder });
289
+ newEntries = parseAuditLogText(lines.join("\n"));
290
+ } else {
291
+ contextCache.setAuditCursor(projectRoot, {
292
+ offset: fileSize,
293
+ remainder: cursor?.remainder ?? ""
294
+ });
295
+ }
296
+ if (effectiveStart === 0 && cursor !== void 0 && cursor.offset > fileSize) {
297
+ return newEntries.filter((e) => ts - e.ts <= windowMs);
298
+ }
299
+ return newEntries.filter((e) => ts - e.ts <= windowMs && e.ts <= ts);
300
+ }
301
+ function parseAuditLogText(raw) {
139
302
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map(parseAuditLogLine).filter((entry) => entry !== null);
140
303
  }
141
304
  function findPrecedingGetRulesEvent(entries, path, ts, windowMs) {
@@ -174,6 +337,7 @@ async function appendAuditLogEntries(projectRoot, entries) {
174
337
  await mkdir(auditDir, { recursive: true });
175
338
  await appendFile(auditPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}
176
339
  `, "utf8");
340
+ contextCache.resetAuditCursor(projectRoot);
177
341
  }
178
342
  function parseAuditLogLine(line) {
179
343
  try {
@@ -583,7 +747,7 @@ async function readSavedForensic(projectRoot) {
583
747
  }
584
748
  async function inspectMetaRevision(projectRoot) {
585
749
  try {
586
- const meta = readAgentsMeta(projectRoot);
750
+ const meta = await readAgentsMeta(projectRoot);
587
751
  const entries = Object.entries(meta.nodes).sort(([left], [right]) => left.localeCompare(right));
588
752
  const missingFiles = [];
589
753
  let driftCount = 0;
@@ -594,7 +758,7 @@ async function inspectMetaRevision(projectRoot) {
594
758
  driftCount += 1;
595
759
  return "missing";
596
760
  }
597
- const actualHash = sha2562(readFileSync2(absolutePath, "utf8"));
761
+ const actualHash = sha2562(readFileSync(absolutePath, "utf8"));
598
762
  if (actualHash !== node.hash) {
599
763
  driftCount += 1;
600
764
  }
@@ -694,7 +858,7 @@ function findLatestGetRulesTs(entries, path, ts) {
694
858
  function readDoctorAuditMode(projectRoot) {
695
859
  const configPath = join5(projectRoot, "fabric.config.json");
696
860
  try {
697
- const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
861
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
698
862
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
699
863
  return "off";
700
864
  }
@@ -830,6 +994,8 @@ function isMissingFileError(error) {
830
994
  }
831
995
 
832
996
  export {
997
+ AGENTS_MD_RESOURCE_URI,
998
+ contextCache,
833
999
  AgentsMetaFileMissingError,
834
1000
  AgentsMetaInvalidError,
835
1001
  resolveProjectRoot,
@@ -1,9 +1,11 @@
1
1
  import {
2
+ AGENTS_MD_RESOURCE_URI,
2
3
  AgentsMetaFileMissingError,
3
4
  AgentsMetaInvalidError,
4
5
  appendLedgerEntry,
5
6
  assertPathWithinProjectRoot,
6
7
  atomicWriteText,
8
+ contextCache,
7
9
  hashHumanLockedContent,
8
10
  readAgentsMeta,
9
11
  readHumanLock,
@@ -11,16 +13,17 @@ import {
11
13
  readHumanLockEntry,
12
14
  readLedger,
13
15
  runDoctorReport
14
- } from "./chunk-U3IQH5H6.js";
16
+ } from "./chunk-DQ7RCYKB.js";
15
17
 
16
18
  // src/http.ts
17
- import { randomUUID as randomUUID2 } from "crypto";
19
+ import { randomUUID } from "crypto";
18
20
  import { appendFile, readFile as readFile2 } from "fs/promises";
19
21
  import { join as join3 } from "path";
20
22
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
21
23
  import {
22
24
  StreamableHTTPServerTransport
23
25
  } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
26
+ import chokidar2 from "chokidar";
24
27
 
25
28
  // src/api/_error.ts
26
29
  function sendError(res, status, code, message, details) {
@@ -111,7 +114,7 @@ function registerDoctorApi(app, projectRoot) {
111
114
  }
112
115
 
113
116
  // src/api/events.ts
114
- import { createHash, randomUUID } from "crypto";
117
+ import { createHash } from "crypto";
115
118
  import { open, readFile, stat } from "fs/promises";
116
119
  import { join } from "path";
117
120
  import {
@@ -130,6 +133,36 @@ var WATCHED_PATHS = [AGENTS_META_PATH, HUMAN_LOCK_PATH, FORENSIC_PATH, LEDGER_PA
130
133
  var CONNECTION_LIMIT = 10;
131
134
  var HEARTBEAT_INTERVAL_MS = 3e4;
132
135
  var WATCH_DEBOUNCE_MS = 75;
136
+ var RING_BUFFER_CAPACITY = 50;
137
+ var RingBuffer = class {
138
+ constructor(capacity) {
139
+ this.capacity = capacity;
140
+ this.buf = new Array(capacity).fill(void 0);
141
+ }
142
+ capacity;
143
+ buf;
144
+ head = 0;
145
+ count = 0;
146
+ push(event) {
147
+ this.buf[this.head] = event;
148
+ this.head = (this.head + 1) % this.capacity;
149
+ if (this.count < this.capacity) {
150
+ this.count++;
151
+ }
152
+ }
153
+ replayFrom(afterId) {
154
+ const result = [];
155
+ const total = this.count;
156
+ const start = this.count < this.capacity ? 0 : this.head;
157
+ for (let i = 0; i < total; i++) {
158
+ const entry = this.buf[(start + i) % this.capacity];
159
+ if (entry !== void 0 && entry.id > afterId) {
160
+ result.push(entry);
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+ };
133
166
  function createEventsHandler(options) {
134
167
  const { projectRoot } = options;
135
168
  const state = {
@@ -137,7 +170,9 @@ function createEventsHandler(options) {
137
170
  pendingTimers: /* @__PURE__ */ new Map(),
138
171
  ledgerOffset: 0,
139
172
  ledgerRemainder: "",
140
- humanLockSnapshot: createEmptyHumanLockSnapshot()
173
+ humanLockSnapshot: createEmptyHumanLockSnapshot(),
174
+ nextEventId: 1,
175
+ ringBuffer: new RingBuffer(RING_BUFFER_CAPACITY)
141
176
  };
142
177
  return async function handleEvents(req, res) {
143
178
  if (state.clients.size >= CONNECTION_LIMIT) {
@@ -161,6 +196,19 @@ function createEventsHandler(options) {
161
196
  res.setHeader("X-Accel-Buffering", "no");
162
197
  res.flushHeaders?.();
163
198
  res.write(": connected\n\n");
199
+ const lastEventId = readLastEventId(req);
200
+ if (lastEventId !== void 0) {
201
+ const missed = state.ringBuffer.replayFrom(lastEventId);
202
+ for (const entry of missed) {
203
+ if (!res.writableEnded) {
204
+ res.write(`id: ${entry.id}
205
+ event: ${entry.type}
206
+ data: ${entry.data}
207
+
208
+ `);
209
+ }
210
+ }
211
+ }
164
212
  state.clients.add(res);
165
213
  const heartbeat = setInterval(() => {
166
214
  if (!res.writableEnded) {
@@ -368,11 +416,14 @@ function parseLedgerAppendedEvent(line) {
368
416
  }
369
417
  function broadcastEvent(state, event) {
370
418
  const payload = fabricEventSchema.parse(event);
371
- const frame = `id: ${randomUUID()}
419
+ const eventId = state.nextEventId++;
420
+ const data = JSON.stringify(payload);
421
+ const frame = `id: ${eventId}
372
422
  event: ${payload.type}
373
- data: ${JSON.stringify(payload)}
423
+ data: ${data}
374
424
 
375
425
  `;
426
+ state.ringBuffer.push({ id: eventId, type: payload.type, data });
376
427
  const disconnectedClients = [];
377
428
  for (const client of state.clients) {
378
429
  try {
@@ -454,6 +505,21 @@ function areSetsEqual(left, right) {
454
505
  }
455
506
  return true;
456
507
  }
508
+ function readLastEventId(req) {
509
+ const header = req.headers["last-event-id"];
510
+ const headerValue = Array.isArray(header) ? header[0] : header;
511
+ const rawUrl = req.url ?? "";
512
+ const queryStart = rawUrl.indexOf("?");
513
+ const queryString = queryStart >= 0 ? rawUrl.slice(queryStart + 1) : "";
514
+ const params = new URLSearchParams(queryString);
515
+ const queryValue = params.get("lastEventId") ?? void 0;
516
+ const raw = headerValue ?? queryValue;
517
+ if (raw === void 0 || raw.length === 0) {
518
+ return void 0;
519
+ }
520
+ const parsed = Number.parseInt(raw, 10);
521
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
522
+ }
457
523
  function normalizePath(value) {
458
524
  return value.replaceAll("\\", "/");
459
525
  }
@@ -730,7 +796,7 @@ function createApproveLedgerEntry(input) {
730
796
  function registerHumanLockApi(app, projectRoot) {
731
797
  app.get("/api/human-lock", async (_req, res) => {
732
798
  try {
733
- readAgentsMeta(projectRoot);
799
+ await readAgentsMeta(projectRoot);
734
800
  res.json(await readHumanLock(projectRoot));
735
801
  } catch (error) {
736
802
  sendUnknownError(res, error);
@@ -746,7 +812,7 @@ function registerHumanLockApi(app, projectRoot) {
746
812
  return;
747
813
  }
748
814
  try {
749
- readAgentsMeta(projectRoot);
815
+ await readAgentsMeta(projectRoot);
750
816
  const entry = await readHumanLockEntry(projectRoot, validation.data.file);
751
817
  if (entry === null) {
752
818
  sendError(
@@ -769,7 +835,7 @@ function registerHumanLockApi(app, projectRoot) {
769
835
  return;
770
836
  }
771
837
  try {
772
- readAgentsMeta(projectRoot);
838
+ await readAgentsMeta(projectRoot);
773
839
  res.json(await approveHumanLock(projectRoot, validation.data));
774
840
  } catch (error) {
775
841
  sendUnknownError(res, error);
@@ -822,7 +888,7 @@ function registerIntentApi(app, projectRoot) {
822
888
  return;
823
889
  }
824
890
  try {
825
- readAgentsMeta(projectRoot);
891
+ await readAgentsMeta(projectRoot);
826
892
  const result = await annotateIntent(projectRoot, validation.data);
827
893
  res.status(result.created ? 201 : 200).json(result);
828
894
  } catch (error) {
@@ -844,7 +910,7 @@ function registerLedgerApi(app, projectRoot) {
844
910
  return;
845
911
  }
846
912
  try {
847
- readAgentsMeta(projectRoot);
913
+ await readAgentsMeta(projectRoot);
848
914
  res.json(await readLedger(projectRoot, validation.data));
849
915
  } catch (error) {
850
916
  sendUnknownError(res, error);
@@ -856,7 +922,7 @@ function registerLedgerApi(app, projectRoot) {
856
922
  function registerRulesApi(app, projectRoot) {
857
923
  app.get("/api/rules", async (_req, res) => {
858
924
  try {
859
- res.json(readAgentsMeta(projectRoot));
925
+ res.json(await readAgentsMeta(projectRoot));
860
926
  } catch (error) {
861
927
  sendUnknownError(res, error);
862
928
  }
@@ -1062,13 +1128,14 @@ function hashToken(token) {
1062
1128
  // src/http.ts
1063
1129
  var DEFAULT_HOST = "127.0.0.1";
1064
1130
  var LEDGER_FILE = ".intent-ledger.jsonl";
1131
+ var NOTIFY_DEBOUNCE_MS = 200;
1065
1132
  var JsonlEventStore = class {
1066
1133
  constructor(ledgerPath) {
1067
1134
  this.ledgerPath = ledgerPath;
1068
1135
  }
1069
1136
  ledgerPath;
1070
1137
  async storeEvent(streamId, message) {
1071
- const eventId = randomUUID2();
1138
+ const eventId = randomUUID();
1072
1139
  const entry = {
1073
1140
  kind: "mcp-event",
1074
1141
  eventId,
@@ -1121,11 +1188,41 @@ function createFabricHttpApp(options) {
1121
1188
  const eventStore = new JsonlEventStore(ledgerPath);
1122
1189
  const sessions = /* @__PURE__ */ new Map();
1123
1190
  process.env.FABRIC_PROJECT_ROOT = projectRoot;
1191
+ const cacheWatcher = chokidar2.watch(
1192
+ [".fabric/agents.meta.json", "AGENTS.md"],
1193
+ {
1194
+ cwd: projectRoot,
1195
+ ignoreInitial: true,
1196
+ awaitWriteFinish: {
1197
+ stabilityThreshold: 120,
1198
+ pollInterval: 20
1199
+ }
1200
+ }
1201
+ );
1202
+ let agentsMdNotifyTimer;
1203
+ let toolListNotifyTimer;
1204
+ cacheWatcher.on("change", (relativePath) => {
1205
+ const normalized = relativePath.replaceAll("\\", "/");
1206
+ if (normalized === ".fabric/agents.meta.json") {
1207
+ contextCache.invalidate("file_watch", projectRoot);
1208
+ clearTimeout(toolListNotifyTimer);
1209
+ toolListNotifyTimer = setTimeout(() => {
1210
+ notifyAllSessions(sessions, "tools/list_changed");
1211
+ }, NOTIFY_DEBOUNCE_MS);
1212
+ } else if (normalized === "AGENTS.md") {
1213
+ contextCache.invalidate("file_watch", projectRoot);
1214
+ clearTimeout(agentsMdNotifyTimer);
1215
+ agentsMdNotifyTimer = setTimeout(() => {
1216
+ notifyAllSessions(sessions, "resource_updated", AGENTS_MD_RESOURCE_URI);
1217
+ }, NOTIFY_DEBOUNCE_MS);
1218
+ }
1219
+ });
1124
1220
  app.disable("x-powered-by");
1125
1221
  if (authToken !== void 0) {
1126
1222
  const bearerAuth = createBearerAuthMiddleware(authToken);
1127
1223
  app.use("/api", bearerAuth);
1128
1224
  app.use("/events", bearerAuth);
1225
+ app.use("/mcp", bearerAuth);
1129
1226
  }
1130
1227
  registerRulesApi(app, projectRoot);
1131
1228
  registerLedgerApi(app, projectRoot);
@@ -1156,11 +1253,25 @@ function createFabricHttpApp(options) {
1156
1253
  registerDashboardStatic(app, { dashboardDistPath, dev });
1157
1254
  return app;
1158
1255
  }
1256
+ function notifyAllSessions(sessions, kind, uri) {
1257
+ for (const { server } of sessions.values()) {
1258
+ try {
1259
+ if (kind === "tools/list_changed") {
1260
+ server.sendToolListChanged();
1261
+ } else if (kind === "resources/list_changed") {
1262
+ server.sendResourceListChanged();
1263
+ } else if (kind === "resource_updated" && uri !== void 0) {
1264
+ void server.server.sendResourceUpdated({ uri });
1265
+ }
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ }
1159
1270
  async function createSession(eventStore, sessions) {
1160
1271
  const { createFabricServer } = await import("./index.js");
1161
1272
  const server = createFabricServer();
1162
1273
  const transport = new StreamableHTTPServerTransport({
1163
- sessionIdGenerator: randomUUID2,
1274
+ sessionIdGenerator: randomUUID,
1164
1275
  enableJsonResponse: true,
1165
1276
  eventStore,
1166
1277
  onsessioninitialized: async (sessionId) => {
@@ -1227,5 +1338,6 @@ function isNodeError2(error) {
1227
1338
  return error instanceof Error;
1228
1339
  }
1229
1340
  export {
1230
- createFabricHttpApp
1341
+ createFabricHttpApp,
1342
+ notifyAllSessions
1231
1343
  };
package/dist/index.d.ts CHANGED
@@ -61,6 +61,12 @@ declare function runDoctorAuditReport(target: string, options?: {
61
61
  windowMs?: number;
62
62
  }): Promise<DoctorAuditReport>;
63
63
 
64
+ /**
65
+ * Shared constants used across the server package.
66
+ */
67
+ /** MCP resource URI for the project's AGENTS.md (L0 rules) file. */
68
+ declare const AGENTS_MD_RESOURCE_URI = "fabric://agents-md";
69
+
64
70
  declare function createFabricServer(): McpServer;
65
71
  declare function startStdioServer(): Promise<void>;
66
72
  declare function startHttpServer(options: {
@@ -72,4 +78,4 @@ declare function startHttpServer(options: {
72
78
  dev?: boolean;
73
79
  }): Promise<Server>;
74
80
 
75
- export { type DoctorAuditReport, type DoctorReport, createFabricServer, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };
81
+ export { AGENTS_MD_RESOURCE_URI, type DoctorAuditReport, type DoctorReport, createFabricServer, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };