@virsanghavi/axis-server 1.0.7 → 1.0.9

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,10 +1,3 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // ../../src/local/mcp-server.ts
9
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -20,25 +13,47 @@ import dotenv2 from "dotenv";
20
13
  import fs from "fs/promises";
21
14
  import path from "path";
22
15
  import { Mutex } from "async-mutex";
16
+ import * as fsSync from "fs";
23
17
  function getEffectiveInstructionsDir() {
24
18
  const cwd = process.cwd();
25
19
  const axisDir = path.resolve(cwd, ".axis");
26
20
  const instructionsDir = path.resolve(axisDir, "instructions");
27
21
  const legacyDir = path.resolve(cwd, "agent-instructions");
22
+ const sharedContextDir = path.resolve(cwd, "shared-context", "agent-instructions");
28
23
  try {
29
- if (__require("fs").existsSync(instructionsDir)) return instructionsDir;
24
+ if (fsSync.existsSync(instructionsDir)) {
25
+ console.error(`[ContextManager] Using instructions dir: ${instructionsDir}`);
26
+ return instructionsDir;
27
+ }
30
28
  } catch {
31
29
  }
30
+ try {
31
+ if (fsSync.existsSync(legacyDir)) {
32
+ console.error(`[ContextManager] Using legacy dir: ${legacyDir}`);
33
+ return legacyDir;
34
+ }
35
+ } catch {
36
+ }
37
+ try {
38
+ if (fsSync.existsSync(sharedContextDir)) {
39
+ console.error(`[ContextManager] Using shared-context dir: ${sharedContextDir}`);
40
+ return sharedContextDir;
41
+ }
42
+ } catch {
43
+ }
44
+ console.error(`[ContextManager] Fallback to legacy dir: ${legacyDir}`);
32
45
  return legacyDir;
33
46
  }
