import { writeAuditLog, ensureAuditLogTable } from "./audit.js";
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
function parseSearchInput(args: unknown): SearchInput {
  const obj = parseObject(args);
  const project_path = optionalString(obj, "project_path");
  return {
    project_path,
    project_id: optionalString(obj, "project_id"),
    query: requireString(obj, "query"),
    scope: optionalString(obj, "scope"),
    topic: optionalString(obj, "topic"),
    source_type: optionalString(obj, "source_type"),
    status: optionalString(obj, "status"),
    mode: parseMode(obj.mode),
    limit: optionalNumber(obj, "limit"),
    include_content: optionalBoolean(obj, "include_content"),
    tags: optionalStringArray(obj, "tags")
  };
}

function parseReadInput(args: unknown): ReadEntryInput {
  const obj = parseObject(args);
  const project_path = optionalString(obj, "project_path");
  return {
    project_path,
    project_id: optionalString(obj, "project_id"),
    entry_id: requireString(obj, "entry_id"),
    mode: parseMode(obj.mode)
  };
}

function parseListTaxonomyInput(args: unknown): ListTaxonomyInput {
  const obj = parseObject(args);
  return {
    project_path: optionalString(obj, "project_path"),
    project_id: optionalString(obj, "project_id"),
    status: optionalString(obj, "status"),
    scope: optionalString(obj, "scope"),
    topic: optionalString(obj, "topic")
  };
}

function parseInvalidateEntryInput(args: unknown): InvalidateEntryInput {
  const obj = parseObject(args);
  return {
    project_path: optionalString(obj, "project_path"),
    project_id: optionalString(obj, "project_id"),
    entry_id: requireString(obj, "entry_id"),
    reason: optionalString(obj, "reason")
  };
}
import { createHash, randomUUID } from "node:crypto";
import sqlite3 from "sqlite3";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  type CallToolRequest,
  type Tool
} from "@modelcontextprotocol/sdk/types.js";
import { loadConfig, resolveAllowedRootsForProject, resolveProject, validateProjectPath } from "./config.js";
import { isPathAllowed } from "./path-policy.js";
import { MemoryNodeError, NotFoundError, ValidationError } from "./errors.js";
import { createOkBase, toMcpError, toMcpSuccess } from "./responses.js";
import type {
  AddEntryInput,
  DatabaseAdapter,
  InvalidateEntryInput,
  ListTaxonomyInput,
  MemoryNodeContext,
  ReadEntryInput,
  SearchInput
} from "./types.js";

const TOOL_MEMORY_STATUS = "memory_status";
const TOOL_MEMORY_ADD_ENTRY = "memory_add_entry";
const TOOL_MEMORY_SEARCH = "memory_search";
const TOOL_MEMORY_READ_ENTRY = "memory_read_entry";
const TOOL_MEMORY_LIST_SCOPES = "memory_list_scopes";
const TOOL_MEMORY_LIST_TOPICS = "memory_list_topics";
const TOOL_MEMORY_LIST_TAGS = "memory_list_tags";
const TOOL_MEMORY_INVALIDATE_ENTRY = "memory_invalidate_entry";

const VERIFICATION_HINT =
  "Contesto operativo: verificare su docs/code/ticket prima di trattarlo come fonte attuale.";

const FTS_FALLBACK_WARNING = "FTS non disponibile: ricerca degradata con fallback LIKE.";

const SOURCE_TYPES = [
  "agent_note",
  "decision",
  "handoff",
  "ticket_summary",
  "commit_context",
  "doc_observation",
  "file_observation",
  "bug_pattern",
  "workaround",
  "user_preference",
  "environment_note"
] as const;

const STATUS_VALUES = ["active", "invalidated", "archived", "superseded"] as const;

class SqliteAdapter implements DatabaseAdapter {
  private readonly db: sqlite3.Database;

  public constructor(path: string) {
    this.db = new sqlite3.Database(path);
  }

  public run(sql: string, params: readonly unknown[] = []): Promise<{ lastID: number; changes: number }> {
    return new Promise((resolve, reject) => {
      this.db.run(sql, params as unknown as unknown[], function runCallback(this: sqlite3.RunResult, err: Error | null) {
        if (err) {
          reject(err);
          return;
        }
        resolve({ lastID: this.lastID, changes: this.changes });
      });
    });
  }

  public get<T>(sql: string, params: readonly unknown[] = []): Promise<T | undefined> {
    return new Promise((resolve, reject) => {
      this.db.get(sql, params as unknown as unknown[], (err: Error | null, row: T | undefined) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(row);
      });
    });
  }

  public all<T>(sql: string, params: readonly unknown[] = []): Promise<T[]> {
    return new Promise((resolve, reject) => {
      this.db.all(sql, params as unknown as unknown[], (err: Error | null, rows: T[]) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(rows);
      });
    });
  }

  public exec(sql: string): Promise<void> {
    return new Promise((resolve, reject) => {
      this.db.exec(sql, (err: Error | null) => {
        if (err) {
          reject(err);
          return;
        }
        resolve();
      });
    });
  }

  public close(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.db.close((err: Error | null) => {
        if (err) {
          reject(err);
          return;
        }
        resolve();
      });
    });
  }
}

function parseObject(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new ValidationError("Input must be an object.");
  }
  return value as Record<string, unknown>;
}

function requireString(obj: Record<string, unknown>, key: string): string {
  const value = obj[key];
  if (typeof value !== "string" || value.trim().length === 0) {
    throw new ValidationError(`${key} must be a non-empty string.`);
  }
  return value.trim();
}

function optionalString(obj: Record<string, unknown>, key: string): string | undefined {
  const value = obj[key];
  if (value === undefined) {
    return undefined;
  }
  if (typeof value !== "string") {
    throw new ValidationError(`${key} must be a string when provided.`);
  }
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function optionalStringArray(obj: Record<string, unknown>, key: string): string[] | undefined {
  const value = obj[key];
  if (value === undefined) {
    return undefined;
  }
  if (!Array.isArray(value)) {
    throw new ValidationError(`${key} must be an array of strings.`);
  }
  return value.map((entry) => {
    if (typeof entry !== "string") {
      throw new ValidationError(`${key} must contain only strings.`);
    }
    return entry;
  });
}

function optionalNumber(obj: Record<string, unknown>, key: string): number | undefined {
  const value = obj[key];
  if (value === undefined) {
    return undefined;
  }
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new ValidationError(`${key} must be a finite number when provided.`);
  }
  return value;
}

function optionalBoolean(obj: Record<string, unknown>, key: string): boolean | undefined {
  const value = obj[key];
  if (value === undefined) {
    return undefined;
  }
  if (typeof value !== "boolean") {
    throw new ValidationError(`${key} must be a boolean when provided.`);
  }
  return value;
}

