septum 0.1.7 → 0.1.8

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.
Files changed (59) hide show
  1. package/README.md +36 -55
  2. package/package.json +3 -2
  3. package/src/cli/commands/check.ts +2 -0
  4. package/src/cli/commands/hook.ts +59 -12
  5. package/src/cli/commands/init.ts +440 -121
  6. package/src/cli/commands/sync.ts +27 -17
  7. package/src/cli/commands/topology.ts +89 -47
  8. package/src/cli/index.ts +5 -67
  9. package/src/cli/scaffold/harness-config.ts +697 -0
  10. package/src/cli/templates/hooks.ts +6 -6
  11. package/src/cli/templates/rules.ts +8 -8
  12. package/src/cli/templates/skill.ts +11 -18
  13. package/src/cli/ui/prompts.ts +122 -0
  14. package/src/core/boundary/staged-auditor.ts +5 -2
  15. package/src/core/database/client.ts +8 -0
  16. package/src/core/database/repositories/dependency.repository.ts +77 -13
  17. package/src/core/database/repositories/domain.repository.ts +86 -33
  18. package/src/core/database/repositories/symbol.repository.ts +128 -117
  19. package/src/core/database/repository.ts +17 -5
  20. package/src/core/database/schema.ts +1 -1
  21. package/src/core/discovery/topology-detector.ts +71 -19
  22. package/src/core/ingestion/pipeline.ts +228 -94
  23. package/src/core/parser/extractors/laravel-semantic.ts +9 -8
  24. package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
  25. package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
  26. package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
  27. package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
  28. package/src/core/resolver/call-graph-tracer.ts +4 -3
  29. package/src/core/resolver/path-resolver.ts +57 -4
  30. package/src/core/resources/ignore.ts +35 -0
  31. package/src/core/resources/limits.ts +14 -0
  32. package/src/core/resources/source-files.ts +62 -0
  33. package/src/core/resources/work-queue.ts +74 -0
  34. package/src/core/session/session-manager.ts +19 -6
  35. package/src/core/telemetry/telemetry.ts +12 -1
  36. package/src/core/topology/environment-detector.ts +21 -11
  37. package/src/mcp/schemas.ts +381 -8
  38. package/src/mcp/server.ts +321 -535
  39. package/src/mcp/tools/boundary.ts +17 -0
  40. package/src/mcp/tools/check-boundary.ts +83 -11
  41. package/src/mcp/tools/domain.ts +40 -0
  42. package/src/mcp/tools/feature.ts +67 -0
  43. package/src/mcp/tools/get-domain-catalog.ts +9 -6
  44. package/src/mcp/tools/get-environment-topology.ts +2 -2
  45. package/src/mcp/tools/get-feature-context.ts +9 -12
  46. package/src/mcp/tools/get-symbol-impact.ts +5 -2
  47. package/src/mcp/tools/get-symbol.ts +2 -1
  48. package/src/mcp/tools/locate-symbol.ts +3 -3
  49. package/src/mcp/tools/register-domain.ts +25 -9
  50. package/src/mcp/tools/register-feature.ts +34 -17
  51. package/src/mcp/tools/slice.ts +15 -0
  52. package/src/mcp/tools/symbol.ts +82 -0
  53. package/src/mcp/workspace-manager.ts +159 -0
  54. package/src/mcp/workspace-watcher.ts +202 -0
  55. package/src/types/index.ts +1 -0
  56. package/src/cli/commands/ingest.ts +0 -29
  57. package/src/cli/commands/locate.ts +0 -61
  58. package/src/cli/commands/query.ts +0 -104
  59. package/src/cli/commands/slice.ts +0 -50
package/src/mcp/server.ts CHANGED
@@ -1,63 +1,36 @@
1
- import { existsSync, statSync, watch, type FSWatcher } from "node:fs";
2
- import { relative, resolve } from "node:path";
1
+ import { WorkspaceManager, resolveServerWorkspace, isWorkspaceInitialized, type WorkspaceLease } from "./workspace-manager.ts";
2
+ import { RESOURCE_LIMITS } from "../core/resources/limits.ts";
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import {
6
6
  CallToolRequestSchema,
7
7
  ListToolsRequestSchema,
8
8
  } from "@modelcontextprotocol/sdk/types.js";
9
- import { BoundaryEvaluator } from "../core/boundary/evaluator.ts";
10
- import { ModuleResolver } from "../core/resolver/module-resolver.ts";
11
9
  import { ConfigLoader } from "../core/config/loader.ts";
12
- import { SeptumDatabase } from "../core/database/client.ts";
13
- import { SeptumRepository } from "../core/database/repository.ts";
14
- import { IngestionPipeline } from "../core/ingestion/pipeline.ts";
15
- import { ASTParserEngine } from "../core/parser/tree-sitter.ts";
16
10
  import { SessionManager } from "../core/session/session-manager.ts";
17
11
  import { SeptumTelemetry } from "../core/telemetry/telemetry.ts";
18
- import { handleCheckBoundary } from "./tools/check-boundary.ts";
19
- import { handleGetDomainCatalog } from "./tools/get-domain-catalog.ts";
20
- import {
21
- handleClearFeatureContext,
22
- handleGetFeatureContext,
23
- } from "./tools/get-feature-context.ts";
24
- import { handleLocateSymbol } from "./tools/locate-symbol.ts";
25
- import { handleTraceVerticalSlice } from "./tools/trace-vertical-slice.ts";
26
- import { handleGetSymbolHotspots } from "./tools/get-symbol-hotspots.ts";
27
- import { handleGetSymbol } from "./tools/get-symbol.ts";
28
- import { handleGetSymbolImpact } from "./tools/get-symbol-impact.ts";
29
- import { handleRegisterDomain } from "./tools/register-domain.ts";
30
- import { handleRegisterFeature } from "./tools/register-feature.ts";
31
- import { handleGetEnvironmentTopology } from "./tools/get-environment-topology.ts";
12
+ import { handleDomainTool } from "./tools/domain.ts";
13
+ import { handleSymbolTool } from "./tools/symbol.ts";
14
+ import { handleSliceTool } from "./tools/slice.ts";
15
+ import { handleBoundaryTool } from "./tools/boundary.ts";
16
+ import { handleFeatureTool } from "./tools/feature.ts";
17
+
32
18
  import {
33
- CheckBoundarySchema,
34
- GetDomainCatalogSchema,
35
- GetEnvironmentTopologySchema,
36
- GetFeatureContextSchema,
37
- GetSymbolHotspotsSchema,
38
- GetSymbolImpactSchema,
39
- GetSymbolSchema,
40
- LocateSymbolSchema,
41
- RegisterDomainSchema,
42
- RegisterFeatureSchema,
43
- TraceVerticalSliceSchema,
19
+ DomainToolSchema,
20
+ SymbolToolSchema,
21
+ SliceToolSchema,
22
+ BoundaryToolSchema,
23
+ FeatureToolSchema,
44
24
  } from "./schemas.ts";