34
47
  var ContextManager = class {
35
48
  mutex;
36
49
  apiUrl;
50
+ // Made public so NerveCenter can access it
37
51
  apiSecret;
38
- constructor(apiUrl, apiSecret) {
52
+ // Made public so NerveCenter can access it
53
+ constructor(apiUrl2, apiSecret2) {
39
54
  this.mutex = new Mutex();
40
- this.apiUrl = apiUrl;
41
- this.apiSecret = apiSecret;
55
+ this.apiUrl = apiUrl2;
56
+ this.apiSecret = apiSecret2;
42
57
  }
43
58
  resolveFilePath(filename) {
44
59
  if (!filename || filename.includes("\0")) {
@@ -221,16 +236,27 @@ var NerveCenter = class {
221
236
  this.contextManager = contextManager;
222
237
  this.stateFilePath = options.stateFilePath || STATE_FILE;
223
238
  this.lockTimeout = options.lockTimeout || LOCK_TIMEOUT_DEFAULT;
224
- this.projectName = options.projectName || process.env.PROJECT_NAME || "default";
225
- const supabaseUrl = options.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL;
226
- const supabaseKey = options.supabaseServiceRoleKey || process.env.SUPABASE_SERVICE_ROLE_KEY;
227
- if (!supabaseUrl || !supabaseKey) {
228
- logger.warn("Supabase credentials missing. Running in local-only mode (state will be ephemeral or file-based).");
239
+ const hasRemoteApi = !!this.contextManager.apiUrl;
240
+ const supabaseUrl = options.supabaseUrl !== void 0 ? options.supabaseUrl : hasRemoteApi ? null : process.env.NEXT_PUBLIC_SUPABASE_URL;
241
+ const supabaseKey = options.supabaseServiceRoleKey !== void 0 ? options.supabaseServiceRoleKey : hasRemoteApi ? null : process.env.SUPABASE_SERVICE_ROLE_KEY;
242
+ if (supabaseUrl && supabaseKey) {
243
+ this.supabase = createClient(supabaseUrl, supabaseKey);
244
+ this.useSupabase = true;
245
+ logger.info("NerveCenter: Using direct Supabase persistence.");
246
+ } else if (this.contextManager.apiUrl) {
229
247
  this.supabase = void 0;
230
248
  this.useSupabase = false;
249
+ logger.info(`NerveCenter: Using Remote API persistence (${this.contextManager.apiUrl})`);
231
250
  } else {
232
- this.supabase = createClient(supabaseUrl, supabaseKey);
233
- this.useSupabase = true;
251
+ this.supabase = void 0;
252
+ this.useSupabase = false;
253
+ logger.warn("NerveCenter: Running in local-only mode. Coordination restricted to this machine.");
254
+ }
255
+ const explicitProjectName = options.projectName || process.env.PROJECT_NAME;
256
+ if (explicitProjectName) {
257
+ this.projectName = explicitProjectName;
258
+ } else {
259
+ this.projectName = "default";
234
260
  }
235
261
  this.state = {
236
262
  locks: {},
@@ -246,10 +272,27 @@ var NerveCenter = class {
246
272
  }
247
273
  async init() {
248
274
  await this.loadState();
249
- await this.detectProjectName();
275
+ if (this.projectName === "default" && (this.useSupabase || !this.contextManager.apiUrl)) {
276
+ await this.detectProjectName();
277
+ }
250
278
  if (this.useSupabase) {
251
279
  await this.ensureProjectId();
252
280
  }
281
+ if (this.contextManager.apiUrl) {
282
+ try {
283
+ const { liveNotepad, projectId } = await this.callCoordination(`sessions/sync?projectName=${this.projectName}`);
284
+ if (projectId) {
285
+ this._projectId = projectId;
286
+ logger.info(`NerveCenter: Resolved projectId from cloud: ${this._projectId}`);
287
+ }
288
+ if (liveNotepad && (!this.state.liveNotepad || this.state.liveNotepad.startsWith("Session Start:"))) {
289
+ this.state.liveNotepad = liveNotepad;
290
+ logger.info(`NerveCenter: Recovered live notepad from cloud for project: ${this.projectName}`);
291
+ }
292
+ } catch (e) {
293
+ logger.warn("Failed to sync project/notepad with Remote API. Using local/fallback.", e);
294
+ }
295
+ }
253
296
  }
254
297
  async detectProjectName() {
255
298
  try {
@@ -259,12 +302,16 @@ var NerveCenter = class {
259
302
  if (config.project) {
260
303
  this.projectName = config.project;
261
304
  logger.info(`Detected project name from .axis/axis.json: ${this.projectName}`);
305
+ console.error(`[NerveCenter] Loaded project name '${this.projectName}' from ${axisConfigPath}`);
306
+ } else {
307
+ console.error(`[NerveCenter] .axis/axis.json found but no 'project' field.`);
262
308
  }
263
309
  } catch (e) {
310
+ console.error(`[NerveCenter] Could not load .axis/axis.json at ${path2.join(process.cwd(), ".axis", "axis.json")}: ${e}`);
264
311
  }
265
312
  }
266
313
  async ensureProjectId() {
267
- if (!this.supabase) return;
314
+ if (!this.supabase || this._projectId) return;
268
315
  const { data: project, error } = await this.supabase.from("projects").select("id").eq("name", this.projectName).maybeSingle();
269
316
  if (error) {
270
317
  logger.error("Failed to load project", error);
@@ -281,6 +328,48 @@ var NerveCenter = class {
281
328
  }
282
329
  this._projectId = created.id;
283
330
  }
331
+ async callCoordination(endpoint, method = "GET", body) {
332
+ logger.info(`[callCoordination] Starting - endpoint: ${endpoint}, method: ${method}`);
333
+ logger.info(`[callCoordination] apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET (" + this.contextManager.apiSecret.substring(0, 10) + "...)" : "NOT SET"}`);
334
+ if (!this.contextManager.apiUrl) {
335
+ logger.error("[callCoordination] Remote API not configured - apiUrl is:", this.contextManager.apiUrl);
336
+ throw new Error("Remote API not configured");
337
+ }
338
+ const url = this.contextManager.apiUrl.endsWith("/v1") ? `${this.contextManager.apiUrl}/${endpoint}` : `${this.contextManager.apiUrl}/v1/${endpoint}`;
339
+ logger.info(`[callCoordination] Full URL: ${method} ${url}`);
340
+ logger.info(`[callCoordination] Request body: ${body ? JSON.stringify({ ...body, projectName: this.projectName }) : "none"}`);
341
+ try {
342
+ const response = await fetch(url, {
343
+ method,
344
+ headers: {
345
+ "Content-Type": "application/json",
346
+ "Authorization": `Bearer ${this.contextManager.apiSecret || ""}`
347
+ },
348
+ body: body ? JSON.stringify({ ...body, projectName: this.projectName }) : void 0
349
+ });
350
+ logger.info(`[callCoordination] Response status: ${response.status} ${response.statusText}`);
351
+ if (!response.ok) {
352
+ const text = await response.text();
353
+ logger.error(`[callCoordination] API Error Response (${response.status}): ${text}`);
354
+ if (response.status === 401) {
355
+ throw new Error(`Authentication failed (401): ${text}. Check if API key is valid and exists in api_keys table.`);
356
+ } else if (response.status === 500) {
357
+ throw new Error(`Server error (500): ${text}. Check Vercel logs for details.`);
358
+ } else {
359
+ throw new Error(`API Error (${response.status}): ${text}`);
360
+ }
361
+ }
362
+ const jsonResult = await response.json();
363
+ logger.info(`[callCoordination] Success - Response: ${JSON.stringify(jsonResult).substring(0, 200)}...`);
364
+ return jsonResult;
365
+ } catch (e) {
366
+ logger.error(`[callCoordination] Fetch failed: ${e.message}`, e);
367
+ if (e.message.includes("Authentication failed") || e.message.includes("401")) {
368
+ throw new Error(`API Authentication Error: ${e.message}. Verify AXIS_API_KEY in MCP config matches a key in the api_keys table.`);
369
+ }
370
+ throw e;
371
+ }
372
+ }
284
373
  jobFromRecord(record) {
285
374
  return {
286
375
  id: record.id,
@@ -290,69 +379,122 @@ var NerveCenter = class {
290
379
  status: record.status,
291
380
  assignedTo: record.assigned_to || void 0,
292
381
  dependencies: record.dependencies || void 0,
382
+ completionKey: record.completion_key || void 0,
293
383
  createdAt: Date.parse(record.created_at),
294
384
  updatedAt: Date.parse(record.updated_at)
295
385
  };
296
386
  }
297
387
  // --- Data Access Layers (Hybrid: Supabase > Local) ---
298
388
  async listJobs() {
299
- if (!this.useSupabase || !this.supabase || !this._projectId) {
300
- return Object.values(this.state.jobs);
389
+ if (this.useSupabase && this.supabase && this._projectId) {
390
+ const { data, error } = await this.supabase.from("jobs").select("id,title,description,priority,status,assigned_to,dependencies,created_at,updated_at").eq("project_id", this._projectId);
391
+ if (error || !data) {
392
+ logger.error("Failed to load jobs from Supabase", error);
393
+ return [];
394
+ }
395
+ return data.map((record) => this.jobFromRecord(record));
301
396
  }
302
- const { data, error } = await this.supabase.from("jobs").select("id,title,description,priority,status,assigned_to,dependencies,created_at,updated_at").eq("project_id", this._projectId);
303
- if (error || !data) {
304
- logger.error("Failed to load jobs", error);
305
- return [];
397
+ if (this.contextManager.apiUrl) {
398
+ try {
399
+ const url = `jobs?projectName=${this.projectName}`;
400
+ const res = await this.callCoordination(url);
401
+ return (res.jobs || []).map((record) => this.jobFromRecord(record));
402
+ } catch (e) {
403
+ logger.error("Failed to load jobs from API", e);
404
+ return Object.values(this.state.jobs);
405
+ }
306
406
  }
307
- return data.map((record) => this.jobFromRecord(record));
407
+ return Object.values(this.state.jobs);
308
408
  }
309
409
  async getLocks() {
310
- if (!this.useSupabase || !this.supabase || !this._projectId) {
311
- return Object.values(this.state.locks);
410
+ logger.info(`[getLocks] Starting - projectName: ${this.projectName}`);
411
+ logger.info(`[getLocks] Config - apiUrl: ${this.contextManager.apiUrl}, useSupabase: ${this.useSupabase}, hasSupabase: ${!!this.supabase}`);
412
+ if (this.contextManager.apiUrl) {
413
+ if (!this.useSupabase || !this.supabase) {
414
+ try {
415
+ logger.info(`[getLocks] Fetching locks from API for project: ${this.projectName}`);
416
+ const res = await this.callCoordination(`locks?projectName=${this.projectName}`);
417
+ logger.info(`[getLocks] API returned ${res.locks?.length || 0} locks`);
418
+ return (res.locks || []).map((row) => ({
419
+ agentId: row.agent_id,
420
+ filePath: row.file_path,
421
+ intent: row.intent,
422
+ userPrompt: row.user_prompt,
423
+ timestamp: Date.parse(row.updated_at || row.timestamp)
424
+ }));
425
+ } catch (e) {
426
+ logger.error(`[getLocks] Failed to fetch locks from API: ${e.message}`, e);
427
+ }
428
+ }
312
429
  }
313
- try {
314
- await this.supabase.rpc("clean_stale_locks", {
315
- p_project_id: this._projectId,
316
- p_timeout_seconds: Math.floor(this.lockTimeout / 1e3)
317
- });
318
- const { data, error } = await this.supabase.from("locks").select("*").eq("project_id", this._projectId);
319
- if (error) throw error;
320
- return (data || []).map((row) => ({
321
- agentId: row.agent_id,
322
- filePath: row.file_path,
323
- intent: row.intent,
324
- userPrompt: row.user_prompt,
325
- timestamp: Date.parse(row.updated_at)
326
- }));
327
- } catch (e) {
328
- logger.warn("Failed to fetch locks from DB, falling back to local memory", e);
329
- return Object.values(this.state.locks);
430
+ if (this.useSupabase && this.supabase && this._projectId) {
431
+ try {
432
+ await this.supabase.rpc("clean_stale_locks", {
433
+ p_project_id: this._projectId,
434
+ p_timeout_seconds: Math.floor(this.lockTimeout / 1e3)
435
+ });
436
+ const { data, error } = await this.supabase.from("locks").select("*").eq("project_id", this._projectId);
437
+ if (error) throw error;
438
+ return (data || []).map((row) => ({
439
+ agentId: row.agent_id,
440
+ filePath: row.file_path,
441
+ intent: row.intent,
442
+ userPrompt: row.user_prompt,
443
+ timestamp: Date.parse(row.updated_at)
444
+ }));
445
+ } catch (e) {
446
+ logger.warn("Failed to fetch locks from DB", e);
447
+ }
448
+ }
449
+ if (this.contextManager.apiUrl) {
450
+ try {
451
+ const res = await this.callCoordination(`locks?projectName=${this.projectName}`);
452
+ return (res.locks || []).map((row) => ({
453
+ agentId: row.agent_id,
454
+ filePath: row.file_path,
455
+ intent: row.intent,
456
+ userPrompt: row.user_prompt,
457
+ timestamp: Date.parse(row.updated_at || row.timestamp)
458
+ }));
459
+ } catch (e) {
460
+ logger.error("Failed to fetch locks from API in fallback", e);
461
+ }
330
462
  }
463
+ return Object.values(this.state.locks);
331
464
  }
332
465
  async getNotepad() {
333
- if (!this.useSupabase || !this.supabase || !this._projectId) {
334
- return this.state.liveNotepad;
466
+ if (this.useSupabase && this.supabase && this._projectId) {
467
+ const { data, error } = await this.supabase.from("projects").select("live_notepad").eq("id", this._projectId).single();
468
+ if (!error && data) return data.live_notepad || "";
335
469
  }
336
- const { data, error } = await this.supabase.from("projects").select("live_notepad").eq("id", this._projectId).single();
337
- if (error || !data) {
338
- logger.error("Failed to fetch notepad", error);
339
- return this.state.liveNotepad;
340
- }
341
- return data.live_notepad || "";
470
+ return this.state.liveNotepad;
342
471
  }
343
472
  async appendToNotepad(text) {
344
- if (!this.useSupabase || !this.supabase || !this._projectId) {
345
- this.state.liveNotepad += text;
346
- await this.saveState();
347
- return;
473
+ this.state.liveNotepad += text;
474
+ await this.saveState();
475
+ if (this.useSupabase && this.supabase && this._projectId) {
476
+ try {
477
+ await this.supabase.rpc("append_to_project_notepad", {
478
+ p_project_id: this._projectId,
479
+ p_text: text
480
+ });
481
+ } catch (e) {
482
+ }
348
483
  }
349
- const { error } = await this.supabase.rpc("append_to_project_notepad", {
350
- p_project_id: this._projectId,
351
- p_text: text
352
- });
353
- if (error) {
354
- const current = await this.getNotepad();
355
- await this.supabase.from("projects").update({ live_notepad: current + text }).eq("id", this._projectId);
484
+ if (this.contextManager.apiUrl) {
485
+ try {
486
+ const res = await this.callCoordination("sessions/sync", "POST", {
487
+ title: `Current Session: ${this.projectName}`,
488
+ context: this.state.liveNotepad,
489
+ metadata: { source: "mcp-server-live" }
490
+ });
491
+ if (res.projectId && !this._projectId) {
492
+ this._projectId = res.projectId;
493
+ logger.info(`NerveCenter: Captured projectId from sync API: ${this._projectId}`);
494
+ }
495
+ } catch (e) {
496
+ logger.warn("Failed to sync notepad to remote API", e);
497
+ }
356
498
  }
357
499
  }
358
500
  async saveState() {
@@ -375,25 +517,35 @@ var NerveCenter = class {
375
517
  async postJob(title, description, priority = "medium", dependencies = []) {
376
518
  return await this.mutex.runExclusive(async () => {
377
519
  let id = `job-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
520
+ const completionKey = Math.random().toString(36).substring(2, 10).toUpperCase();
378
521
  if (this.useSupabase && this.supabase && this._projectId) {
379
- const now = (/* @__PURE__ */ new Date()).toISOString();
380
522
  const { data, error } = await this.supabase.from("jobs").insert({
381
523
  project_id: this._projectId,
382
524
  title,
383
525
  description,
384
526
  priority,
385
527
  status: "todo",
386
- assigned_to: null,
387
528
  dependencies,
388
- created_at: now,
389
- updated_at: now
529
+ completion_key: completionKey
390
530
  }).select("id").single();
391
- if (error) {
392
- logger.error("Failed to post job", error);
393
- } else if (data?.id) {
394
- id = data.id;
531
+ if (data?.id) id = data.id;
532
+ if (error) logger.error("Failed to post job to Supabase", error);
533
+ } else if (this.contextManager.apiUrl) {
534
+ try {
535
+ const data = await this.callCoordination("jobs", "POST", {
536
+ action: "post",
537
+ title,
538
+ description,
539
+ priority,
540
+ dependencies,
541
+ completion_key: completionKey
542
+ });
543
+ if (data?.id) id = data.id;
544
+ } catch (e) {
545
+ logger.error("Failed to post job to API", e);
395
546
  }
396
- } else {
547
+ }
548
+ if (!this.useSupabase && !this.contextManager.apiUrl) {
397
549
  this.state.jobs[id] = {
398
550
  id,
399
551
  title,
@@ -402,21 +554,54 @@ var NerveCenter = class {
402
554
  dependencies,
403
555
  status: "todo",
404
556
  createdAt: Date.now(),
405
- updatedAt: Date.now()
557
+ updatedAt: Date.now(),
558
+ completionKey
406
559
  };
407
560
  }
408
561
  const depText = dependencies.length ? ` (Depends on: ${dependencies.join(", ")})` : "";
409
562
  const logEntry = `
410
563
  - [JOB POSTED] [${priority.toUpperCase()}] ${title} (ID: ${id})${depText}`;
411
564
  await this.appendToNotepad(logEntry);
412
- logger.info(`Job posted: ${title}`, { jobId: id, priority });
413
- return { jobId: id, status: "POSTED" };
565
+ return { jobId: id, status: "POSTED", completionKey };
414
566
  });
415
567
  }
416
568
  async claimNextJob(agentId) {
417
569
  return await this.mutex.runExclusive(async () => {
570
+ if (this.useSupabase && this.supabase && this._projectId) {
571
+ const { data, error } = await this.supabase.rpc("claim_next_job", {
572
+ p_project_id: this._projectId,
573
+ p_agent_id: agentId
574
+ });
575
+ if (error) {
576
+ logger.error("Failed to claim job via RPC", error);
577
+ } else if (data && data.status === "CLAIMED") {
578
+ const job2 = this.jobFromRecord(data.job);
579
+ await this.appendToNotepad(`
580
+ - [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`);
581
+ return { status: "CLAIMED", job: job2 };
582
+ }
583
+ return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
584
+ }
585
+ if (this.contextManager.apiUrl) {
586
+ try {
587
+ const data = await this.callCoordination("jobs", "POST", {
588
+ action: "claim",
589
+ agentId
590
+ });
591
+ if (data && data.status === "CLAIMED") {
592
+ const job2 = this.jobFromRecord(data.job);
593
+ await this.appendToNotepad(`
594
+ - [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`);
595
+ return { status: "CLAIMED", job: job2 };
596
+ }
597
+ return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
598
+ } catch (e) {
599
+ logger.error("Failed to claim job via API", e);
600
+ return { status: "NO_JOBS_AVAILABLE", message: `Claim failed: ${e.message}` };
601
+ }
602
+ }
418
603
  const priorities = ["critical", "high", "medium", "low"];
419
- const allJobs = await this.listJobs();
604
+ const allJobs = Object.values(this.state.jobs);
420
605
  const jobsById = new Map(allJobs.map((job2) => [job2.id, job2]));
421
606
  const availableJobs = allJobs.filter((job2) => job2.status === "todo").filter((job2) => {
422
607
  if (!job2.dependencies || job2.dependencies.length === 0) return true;
@@ -430,118 +615,110 @@ var NerveCenter = class {
430
615
  if (availableJobs.length === 0) {
431
616
  return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
432
617
  }
433
- if (this.useSupabase && this.supabase) {
434
- for (const candidate of availableJobs) {
435
- const now = (/* @__PURE__ */ new Date()).toISOString();
436
- const { data, error } = await this.supabase.from("jobs").update({
437
- status: "in_progress",
438
- assigned_to: agentId,
439
- updated_at: now
440
- }).eq("id", candidate.id).eq("status", "todo").select("id,title,description,priority,status,assigned_to,dependencies,created_at,updated_at");
441
- if (error) {
442
- logger.error("Failed to claim job", error);
443
- continue;
444
- }
445
- if (data && data.length > 0) {
446
- const job2 = this.jobFromRecord(data[0]);
447
- await this.appendToNotepad(`
448
- - [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`);
449
- logger.info(`Job claimed`, { jobId: job2.id, agentId });
450
- return { status: "CLAIMED", job: job2 };
451
- }
452
- }
453
- return { status: "NO_JOBS_AVAILABLE", message: "All available jobs were just claimed." };
454
- }
455
618
  const job = availableJobs[0];
456
619
  job.status = "in_progress";
457
620
  job.assignedTo = agentId;
458
621
  job.updatedAt = Date.now();
459
- this.state.liveNotepad += `
460
- - [JOB CLAIMED] Agent '${agentId}' picked up: ${job.title}`;
461
- logger.info(`Job claimed`, { jobId: job.id, agentId });
462
- await this.saveState();
622
+ await this.appendToNotepad(`
623
+ - [JOB CLAIMED] Agent '${agentId}' picked up: ${job.title}`);
463
624
  return { status: "CLAIMED", job };
464
625
  });
465
626
  }
466
627
  async cancelJob(jobId, reason) {
467
628
  return await this.mutex.runExclusive(async () => {
468
- if (this.useSupabase && this.supabase) {
469
- const { data, error } = await this.supabase.from("jobs").update({ status: "cancelled", cancel_reason: reason, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId).select("id,title");
470
- if (error || !data || data.length === 0) {
471
- return { error: "Job not found" };
629
+ if (this.useSupabase && this.supabase && this._projectId) {
630
+ await this.supabase.from("jobs").update({ status: "cancelled", cancel_reason: reason, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId);
631
+ } else if (this.contextManager.apiUrl) {
632
+ try {
633
+ await this.callCoordination("jobs", "POST", { action: "update", jobId, status: "cancelled", cancel_reason: reason });
634
+ } catch (e) {
635
+ logger.error("Failed to cancel job via API", e);
472
636
  }
473
- this.state.liveNotepad += `
474
- - [JOB CANCELLED] ${data[0].title} (ID: ${jobId}). Reason: ${reason}`;
475
- await this.saveState();
476
- return { status: "CANCELLED" };
477
637
  }
478
- const job = this.state.jobs[jobId];
479
- if (!job) return { error: "Job not found" };
480
- job.status = "cancelled";
481
- job.updatedAt = Date.now();
482
- this.state.liveNotepad += `
483
- - [JOB CANCELLED] ${job.title} (ID: ${jobId}). Reason: ${reason}`;
484
- await this.saveState();
485
- return { status: "CANCELLED" };
486
- });
487
- }
488
- async forceUnlock(filePath, adminReason) {
489
- return await this.mutex.runExclusive(async () => {
490
- if (this.useSupabase && this.supabase && this._projectId) {
491
- const { error } = await this.supabase.from("locks").delete().eq("project_id", this._projectId).eq("file_path", filePath);
492
- if (error) return { error: "DB Error" };
493
- this.state.liveNotepad += `
494
- - [ADMIN] Force unlocked '${filePath}'. Reason: ${adminReason}`;
638
+ if (this.state.jobs[jobId]) {
639
+ this.state.jobs[jobId].status = "cancelled";
640
+ this.state.jobs[jobId].updatedAt = Date.now();
495
641
  await this.saveState();
496
- return { status: "UNLOCKED" };
497
642
  }
498
- const lock = this.state.locks[filePath];
499
- if (!lock) return { message: "File was not locked." };
500
- delete this.state.locks[filePath];
501
- this.state.liveNotepad += `
502
- - [ADMIN] Force unlocked '${filePath}'. Reason: ${adminReason}`;
503
- await this.saveState();
504
- return { status: "UNLOCKED", previousOwner: lock.agentId };
643
+ await this.appendToNotepad(`
644
+ - [JOB CANCELLED] ID: ${jobId}. Reason: ${reason}`);
645
+ return "Job cancelled.";
505
646
  });
506
647
  }
507
- async completeJob(agentId, jobId, outcome) {
648
+ async completeJob(agentId, jobId, outcome, completionKey) {
508
649
  return await this.mutex.runExclusive(async () => {
509
650
  if (this.useSupabase && this.supabase) {
510
- const { data, error } = await this.supabase.from("jobs").select("id,title,assigned_to").eq("id", jobId).single();
651
+ const { data, error } = await this.supabase.from("jobs").select("id,title,assigned_to,completion_key").eq("id", jobId).single();
511
652
  if (error || !data) return { error: "Job not found" };
512
- if (data.assigned_to !== agentId) return { error: "You don't own this job." };
513
- const { error: updateError } = await this.supabase.from("jobs").update({ status: "done", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId).eq("assigned_to", agentId);
653
+ const isOwner2 = data.assigned_to === agentId;
654
+ const isKeyValid2 = completionKey && data.completion_key === completionKey;
655
+ if (!isOwner2 && !isKeyValid2) {
656
+ return { error: "You don't own this job and provided no valid key." };
657
+ }
658
+ const { error: updateError } = await this.supabase.from("jobs").update({ status: "done", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId);
514
659
  if (updateError) return { error: "Failed to complete job" };
515
660
  await this.appendToNotepad(`
516
- - [JOB DONE] ${data.title} by ${agentId}. Outcome: ${outcome}`);
517
- logger.info(`Job completed`, { jobId, agentId });
661
+ - [JOB DONE] Agent '${agentId}' finished: ${data.title}
662
+ Outcome: ${outcome}`);
518
663
  return { status: "COMPLETED" };
664
+ } else if (this.contextManager.apiUrl) {
665
+ try {
666
+ await this.callCoordination("jobs", "POST", {
667
+ action: "update",
668
+ jobId,
669
+ status: "done",
670
+ assigned_to: agentId,
671
+ completion_key: completionKey
672
+ });
673
+ await this.appendToNotepad(`
674
+ - [JOB DONE] Agent '${agentId}' finished: ${jobId}
675
+ Outcome: ${outcome}`);
676
+ return { status: "COMPLETED" };
677
+ } catch (e) {
678
+ logger.error("Failed to complete job via API", e);
679
+ }
519
680
  }
520
681
  const job = this.state.jobs[jobId];
521
682
  if (!job) return { error: "Job not found" };
522
- if (job.assignedTo !== agentId) return { error: "You don't own this job." };
683
+ const isOwner = job.assignedTo === agentId;
684
+ const isKeyValid = completionKey && job.completionKey === completionKey;
685
+ if (!isOwner && !isKeyValid) {
686
+ return { error: "You don't own this job and provided no valid key." };
687
+ }
523
688
  job.status = "done";
524
689
  job.updatedAt = Date.now();
525
- this.state.liveNotepad += `
526
- - [JOB DONE] ${job.title} by ${agentId}. Outcome: ${outcome}`;
527
- await this.saveState();
690
+ await this.appendToNotepad(`
691
+ - [JOB DONE] Agent '${agentId}' finished: ${job.title}
692
+ Outcome: ${outcome}`);
528
693
  return { status: "COMPLETED" };
529
694
  });
530
695
  }
531
- // --- Core State Management ---
532
- async getLiveContext() {
533
- const locks = await this.getLocks();
534
- const lockSummary = locks.map(
535
- (l) => `- [LOCKED] ${l.filePath} by ${l.agentId}
536
- Intent: ${l.intent}
537
- Prompt: "${l.userPrompt?.substring(0, 100)}..."`
538
- ).join("\n");
696
+ async forceUnlock(filePath, reason) {
697
+ return await this.mutex.runExclusive(async () => {
698
+ if (this.useSupabase && this.supabase && this._projectId) {
699
+ await this.supabase.from("locks").delete().eq("project_id", this._projectId).eq("file_path", filePath);
700
+ } else if (this.contextManager.apiUrl) {
701
+ try {
702
+ await this.callCoordination("locks", "POST", { action: "unlock", filePath, reason });
703
+ } catch (e) {
704
+ logger.error("Failed to force unlock via API", e);
705
+ }
706
+ }
707
+ if (this.state.locks[filePath]) {
708
+ delete this.state.locks[filePath];
709
+ await this.saveState();
710
+ }
711
+ await this.appendToNotepad(`
712
+ - [FORCE UNLOCK] ${filePath} unlocked by admin. Reason: ${reason}`);
713
+ return `File ${filePath} has been forcibly unlocked.`;
714
+ });
715
+ }
716
+ async getCoreContext() {
539
717
  const jobs = await this.listJobs();
540
- const jobSummary = jobs.map(
541
- (j) => `- [${j.status.toUpperCase()}] ${j.title} ${j.assignedTo ? "(" + j.assignedTo + ")" : "(Open)"}
542
- ID: ${j.id}`
543
- ).join("\n");
718
+ const locks = await this.getLocks();
544
719
  const notepad = await this.getNotepad();
720
+ const jobSummary = jobs.filter((j) => j.status !== "done" && j.status !== "cancelled").map((j) => `- [${j.status.toUpperCase()}] ${j.title} (ID: ${j.id}, Priority: ${j.priority}${j.assignedTo ? `, Assigned: ${j.assignedTo}` : ""})`).join("\n");
721
+ const lockSummary = locks.map((l) => `- ${l.filePath} (Locked by: ${l.agentId}, Intent: ${l.intent})`).join("\n");
545
722
  return `# Active Session Context
546
723
 
547
724
  ## Job Board (Active Orchestration)
@@ -556,40 +733,89 @@ ${notepad}`;
556
733
  // --- Decision & Orchestration ---
557
734
  async proposeFileAccess(agentId, filePath, intent, userPrompt) {
558
735
  return await this.mutex.runExclusive(async () => {
559
- if (!this.supabase || !this._projectId) throw new Error("Database not connected");
560
- const { data: existing } = await this.supabase.from("locks").select("*").eq("project_id", this._projectId).eq("file_path", filePath).maybeSingle();
736
+ logger.info(`[proposeFileAccess] Starting - agentId: ${agentId}, filePath: ${filePath}`);
737
+ if (this.contextManager.apiUrl) {
738
+ try {
739
+ const result = await this.callCoordination("locks", "POST", {
740
+ action: "lock",
741
+ filePath,
742
+ agentId,
743
+ intent,
744
+ userPrompt
745
+ });
746
+ if (result.status === "DENIED") {
747
+ logger.info(`[proposeFileAccess] DENIED by server: ${result.message}`);
748
+ return {
749
+ status: "REQUIRES_ORCHESTRATION",
750
+ message: result.message || `File '${filePath}' is locked by another agent`,
751
+ currentLock: result.current_lock
752
+ };
753
+ }
754
+ logger.info(`[proposeFileAccess] GRANTED by server`);
755
+ await this.appendToNotepad(`
756
+ - [LOCK] ${agentId} locked ${filePath}
757
+ Intent: ${intent}`);
758
+ return { status: "GRANTED", message: `Access granted for ${filePath}` };
759
+ } catch (e) {
760
+ if (e.message && e.message.includes("409")) {
761
+ logger.info(`[proposeFileAccess] Lock conflict (409)`);
762
+ return {
763
+ status: "REQUIRES_ORCHESTRATION",
764
+ message: `File '${filePath}' is locked by another agent`
765
+ };
766
+ }
767
+ logger.error(`[proposeFileAccess] API lock failed: ${e.message}`, e);
768
+ return { error: `Failed to acquire lock via API: ${e.message}` };
769
+ }
770
+ }
771
+ if (this.useSupabase && this.supabase && this._projectId) {
772
+ try {
773
+ const { data, error } = await this.supabase.rpc("try_acquire_lock", {
774
+ p_project_id: this._projectId,
775
+ p_file_path: filePath,
776
+ p_agent_id: agentId,
777
+ p_intent: intent,
778
+ p_user_prompt: userPrompt,
779
+ p_timeout_seconds: Math.floor(this.lockTimeout / 1e3)
780
+ });
781
+ if (error) throw error;
782
+ const row = Array.isArray(data) ? data[0] : data;
783
+ if (row && row.status === "DENIED") {
784
+ return {
785
+ status: "REQUIRES_ORCHESTRATION",
786
+ message: `Conflict: File '${filePath}' is locked by '${row.owner_id}'`,
787
+ currentLock: {
788
+ agentId: row.owner_id,
789
+ filePath,
790
+ intent: row.intent,
791
+ timestamp: row.updated_at ? Date.parse(row.updated_at) : Date.now()
792
+ }
793
+ };
794
+ }
795
+ await this.appendToNotepad(`
796
+ - [LOCK] ${agentId} locked ${filePath}
797
+ Intent: ${intent}`);
798
+ return { status: "GRANTED", message: `Access granted for ${filePath}` };
799
+ } catch (e) {
800
+ logger.warn("[NerveCenter] Lock RPC failed. Falling back to local.", e);
801
+ }
802
+ }
803
+ const existing = Object.values(this.state.locks).find((l) => l.filePath === filePath);
561
804
  if (existing) {
562
- const updatedAt = new Date(existing.updated_at).getTime();
563
- const isStale = Date.now() - updatedAt > this.lockTimeout;
564
- if (!isStale && existing.agent_id !== agentId) {
805
+ const isStale = Date.now() - existing.timestamp > this.lockTimeout;
806
+ if (!isStale && existing.agentId !== agentId) {
565
807
  return {
566
808
  status: "REQUIRES_ORCHESTRATION",
567
- message: `Conflict: File '${filePath}' is currently locked by agent '${existing.agent_id}'`,
568
- currentLock: {
569
- agentId: existing.agent_id,
570
- intent: existing.intent,
571
- timestamp: updatedAt
572
- }
809
+ message: `Conflict: File '${filePath}' is currently locked by '${existing.agentId}'`,
810
+ currentLock: existing
573
811
  };
574
812
  }
575
813
  }
576
- const { error } = await this.supabase.from("locks").upsert({
577
- project_id: this._projectId,
578
- file_path: filePath,
579
- agent_id: agentId,
580
- intent,
581
- user_prompt: userPrompt,
582
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
583
- }, { onConflict: "project_id,file_path" });
584
- if (error) {
585
- logger.error("Lock upsert failed", error);
586
- return { status: "ERROR", message: "Database lock failed." };
587
- }
814
+ this.state.locks[filePath] = { agentId, filePath, intent, userPrompt, timestamp: Date.now() };
815
+ await this.saveState();
588
816
  await this.appendToNotepad(`
589
-
590
- ### [${agentId}] Locked '${filePath}'
591
- **Intent:** ${intent}
592
- **Prompt:** "${userPrompt}"`);
817
+ - [LOCK] ${agentId} locked ${filePath}
818
+ Intent: ${intent}`);
593
819
  return { status: "GRANTED", message: `Access granted for ${filePath}` };
594
820
  });
595
821
  }
@@ -605,7 +831,12 @@ ${notepad}`;
605
831
  const content = await this.getNotepad();
606
832
  const filename = `session-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.md`;
607
833
  const historyPath = path2.join(process.cwd(), "history", filename);
608
- await fs2.writeFile(historyPath, content);
834
+ try {
835
+ await fs2.mkdir(path2.dirname(historyPath), { recursive: true });
836
+ await fs2.writeFile(historyPath, content);
837
+ } catch (e) {
838
+ logger.warn("Failed to write local session log", e);
839
+ }
609
840
  if (this.useSupabase && this.supabase && this._projectId) {
610
841
  await this.supabase.from("sessions").insert({
611
842
  project_id: this._projectId,
@@ -616,13 +847,18 @@ ${notepad}`;
616
847
  await this.supabase.from("projects").update({ live_notepad: "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n" }).eq("id", this._projectId);
617
848
  await this.supabase.from("jobs").delete().eq("project_id", this._projectId).in("status", ["done", "cancelled"]);
618
849
  await this.supabase.from("locks").delete().eq("project_id", this._projectId);
619
- } else {
620
- this.state.liveNotepad = "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n";
621
- this.state.locks = {};
622
- this.state.jobs = Object.fromEntries(
623
- Object.entries(this.state.jobs).filter(([_, j]) => j.status !== "done" && j.status !== "cancelled")
624
- );
850
+ } else if (this.contextManager.apiUrl) {
851
+ try {
852
+ await this.callCoordination("sessions/finalize", "POST", { content });
853
+ } catch (e) {
854
+ logger.error("Failed to finalize session via API", e);
855
+ }
625
856
  }
857
+ this.state.liveNotepad = "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n";
858
+ this.state.locks = {};
859
+ this.state.jobs = Object.fromEntries(
860
+ Object.entries(this.state.jobs).filter(([_, j]) => j.status !== "done" && j.status !== "cancelled")
861
+ );
626
862
  await this.saveState();
627
863
  return {
628
864
  status: "SESSION_FINALIZED",
@@ -648,32 +884,55 @@ ${conventions}`;
648
884
  }
649
885
  // --- Billing & Usage ---
650
886
  async getSubscriptionStatus(email) {
651
- if (!this.useSupabase || !this.supabase) {
652
- return { error: "Supabase not configured." };
887
+ logger.info(`[getSubscriptionStatus] Starting - email: ${email}`);
888
+ logger.info(`[getSubscriptionStatus] Config - apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET" : "NOT SET"}, useSupabase: ${this.useSupabase}`);
889
+ if (this.contextManager.apiUrl) {
890
+ try {
891
+ logger.info(`[getSubscriptionStatus] Attempting API call to: usage?email=${encodeURIComponent(email)}`);
892
+ const result = await this.callCoordination(`usage?email=${encodeURIComponent(email)}`);
893
+ logger.info(`[getSubscriptionStatus] API call successful: ${JSON.stringify(result).substring(0, 200)}`);
894
+ return result;
895
+ } catch (e) {
896
+ logger.error(`[getSubscriptionStatus] API call failed: ${e.message}`, e);
897
+ return { error: `API call failed: ${e.message}` };
898
+ }
899
+ } else {
900
+ logger.warn("[getSubscriptionStatus] No API URL configured");
653
901
  }
654
- const { data: profile, error } = await this.supabase.from("profiles").select("subscription_status, stripe_customer_id, current_period_end").eq("email", email).single();
655
- if (error || !profile) {
656
- return { status: "unknown", message: "Profile not found." };
902
+ if (this.useSupabase && this.supabase) {
903
+ const { data: profile, error } = await this.supabase.from("profiles").select("subscription_status, stripe_customer_id, current_period_end").eq("email", email).single();
904
+ if (error || !profile) {
905
+ return { status: "unknown", message: "Profile not found." };
906
+ }
907
+ const isActive = profile.subscription_status === "pro" || profile.current_period_end && new Date(profile.current_period_end) > /* @__PURE__ */ new Date();
908
+ return {
909
+ email,
910
+ plan: isActive ? "Pro" : "Free",
911
+ status: profile.subscription_status || "free",
912
+ validUntil: profile.current_period_end
913
+ };
657
914
  }
658
- const isActive = profile.subscription_status === "pro" || profile.current_period_end && new Date(profile.current_period_end) > /* @__PURE__ */ new Date();
659
- return {
660
- email,
661
- plan: isActive ? "Pro" : "Free",
662
- status: profile.subscription_status || "free",
663
- validUntil: profile.current_period_end
664
- };
915
+ return { error: "Coordination not configured. API URL not set and Supabase not available." };
665
916
  }
666
917
  async getUsageStats(email) {
667
- if (!this.useSupabase || !this.supabase) {
668
- return { error: "Supabase not configured." };
918
+ logger.info(`[getUsageStats] Starting - email: ${email}`);
919
+ logger.info(`[getUsageStats] Config - apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET" : "NOT SET"}, useSupabase: ${this.useSupabase}`);
920
+ if (this.contextManager.apiUrl) {
921
+ try {
922
+ logger.info(`[getUsageStats] Attempting API call to: usage?email=${encodeURIComponent(email)}`);
923
+ const result = await this.callCoordination(`usage?email=${encodeURIComponent(email)}`);
924
+ logger.info(`[getUsageStats] API call successful: ${JSON.stringify(result).substring(0, 200)}`);
925
+ return { email, usageCount: result.usageCount || 0 };
926
+ } catch (e) {
927
+ logger.error(`[getUsageStats] API call failed: ${e.message}`, e);
928
+ return { error: `API call failed: ${e.message}` };
929
+ }
669
930
  }
670
- const { data: profile } = await this.supabase.from("profiles").select("usage_count").eq("email", email).single();
671
- return {
672
- email,
673
- usageCount: profile?.usage_count || 0,
674
- limit: 1e3
675
- // Hardcoded placeholder limit
676
- };
931
+ if (this.useSupabase && this.supabase) {
932
+ const { data: profile } = await this.supabase.from("profiles").select("usage_count").eq("email", email).single();
933
+ return { email, usageCount: profile?.usage_count || 0 };
934
+ }
935
+ return { error: "Coordination not configured. API URL not set and Supabase not available." };
677
936
  }
678
937
  };
679
938
 
@@ -734,7 +993,7 @@ var RagEngine = class {
734
993
  const embedding = resp.data[0].embedding;
735
994
  const { data, error } = await this.supabase.rpc("match_embeddings", {
736
995
  query_embedding: embedding,
737
- match_threshold: 0.5,
996
+ match_threshold: 0.1,
738
997
  match_count: limit,
739
998
  p_project_id: this.projectId
740
999
  });
@@ -751,44 +1010,101 @@ var RagEngine = class {
751
1010
  };
752
1011
 
753
1012
  // ../../src/local/mcp-server.ts
754
- dotenv2.config({ path: ".env.local" });
755
- if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
756
- logger.warn("Supabase credentials missing. RAG & Persistence disabled. Running in local/ephemeral mode.");
1013
+ import path3 from "path";
1014
+ import fs3 from "fs";
1015
+ if (process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_KEY) {
1016
+ logger.info("Using configuration from MCP client (mcp.json)");
1017
+ } else {
1018
+ const cwd = process.cwd();
1019
+ const possiblePaths = [
1020
+ path3.join(cwd, ".env.local"),
1021
+ path3.join(cwd, "..", ".env.local"),
1022
+ path3.join(cwd, "..", "..", ".env.local"),
1023
+ path3.join(cwd, "shared-context", ".env.local"),
1024
+ path3.join(cwd, "..", "shared-context", ".env.local")
1025
+ ];
1026
+ let envLoaded = false;
1027
+ for (const envPath of possiblePaths) {
1028
+ try {
1029
+ if (fs3.existsSync(envPath)) {
1030
+ logger.info(`[Fallback] Loading .env.local from: ${envPath}`);
1031
+ dotenv2.config({ path: envPath });
1032
+ envLoaded = true;
1033
+ break;
1034
+ }
1035
+ } catch (e) {
1036
+ }
1037
+ }
1038
+ if (!envLoaded) {
1039
+ logger.warn("No configuration found from MCP client (mcp.json) or .env.local");
1040
+ logger.warn("MCP server will use default API URL: https://aicontext.vercel.app/api/v1");
1041
+ }
757
1042
  }
758
- var manager = new ContextManager(
759
- process.env.SHARED_CONTEXT_API_URL || "https://aicontext.vercel.app/api/v1",
760
- process.env.AXIS_API_KEY || process.env.SHARED_CONTEXT_API_SECRET
761
- );
1043
+ logger.info("=== Axis MCP Server Starting ===");
1044
+ logger.info("Environment check:", {
1045
+ hasSHARED_CONTEXT_API_URL: !!process.env.SHARED_CONTEXT_API_URL,
1046
+ hasAXIS_API_KEY: !!process.env.AXIS_API_KEY,
1047
+ hasSHARED_CONTEXT_API_SECRET: !!process.env.SHARED_CONTEXT_API_SECRET,
1048
+ hasNEXT_PUBLIC_SUPABASE_URL: !!process.env.NEXT_PUBLIC_SUPABASE_URL,
1049
+ hasSUPABASE_SERVICE_ROLE_KEY: !!process.env.SUPABASE_SERVICE_ROLE_KEY,
1050
+ PROJECT_NAME: process.env.PROJECT_NAME || "default"
1051
+ });
1052
+ var apiUrl = process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_URL || "https://aicontext.vercel.app/api/v1";
1053
+ var apiSecret = process.env.AXIS_API_KEY || process.env.SHARED_CONTEXT_API_SECRET || process.env.AXIS_API_SECRET;
1054
+ var useRemoteApiOnly = !!process.env.SHARED_CONTEXT_API_URL || !!process.env.AXIS_API_KEY;
1055
+ if (useRemoteApiOnly) {
1056
+ logger.info("Running in REMOTE API mode - Supabase credentials not needed locally.");
1057
+ logger.info(`Remote API: ${apiUrl}`);
1058
+ logger.info(`API Key: ${apiSecret ? apiSecret.substring(0, 15) + "..." : "NOT SET"}`);
1059
+ } else if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
1060
+ logger.warn("No remote API configured and Supabase credentials missing. Running in local/ephemeral mode.");
1061
+ } else {
1062
+ logger.info("Running in DIRECT SUPABASE mode (development).");
1063
+ }
1064
+ logger.info("ContextManager config:", {
1065
+ apiUrl,
1066
+ hasApiSecret: !!apiSecret,
1067
+ source: useRemoteApiOnly ? "MCP config (mcp.json)" : "default/fallback"
1068
+ });
1069
+ var manager = new ContextManager(apiUrl, apiSecret);
1070
+ logger.info("NerveCenter config:", {
1071
+ useRemoteApiOnly,
1072
+ supabaseUrl: useRemoteApiOnly ? "DISABLED (using remote API)" : process.env.NEXT_PUBLIC_SUPABASE_URL ? "SET" : "NOT SET",
1073
+ supabaseKey: useRemoteApiOnly ? "DISABLED (using remote API)" : process.env.SUPABASE_SERVICE_ROLE_KEY ? "SET" : "NOT SET",
1074
+ projectName: process.env.PROJECT_NAME || "default"
1075
+ });
762
1076
  var nerveCenter = new NerveCenter(manager, {
763
- supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
764
- supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
1077
+ supabaseUrl: useRemoteApiOnly ? null : process.env.NEXT_PUBLIC_SUPABASE_URL,
1078
+ supabaseServiceRoleKey: useRemoteApiOnly ? null : process.env.SUPABASE_SERVICE_ROLE_KEY,
765
1079
  projectName: process.env.PROJECT_NAME || "default"
766
1080
  });
1081
+ logger.info("=== Axis MCP Server Initialized ===");
767
1082
  var ragEngine;
768
- if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
1083
+ if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
769
1084
  ragEngine = new RagEngine(
770
1085
  process.env.NEXT_PUBLIC_SUPABASE_URL,
771
1086
  process.env.SUPABASE_SERVICE_ROLE_KEY,
772
1087
  process.env.OPENAI_API_KEY || ""
773
1088
  );
1089
+ logger.info("Local RAG Engine initialized.");
774
1090
  }
775
1091
  async function ensureFileSystem() {
776
1092
  try {
777
- const fs3 = await import("fs/promises");
778
- const path3 = await import("path");
779
- const fsSync = await import("fs");
1093
+ const fs4 = await import("fs/promises");
1094
+ const path4 = await import("path");
1095
+ const fsSync2 = await import("fs");
780
1096
  const cwd = process.cwd();
781
1097
  logger.info(`Server CWD: ${cwd}`);
782
- const historyDir = path3.join(cwd, "history");
783
- await fs3.mkdir(historyDir, { recursive: true }).catch(() => {
1098
+ const historyDir = path4.join(cwd, "history");
1099
+ await fs4.mkdir(historyDir, { recursive: true }).catch(() => {
784
1100
  });
785
- const axisDir = path3.join(cwd, ".axis");
786
- const axisInstructions = path3.join(axisDir, "instructions");
787
- const legacyInstructions = path3.join(cwd, "agent-instructions");
788
- if (fsSync.existsSync(legacyInstructions) && !fsSync.existsSync(axisDir)) {
1101
+ const axisDir = path4.join(cwd, ".axis");
1102
+ const axisInstructions = path4.join(axisDir, "instructions");
1103
+ const legacyInstructions = path4.join(cwd, "agent-instructions");
1104
+ if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
789
1105
  logger.info("Using legacy agent-instructions directory");
790
1106
  } else {
791
- await fs3.mkdir(axisInstructions, { recursive: true }).catch(() => {
1107
+ await fs4.mkdir(axisInstructions, { recursive: true }).catch(() => {
792
1108
  });
793
1109
  const defaults = [
794
1110
  ["context.md", "# Project Context\n\n"],
@@ -796,11 +1112,11 @@ async function ensureFileSystem() {
796
1112
  ["activity.md", "# Activity Log\n\n"]
797
1113
  ];
798
1114
  for (const [file, content] of defaults) {
799
- const p = path3.join(axisInstructions, file);
1115
+ const p = path4.join(axisInstructions, file);
800
1116
  try {
801
- await fs3.access(p);
1117
+ await fs4.access(p);
802
1118
  } catch {
803
- await fs3.writeFile(p, content);
1119
+ await fs4.writeFile(p, content);
804
1120
  logger.info(`Created default context file: ${file}`);
805
1121
  }
806
1122
  }
@@ -826,17 +1142,18 @@ var UPDATE_CONTEXT_TOOL = "update_context";
826
1142
  var SEARCH_CONTEXT_TOOL = "search_codebase";
827
1143
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
828
1144
  try {
829
- return {
830
- resources: [
831
- {
832
- uri: "mcp://context/current",
833
- name: "Live Session Context",
834
- mimeType: "text/markdown",
835
- description: "The realtime state of the Nerve Center (Notepad + Locks)"
836
- },
837
- ...await manager.listFiles()
838
- ]
839
- };
1145
+ const files = await manager.listFiles();
1146
+ const resources = [
1147
+ {
1148
+ uri: "mcp://context/current",
1149
+ name: "Live Session Context",
1150
+ mimeType: "text/markdown",
1151
+ description: "The realtime state of the Nerve Center (Notepad + Locks)"
1152
+ },
1153
+ ...files
1154
+ ];
1155
+ logger.info(`[ListResources] Returning ${resources.length} resources to MCP client`);
1156
+ return { resources };
840
1157
  } catch (error) {
841
1158
  logger.error("Error listing resources", error);
842
1159
  return { resources: [] };
@@ -850,7 +1167,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
850
1167
  contents: [{
851
1168
  uri,
852
1169
  mimeType: "text/markdown",
853
- text: await nerveCenter.getLiveContext()
1170
+ text: await nerveCenter.getCoreContext()
854
1171
  }]
855
1172
  };
856
1173
  }
@@ -873,192 +1190,193 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
873
1190
  }
874
1191
  });
875
1192
  server.setRequestHandler(ListToolsRequestSchema, async () => {
876
- return {
877
- tools: [
878
- {
879
- name: READ_CONTEXT_TOOL,
880
- description: "Read the shared context files (context.md, conventions.md, activity.md)",
881
- inputSchema: {
882
- type: "object",
883
- properties: {
884
- filename: { type: "string", description: "The name of the file to read (e.g., 'context.md')" }
885
- },
886
- required: ["filename"]
887
- }
888
- },
889
- {
890
- name: UPDATE_CONTEXT_TOOL,
891
- description: "Update a shared context file",
892
- inputSchema: {
893
- type: "object",
894
- properties: {
895
- filename: { type: "string", description: "File to update" },
896
- content: { type: "string", description: "New content" },
897
- append: { type: "boolean", description: "Whether to append or overwrite (default: overwrite)" }
898
- },
899
- required: ["filename", "content"]
900
- }
901
- },
902
- {
903
- name: SEARCH_CONTEXT_TOOL,
904
- description: "Search the codebase using vector similarity.",
905
- inputSchema: {
906
- type: "object",
907
- properties: {
908
- query: { type: "string", description: "Search query" }
909
- },
910
- required: ["query"]
911
- }
912
- },
913
- // --- Billing & Usage ---
914
- {
915
- name: "get_subscription_status",
916
- description: "Check the subscription status of a user (Pro vs Free).",
917
- inputSchema: {
918
- type: "object",
919
- properties: {
920
- email: { type: "string", description: "User email to check." }
921
- },
922
- required: ["email"]
923
- }
924
- },
925
- {
926
- name: "get_usage_stats",
927
- description: "Get API usage statistics for a user.",
928
- inputSchema: {
929
- type: "object",
930
- properties: {
931
- email: { type: "string", description: "User email to check." }
932
- },
933
- required: ["email"]
934
- }
935
- },
936
- {
937
- name: "search_docs",
938
- description: "Search the Axis documentation.",
939
- inputSchema: {
940
- type: "object",
941
- properties: {
942
- query: { type: "string", description: "Search query." }
943
- },
944
- required: ["query"]
945
- }
946
- },
947
- // --- Decision & Orchestration ---
948
- {
949
- name: "propose_file_access",
950
- description: "Request a lock on a file. Checks for conflicts with other agents.",
951
- inputSchema: {
952
- type: "object",
953
- properties: {
954
- agentId: { type: "string" },
955
- filePath: { type: "string" },
956
- intent: { type: "string" },
957
- userPrompt: { type: "string", description: "The full prompt provided by the user that initiated this action." }
958
- },
959
- required: ["agentId", "filePath", "intent", "userPrompt"]
960
- }
961
- },
962
- {
963
- name: "update_shared_context",
964
- description: "Write to the in-memory Live Notepad.",
965
- inputSchema: {
966
- type: "object",
967
- properties: {
968
- agentId: { type: "string" },
969
- text: { type: "string" }
970
- },
971
- required: ["agentId", "text"]
972
- }
973
- },
974
- // --- Permanent Memory ---
975
- {
976
- name: "finalize_session",
977
- description: "End the session, archive the notepad, and clear locks.",
978
- inputSchema: { type: "object", properties: {}, required: [] }
979
- },
980
- {
981
- name: "get_project_soul",
982
- description: "Get high-level project goals and context.",
983
- inputSchema: { type: "object", properties: {}, required: [] }
984
- },
985
- // --- Job Board (Task Orchestration) ---
986
- {
987
- name: "post_job",
988
- description: "Post a new job/ticket. Supports priority and dependencies.",
989
- inputSchema: {
990
- type: "object",
991
- properties: {
992
- title: { type: "string" },
993
- description: { type: "string" },
994
- priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
995
- dependencies: { type: "array", items: { type: "string" } }
996
- },
997
- required: ["title", "description"]
998
- }
999
- },
1000
- {
1001
- name: "cancel_job",
1002
- description: "Cancel a job that is no longer needed.",
1003
- inputSchema: {
1004
- type: "object",
1005
- properties: {
1006
- jobId: { type: "string" },
1007
- reason: { type: "string" }
1008
- },
1009
- required: ["jobId", "reason"]
1010
- }
1011
- },
1012
- {
1013
- name: "force_unlock",
1014
- description: "Admin tool to forcibly remove a lock from a file.",
1015
- inputSchema: {
1016
- type: "object",
1017
- properties: {
1018
- filePath: { type: "string" },
1019
- reason: { type: "string" }
1020
- },
1021
- required: ["filePath", "reason"]
1022
- }
1023
- },
1024
- {
1025
- name: "claim_next_job",
1026
- description: "Auto-assign the next available 'todo' job to yourself.",
1027
- inputSchema: {
1028
- type: "object",
1029
- properties: {
1030
- agentId: { type: "string" }
1031
- },
1032
- required: ["agentId"]
1033
- }
1034
- },
1035
- {
1036
- name: "complete_job",
1037
- description: "Mark your assigned job as done.",
1038
- inputSchema: {
1039
- type: "object",
1040
- properties: {
1041
- agentId: { type: "string" },
1042
- jobId: { type: "string" },
1043
- outcome: { type: "string" }
1044
- },
1045
- required: ["agentId", "jobId", "outcome"]
1046
- }
1047
- },
1048
- {
1049
- name: "index_file",
1050
- description: "Force re-index a file into the RAG vector database.",
1051
- inputSchema: {
1052
- type: "object",
1053
- properties: {
1054
- filePath: { type: "string" },
1055
- content: { type: "string" }
1056
- },
1057
- required: ["filePath", "content"]
1058
- }
1193
+ const tools = [
1194
+ {
1195
+ name: READ_CONTEXT_TOOL,
1196
+ description: "**READ THIS FIRST** to understand the project's architecture, coding conventions, and active state.\n- Returns the content of core context files like `context.md` (Project Goals), `conventions.md` (Style Guide), or `activity.md`.\n- Usage: Call with `filename='context.md'` effectively.\n- Note: If you need the *current* runtime state (active locks, jobs), use the distinct resource `mcp://context/current` instead.",
1197
+ inputSchema: {
1198
+ type: "object",
1199
+ properties: {
1200
+ filename: { type: "string", description: "The name of the file to read (e.g., 'context.md', 'conventions.md')" }
1201
+ },
1202
+ required: ["filename"]
1203
+ }
1204
+ },
1205
+ {
1206
+ name: UPDATE_CONTEXT_TOOL,
1207
+ description: "**APPEND OR OVERWRITE** shared context files.\n- Use this to update the project's long-term memory (e.g., adding a new convention, updating the architectural goal).\n- For short-term updates (like 'I just fixed bug X'), use `update_shared_context` (Notepad) instead.\n- Supports `append: true` (default: false) to add to the end of a file.",
1208
+ inputSchema: {
1209
+ type: "object",
1210
+ properties: {
1211
+ filename: { type: "string", description: "File to update (e.g. 'activity.md')" },
1212
+ content: { type: "string", description: "The new content to write or append." },
1213
+ append: { type: "boolean", description: "Whether to append to the end of the file (true) or overwrite it (false). Default: false." }
1214
+ },
1215
+ required: ["filename", "content"]
1216
+ }
1217
+ },
1218
+ {
1219
+ name: SEARCH_CONTEXT_TOOL,
1220
+ description: "**SEMANTIC SEARCH** for the codebase.\n- Uses vector similarity to find relevant code snippets or documentation.\n- Best for: 'Where is the auth logic?', 'How do I handle billing?', 'Find the class that manages locks'.\n- Note: This searches *indexed* content only. For exact string matches, use `grep` (if available) or `warpgrep`.",
1221
+ inputSchema: {
1222
+ type: "object",
1223
+ properties: {
1224
+ query: { type: "string", description: "Natural language search query." }
1225
+ },
1226
+ required: ["query"]
1227
+ }
1228
+ },
1229
+ // --- Billing & Usage ---
1230
+ {
1231
+ name: "get_subscription_status",
1232
+ description: "**BILLING CHECK**: specific to the Axis business logic.\n- Returns the user's subscription tier (Pro vs Free), Stripe customer ID, and current period end.\n- Critical for gating features behind paywalls.\n- Returns 'Profile not found' if the user doesn't exist in the database.",
1233
+ inputSchema: {
1234
+ type: "object",
1235
+ properties: {
1236
+ email: { type: "string", description: "User email to check." }
1237
+ },
1238
+ required: ["email"]
1059
1239
  }
1060
- ]
1061
- };
1240
+ },
1241
+ {
1242
+ name: "get_usage_stats",
1243
+ description: "**API USAGE**: Returns a user's token usage and request counts.\n- Useful for debugging rate limits or explaining quota usage to users.",
1244
+ inputSchema: {
1245
+ type: "object",
1246
+ properties: {
1247
+ email: { type: "string", description: "User email to check." }
1248
+ },
1249
+ required: ["email"]
1250
+ }
1251
+ },
1252
+ {
1253
+ name: "search_docs",
1254
+ description: "**DOCUMENTATION SEARCH**: Searches the official Axis documentation (if indexed).\n- Use this when you need info on *how* to use Axis features, not just codebase structure.\n- Falls back to local RAG search if the remote API is unavailable.",
1255
+ inputSchema: {
1256
+ type: "object",
1257
+ properties: {
1258
+ query: { type: "string", description: "Natural language search query." }
1259
+ },
1260
+ required: ["query"]
1261
+ }
1262
+ },
1263
+ // --- Decision & Orchestration ---
1264
+ {
1265
+ name: "propose_file_access",
1266
+ description: "**CRITICAL: REQUEST FILE LOCK**.\n- **MUST** be called *before* editing any file to prevent conflicts with other agents.\n- Checks if another agent currently holds a lock.\n- Returns `GRANTED` if safe to proceed, or `REQUIRES_ORCHESTRATION` if someone else is editing.\n- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute), and `intent` (what you are doing).\n- Note: Locks expire after 30 minutes. Use `force_unlock` only if you are certain a lock is stale and blocking progress.",
1267
+ inputSchema: {
1268
+ type: "object",
1269
+ properties: {
1270
+ agentId: { type: "string" },
1271
+ filePath: { type: "string" },
1272
+ intent: { type: "string" },
1273
+ userPrompt: { type: "string", description: "The full prompt provided by the user that initiated this action." }
1274
+ },
1275
+ required: ["agentId", "filePath", "intent", "userPrompt"]
1276
+ }
1277
+ },
1278
+ {
1279
+ name: "update_shared_context",
1280
+ description: "**LIVE NOTEPAD**: The project's short-term working memory.\n- **ALWAYS** call this after completing a significant step (e.g., 'Fixed bug in auth.ts', 'Ran tests, all passed').\n- This content is visible to *all* other agents immediately.\n- Think of this as a team chat or 'standup' update.",
1281
+ inputSchema: {
1282
+ type: "object",
1283
+ properties: {
1284
+ agentId: { type: "string" },
1285
+ text: { type: "string" }
1286
+ },
1287
+ required: ["agentId", "text"]
1288
+ }
1289
+ },
1290
+ // --- Permanent Memory ---
1291
+ {
1292
+ name: "finalize_session",
1293
+ description: "**END OF SESSION HOUSEKEEPING**.\n- Archives the current Live Notepad to a permanent session log.\n- Clears all active locks and completed jobs.\n- Resets the Live Notepad for the next session.\n- Call this when the user says 'we are done' or 'start fresh'.",
1294
+ inputSchema: { type: "object", properties: {}, required: [] }
1295
+ },
1296
+ {
1297
+ name: "get_project_soul",
1298
+ description: "**HIGH-LEVEL INTENT**: Returns the 'Soul' of the project.\n- Combines `context.md`, `conventions.md`, and other core directives into a single prompt.\n- Use this at the *start* of a conversation to ground yourself in the project's reality.",
1299
+ inputSchema: { type: "object", properties: {}, required: [] }
1300
+ },
1301
+ // --- Job Board (Task Orchestration) ---
1302
+ {
1303
+ name: "post_job",
1304
+ description: "**CREATE TICKET**: Post a new task to the Job Board.\n- Use this when you identify work that needs to be done but *cannot* be done right now (e.g., refactoring, new feature).\n- Supports `dependencies` (list of other Job IDs that must be done first).\n- Priority: low, medium, high, critical.",
1305
+ inputSchema: {
1306
+ type: "object",
1307
+ properties: {
1308
+ title: { type: "string" },
1309
+ description: { type: "string" },
1310
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
1311
+ dependencies: { type: "array", items: { type: "string" }, description: "Array of Job IDs that must be completed before this job can be claimed." }
1312
+ },
1313
+ required: ["title", "description"]
1314
+ }
1315
+ },
1316
+ {
1317
+ name: "cancel_job",
1318
+ description: "**KILL TICKET**: Cancel a job that is no longer needed.\n- Requires `jobId` and a `reason`.",
1319
+ inputSchema: {
1320
+ type: "object",
1321
+ properties: {
1322
+ jobId: { type: "string" },
1323
+ reason: { type: "string" }
1324
+ },
1325
+ required: ["jobId", "reason"]
1326
+ }
1327
+ },
1328
+ {
1329
+ name: "force_unlock",
1330
+ description: "**ADMIN OVERRIDE**: Break a file lock.\n- **WARNING**: Only use this if a lock is clearly stale or the locking agent has crashed.\n- Will forcibly remove the lock from the database.",
1331
+ inputSchema: {
1332
+ type: "object",
1333
+ properties: {
1334
+ filePath: { type: "string" },
1335
+ reason: { type: "string" }
1336
+ },
1337
+ required: ["filePath", "reason"]
1338
+ }
1339
+ },
1340
+ {
1341
+ name: "claim_next_job",
1342
+ description: "**AUTO-ASSIGNMENT**: Ask the Job Board for the next most important task.\n- Respects priority (Critical > High > ...) and dependencies (won't assign a job if its deps aren't done).\n- Returns the Job object if successful, or 'NO_JOBS_AVAILABLE'.\n- Use this when you are idle and looking for work.",
1343
+ inputSchema: {
1344
+ type: "object",
1345
+ properties: {
1346
+ agentId: { type: "string" }
1347
+ },
1348
+ required: ["agentId"]
1349
+ }
1350
+ },
1351
+ {
1352
+ name: "complete_job",
1353
+ description: "**CLOSE TICKET**: Mark a job as done.\n- Requires `outcome` (what was done).\n- If you are not the assigned agent, you must provide the `completionKey`.",
1354
+ inputSchema: {
1355
+ type: "object",
1356
+ properties: {
1357
+ agentId: { type: "string" },
1358
+ jobId: { type: "string" },
1359
+ outcome: { type: "string" },
1360
+ completionKey: { type: "string", description: "Optional key to authorize completion if not the assigned agent." }
1361
+ },
1362
+ required: ["agentId", "jobId", "outcome"]
1363
+ }
1364
+ },
1365
+ {
1366
+ name: "index_file",
1367
+ description: "**UPDATE SEARCH INDEX**: Add a file's content to the RAG vector database.\n- Call this *immediately* after creating a new file or significantly refactoring an existing one.\n- Ensures future `search_codebase` calls return up-to-date results.",
1368
+ inputSchema: {
1369
+ type: "object",
1370
+ properties: {
1371
+ filePath: { type: "string" },
1372
+ content: { type: "string" }
1373
+ },
1374
+ required: ["filePath", "content"]
1375
+ }
1376
+ }
1377
+ ];
1378
+ logger.info(`[ListTools] Returning ${tools.length} tools to MCP client`);
1379
+ return { tools };
1062
1380
  });
1063
1381
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1064
1382
  const { name, arguments: args } = request.params;
@@ -1122,20 +1440,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1122
1440
  }
1123
1441
  if (name === "get_subscription_status") {
1124
1442
  const email = String(args?.email);
1125
- const result = await nerveCenter.getSubscriptionStatus(email);
1126
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1443
+ logger.info(`[get_subscription_status] Called with email: ${email}`);
1444
+ try {
1445
+ const result = await nerveCenter.getSubscriptionStatus(email);
1446
+ logger.info(`[get_subscription_status] Result: ${JSON.stringify(result).substring(0, 200)}`);
1447
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1448
+ } catch (e) {
1449
+ logger.error(`[get_subscription_status] Exception: ${e.message}`, e);
1450
+ return { content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
1451
+ }
1127
1452
  }
1128
1453
  if (name === "get_usage_stats") {
1129
1454
  const email = String(args?.email);
1130
- const result = await nerveCenter.getUsageStats(email);
1131
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1455
+ logger.info(`[get_usage_stats] Called with email: ${email}`);
1456
+ try {
1457
+ const result = await nerveCenter.getUsageStats(email);
1458
+ logger.info(`[get_usage_stats] Result: ${JSON.stringify(result).substring(0, 200)}`);
1459
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1460
+ } catch (e) {
1461
+ logger.error(`[get_usage_stats] Exception: ${e.message}`, e);
1462
+ return { content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
1463
+ }
1132
1464
  }
1133
1465
  if (name === "search_docs") {
1134
1466
  const query = String(args?.query);
1135
1467
  try {
1136
- const formatted = await manager.searchContext(query);
1468
+ const formatted = await manager.searchContext(query, nerveCenter.currentProjectName);
1137
1469
  return { content: [{ type: "text", text: formatted }] };
1138
1470
  } catch (err) {
1471
+ if (ragEngine) {
1472
+ const results = await ragEngine.search(query);
1473
+ return { content: [{ type: "text", text: results.join("\n---\n") }] };
1474
+ }
1139
1475
  return {
1140
1476
  content: [{ type: "text", text: `Search Error: ${err}` }],
1141
1477
  isError: true
@@ -1181,8 +1517,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1181
1517
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1182
1518
  }
1183
1519
  if (name === "complete_job") {
1184
- const { agentId, jobId, outcome } = args;
1185
- const result = await nerveCenter.completeJob(agentId, jobId, outcome);
1520
+ const { agentId, jobId, outcome, completionKey } = args;
1521
+ const result = await nerveCenter.completeJob(agentId, jobId, outcome, completionKey);
1186
1522
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1187
1523
  }
1188
1524
  throw new Error(`Tool not found: ${name}`);
@@ -1194,9 +1530,11 @@ async function main() {
1194
1530
  ragEngine.setProjectId(nerveCenter.projectId);
1195
1531
  logger.info(`Local RAG Engine linked to Project ID: ${nerveCenter.projectId}`);
1196
1532
  }
1533
+ logger.info("MCP server ready - all tools and resources registered");
1197
1534
  const transport = new StdioServerTransport();
1198
1535
  await server.connect(transport);
1199
1536
  logger.info("Shared Context MCP Server running on stdio");
1537
+ logger.info("Server is now accepting tool calls from MCP clients");
1200
1538
  }
1201
1539
  main().catch((error) => {
1202
1540
  logger.error("Server error", error);