function parseMode(value: unknown): "project" | "cross_project" | undefined {
  if (value === undefined) {
    return undefined;
  }
  if (value === "project" || value === "cross_project") {
    return value;
  }
  throw new ValidationError("mode must be either 'project' or 'cross_project'.");
}

function assertSupportedSourceType(value: string): void {
  if (!SOURCE_TYPES.includes(value as (typeof SOURCE_TYPES)[number])) {
    throw new ValidationError("source_type is not supported.");
  }
}

function assertSupportedStatus(value: string): void {
  if (!STATUS_VALUES.includes(value as (typeof STATUS_VALUES)[number])) {
    throw new ValidationError("status is not supported.");
  }
}

function normalizeTags(tags?: readonly string[]): string[] {
  if (!tags) {
    return [];
  }
  const unique = new Set<string>();
  for (const rawTag of tags) {
    const trimmed = rawTag.trim().toLowerCase();
    if (trimmed.length > 0) {
      unique.add(trimmed);
    }
  }
  return [...unique].sort();
}

function nowIso(): string {
  return new Date().toISOString();
}

export function createContentSha256(content: string): string {
  return createHash("sha256").update(content, "utf8").digest("hex");
}

function parseAddEntryInput(args: unknown): AddEntryInput {
  const obj = parseObject(args);
  const sourceType = requireString(obj, "source_type");
  assertSupportedSourceType(sourceType);

  const importance = optionalNumber(obj, "importance");
  if (importance !== undefined && (!Number.isInteger(importance) || importance < 1 || importance > 5)) {
    throw new ValidationError("importance must be an integer between 1 and 5.");
  }

  const confidence = optionalNumber(obj, "confidence");
  if (confidence !== undefined && (confidence < 0 || confidence > 1)) {
    throw new ValidationError("confidence must be between 0 and 1.");
  }

  const project_path = requireString(obj, "project_path");
  const source_path = optionalString(obj, "source_path");

  return {
    project_path,
    project_id: optionalString(obj, "project_id"),
    scope: requireString(obj, "scope"),
    topic: requireString(obj, "topic"),
    title: optionalString(obj, "title"),
    content: requireString(obj, "content"),
    summary: optionalString(obj, "summary"),
    source_type: sourceType,
    source_ref: optionalString(obj, "source_ref"),
    source_path,
    source_uri: optionalString(obj, "source_uri"),
    importance,
    confidence,
    tags: optionalStringArray(obj, "tags"),
    dedupe: optionalBoolean(obj, "dedupe")
  };
}
function parseStatusInput(args: unknown): { project_path?: string; project_id?: string } {
  const obj = parseObject(args);
  return {
    project_path: optionalString(obj, "project_path"),
    project_id: optionalString(obj, "project_id")
  };
}

function assertMaxLength(value: string | undefined, limit: number, fieldName: string): void {
  if (value !== undefined && value.length > limit) {
    throw new ValidationError(`${fieldName} exceeds max length (${limit}).`);
  }
}

function validateTags(context: MemoryNodeContext, tags: readonly string[] | undefined): void {
  const limits = context.config.inputLimits;
  if (tags && tags.length > limits.maxTags) {
    throw new ValidationError(`tags exceeds MEMORY_MAX_TAGS (${limits.maxTags}).`);
  }
  for (const tag of tags ?? []) {
    assertMaxLength(tag, limits.maxTagChars, "tag");
  }
}

function validateProjectScopedInputLimits(
  context: MemoryNodeContext,
  input: { project_id?: string; scope?: string; topic?: string; tags?: readonly string[] }
): void {
  const limits = context.config.inputLimits;
  assertMaxLength(input.project_id, limits.maxProjectIdChars, "project_id");
  assertMaxLength(input.scope, limits.maxScopeChars, "scope");
  assertMaxLength(input.topic, limits.maxTopicChars, "topic");
  validateTags(context, input.tags);
}

function validateAddEntryInputLimits(context: MemoryNodeContext, input: AddEntryInput): void {
  const limits = context.config.inputLimits;
  validateProjectScopedInputLimits(context, input);
  assertMaxLength(input.title, limits.maxTitleChars, "title");
  assertMaxLength(input.summary, limits.maxSummaryChars, "summary");
  assertMaxLength(input.source_ref, limits.maxSourceRefChars, "source_ref");
  assertMaxLength(input.source_uri, limits.maxSourceUriChars, "source_uri");
}

function validateSearchInput(context: MemoryNodeContext, input: SearchInput): void {
  validateProjectScopedInputLimits(context, input);
  if (input.source_type) {
    assertSupportedSourceType(input.source_type);
  }
  if (input.status) {
    assertSupportedStatus(input.status);
  }
}

function validateListTaxonomyInput(context: MemoryNodeContext, input: ListTaxonomyInput): void {
  validateProjectScopedInputLimits(context, input);
  if (input.status) {
    assertSupportedStatus(input.status);
  }
}

function validateInvalidateEntryInput(context: MemoryNodeContext, input: InvalidateEntryInput): void {
  const limits = context.config.inputLimits;
  assertMaxLength(input.project_id, limits.maxProjectIdChars, "project_id");
  assertMaxLength(input.reason, limits.maxSummaryChars, "reason");
}

async function maybeWriteAuditLog(
  context: MemoryNodeContext,
  payload: Parameters<typeof writeAuditLog>[1]
): Promise<void> {
  if (!context.config.auditLogEnabled) {
    return;
  }
  await writeAuditLog(context.db, payload);
}

function assertAllowedPaths(projectPath: string, sourcePath?: string): string {
  const validatedProjectPath = validateProjectPath(projectPath);
  const allowedRoots = resolveAllowedRootsForProject(validatedProjectPath);
  if (!isPathAllowed(validatedProjectPath, allowedRoots)) {
    throw new ValidationError("project_path is not allowed by allowedRoots policy.");
  }

  if (sourcePath && !isPathAllowed(sourcePath, allowedRoots)) {
    throw new ValidationError("source_path is not allowed by allowedRoots policy.");
  }

  return validatedProjectPath;
}

async function ensureProjectRow(context: MemoryNodeContext, projectId: string, projectPath?: string): Promise<void> {
  const now = nowIso();
  await context.db.run(
    `
      INSERT INTO projects(id, canonical_path, display_name, git_remote, created_at, updated_at, metadata_json)
      VALUES (?, ?, ?, NULL, ?, ?, NULL)
      ON CONFLICT(id) DO UPDATE SET
        canonical_path = COALESCE(excluded.canonical_path, projects.canonical_path),
        updated_at = excluded.updated_at
    `,
    [projectId, projectPath ?? null, projectPath ?? projectId, now, now]
  );
}