45
25
  import { SEPTUM_VERSION } from "../version.ts";
46
- import { resolveWorkspaceRoot } from "../core/resolver/path-resolver.ts";
47
- import type { ValidatedSeptumConfig } from "../core/config/schema.ts";
48
-
49
- interface WorkspaceContext {
50
- root: string;
51
- config: ValidatedSeptumConfig;
52
- db: SeptumDatabase;
53
- repo: SeptumRepository;
54
- evaluator: BoundaryEvaluator;
55
- pipeline: IngestionPipeline;
56
- ready: Promise<void>;
57
- watcher: { flush: () => Promise<void>; close: () => void };
58
- }
59
26
 
60
- const workspaceContextCache = new Map<string, WorkspaceContext>();
27
+ const TOOL_SCHEMAS = {
28
+ septum_domain: DomainToolSchema,
29
+ septum_symbol: SymbolToolSchema,
30
+ septum_slice: SliceToolSchema,
31
+ septum_boundary: BoundaryToolSchema,
32
+ septum_feature: FeatureToolSchema,
33
+ };
61
34
 
62
35
  export function extractPathHint(args: unknown): string | undefined {
63
36
  if (!args) return undefined;
@@ -130,112 +103,16 @@ export function extractPathHint(args: unknown): string | undefined {
130
103
  return undefined;
131
104
  }
132
105
 
133
- function createWorkspaceWatcher(root: string, repo: SeptumRepository, pipeline: IngestionPipeline, ready: Promise<void>) {
134
- const pending = new Set<string>();
135
- let timer: ReturnType<typeof setTimeout> | null = null;
136
- let flushing: Promise<void> | null = null;
137
- let closed = false;
138
- let needsFullSync = false;
139
- const ignored = new Set(["node_modules", "vendor", ".git", ".septum", ".strata", "dist", "build", ".output", ".nuxt", ".next", ".cache", "coverage", ".turbo"]);
140
- let watcher: FSWatcher | null = null;
141
- let polling: ReturnType<typeof setInterval> | null = null;
142
-
143
- const flush = async (): Promise<void> => {
144
- if (timer) { clearTimeout(timer); timer = null; }
145
- if (flushing) return flushing;
146
- flushing = (async () => {
147
- await ready;
148
- if (!closed && (!watcher || needsFullSync || repo.getMeta("sync_error"))) {
149
- needsFullSync = false;
150
- await pipeline.run(ConfigLoader.load(undefined, root), { verifyContent: true });
151
- repo.setMeta("sync_error", "");
152
- }
153
- while (!closed && pending.size > 0) {
154
- const paths = [...pending];
155
- pending.clear();
156
- const config = ConfigLoader.load(undefined, root);
157
- for (const filename of paths) {
158
- const target = resolve(root, filename);
159
- const relPath = relative(root, target).replace(/\\/g, "/");
160
- if (!existsSync(target)) {
161
- repo.runInTransaction(() => { repo.deleteFileByPath(relPath); repo.clearVerticalSlices();
162
- const unsupported = JSON.parse(repo.getMeta("unsupported_files") || "[]") as string[];
163
- repo.setMeta("unsupported_files", JSON.stringify(unsupported.filter(path => path !== relPath))); });
164
- } else {
165
- await pipeline.ingestFile(target, config);
166
- }
167
- }
168
- }
169
- })();
170
- try { await flushing; } finally { flushing = null; }
171
- };
172
-
173
- const usePolling = (error: unknown) => {
174
- watcher?.close();
175
- watcher = null;
176
- repo.setMeta("watcher_mode", "polling");
177
- console.error(`[Septum Watcher Warning] Using polling: ${String(error)}`);
178
- if (!polling) polling = setInterval(() => {
179
- void flush().catch(failure => repo.setMeta("sync_error", String(failure)));
180
- }, 1000);
181
- };
182
-
183
- try {
184
- watcher = watch(root, { recursive: true }, (_event, filename) => {
185
- if (!filename || closed) return;
186
- const norm = filename.replace(/\\/g, "/");
187
- if (norm.split("/").some(segment => ignored.has(segment))) return;
188
- const target = resolve(root, norm);
189
- let directory = false;
190
- try { directory = statSync(target).isDirectory(); } catch { /* The path may have been removed before the event arrives. */ }
191
- if (directory || (!ASTParserEngine.isSourceFile(norm) && (repo.getAllFiles().some(file => file.path.startsWith(norm + "/")) || (JSON.parse(repo.getMeta("unsupported_files") || "[]") as string[]).some(file => file.startsWith(norm + "/"))))) {
192
- needsFullSync = true;
193
- } else if (ASTParserEngine.isSourceFile(norm)) {
194
- pending.add(norm);
195
- } else {
196
- return;
197
- }
198
- if (timer) clearTimeout(timer);
199
- timer = setTimeout(() => {
200
- void flush().catch(error => {
201
- repo.setMeta("sync_error", String(error));
202
- console.error(`[Septum Watcher Warning] ${String(error)}`);
203
- });
204
- }, 300);
205
- });
206
- repo.setMeta("watcher_mode", "native");
207
- watcher.on("error", usePolling);
208
- } catch (error) {
209
- usePolling(error);
106
+ function countLines(text: string): number {
107
+ let count = 1;
108
+ for (let i = 0; i < text.length; i++) {
109
+ if (text.charCodeAt(i) === 10) count++;
210
110
  }
211
- return { flush, close: () => { closed = true; if (timer) clearTimeout(timer); if (polling) clearInterval(polling); watcher?.close(); } };
212
- }
213
-
214
- function getWorkspaceContext(pathHint?: string): WorkspaceContext {
215
- const root = resolveWorkspaceRoot(pathHint);
216
- let ctx = workspaceContextCache.get(root);
217
- if (ctx) {
218
- return ctx;
219
- }
220
-
221
- const config = ConfigLoader.load(undefined, root);
222
- const rawDbPath = config.settings.db_path || ".septum/septum.db";
223
- const resolvedDbPath = resolve(root, rawDbPath);
224
- const db = new SeptumDatabase(resolvedDbPath);
225
- const repo = new SeptumRepository(db.raw);
226
- const evaluator = new BoundaryEvaluator(repo, new ModuleResolver(root, repo));
227
- const pipeline = new IngestionPipeline(repo, root);
228
-
229
- const ready = pipeline.run(config, { verifyContent: true }).then(() => {});
230
- const watcher = createWorkspaceWatcher(root, repo, pipeline, ready);
231
- ctx = { root, config, db, repo, evaluator, pipeline, ready, watcher };
232
- workspaceContextCache.set(root, ctx);
233
- return ctx;
111
+ return count;
234
112
  }
235
113
 
236
114
  export async function runMCPServer(): Promise<void> {
237
- const initialCtx = getWorkspaceContext();
238
- await initialCtx.ready;
115
+ const manager = new WorkspaceManager();
239
116
 
240
117
  const server = new Server(
241
118
  {
@@ -253,425 +130,377 @@ export async function runMCPServer(): Promise<void> {
253
130
  return {
254
131
  tools: [
255
132
  {
256
- name: "septum_get_domain_catalog",
133
+ name: "septum_domain",
257
134
  description:
258
- "Returns the authoritative structural catalog of a domain (files, archetypes, public signatures, and boundary rules). If 'domain' is omitted, returns the complete Macro Semantic Map (Telescope View) of the whole project. Token-efficient (< 400 tokens). Use this INSTEAD of browsing raw files.",
135
+ "Domain Catalog, Registration & Environment Topology Engine. Manages macro architecture, domain topology, bounded-context registration, and infrastructure plane (Edge, Gateway, Host, Runtime, Storage, Telemetry). Token-efficient (< 400 tokens).",
259
136
  inputSchema: {
260
137
  type: "object",
261
138
  properties: {
262
- domain: {
139
+ action: {
263
140
  type: "string",
264
- description: "Optional domain to inspect (e.g. 'orders', 'core'). If omitted, returns the project-wide Macro Semantic Map.",
141
+ enum: ["catalog", "register", "topology"],
142
+ default: "catalog",
143
+ description: "Sub-operation to execute: 'catalog' (inspect domain/macro map), 'register' (dynamically register bounded-context domain), or 'topology' (6-layer infrastructure plane).",
265
144
  },
266
- archetype_filter: {
145
+ domain: {
267
146
  type: "string",
268
- description: "Optional filter: 'service' | 'model' | 'controller' | 'repository'",
147
+ description: "Target domain name (e.g. 'orders', 'core'). If omitted in catalog action, returns project-wide Macro Semantic Map.",
269
148
  },
270
149
  detail_level: {
271
150
  type: "string",
272
151
  enum: ["compact", "full"],
273
152
  default: "compact",
274
- description: "Catalog detail level: 'compact' (default, token-efficient summary with file connections) or 'full' (includes full method/property signatures).",
153
+ description: "Catalog detail level: 'compact' (default, token-efficient summary with file connections) or 'full' (includes full signatures).",
275
154
  },
276
- file_filter: {
155
+ archetype_filter: {
277
156
  type: "string",
278
- description: "Optional substring filter to inspect specific file(s) within the domain.",
157
+ description: "Optional archetype filter (e.g. 'service', 'model', 'controller', 'repository').",
279
158
  },
280
- },
281
- required: [],
282
- },
283
- },
284
- {
285
- name: "septum_get_feature_context",
286
- description:
287
- "Returns authoritative task/feature context (< 300 tokens) including allowed_touchpoints, reusable symbols with signatures, and contracts. Use this when implementing a specific feature.",
288
- inputSchema: {
289
- type: "object",
290
- properties: {
291
- feature_key: {
159
+ file_filter: {
292
160
  type: "string",
293
- description:
294
- "Unique identifier for the feature to inspect (e.g. 'checkout_flow'). Symmetrically accepts 'feature'.",
161
+ description: "Optional substring filter to inspect specific file(s) within the domain.",
295
162
  },
296
- feature: {
163
+ root: {
297
164
  type: "string",
298
- description: "Alias for 'feature_key'.",
165
+ description: "Root directory path for domain registration (action='register').",
299
166
  },
300
- },
301
- required: [],
302
- },
303
- },
304
- {
305
- name: "septum_check_boundary",
306
- description:
307
- "Pre-flight boundary check to verify proposed file changes and imports against domain rules and feature touchpoints, or audit the entire repository / staged files / domain for cross-domain and circular dependencies.",
308
- inputSchema: {
309
- type: "object",
310
- properties: {
311
- file_path: {
167
+ description: {
312
168
  type: "string",
313
- description: "Path of the file being edited or created (single file check)",
169
+ description: "Architectural purpose of the domain (action='register').",
314
170
  },
315
- file_paths: {
171
+ allowed_dependencies: {
316
172
  type: "array",
317
173
  items: { type: "string" },
318
- description: "List of file paths being edited or created in a batch check",
174
+ description: "List of domain names this domain is allowed to import (action='register').",
319
175
  },
320
- proposed_imports: {
176
+ forbidden_dependencies: {
321
177
  type: "array",
322
178
  items: { type: "string" },
323
- description: "List of import statements or target symbols proposed for the file",
179
+ description: "List of domain names strictly forbidden from being imported (action='register').",
324
180
  },
325
- feature_key: {
326
- type: "string",
327
- description: "Optional feature key to validate touchpoints against.",
181
+ archetypes: {
182
+ type: "object",
183
+ description: "Layer archetypes mapping (action='register').",
328
184
  },
329
- scope: {
330
- type: "string",
331
- enum: ["file", "all", "staged", "domain"],
332
- description: "Audit scope when file_path is omitted: 'all' (entire codebase), 'staged' (git staged files), 'domain' (specific domain), or 'file'.",
185
+ ingest_now: {
186
+ type: "boolean",
187
+ default: false,
188
+ description: "Whether to immediately run AST ingestion on domain files upon registration.",
333
189
  },
334
- domain: {
190
+ resolve: {
191
+ type: "object",
192
+ properties: {
193
+ layer: {
194
+ type: "string",
195
+ enum: ["edge", "gateway", "host", "runtime", "storage", "telemetry"],
196
+ },
197
+ platform: { type: "string" },
198
+ constraints: { type: "array", items: { type: "string" } },
199
+ },
200
+ description: "Record and verify infrastructure layer in SQLite SSOT (action='topology').",
201
+ },
202
+ resolve_node: {
203
+ type: "object",
204
+ properties: {
205
+ id: { type: "string" },
206
+ layer: { type: "string", enum: ["edge", "gateway", "host", "runtime", "storage", "telemetry"] },
207
+ name: { type: "string" },
208
+ platform: { type: "string" },
209
+ domain_or_ip: { type: "string" },
210
+ role: { type: "string" },
211
+ connected_to: { type: "array", items: { type: "string" } },
212
+ constraints: { type: "array", items: { type: "string" } },
213
+ },
214
+ description: "Record distributed topology node in SQLite SSOT (action='topology').",
215
+ },
216
+ refresh: {
217
+ type: "boolean",
218
+ description: "Force re-detection from codebase files, ignoring cached status (action='topology').",
219
+ },
220
+ workspace_path: {
335
221
  type: "string",
336
- description: "Optional domain name to filter boundary audit when scope is 'domain' or 'all'.",
222
+ description: "Explicit workspace root path. Specifies the target project root in multi-root environments.",
337
223
  },
338
224
  },
225
+ required: [],
339
226
  },
340
227
  },
341
228
  {
342
- name: "septum_get_symbol_hotspots",
229
+ name: "septum_symbol",
343
230
  description:
344
- "Discovers oversized functions, methods, and classes exceeding a specified line count threshold or cyclomatic control flow nesting depth (> 4). Essential for Single Responsibility Principle (SRP) enforcement, finding God Functions / bug magnets, and isolating refactoring targets deterministically without reading raw files.",
231
+ "Unified Symbol Engine: single entrypoint for symbol location, full contract inspection, refactoring blast radius (impact), and Single Responsibility / God Function complexity hotspots.",
345
232
  inputSchema: {
346
233
  type: "object",
347
234
  properties: {
348
- min_lines: {
349
- type: "number",
350
- description: "Minimum physical lines of code (LoC) threshold (default: 30).",
351
- },
352
- min_nesting: {
353
- type: "number",
354
- description: "Minimum control-flow nesting depth (e.g. nested if/for/while/try > 4).",
235
+ action: {
236
+ type: "string",
237
+ enum: ["locate", "inspect", "impact", "hotspots"],
238
+ default: "locate",
239
+ description: "Sub-operation: 'locate' (fuzzy search/error log resolution), 'inspect' (physical coordinates & signatures), 'impact' (blast radius & inbound callers), or 'hotspots' (complexity & SRP smells).",
355
240
  },
356
- kind: {
241
+ query: {
357
242
  type: "string",
358
- description: "Filter by symbol kind ('function', 'method', 'class', 'struct', 'interface').",
243
+ description: "Symbol name, call syntax (e.g. 'OrderController::cancelOrder'), or search query (for locate, inspect, impact).",
359
244
  },
360
245
  domain: {
361
246
  type: "string",
362
- description: "Filter by domain name.",
247
+ description: "Optional domain filter (e.g. 'orders').",
363
248
  },
364
- limit: {
365
- type: "number",
366
- description: "Maximum results to return (default: 30).",
249
+ include_dependencies: {
250
+ type: "boolean",
251
+ default: true,
252
+ description: "Whether to include injected dependencies and outbound calls (action='inspect').",
367
253
  },
368
- sort_by: {
369
- type: "string",
370
- enum: ["lines", "nesting", "risk_score"],
371
- description: "Sorting criteria: 'risk_score' (combines lines & nesting depth), 'nesting', or 'lines'. Default: 'risk_score'.",
254
+ min_lines: {
255
+ type: "number",
256
+ description: "Minimum physical lines of code threshold (action='hotspots', default: 30).",
372
257
  },
373
- },
374
- },
375
- },
376
- {
377
- name: "septum_clear_feature_context",
378
- description:
379
- "Releases the active feature session lock (.septum/session.json) once feature implementation or editing is complete.",
380
- inputSchema: {
381
- type: "object",
382
- properties: {},
383
- },
384
- },
385
- {
386
- name: "septum_locate_symbol",
387
- description:
388
- "Locates a symbol or resolves diagnostic error logs (e.g. 'OrderController::calculateTotal()' or 'Method X::y() does not exist'). Returns target file path, existing sibling methods, and ranked alternative suggestions when a method doesn't exist.",
389
- inputSchema: {
390
- type: "object",
391
- properties: {
392
- query: {
393
- type: "string",
394
- description:
395
- "Symbol name, call syntax (e.g. 'OrderController::calculateTotal'), or error log snippet. Symmetrically accepts 'symbol'.",
258
+ min_nesting: {
259
+ type: "number",
260
+ description: "Minimum control-flow nesting depth threshold (action='hotspots').",
396
261
  },
397
- symbol: {
262
+ kind: {
398
263
  type: "string",
399
- description: "Alias for 'query'.",
264
+ description: "Filter symbol kind: 'function' | 'method' | 'class' | 'struct' | 'interface' (action='hotspots').",
400
265
  },
401
- domain: {
266
+ sort_by: {
402
267
  type: "string",
403
- description: "Optional domain filter (e.g. 'orders')",
268
+ enum: ["risk_score", "line_count", "lines", "nesting"],
269
+ default: "risk_score",
270
+ description: "Sorting criteria for hotspots (action='hotspots').",
404
271
  },
405
272
  limit: {
406
273
  type: "number",
407
- description: "Maximum number of suggestions to return (default: 10, max: 50).",
274
+ description: "Maximum results to return (default: 10 for locate, 30 for hotspots).",
275
+ },
276
+ workspace_path: {
277
+ type: "string",
278
+ description: "Explicit workspace root path. Specifies the target project root in multi-root environments.",
408
279
  },
409
280
  },
410
- required: ["query"],
281
+ required: [],
411
282
  },
412
283
  },
413
284
  {
414
- name: "septum_trace_vertical_slice",
285
+ name: "septum_slice",
415
286
  description:
416
- "Traces the end-to-end vertical slice of an MVC/Inertia application (Route -> Request Validation -> Controller Action -> Model -> Frontend View/Component). Accepts route URIs, controller actions, or high-level feature intents without requiring manual file guessing.",
287
+ "Vertical Execution Slice Tracer: traces end-to-end execution flow (Ingress -> Validation -> Controller -> Service -> Repository -> Entity) from route URI, controller action, or high-level intent without guessing files.",
417
288
  inputSchema: {
418
289
  type: "object",
419
290
  properties: {
420
291
  query: {
421
292
  type: "string",
422
- description:
423
- "Route URI (e.g. 'POST /orders/{id}/status'), action (e.g. 'OrderController@updateStatus'), or intent (e.g. 'Ubah status order di dashboard'). Symmetrically accepts 'symbol'.",
424
- },
425
- symbol: {
426
- type: "string",
427
- description: "Alias for 'query'.",
293
+ description: "Route URI (e.g. 'POST /orders/{id}/pay'), action (e.g. 'OrderController@pay'), or high-level task intent.",
428
294
  },
429
295
  max_depth: {
430
296
  type: "number",
431
297
  default: 5,
432
298
  minimum: 1,
433
299
  maximum: 8,
434
- description: "Maximum depth to trace (default: 5, min: 1, max: 8).",
435
- },
436
- },
437
- required: ["query"],
438
- },
439
- },
440
- {
441
- name: "septum_get_symbol",
442
- description:
443
- "Retrieves physical coordinates and structural metadata of a symbol (exact start_line, end_line, total_lines, signature, visibility, injected dependencies, and outbound calls). Returns deterministic JSON for direct piping into view_file without line guessing.",
444
- inputSchema: {
445
- type: "object",
446
- properties: {
447
- symbol: {
448
- type: "string",
449
- description:
450
- "Symbol name, e.g. 'OrderController::cancelOrder', 'OrderController', or 'cancelOrder'. Symmetrically accepts 'query'.",
451
- },
452
- query: {
453
- type: "string",
454
- description: "Alias for 'symbol'.",
455
- },
456
- domain: {
457
- type: "string",
458
- description: "Optional domain filter (e.g. 'orders')",
459
- },
460
- include_dependencies: {
461
- type: "boolean",
462
- description: "Whether to include injected dependencies and outbound calls (default: true)",
463
- },
464
- },
465
- required: ["symbol"],
466
- },
467
- },
468
- {
469
- name: "septum_get_symbol_impact",
470
- description:
471
- "Calculates blast radius and maps all inbound callers/dependents of a symbol across domains. Prevents Local Scope Myopia by reporting all files, callers, and line numbers that depend on the target symbol before refactoring.",
472
- inputSchema: {
473
- type: "object",
474
- properties: {
475
- symbol: {
476
- type: "string",
477
- description:
478
- "Target symbol to inspect, e.g. 'OrderService::cancelOrder' or 'OrderService'. Symmetrically accepts 'query'.",
300
+ description: "Maximum depth to trace (default: 5, min: 1, max: 8). Use 6-8 for deep Event/Job/Queue traces.",
479
301
  },
480
- query: {
302
+ workspace_path: {
481
303
  type: "string",
482
- description: "Alias for 'symbol'.",
304
+ description: "Explicit workspace root path. Specifies the target project root in multi-root environments.",
483
305
  },
484
306
  },
485
- required: ["symbol"],
307
+ required: ["query"],
486
308
  },
487
309
  },
488
310
  {
489
- name: "septum_register_domain",
311
+ name: "septum_boundary",
490
312
  description:
491
- "Directly registers or updates a bounded-context domain in SQLite SSOT (Zero-Config). Use this to dynamically teach Septum about custom domain boundaries, allowed dependencies, and archetypes without touching any YAML files.",
313
+ "Architecture Guard & Circular Dependency Gate: Pre-flight boundary check verifying proposed file edits, additions, and imports against domain rules and feature touchpoints, or auditing the entire repository / staged files for cross-domain and circular dependencies.",
492
314
  inputSchema: {
493
315
  type: "object",
494
316
  properties: {
495
- domain: {
496
- type: "string",
497
- description:
498
- "Unique domain name/identifier (e.g. 'orders', 'checkout', 'billing'). Symmetrically accepts 'name'.",
499
- },
500
- name: {
501
- type: "string",
502
- description: "Alias for 'domain'.",
503
- },
504
- root: {
317
+ scope: {
505
318
  type: "string",
506
- description: "Root directory path for this domain (e.g. 'src/domains/orders', 'app/Domain/Orders')",
319
+ enum: ["file", "staged", "all", "domain"],
320
+ description: "Audit scope: 'file' requires file_path or non-empty file_paths; 'staged', 'all', and 'domain' reject file targets and proposed_imports. 'domain' requires a registered domain; 'all' rejects domain filters. If omitted, infer file/staged/domain/all from arguments.",
507
321
  },
508
- description: {
322
+ file_path: {
509
323
  type: "string",
510
- description: "Optional high-level architectural purpose of the domain",
324
+ description: "Path of the file being edited or created (scope='file' only). Use either file_path or file_paths.",
511
325
  },
512
- allowed_dependencies: {
326
+ file_paths: {
513
327
  type: "array",
514
328
  items: { type: "string" },
515
- description: "List of domain names this domain is allowed to import",
329
+ description: "Non-empty list of file paths for batch proposed-import checks (scope='file' only). Use either file_path or file_paths.",
516
330
  },
517
- forbidden_dependencies: {
331
+ proposed_imports: {
518
332
  type: "array",
519
333
  items: { type: "string" },
520
- description: "List of domain names strictly forbidden from being imported",
334
+ description: "List of proposed import statements or target symbols (scope='file' only).",
521
335
  },
522
- archetypes: {
523
- type: "object",
524
- description: "Layer archetypes (e.g. { 'service': 'src/services/**', 'model': 'src/models/**' })",
336
+ domain: {
337
+ type: "string",
338
+ description: "Registered domain name, required for scope='domain'; optional for staged/file checks. Explicit scope='all' does not accept a domain filter.",
525
339
  },
526
- ingest_now: {
527
- type: "boolean",
528
- description: "Whether to immediately run AST ingestion on this domain's files (default: false)",
340
+ feature_key: {
341
+ type: "string",
342
+ description: "Optional feature key to validate touchpoints against.",
343
+ },
344
+ workspace_path: {
345
+ type: "string",
346
+ description: "Explicit workspace root path. Specifies the target project root in multi-root environments.",
529
347
  },
530
348
  },
531
- required: ["domain", "root"],
349
+ required: [],
532
350
  },
533
351
  },
534
352
  {
535
- name: "septum_register_feature",
353
+ name: "septum_feature",
536
354
  description:
537
- "Directly registers or updates a feature/task context in Septum SQLite SSOT. Establishes an active session lease (.septum/session.json) with allowed_touchpoints and reuse_symbols to enforce surgical blast-radius boundaries during code modifications.",
355
+ "Active Touchpoint & Scope Lock Engine: manages feature task context, touchpoint locks (.septum/session.json), and session release to enforce surgical blast-radius boundaries.",
538
356
  inputSchema: {
539
357
  type: "object",
540
358
  properties: {
541
- feature_key: {
359
+ action: {
542
360
  type: "string",
543
- description:
544
- "Unique identifier for the feature/task (e.g. 'penjualan', 'checkout-pos'). Symmetrically accepts 'feature'.",
361
+ enum: ["get", "lock", "release"],
362
+ default: "get",
363
+ description: "Feature lifecycle action: 'get' (retrieve feature context), 'lock' (register feature & establish active session lease), or 'release' (release active session lock).",
545
364
  },
546
- feature: {
365
+ feature_key: {
547
366
  type: "string",
548
- description: "Alias for 'feature_key'.",
367
+ description: "Unique feature identifier (e.g. 'checkout_flow'). Symmetrically accepts 'feature'.",
549
368
  },
550
369
  domain: {
551
370
  type: "string",
552
- description: "Bounded-context domain to which this feature belongs",
371
+ description: "Bounded-context domain for this feature (action='lock').",
553
372
  },
554
373
  description: {
555
374
  type: "string",
556
- description: "Scope and rationale for the feature",
375
+ description: "Scope and rationale for the feature (action='lock').",
557
376
  },
558
377
  allowed_touchpoints: {
559
378
  type: "array",
560
379
  items: { type: "string" },
561
- description: "List of file paths or glob patterns permitted to be edited for this feature",
380
+ description: "Permitted file paths or glob patterns (action='lock').",
562
381
  },
563
382
  reuse_symbols: {
564
383
  type: "array",
565
384
  items: { type: "string" },
566
- description: "Symbols/services/methods that must be reused rather than duplicated",
385
+ description: "Mandatory reusable symbols (action='lock').",
567
386
  },
568
387
  input_contract: {
569
388
  type: "object",
570
- description: "Expected input payload / DTO schema",
389
+ description: "Expected input payload / DTO schema (action='lock').",
571
390
  },
572
391
  output_contract: {
573
392
  type: "object",
574
- description: "Expected output response / return contract",
393
+ description: "Expected output response contract (action='lock').",
575
394
  },
576
- },
577
- required: ["feature_key", "domain"],
578
- },
579
- },
580
- {
581
- name: "septum_get_environment_topology",
582
- description:
583
- "Self-Aware Production Topology Engine: Maps and verifies the 6-layer environment topology (Edge/WAF, Gateway/Web Server, Host Platform, Runtime, Storage/Cache, Telemetry). Identifies cross-layer constraints (e.g. Cloudflare + LiteSpeed + Laravel throttle), surfaces pending architectural inquiries if layers are ambiguous, and allows recording verified environment answers to SQLite SSOT.",
584
- inputSchema: {
585
- type: "object",
586
- properties: {
587
- resolve: {
588
- type: "object",
589
- properties: {
590
- layer: {
591
- type: "string",
592
- enum: ["edge", "gateway", "host", "runtime", "storage", "telemetry"],
593
- description: "Target layer to manually resolve/verify",
594
- },
595
- platform: {
596
- type: "string",
597
- description: "Confirmed platform name (e.g. 'Hostinger Shared Hosting', 'Cloudflare WAF', 'LiteSpeed')",
598
- },
599
- constraints: {
600
- type: "array",
601
- items: { type: "string" },
602
- description: "Optional list of architectural constraints for this layer",
603
- },
604
- },
605
- required: ["layer", "platform"],
606
- description: "Optional argument to record and verify a specific layer into SQLite SSOT",
607
- },
608
- resolveNode: {
609
- type: "object",
610
- properties: {
611
- id: {
612
- type: "string",
613
- description: "Unique node identifier (e.g. 'api-gateway-1', 'db-primary')",
614
- },
615
- layer: {
616
- type: "string",
617
- enum: ["edge", "gateway", "host", "runtime", "storage", "telemetry"],
618
- description: "Topology layer for this node",
619
- },
620
- name: {
621
- type: "string",
622
- description: "Human-readable node name",
623
- },
624
- platform: {
625
- type: "string",
626
- description: "Platform or infrastructure provider for this node",
627
- },
628
- domain_or_ip: {
629
- type: "string",
630
- description: "Optional domain or IP address of the node",
631
- },
632
- role: {
633
- type: "string",
634
- description: "Optional architectural role of the node",
635
- },
636
- connected_to: {
637
- type: "array",
638
- items: { type: "string" },
639
- description: "Optional list of target node IDs this node connects to",
640
- },
641
- },
642
- required: ["id", "layer", "name", "platform"],
643
- description: "Optional argument to record and verify a distributed topology node into SQLite SSOT",
644
- },
645
- refresh: {
395
+ force: {
646
396
  type: "boolean",
647
- description: "Force re-detection from codebase files, ignoring cached status",
397
+ default: false,
398
+ description: "Force release session (action='release').",
399
+ },
400
+ workspace_path: {
401
+ type: "string",
402
+ description: "Explicit workspace root path. Specifies the target project root in multi-root environments.",
648
403
  },
649
404
  },
405
+ required: [],
650
406
  },
651
407
  },
652
408
  ],
653
409
  };
654
410
  });
655
411
 
656
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
412
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
657
413
  const { name, arguments: args } = request.params;
658
414
  const startTime = performance.now();
659
- const pathHint = extractPathHint(args as Record<string, any>);
660
- const ctx = getWorkspaceContext(pathHint);
415
+ let lease: WorkspaceLease | undefined;
416
+ try {
417
+ extra?.signal?.throwIfAborted();
418
+ const schema = TOOL_SCHEMAS[name as keyof typeof TOOL_SCHEMAS];
419
+ if (!schema) throw new Error(`Unknown tool: '${name}'`);
420
+ const validated = schema.safeParse(args ?? {});
421
+ if (!validated.success) throw new Error(`[Septum Validation Error] Invalid arguments for ${name}: ${validated.error.message}`);
422
+ const record = (validated.data ?? {}) as Record<string, any>;
423
+ const raw = (args ?? {}) as Record<string, any>;
424
+ const workspacePath = record.workspace_path ?? raw.workspace_path ?? raw.workspacePath ?? raw.workspace ?? raw.project_root ?? raw.projectRoot;
425
+ const knownRoots = manager.activeRoots;
426
+ if (manager.initialRoot && !knownRoots.includes(manager.initialRoot)) {
427
+ knownRoots.unshift(manager.initialRoot);
428
+ }
429
+ const fallbackRoot = manager.initialRoot || manager.lastRoot;
430
+ const root = resolveServerWorkspace({
431
+ workspacePath,
432
+ filePath: workspacePath ? undefined : extractPathHint(args),
433
+ fallbackRoot,
434
+ knownRoots,
435
+ });
436
+
437
+ if (!isWorkspaceInitialized(root)) {
438
+ if (name === "septum_domain" && (!record.action || record.action === "catalog")) {
439
+ const diagnostic = {
440
+ status: "NOT_INDEXED",
441
+ total_domains: 0,
442
+ message: `Workspace '${root}' belum diinisialisasi dengan Septum. Jalankan 'septum init' terlebih dahulu di terminal workspace.`,
443
+ suggested_action: {
444
+ tool: "septum_domain",
445
+ params: {
446
+ action: "register",
447
+ name: "core",
448
+ root: "src",
449
+ ingest_now: true,
450
+ },
451
+ },
452
+ agent_guidance:
453
+ "Panggil tool 'septum_domain(action: \"register\", name: \"core\", root: \"src\", ingest_now: true)' untuk mendaftarkan dan mengindeks domain utama secara otomatis, atau jalankan 'septum init' di terminal workspace.",
454
+ };
455
+ return {
456
+ content: [
457
+ {
458
+ type: "text",
459
+ text: JSON.stringify(diagnostic, null, 2),
460
+ },
461
+ ],
462
+ metadata: diagnostic,
463
+ };
464
+ }
465
+ const isRegistration = name === "septum_domain" && record.action === "register";
466
+ if (!isRegistration) {
467
+ return {
468
+ isError: true,
469
+ content: [
470
+ {
471
+ type: "text",
472
+ text: `[Septum MCP Error] Workspace '${root}' belum diinisialisasi. Jalankan 'septum init' di terminal workspace atau gunakan tool 'septum_domain' dengan action='register' untuk mendaftarkan domain.`,
473
+ },
474
+ ],
475
+ };
476
+ }
477
+ }
478
+
479
+ extra?.signal?.throwIfAborted();
480
+ lease = manager.acquire(root);
481
+ const ctx = lease.context;
661
482
  await ctx.ready;
662
- await ctx.watcher.flush();
483
+ extra?.signal?.throwIfAborted();
484
+ await ctx.watcher.flush(true, extra?.signal);
663
485
  if (ctx.repo.getMeta("sync_error")) throw new Error(`Workspace index synchronization failed: ${ctx.repo.getMeta("sync_error")}`);
664
486
  ctx.config = ConfigLoader.load(undefined, ctx.root);
665
487
  ctx.evaluator.getResolver().setRepository(ctx.repo);
666
488
 
667
- const activeSession = SessionManager.getActiveSession(ctx.root);
489
+ let activeSession = null;
490
+ let sessionCorruptError: Error | null = null;
491
+ try {
492
+ activeSession = SessionManager.getActiveSession(ctx.root);
493
+ } catch (err: any) {
494
+ sessionCorruptError = err instanceof Error ? err : new Error(String(err));
495
+ }
668
496
  const sessionPayload = activeSession
669
497
  ? { feature_key: activeSession.feature_key, domain: activeSession.domain }
670
498
  : null;
671
499
 
672
500
  const executeTool = async () => {
673
- if (name === "septum_get_domain_catalog") {
674
- const parsed = GetDomainCatalogSchema.safeParse(args ?? {});
501
+ // 5 Unified Tools Handlers
502
+ if (name === "septum_domain") {
503
+ const parsed = DomainToolSchema.safeParse(args ?? {});
675
504
  if (!parsed.success) {
676
505
  return {
677
506
  isError: true,
@@ -683,9 +512,9 @@ export async function runMCPServer(): Promise<void> {
683
512
  ],
684
513
  };
685
514
  }
686
- return handleGetDomainCatalog(ctx.repo, ctx.config, parsed.data, ctx.root);
687
- } else if (name === "septum_get_feature_context") {
688
- const parsed = GetFeatureContextSchema.safeParse(args ?? {});
515
+ return await handleDomainTool(ctx.repo, ctx.config, parsed.data, ctx.root, extra?.signal);
516
+ } else if (name === "septum_symbol") {
517
+ const parsed = SymbolToolSchema.safeParse(args ?? {});
689
518
  if (!parsed.success) {
690
519
  return {
691
520
  isError: true,
@@ -697,9 +526,9 @@ export async function runMCPServer(): Promise<void> {
697
526
  ],
698
527
  };
699
528
  }
700
- return handleGetFeatureContext(ctx.repo, ctx.config, parsed.data, ctx.root);
701
- } else if (name === "septum_locate_symbol") {
702
- const parsed = LocateSymbolSchema.safeParse(args ?? {});
529
+ return await handleSymbolTool(ctx.repo, ctx.config, parsed.data, ctx.pipeline, ctx.root);
530
+ } else if (name === "septum_slice") {
531
+ const parsed = SliceToolSchema.safeParse(args ?? {});
703
532
  if (!parsed.success) {
704
533
  return {
705
534
  isError: true,
@@ -711,23 +540,20 @@ export async function runMCPServer(): Promise<void> {
711
540
  ],
712
541
  };
713
542
  }
714
- return await handleLocateSymbol(ctx.repo, ctx.config, parsed.data, ctx.pipeline, ctx.root);
715
- } else if (name === "septum_get_symbol") {
716
- const parsed = GetSymbolSchema.safeParse(args ?? {});
717
- if (!parsed.success) {
543
+ return handleSliceTool(ctx.repo, ctx.config, parsed.data, ctx.root);
544
+ } else if (name === "septum_boundary") {
545
+ if (sessionCorruptError) {
718
546
  return {
719
547
  isError: true,
720
548
  content: [
721
549
  {
722
550
  type: "text",
723
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
551
+ text: `[Septum Session Error] Active feature session in '.septum/session.json' is corrupted or invalid: ${sessionCorruptError.message}. Release it explicitly with 'septum_feature({action: "release", force: true})' before evaluating boundaries.`,
724
552
  },
725
553
  ],
726
554
  };
727
555
  }
728
- return handleGetSymbol(ctx.repo, ctx.config, parsed.data, ctx.root);
729
- } else if (name === "septum_get_symbol_impact") {
730
- const parsed = GetSymbolImpactSchema.safeParse(args ?? {});
556
+ const parsed = BoundaryToolSchema.safeParse(args ?? {});
731
557
  if (!parsed.success) {
732
558
  return {
733
559
  isError: true,
@@ -739,9 +565,9 @@ export async function runMCPServer(): Promise<void> {
739
565
  ],
740
566
  };
741
567
  }
742
- return handleGetSymbolImpact(ctx.repo, ctx.config, parsed.data, ctx.root);
743
- } else if (name === "septum_trace_vertical_slice") {
744
- const parsed = TraceVerticalSliceSchema.safeParse(args ?? {});
568
+ return await handleBoundaryTool(ctx.evaluator, ctx.config, parsed.data, ctx.repo, ctx.root);
569
+ } else if (name === "septum_feature") {
570
+ const parsed = FeatureToolSchema.safeParse(args ?? {});
745
571
  if (!parsed.success) {
746
572
  return {
747
573
  isError: true,
@@ -753,96 +579,26 @@ export async function runMCPServer(): Promise<void> {
753
579
  ],
754
580
  };
755
581
  }
756
- return handleTraceVerticalSlice(ctx.repo, ctx.config, parsed.data, ctx.root);
757
- } else if (name === "septum_clear_feature_context") {
758
- const cleared = handleClearFeatureContext(ctx.root);
759
- return {
760
- content: [
761
- {
762
- type: "text",
763
- text: cleared
764
- ? "Active feature session successfully released."
765
- : "No active feature session found to release.",
766
- },
767
- ],
768
- };
769
- } else if (name === "septum_check_boundary") {
770
- const parsed = CheckBoundarySchema.safeParse(args ?? {});
771
- if (!parsed.success) {
772
- return {
773
- isError: true,
774
- content: [
775
- {
776
- type: "text",
777
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
778
- },
779
- ],
780
- };
781
- }
782
- return handleCheckBoundary(ctx.evaluator, ctx.config, parsed.data, ctx.repo, ctx.root);
783
- } else if (name === "septum_get_symbol_hotspots") {
784
- const parsed = GetSymbolHotspotsSchema.safeParse(args ?? {});
785
- if (!parsed.success) {
582
+ if (sessionCorruptError && (parsed.data.action !== "release" || !parsed.data.force)) {
786
583
  return {
787
584
  isError: true,
788
585
  content: [
789
586
  {
790
587
  type: "text",
791
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
588
+ text: `[Septum Session Error] Active feature session in '.septum/session.json' is corrupted or invalid: ${sessionCorruptError.message}. Release it explicitly with 'septum_feature({action: "release", force: true})'.`,
792
589
  },
793
590
  ],
794
591
  };
795
592
  }
796
- return handleGetSymbolHotspots(ctx.repo, ctx.config, parsed.data, ctx.root);
797
- } else if (name === "septum_register_domain") {
798
- const parsed = RegisterDomainSchema.safeParse(args ?? {});
799
- if (!parsed.success) {
800
- return {
801
- isError: true,
802
- content: [
803
- {
804
- type: "text",
805
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
806
- },
807
- ],
808
- };
809
- }
810
- return await handleRegisterDomain(ctx.repo, ctx.config, parsed.data, ctx.root);
811
- } else if (name === "septum_register_feature") {
812
- const parsed = RegisterFeatureSchema.safeParse(args ?? {});
813
- if (!parsed.success) {
814
- return {
815
- isError: true,
816
- content: [
817
- {
818
- type: "text",
819
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
820
- },
821
- ],
822
- };
823
- }
824
- return await handleRegisterFeature(ctx.repo, ctx.config, parsed.data, ctx.root);
825
- } else if (name === "septum_get_environment_topology") {
826
- const parsed = GetEnvironmentTopologySchema.safeParse(args ?? {});
827
- if (!parsed.success) {
828
- return {
829
- isError: true,
830
- content: [
831
- {
832
- type: "text",
833
- text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
834
- },
835
- ],
836
- };
837
- }
838
- return handleGetEnvironmentTopology(ctx.repo, ctx.config, parsed.data, ctx.root);
593
+ extra?.signal?.throwIfAborted();
594
+ return await handleFeatureTool(ctx.repo, ctx.config, parsed.data, ctx.root);
839
595
  } else {
840
596
  throw new Error(`Unknown tool: '${name}'`);
841
597
  }
842
598
  };
843
599
 
844
600
  try {
845
- const response = await executeTool();
601
+ const response = await manager.runTool(executeTool, extra?.signal);
846
602
  const durationMs = Math.round(performance.now() - startTime);
847
603
 
848
604
  let bytesOut = 0;
@@ -851,7 +607,7 @@ export async function runMCPServer(): Promise<void> {
851
607
  for (const item of response.content) {
852
608
  if (typeof item.text === "string") {
853
609
  bytesOut += Buffer.byteLength(item.text, "utf8");
854
- linesOut += item.text.split("\n").length;
610
+ linesOut += countLines(item.text);
855
611
  }
856
612
  }
857
613
  }
@@ -894,37 +650,67 @@ export async function runMCPServer(): Promise<void> {
894
650
  ],
895
651
  };
896
652
  }
653
+ } catch (error) {
654
+ return {isError: true, content: [{type: "text" as const, text: `[Septum MCP Error] ${error instanceof Error ? error.message : String(error)}`}]};
655
+ } finally { lease?.release(); }
897
656
  });
898
657
 
899
658
  const transport = new StdioServerTransport();
900
659
 
660
+ let stopping = false;
661
+ let memoryTimer: ReturnType<typeof setInterval> | undefined;
901
662
  const cleanup = () => {
902
- for (const ctx of workspaceContextCache.values()) {
663
+ if (stopping) return;
664
+ stopping = true;
665
+ if (memoryTimer) clearInterval(memoryTimer);
666
+ const deadline = setTimeout(() => {
667
+ console.error("[Septum MCP] Shutdown deadline exceeded; indexing may require reconciliation.");
668
+ process.exit(1);
669
+ }, RESOURCE_LIMITS.shutdownMs);
670
+ void (async () => {
903
671
  try {
904
- ctx.watcher.close();
905
- ctx.db.close();
906
- } catch (err) {
907
- // Ignore close error during shutdown
672
+ await server.close();
673
+ await manager.close();
674
+ clearTimeout(deadline);
675
+ console.error("[Septum MCP] All workspace resources closed cleanly.");
676
+ process.exit(0);
677
+ } catch (error) {
678
+ console.error(`[Septum MCP] Cleanup failed: ${String(error)}`);
679
+ clearTimeout(deadline);
680
+ process.exit(1);
908
681
  }
909
- }
910
- console.error("[Septum MCP] All database connections closed cleanly.");
911
- process.exit(0);
682
+ })();
912
683
  };
913
-
914
- process.on("SIGINT", cleanup);
915
- process.on("SIGTERM", cleanup);
916
- process.on("beforeExit", () => {
917
- for (const ctx of workspaceContextCache.values()) {
918
- try {
919
- ctx.watcher.close();
920
- ctx.db.close();
921
- } catch {
922
- // Ignore close error
923
- }
684
+ process.once("SIGINT", cleanup);
685
+ process.once("SIGTERM", cleanup);
686
+ process.stdin.once("end", cleanup);
687
+ process.stdin.once("close", cleanup);
688
+ server.onclose = cleanup;
689
+ await server.connect(transport);
690
+ try {
691
+ const initialRoot = resolveServerWorkspace({initial:true});
692
+ manager.initialRoot = initialRoot;
693
+ manager.lastRoot = initialRoot;
694
+ if (isWorkspaceInitialized(initialRoot)) {
695
+ const initial = manager.acquire(initialRoot);
696
+ try { await initial.context.ready; } finally { initial.release(); }
924
697
  }
925
- });
698
+ if (process.env.SEPTUM_MEMORY_DIAGNOSTICS === "1") {
699
+ memoryTimer = setInterval(() => {
700
+ const sample = { ...process.memoryUsage(), workspaces: manager.size, watches: manager.watchCount, pending_paths: manager.pendingPaths, sync_duration_ms: manager.syncDurationMs };
701
+ if (manager.lastRoot && isWorkspaceInitialized(manager.lastRoot)) {
702
+ SeptumTelemetry.recordMemorySample(sample, manager.lastRoot);
703
+ } else {
704
+ console.error(`[Septum Memory] ${JSON.stringify({timestamp: new Date().toISOString(), event: "memory_sample", metrics: sample})}`);
705
+ }
706
+ }, 60_000);
707
+ memoryTimer.unref();
708
+ }
709
+ } catch (error) {
710
+ await manager.close();
711
+ throw error;
712
+ }
926
713
 
927
- await server.connect(transport);
928
714
  if (process.stdin.isTTY) {
929
715
  console.error(
930
716
  `\x1b[36m[Septum MCP]\x1b[0m Server daemon running on stdio (PID: ${process.pid}, v${SEPTUM_VERSION}).`