async function upsertTags(context: MemoryNodeContext, projectId: string, entryId: string, tags: readonly string[]): Promise<void> {
  await context.db.run("DELETE FROM memory_entry_tags WHERE entry_id = ?", [entryId]);
  for (const tagName of tags) {
    await context.db.run(
      `
        INSERT INTO memory_tags(project_id, name, description, created_at)
        VALUES (?, ?, NULL, ?)
        ON CONFLICT(project_id, name) DO NOTHING
      `,
      [projectId, tagName, nowIso()]
    );

    const tag = await context.db.get<{ id: number }>(
      "SELECT id FROM memory_tags WHERE project_id = ? AND name = ?",
      [projectId, tagName]
    );
    if (tag) {
      await context.db.run(
        "INSERT OR IGNORE INTO memory_entry_tags(entry_id, tag_id) VALUES (?, ?)",
        [entryId, tag.id]
      );
    }
  }

  try {
    await context.db.run("UPDATE memory_entries_fts SET tags = ? WHERE entry_id = ?", [tags.join(" "), entryId]);
  } catch {
    // FTS table may be unavailable in degraded mode.
  }
}

async function readEntryTags(context: MemoryNodeContext, entryId: string): Promise<string[]> {
  const rows = await context.db.all<{ name: string }>(
    `
      SELECT mt.name
      FROM memory_entry_tags met
      INNER JOIN memory_tags mt ON mt.id = met.tag_id
      WHERE met.entry_id = ?
      ORDER BY mt.name ASC
    `,
    [entryId]
  );
  return rows.map((row) => row.name);
}

async function resolveProjectFromInput(
  projectPath: string | undefined,
  projectId: string | undefined
): Promise<{ projectId: string; projectPath?: string; warnings: string[] }> {
  const validatedProjectPath = projectPath ? validateProjectPath(projectPath) : undefined;
  const resolution = resolveProject({ projectPath: validatedProjectPath, inputProjectId: projectId });
  return {
    projectId: resolution.projectId,
    projectPath: validatedProjectPath,
    warnings: [...resolution.warnings]
  };
}

async function resolveRequiredProject(
  context: MemoryNodeContext,
  projectPath: string | undefined,
  projectId: string | undefined
): Promise<{ projectId: string; projectPath?: string; warnings: string[] }> {
  if (!projectPath && !projectId) {
    throw new ValidationError("project_path or project_id is required.");
  }
  if (projectPath) {
    assertAllowedPaths(projectPath);
  }
  const resolved = await resolveProjectFromInput(projectPath, projectId);
  await ensureProjectRow(context, resolved.projectId, resolved.projectPath);
  return resolved;
}

async function resolveRequiredProjectReadOnly(
  projectPath: string | undefined,
  projectId: string | undefined
): Promise<{ projectId: string; projectPath?: string; warnings: string[] }> {
  if (!projectPath && !projectId) {
    throw new ValidationError("project_path or project_id is required.");
  }
  if (projectPath) {
    assertAllowedPaths(projectPath);
  }
  return resolveProjectFromInput(projectPath, projectId);
}

export async function initDb(db: DatabaseAdapter): Promise<void> {
  await ensureAuditLogTable(db);
  await db.exec(`
    PRAGMA foreign_keys = ON;

    CREATE TABLE IF NOT EXISTS projects (
      id TEXT PRIMARY KEY,
      canonical_path TEXT,
      display_name TEXT,
      git_remote TEXT,
      created_at TEXT NOT NULL,
      updated_at TEXT NOT NULL,
      metadata_json TEXT
    );

    CREATE TABLE IF NOT EXISTS memory_entries (
      id TEXT PRIMARY KEY,
      project_id TEXT NOT NULL,
      scope TEXT NOT NULL,
      topic TEXT NOT NULL,
      title TEXT,
      content TEXT NOT NULL,
      summary TEXT,
      source_type TEXT NOT NULL,
      source_ref TEXT,
      source_path TEXT,
      source_uri TEXT,
      content_sha256 TEXT NOT NULL,
      status TEXT NOT NULL DEFAULT 'active',
      confidence REAL,
      importance INTEGER NOT NULL DEFAULT 3,
      created_by TEXT,
      created_at TEXT NOT NULL,
      updated_at TEXT NOT NULL,
      invalidated_at TEXT,
      invalidation_reason TEXT,
      metadata_json TEXT,
      FOREIGN KEY(project_id) REFERENCES projects(id)
    );

    CREATE TABLE IF NOT EXISTS memory_tags (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      project_id TEXT NOT NULL,
      name TEXT NOT NULL,
      description TEXT,
      created_at TEXT NOT NULL,
      UNIQUE(project_id, name)
    );

    CREATE TABLE IF NOT EXISTS memory_entry_tags (
      entry_id TEXT NOT NULL,
      tag_id INTEGER NOT NULL,
      PRIMARY KEY(entry_id, tag_id),
      FOREIGN KEY(entry_id) REFERENCES memory_entries(id) ON DELETE CASCADE,
      FOREIGN KEY(tag_id) REFERENCES memory_tags(id) ON DELETE CASCADE
    );
  `);

  try {
    await db.exec(`
      CREATE VIRTUAL TABLE IF NOT EXISTS memory_entries_fts USING fts5(
        entry_id UNINDEXED,
        title,
        content,
        summary,
        scope,
        topic,
        tags
      );

      CREATE TRIGGER IF NOT EXISTS memory_entries_ai AFTER INSERT ON memory_entries BEGIN
        INSERT INTO memory_entries_fts(entry_id, title, content, summary, scope, topic, tags)
        VALUES (new.id, coalesce(new.title, ''), new.content, coalesce(new.summary, ''), new.scope, new.topic, '');
      END;

      CREATE TRIGGER IF NOT EXISTS memory_entries_au AFTER UPDATE ON memory_entries BEGIN
        UPDATE memory_entries_fts
        SET title = coalesce(new.title, ''),
            content = new.content,
            summary = coalesce(new.summary, ''),
            scope = new.scope,
            topic = new.topic
        WHERE entry_id = new.id;
      END;

      CREATE TRIGGER IF NOT EXISTS memory_entries_ad AFTER DELETE ON memory_entries BEGIN
        DELETE FROM memory_entries_fts WHERE entry_id = old.id;
      END;
    `);
  } catch {
    // Degraded mode: FTS unavailable in this SQLite build.
  }
}

async function isFtsAvailable(db: DatabaseAdapter): Promise<boolean> {
  const row = await db.get<{ has_fts: number }>(
    `
      SELECT CASE WHEN EXISTS(
        SELECT 1
        FROM sqlite_master
        WHERE type = 'table' AND name = 'memory_entries_fts'
      ) THEN 1 ELSE 0 END AS has_fts
    `
  );
  return (row?.has_fts ?? 0) === 1;
}

function escapeLikePattern(value: string): string {
  return value.replace(/[\\%_]/gu, "\\$&");
}

function tokenizeSearchQuery(query: string): string[] {
  return String(query || "")
    .trim()
    .split(/\s+/u)
    .filter(Boolean);
}

function buildPlainTextSearchQuery(query: string): string {
  const terms = tokenizeSearchQuery(query);
  if (terms.length === 0) return "";
  return terms.map((term) => `"${term.replace(/"/gu, '""')}"`).join(" AND ");
}

function buildTokenLikeClause(columnNames: string[], terms: string[]): { clause: string; params: string[] } {
  if (terms.length === 0) {
    return { clause: "", params: [] };
  }

  const params: string[] = [];
  const clause = terms
    .map((term) => {
      const escapedTerm = `%${escapeLikePattern(term)}%`;
      const perColumnClause = columnNames
        .map((columnName) => {
          params.push(escapedTerm);
          return `${columnName} LIKE ? ESCAPE '\\'`;
        })
        .join(" OR ");
      return `(${perColumnClause})`;
    })
    .join(" AND ");

  return { clause, params };
}

function isFtsQuerySyntaxError(error: unknown): boolean {
  const message = String((error as { message?: string })?.message || "");
  return /fts5: syntax error|malformed MATCH expression/i.test(message);
}

function createLikeSnippet(query: string, title: string | null, summary: string | null, content: string): string {
  const needle = query.toLowerCase();
  const haystacks = [title ?? "", summary ?? "", content];

  for (const text of haystacks) {
    const index = text.toLowerCase().indexOf(needle);
    if (index >= 0) {
      const start = Math.max(0, index - 24);
      const end = Math.min(text.length, index + query.length + 24);
      const prefix = start > 0 ? "..." : "";
      const suffix = end < text.length ? "..." : "";
      const head = text.slice(start, index);
      const match = text.slice(index, index + query.length);
      const tail = text.slice(index + query.length, end);
      return `${prefix}${head}>>>${match}<<<${tail}${suffix}`;
    }
  }

  const fallback = (title ?? summary ?? content).slice(0, 72);
  return fallback.length < (title ?? summary ?? content).length ? `${fallback}...` : fallback;
}

function clampSearchLimit(configLimit: number, requestedLimit?: number): number {
  if (requestedLimit === undefined) {
    return configLimit;
  }
  if (!Number.isInteger(requestedLimit) || requestedLimit <= 0) {
    throw new ValidationError("limit must be a positive integer.");
  }
  return Math.min(requestedLimit, configLimit);
}

export async function handleMemoryStatus(context: MemoryNodeContext, args: unknown): Promise<Record<string, unknown>> {
  const input = parseStatusInput(args);
  assertMaxLength(input.project_id, context.config.inputLimits.maxProjectIdChars, "project_id");
  let resolved: { projectId: string; projectPath?: string; warnings: string[] } | undefined;

  if (input.project_path || input.project_id) {
    if (input.project_path) {
      assertAllowedPaths(input.project_path);
    }
    resolved = await resolveProjectFromInput(input.project_path, input.project_id);
  }

  const counts = resolved
    ? await context.db.get<{
        entries: number;
        active: number;
        invalidated: number;
        archived: number;
        superseded: number;
      }>(
        `
          SELECT
            COUNT(*) AS entries,
            SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active,
            SUM(CASE WHEN status = 'invalidated' THEN 1 ELSE 0 END) AS invalidated,
            SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) AS archived,
            SUM(CASE WHEN status = 'superseded' THEN 1 ELSE 0 END) AS superseded
          FROM memory_entries
          WHERE project_id = ?
        `,
        [resolved.projectId]
      )
    : await context.db.get<{
        entries: number;
        active: number;
        invalidated: number;
        archived: number;
        superseded: number;
      }>(
        `
          SELECT
            COUNT(*) AS entries,
            SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active,
            SUM(CASE WHEN status = 'invalidated' THEN 1 ELSE 0 END) AS invalidated,
            SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) AS archived,
            SUM(CASE WHEN status = 'superseded' THEN 1 ELSE 0 END) AS superseded
          FROM memory_entries
        `
      );

  const projectRow = resolved
    ? await context.db.get<{ id: string; display_name: string | null; canonical_path: string | null }>(
        "SELECT id, display_name, canonical_path FROM projects WHERE id = ?",
        [resolved.projectId]
      )
    : undefined;

  const ftsEnabled = await isFtsAvailable(context.db);
  const warnings = resolved ? [...resolved.warnings] : [];
  if (!ftsEnabled) {
    warnings.push(FTS_FALLBACK_WARNING);
  }

  return {
    ...(resolved ? createOkBase(resolved.projectId, warnings) : { ok: true, warnings }),
    db_path: context.config.dbPath,
    project: resolved
      ? {
          id: projectRow?.id ?? resolved.projectId,
          display_name: projectRow?.display_name ?? null,
          canonical_path: projectRow?.canonical_path ?? resolved.projectPath ?? null
        }
      : null,
    counts: {
      entries: counts?.entries ?? 0,
      active: counts?.active ?? 0,
      invalidated: counts?.invalidated ?? 0,
      archived: counts?.archived ?? 0,
      superseded: counts?.superseded ?? 0
    },
    features: {
      fts: ftsEnabled,
      vector: context.config.featureFlags.vectorSearch,
      audit_log: context.config.auditLogEnabled
    }
  };
}

export async function handleMemoryAddEntry(context: MemoryNodeContext, args: unknown): Promise<Record<string, unknown>> {
  const input = parseAddEntryInput(args);
  validateAddEntryInputLimits(context, input);
  assertAllowedPaths(input.project_path, input.source_path);
  const resolved = await resolveProjectFromInput(input.project_path, input.project_id);
  await ensureProjectRow(context, resolved.projectId, resolved.projectPath);

  const contentBytes = Buffer.byteLength(input.content, "utf8");
  if (contentBytes > context.config.maxEntryBytes) {
    throw new ValidationError(`content exceeds MEMORY_MAX_ENTRY_BYTES (${context.config.maxEntryBytes}).`);
  }

  const normalizedTags = normalizeTags(input.tags);
  const contentSha256 = createContentSha256(input.content);
  const dedupeEnabled = input.dedupe === true;

  if (dedupeEnabled) {
    const existing = await context.db.get<{ id: string }>(
      `
        SELECT id
        FROM memory_entries
        WHERE project_id = ?
          AND scope = ?
          AND topic = ?
          AND content_sha256 = ?
      `,
      [resolved.projectId, input.scope, input.topic, contentSha256]
    );
    if (existing) {
      await maybeWriteAuditLog(context, {
        project_id: resolved.projectId,
        user: undefined,
        action: "dedupe_memory_add_entry",
        entry_id: existing.id,
        details: { ...input, deduped: true }
      });
      return {
        ...createOkBase(resolved.projectId, resolved.warnings),
        deduped: true,
        existing_entry_id: existing.id
      };
    }
  }

  const entryId = randomUUID();
  const now = nowIso();
  await context.db.run(
    `
      INSERT INTO memory_entries(
        id, project_id, scope, topic, title, content, summary, source_type, source_ref, source_path, source_uri,
        content_sha256, status, confidence, importance, created_by, created_at, updated_at, invalidated_at,
        invalidation_reason, metadata_json
      )
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, NULL, ?, ?, NULL, NULL, NULL)
    `,
    [
      entryId,
      resolved.projectId,
      input.scope,
      input.topic,
      input.title ?? null,
      input.content,
      input.summary ?? null,
      input.source_type,
      input.source_ref ?? null,
      input.source_path ?? null,
      input.source_uri ?? null,
      contentSha256,
      input.confidence ?? null,
      input.importance ?? 3,
      now,
      now
    ]
  );

  await upsertTags(context, resolved.projectId, entryId, normalizedTags);

  await maybeWriteAuditLog(context, {
    project_id: resolved.projectId,
    user: undefined,
    action: "memory_add_entry",
    entry_id: entryId,
    details: { ...input, deduped: false }
  });

  return {
    ...createOkBase(resolved.projectId, resolved.warnings),
    deduped: false,
    entry_id: entryId
  };
}

export async function handleMemorySearch(context: MemoryNodeContext, args: unknown): Promise<Record<string, unknown>> {
  const input = parseSearchInput(args);
  validateSearchInput(context, input);
  if (input.project_path) {
    assertAllowedPaths(input.project_path);
  }
  const mode = input.mode ?? "project";
  const includeContent = input.include_content === true;
  const limit = clampSearchLimit(context.config.maxSearchResults, input.limit);
  const status = input.status ?? "active";
  const tags = normalizeTags(input.tags);

  let resolved: { projectId: string; projectPath?: string; warnings: string[] } | undefined;
  if (mode === "project") {
    if (!input.project_path && !input.project_id) {
      throw new ValidationError("project mode requires project_path or project_id.");
    }
    resolved = await resolveProjectFromInput(input.project_path, input.project_id);
  } else if (input.project_path || input.project_id) {
    resolved = await resolveProjectFromInput(input.project_path, input.project_id);
  }

  const ftsEnabled = await isFtsAvailable(context.db);
  let usedLikeFallback = !ftsEnabled;
  let fallbackReason: "fts_unavailable" | "fts_match_error" | null = !ftsEnabled ? "fts_unavailable" : null;
  const queryTerms = tokenizeSearchQuery(input.query);
  const where: string[] = [];
  const params: unknown[] = [];

  if (resolved && mode === "project") {
    where.push("e.project_id = ?");
    params.push(resolved.projectId);
  }
  if (input.scope) {
    where.push("e.scope = ?");
    params.push(input.scope);
  }
  if (input.topic) {
    where.push("e.topic = ?");
    params.push(input.topic);
  }
  if (input.source_type) {
    where.push("e.source_type = ?");
    params.push(input.source_type);
  }
  if (status) {
    where.push("e.status = ?");
    params.push(status);
  }
  if (tags.length > 0) {
    const placeholders = tags.map(() => "?").join(",");
    where.push(`
      EXISTS (
        SELECT 1
        FROM memory_entry_tags met
        INNER JOIN memory_tags mt ON mt.id = met.tag_id
        WHERE met.entry_id = e.id
          AND mt.project_id = e.project_id
          AND mt.name IN (${placeholders})
      )
    `);
    params.push(...tags);
  }

  params.push(limit);

  let rows: Array<{
    id: string;
    project_id: string;
    scope: string;
    topic: string;
    title: string | null;
    content: string;
    summary: string | null;
    source_type: string;
    source_ref: string | null;
    status: string;
    confidence: number | null;
    importance: number;
    created_at: string;
    updated_at: string;
    snippet: string;
  }> = [];

  if (ftsEnabled) {
    const ftsQuery = buildPlainTextSearchQuery(input.query);
    const ftsWhere = ["memory_entries_fts MATCH ?", ...where];
    try {
      rows = await context.db.all<{
        id: string;
        project_id: string;
        scope: string;
        topic: string;
        title: string | null;
        content: string;
        summary: string | null;
        source_type: string;
        source_ref: string | null;
        status: string;
        confidence: number | null;
        importance: number;
        created_at: string;
        updated_at: string;
        snippet: string;
      }>(
        `
          SELECT
            e.id,
            e.project_id,
            e.scope,
            e.topic,
            e.title,
            e.content,
            e.summary,
            e.source_type,
            e.source_ref,
            e.status,
            e.confidence,
            e.importance,
            e.created_at,
            e.updated_at,
            snippet(memory_entries_fts, -1, '>>>', '<<<', '...', 24) AS snippet
          FROM memory_entries_fts
          INNER JOIN memory_entries e ON e.id = memory_entries_fts.entry_id
          WHERE ${ftsWhere.join(" AND ")}
          ORDER BY bm25(memory_entries_fts), e.importance DESC, COALESCE(e.confidence, 0) DESC, e.updated_at DESC
          LIMIT ?
        `,
        [ftsQuery, ...params]
      );
    } catch (error) {
      if (!isFtsQuerySyntaxError(error)) {
        throw error;
      }
      usedLikeFallback = true;
      fallbackReason = "fts_match_error";

      const likeFilter = buildTokenLikeClause(
        ["lower(coalesce(e.title, ''))", "lower(coalesce(e.summary, ''))", "lower(e.content)"],
        queryTerms.map((term) => term.toLowerCase())
      );
      const likeWhere = [likeFilter.clause, ...where].filter((entry) => entry.length > 0);
      rows = await context.db.all<{
        id: string;
        project_id: string;
        scope: string;
        topic: string;
        title: string | null;
        content: string;
        summary: string | null;
        source_type: string;
        source_ref: string | null;
        status: string;
        confidence: number | null;
        importance: number;
        created_at: string;
        updated_at: string;
        snippet: string;
      }>(
        `
          SELECT
            e.id,
            e.project_id,
            e.scope,
            e.topic,
            e.title,
            e.content,
            e.summary,
            e.source_type,
            e.source_ref,
            e.status,
            e.confidence,
            e.importance,
            e.created_at,
            e.updated_at,
            '' AS snippet,
            (
              CASE WHEN lower(coalesce(e.title, '')) LIKE ? ESCAPE '\\' THEN 8 ELSE 0 END +
              CASE WHEN lower(coalesce(e.summary, '')) LIKE ? ESCAPE '\\' THEN 5 ELSE 0 END +
              CASE WHEN lower(e.content) LIKE ? ESCAPE '\\' THEN 3 ELSE 0 END
            ) AS like_score
          FROM memory_entries e
          WHERE ${likeWhere.join(" AND ")}
          ORDER BY like_score DESC, e.importance DESC, COALESCE(e.confidence, 0) DESC, e.updated_at DESC
          LIMIT ?
        `,
        [
          `%${escapeLikePattern(input.query.toLowerCase())}%`,
          `%${escapeLikePattern(input.query.toLowerCase())}%`,
          `%${escapeLikePattern(input.query.toLowerCase())}%`,
          ...likeFilter.params,
          ...params
        ]
      );
    }
  } else {
    const likeFilter = buildTokenLikeClause(
      ["lower(coalesce(e.title, ''))", "lower(coalesce(e.summary, ''))", "lower(e.content)"],
      queryTerms.map((term) => term.toLowerCase())
    );
    const likeWhere = [likeFilter.clause, ...where].filter((entry) => entry.length > 0);
    rows = await context.db.all<{
      id: string;
      project_id: string;
      scope: string;
      topic: string;
      title: string | null;
      content: string;
      summary: string | null;
      source_type: string;
      source_ref: string | null;
      status: string;
      confidence: number | null;
      importance: number;
      created_at: string;
      updated_at: string;
      snippet: string;
    }>(
      `
        SELECT
          e.id,
          e.project_id,
          e.scope,
          e.topic,
          e.title,
          e.content,
          e.summary,
          e.source_type,
          e.source_ref,
          e.status,
          e.confidence,
          e.importance,
          e.created_at,
          e.updated_at,
          '' AS snippet,
          (
            CASE WHEN lower(coalesce(e.title, '')) LIKE ? ESCAPE '\\' THEN 8 ELSE 0 END +
            CASE WHEN lower(coalesce(e.summary, '')) LIKE ? ESCAPE '\\' THEN 5 ELSE 0 END +
            CASE WHEN lower(e.content) LIKE ? ESCAPE '\\' THEN 3 ELSE 0 END
          ) AS like_score
        FROM memory_entries e
        WHERE ${likeWhere.join(" AND ")}
        ORDER BY like_score DESC, e.importance DESC, COALESCE(e.confidence, 0) DESC, e.updated_at DESC
        LIMIT ?
      `,
      [
        `%${escapeLikePattern(input.query.toLowerCase())}%`,
        `%${escapeLikePattern(input.query.toLowerCase())}%`,
        `%${escapeLikePattern(input.query.toLowerCase())}%`,
        ...likeFilter.params,
        ...params
      ]
    );
  }

  const results = await Promise.all(
    rows.map(async (row) => {
      const rowTags = await readEntryTags(context, row.id);
      return {
        id: row.id,
        project_id: row.project_id,
        scope: row.scope,
        topic: row.topic,
        title: row.title,
        snippet: usedLikeFallback ? createLikeSnippet(input.query, row.title, row.summary, row.content) : row.snippet,
        tags: rowTags,
        source_type: row.source_type,
        source_ref: row.source_ref,
        status: row.status,
        confidence: row.confidence,
        importance: row.importance,
        created_at: row.created_at,
        updated_at: row.updated_at,
        verification_hint: VERIFICATION_HINT,
        ...(includeContent ? { content: row.content } : {})
      };
    })
  );

  await maybeWriteAuditLog(context, {
    project_id: resolved?.projectId,
    user: undefined,
    action: "memory_search",
    entry_id: undefined,
    details: {
      ...input,
      result_count: results.length,
      fts_enabled: ftsEnabled,
      used_like_fallback: usedLikeFallback,
      fallback_reason: fallbackReason
    }
  });
  const warnings = resolved ? [...resolved.warnings] : [];
  if (usedLikeFallback) {
    warnings.push(FTS_FALLBACK_WARNING);
  }
  return {
    ...(resolved ? createOkBase(resolved.projectId, warnings) : { ok: true, warnings }),
    mode,
    limit,
    include_content: includeContent,
    results
  };
}

async function handleMemoryListScopes(context: MemoryNodeContext, args: unknown): Promise<Record<string, unknown>> {
  const input = parseListTaxonomyInput(args);
  validateListTaxonomyInput(context, input);
  const resolved = await resolveRequiredProjectReadOnly(input.project_path, input.project_id);
  const status = input.status ?? "active";

  const rows = await context.db.all<{ name: string; count: number }>(
    `
      SELECT scope AS name, COUNT(*) AS count
      FROM memory_entries
      WHERE project_id = ?
        AND status = ?
      GROUP BY scope
      ORDER BY count DESC, name ASC
    `,
    [resolved.projectId, status]
  );

  return {
    ...createOkBase(resolved.projectId, resolved.warnings),
    status,
    items: rows.map((row) => ({ name: row.name, count: row.count }))
  };
}

async function handleMemoryListTopics(context: MemoryNodeContext, args: unknown): Promise<Record<string, unknown>> {
  const input = parseListTaxonomyInput(args);
  validateListTaxonomyInput(context, input);
  const resolved = await resolveRequiredProjectReadOnly(input.project_path, input.project_id);
  const status = input.status ?? "active";
  const where = ["project_id = ?", "status = ?"];
  const params: unknown[] = [resolved.projectId, status];
  if (input.scope) {
    where.push("scope = ?");
    params.push(input.scope);
  }

  const rows = await context.db.all<{ name: string; count: number }>(
    `
      SELECT topic AS name, COUNT(*) AS count
      FROM memory_entries
      WHERE ${where.join(" AND ")}
      GROUP BY topic
      ORDER BY count DESC, name ASC
    `,
    params
  );

  return {
    ...createOkBase(resolved.projectId, resolved.warnings),
    status,
    ...(input.scope ? { scope: input.scope } : {}),
    items: rows.map((row) => ({ name: row.name, count: row.count }))
  };
}

async function handleMemoryListTags(context: MemoryNodeContext, args: unknown): Promise<Record<string, unknown>> {
  const input = parseListTaxonomyInput(args);
  validateListTaxonomyInput(context, input);
  const resolved = await resolveRequiredProjectReadOnly(input.project_path, input.project_id);
  const status = input.status ?? "active";
  const where = ["e.project_id = ?", "e.status = ?"];
  const params: unknown[] = [resolved.projectId, status];
  if (input.scope) {
    where.push("e.scope = ?");
    params.push(input.scope);
  }
  if (input.topic) {
    where.push("e.topic = ?");
    params.push(input.topic);
  }

  const rows = await context.db.all<{ name: string; count: number }>(
    `
      SELECT mt.name AS name, COUNT(DISTINCT e.id) AS count
      FROM memory_entries e
      INNER JOIN memory_entry_tags met ON met.entry_id = e.id
      INNER JOIN memory_tags mt ON mt.id = met.tag_id
      WHERE ${where.join(" AND ")}
      GROUP BY mt.name
      ORDER BY count DESC, name ASC
    `,
    params
  );

  return {
    ...createOkBase(resolved.projectId, resolved.warnings),
    status,
    ...(input.scope ? { scope: input.scope } : {}),
    ...(input.topic ? { topic: input.topic } : {}),
    items: rows.map((row) => ({ name: row.name, count: row.count }))
  };
}

async function handleMemoryInvalidateEntry(context: MemoryNodeContext, args: unknown): Promise<Record<string, unknown>> {
  const input = parseInvalidateEntryInput(args);
  validateInvalidateEntryInput(context, input);
  const resolved = await resolveRequiredProject(context, input.project_path, input.project_id);
  const row = await context.db.get<{
    id: string;
    status: string;
    invalidated_at: string | null;
  }>(
    `
      SELECT id, status, invalidated_at
      FROM memory_entries
      WHERE id = ?
        AND project_id = ?
    `,
    [input.entry_id, resolved.projectId]
  );

  if (!row) {
    throw new NotFoundError(`entry_id ${input.entry_id} not found in resolved project.`);
  }

  const invalidatedAt = row.invalidated_at ?? nowIso();
  const changed = row.status !== "invalidated";
  if (changed) {
    await context.db.run(
      `
        UPDATE memory_entries
        SET status = 'invalidated',
            invalidated_at = ?,
            invalidation_reason = ?,
            updated_at = ?
        WHERE id = ?
          AND project_id = ?
      `,
      [invalidatedAt, input.reason ?? null, invalidatedAt, input.entry_id, resolved.projectId]
    );
  }

  await maybeWriteAuditLog(context, {
    project_id: resolved.projectId,
    user: undefined,
    action: "memory_invalidate_entry",
    entry_id: input.entry_id,
    details: {
      ...input,
      previous_status: row.status,
      status: "invalidated",
      changed
    }
  });

  return {
    ...createOkBase(resolved.projectId, resolved.warnings),
    entry_id: input.entry_id,
    previous_status: row.status,
    status: "invalidated",
    invalidated_at: invalidatedAt,
    changed
  };
}

export async function handleMemoryReadEntry(context: MemoryNodeContext, args: unknown): Promise<Record<string, unknown>> {
  const input = parseReadInput(args);
  assertMaxLength(input.project_id, context.config.inputLimits.maxProjectIdChars, "project_id");
  if (input.project_path) {
    assertAllowedPaths(input.project_path);
  }
  const mode = input.mode ?? "project";

  let resolved: { projectId: string; projectPath?: string; warnings: string[] } | undefined;
  if (mode === "project") {
    if (!input.project_path && !input.project_id) {
      throw new ValidationError("project mode requires project_path or project_id.");
    }
    resolved = await resolveProjectFromInput(input.project_path, input.project_id);
  } else if (input.project_path || input.project_id) {
    resolved = await resolveProjectFromInput(input.project_path, input.project_id);
  }

  const row = await context.db.get<{
    id: string;
    project_id: string;
    scope: string;
    topic: string;
    title: string | null;
    content: string;
    summary: string | null;
    source_type: string;
    source_ref: string | null;
    source_path: string | null;
    source_uri: string | null;
    status: string;
    confidence: number | null;
    importance: number;
    created_at: string;
    updated_at: string;
  }>(
    `
      SELECT
        id, project_id, scope, topic, title, content, summary, source_type, source_ref, source_path, source_uri,
        status, confidence, importance, created_at, updated_at
      FROM memory_entries
      WHERE id = ?
    `,
    [input.entry_id]
  );

  if (!row) {
    throw new NotFoundError(`entry_id ${input.entry_id} not found.`);
  }
  if (mode === "project" && resolved && row.project_id !== resolved.projectId) {
    throw new NotFoundError(`entry_id ${input.entry_id} not found in resolved project.`);
  }

  const tags = await readEntryTags(context, row.id);
  await maybeWriteAuditLog(context, {
    project_id: resolved?.projectId,
    user: undefined,
    action: "memory_read_entry",
    entry_id: input.entry_id,
    details: { ...input, found: !!row }
  });
  return {
    ...(resolved ? createOkBase(resolved.projectId, resolved.warnings) : { ok: true, warnings: [] }),
    mode,
    entry: {
      ...row,
      tags,
      verification_hint: VERIFICATION_HINT
    }
  };
}

export async function dispatchToolCall(
  context: MemoryNodeContext,
  toolName: string,
  args: unknown
): Promise<Record<string, unknown>> {
  switch (toolName) {
    case TOOL_MEMORY_STATUS:
      return handleMemoryStatus(context, args);
    case TOOL_MEMORY_ADD_ENTRY:
      return handleMemoryAddEntry(context, args);
    case TOOL_MEMORY_SEARCH:
      return handleMemorySearch(context, args);
    case TOOL_MEMORY_READ_ENTRY:
      return handleMemoryReadEntry(context, args);
    case TOOL_MEMORY_LIST_SCOPES:
      return handleMemoryListScopes(context, args);
    case TOOL_MEMORY_LIST_TOPICS:
      return handleMemoryListTopics(context, args);
    case TOOL_MEMORY_LIST_TAGS:
      return handleMemoryListTags(context, args);
    case TOOL_MEMORY_INVALIDATE_ENTRY:
      return handleMemoryInvalidateEntry(context, args);
    default:
      throw new ValidationError(`Unknown tool: ${toolName}`);
  }
}

function toolDefinitions(): Tool[] {
  const readOnlyAnnotations = {
    readOnlyHint: true,
    destructiveHint: false,
    idempotentHint: true,
    openWorldHint: false
  };
  const writeAnnotations = {
    readOnlyHint: false,
    destructiveHint: false,
    idempotentHint: false,
    openWorldHint: false
  };
  const destructiveWriteAnnotations = {
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: false
  };

  return [
    {
      name: TOOL_MEMORY_STATUS,
      description: "Returns memory-node DB status and project-scoped counts.",
      annotations: readOnlyAnnotations,
      inputSchema: {
        type: "object",
        additionalProperties: false,
        properties: {
          project_path: { type: "string" },
          project_id: { type: "string" }
        }
      }
    },
    {
      name: TOOL_MEMORY_ADD_ENTRY,
      description: "Adds operational memory for a project and supports idempotent dedupe.",
      annotations: writeAnnotations,
      inputSchema: {
        type: "object",
        additionalProperties: false,
        required: ["project_path", "scope", "topic", "content", "source_type"],
        properties: {
          project_path: { type: "string", minLength: 1 },
          project_id: { type: "string", minLength: 1 },
          scope: { type: "string", minLength: 1 },
          topic: { type: "string", minLength: 1 },
          title: { type: "string" },
          content: { type: "string", minLength: 1 },
          summary: { type: "string" },
          source_type: {
            type: "string",
            enum: [
              "agent_note",
              "decision",
              "handoff",
              "ticket_summary",
              "commit_context",
              "doc_observation",
              "file_observation",
              "bug_pattern",
              "workaround",
              "user_preference",
              "environment_note"
            ]
          },
          source_ref: { type: "string" },
          source_path: { type: "string" },
          source_uri: { type: "string" },
          tags: { type: "array", items: { type: "string" } },
          importance: { type: "integer", minimum: 1, maximum: 5 },
          confidence: { type: "number", minimum: 0, maximum: 1 },
          dedupe: { type: "boolean" }
        }
      }
    },
    {
      name: TOOL_MEMORY_SEARCH,
      description: "Searches memory entries; defaults to project mode unless cross_project is explicit.",
      annotations: readOnlyAnnotations,
      inputSchema: {
        type: "object",
        additionalProperties: false,
        required: ["query"],
        properties: {
          project_path: { type: "string", minLength: 1 },
          project_id: { type: "string", minLength: 1 },
          query: { type: "string", minLength: 1 },
          scope: { type: "string" },
          topic: { type: "string" },
          tags: { type: "array", items: { type: "string" } },
          source_type: {
            type: "string",
            enum: [
              "agent_note",
              "decision",
              "handoff",
              "ticket_summary",
              "commit_context",
              "doc_observation",
              "file_observation",
              "bug_pattern",
              "workaround",
              "user_preference",
              "environment_note"
            ]
          },
          status: { type: "string", enum: ["active", "invalidated", "archived", "superseded"] },
          mode: { type: "string", enum: ["project", "cross_project"] },
          limit: { type: "integer", minimum: 1 },
          include_content: { type: "boolean" }
        }
      }
    },
    {
      name: TOOL_MEMORY_READ_ENTRY,
      description: "Reads a full memory entry by id with project guards by default.",
      annotations: readOnlyAnnotations,
      inputSchema: {
        type: "object",
        additionalProperties: false,
        required: ["entry_id"],
        properties: {
          project_path: { type: "string", minLength: 1 },
          project_id: { type: "string", minLength: 1 },
          entry_id: { type: "string", minLength: 1 },
          mode: { type: "string", enum: ["project", "cross_project"] }
        }
      }
    },
    {
      name: TOOL_MEMORY_LIST_SCOPES,
      description: "Lists project memory scopes with entry counts.",
      annotations: readOnlyAnnotations,
      inputSchema: {
        type: "object",
        additionalProperties: false,
        properties: {
          project_path: { type: "string", minLength: 1 },
          project_id: { type: "string", minLength: 1 },
          status: { type: "string", enum: ["active", "invalidated", "archived", "superseded"] }
        }
      }
    },
    {
      name: TOOL_MEMORY_LIST_TOPICS,
      description: "Lists project memory topics with entry counts.",
      annotations: readOnlyAnnotations,
      inputSchema: {
        type: "object",
        additionalProperties: false,
        properties: {
          project_path: { type: "string", minLength: 1 },
          project_id: { type: "string", minLength: 1 },
          status: { type: "string", enum: ["active", "invalidated", "archived", "superseded"] },
          scope: { type: "string" }
        }
      }
    },
    {
      name: TOOL_MEMORY_LIST_TAGS,
      description: "Lists project memory tags with entry counts.",
      annotations: readOnlyAnnotations,
      inputSchema: {
        type: "object",
        additionalProperties: false,
        properties: {
          project_path: { type: "string", minLength: 1 },
          project_id: { type: "string", minLength: 1 },
          status: { type: "string", enum: ["active", "invalidated", "archived", "superseded"] },
          scope: { type: "string" },
          topic: { type: "string" }
        }
      }
    },
    {
      name: TOOL_MEMORY_INVALIDATE_ENTRY,
      description: "Soft-invalidates a memory entry in the resolved project.",
      annotations: destructiveWriteAnnotations,
      inputSchema: {
        type: "object",
        additionalProperties: false,
        required: ["entry_id"],
        properties: {
          project_path: { type: "string", minLength: 1 },
          project_id: { type: "string", minLength: 1 },
          entry_id: { type: "string", minLength: 1 },
          reason: { type: "string" }
        }
      }
    }
  ];
}

function createContext(): MemoryNodeContext {
  const config = loadConfig();
  mkdirSync(dirname(config.dbPath), { recursive: true });
  const db = new SqliteAdapter(config.dbPath);
  return { config, db };
}

export async function bootstrap(): Promise<void> {
  const context = createContext();
  await initDb(context.db);

  const server = new Server(
    {
      name: context.config.serverName,
      version: context.config.serverVersion
    },
    {
      capabilities: {
        tools: {}
      }
    }
  );

  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions() }));

  server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
    try {
      const result = await dispatchToolCall(context, request.params.name, request.params.arguments ?? {});
      const projectId = typeof result.project_id === "string" ? result.project_id : "cross_project";
      return toMcpSuccess(`${request.params.name} completed for ${projectId}.`, result);
    } catch (error) {
      if (error instanceof MemoryNodeError) {
        return toMcpError(error.code, error.message);
      }
      const detail = error instanceof Error ? error.message : "Unknown error";
      return toMcpError("INTERNAL_ERROR", detail);
    }
  });

  const transport = new StdioServerTransport();
  await server.connect(transport);
}

function isMainModule(): boolean {
  return typeof process.argv[1] === "string" && process.argv[1].endsWith("index.js");
}

if (isMainModule()) {
  bootstrap().catch((error: unknown) => {
    const detail = error instanceof Error ? error.message : "Unknown bootstrap error";
    console.error(JSON.stringify({ ok: false, error_code: "BOOTSTRAP_ERROR", detail }));
    process.exitCode = 1;
  });
}
