import { createHash, randomUUID } from 'node:crypto';
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { spawn } from 'node:child_process';
import { createReadStream, promises as fs } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { createInterface } from 'node:readline';
import { fileURLToPath } from 'node:url';

import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { McpServer, type CallToolResult } from '@modelcontextprotocol/server';
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright-core';
import * as ts from 'typescript';
import * as z from 'zod/v4';

import { OAuthManager, type AuthenticatedIncomingMessage } from './oauth.js';

const PROJECT_ROOT = path.resolve(process.env.PROJECT_ROOT ?? '/Volumes/Dev/apps/bent.ge');
const DEFAULT_PROJECT_ROOT = path.resolve(process.env.DEFAULT_PROJECT_ROOT ?? '/Volumes/Dev/apps/bent.ge');
const SECONDARY_PROJECT_ROOT = path.resolve(process.env.SECONDARY_PROJECT_ROOT ?? '/Volumes/Dev/apps/hub.bent.ge');
const ALLOWED_ROOTS = [DEFAULT_PROJECT_ROOT, SECONDARY_PROJECT_ROOT];
const PRIMARY_PACKAGE_ROOT = existsSync(path.join(DEFAULT_PROJECT_ROOT, 'frontend', 'package.json'))
  ? path.join(DEFAULT_PROJECT_ROOT, 'frontend')
  : DEFAULT_PROJECT_ROOT;
const HUB_LIVE_REMOTE_HOST = process.env.HUB_LIVE_REMOTE_HOST ?? 'bent-db';
const HUB_LIVE_ROOT = process.env.HUB_LIVE_ROOT ?? '/home/ontripge/domains/api.hub.bent.ge';
const HUB_LIVE_SSH_CONNECT_TIMEOUT_SECONDS = process.env.HUB_LIVE_SSH_CONNECT_TIMEOUT_SECONDS ?? '20';
const HUB_LIVE_ARTISAN_SSH_CONNECT_TIMEOUT_SECONDS = process.env.HUB_LIVE_ARTISAN_SSH_CONNECT_TIMEOUT_SECONDS ?? '5';
const HUB_BACKEND_LIVE_DIRS = ['app', 'bootstrap', 'config', 'database', 'public', 'resources', 'routes'];
const MCP_SERVER_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const HOST = process.env.HOST ?? '127.0.0.1';
const PORT = Number(process.env.PORT ?? '3333');
const MCP_ALLOWED_HOSTS = (process.env.MCP_ALLOWED_HOSTS ?? '')
  .split(',')
  .map(value => value.trim().toLowerCase())
  .filter(Boolean);
const oauth = new OAuthManager();

const SECRET_NAME_RE = /(^|[\\/])(\.env(?:\..*)?|.*\.(?:key|pem|p12)|id_rsa(?:\..*)?|.*\.token|secrets)(?:$|[\\/])/i;
const MAX_FILE_BYTES = 1024 * 1024;
const MAX_RANGE_LINES = 10_000;
const MAX_TREE_ENTRIES = 2500;
const MAX_SEARCH_RESULTS = 200;
const MAX_GIT_DIFF_CHARS = 80_000;

type RootView = {
  alias: string;
  root: string;
};

type FileNode = {
  name: string;
  path: string;
  kind: 'file' | 'directory';
  children?: FileNode[];
};

type ListEntry = {
  path: string;
  type: 'file' | 'dir';
};

type SearchMatch = {
  path: string;
  line: number;
  column: number;
  text: string;
};

type RunCommandInput = {
  command: string;
  args?: string[];
  cwd?: string;
};

type RunNodeScriptInput = {
  script: string;
  cwd?: string;
};

type LivePhpSyncFile = {
  localPath: string;
  absolutePath: string;
  backendRelativePath: string;
  remotePath: string;
};

type BentDbSqlResult = {
  stdout: string;
  stderr: string;
  exitCode: number;
  command: string[];
  sqlFile: string;
};

type LaravelArtisanResult = {
  stdout: string;
  stderr: string;
  exitCode: number;
  command: string[];
  cwd: string;
};

type ToolSuccess = CallToolResult;

type ProjectFileEntry = {
  path: string;
  absolutePath: string;
  size: number;
  mtimeMs: number;
  language: string;
};

type ProjectSymbol = {
  name: string;
  kind: string;
  path: string;
  line: number;
  column: number;
  language: string;
  detail?: string;
  exported?: boolean;
};

type ProjectIndex = {
  scope: string;
  builtAt: number;
  roots: RootView[];
  files: ProjectFileEntry[];
  symbols: ProjectSymbol[];
  languages: Record<string, number>;
};

type ProjectIndexCacheEntry = {
  signature: string;
  builtAt: number;
  index: ProjectIndex;
};

type RuntimeInspectResult = {
  url: string;
  title: string;
  readyState: string;
  selector?: string;
  console: string[];
  element: {
    tagName: string;
    text: string;
    html: string;
    attributes: Record<string, string>;
    dataset: Record<string, string>;
    vueProps: Record<string, unknown> | null;
    reactProps: Record<string, unknown> | null;
  } | null;
  pageText: string;
  evalResult?: unknown;
};

type BrowserScreenshotResult = {
  sessionId: string;
  url: string;
  fullPage: boolean;
  selector?: string;
  mimeType: 'image/jpeg';
  bytes: number;
};

type BrowserSessionRecord = {
  browser: Browser;
  context: BrowserContext;
  page: Page;
  url: string;
  logs: string[];
  createdAt: number;
  updatedAt: number;
  captureConsole: boolean;
};

type LocalProcessRecord = {
  name: string;
  command: string;
  args: string[];
  cwd: string;
  host: string;
  port: number;
  url: string;
  child: ReturnType<typeof spawn>;
  logs: string[];
  startedAt: number;
  updatedAt: number;
  exitCode: number | null;
};

const PROJECT_INDEX_CACHE = new Map<string, ProjectIndexCacheEntry>();
const BROWSER_SESSIONS = new Map<string, BrowserSessionRecord>();
const LOCAL_PROCESSES = new Map<string, LocalProcessRecord>();
const INDEXABLE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.php', '.json']);
const INDEX_IGNORE_DIRS = new Set(['node_modules', '.git', '.nuxt', 'dist', 'coverage', 'vendor']);
const MAX_INDEX_FILE_BYTES = 1024 * 1024;
const MAX_INDEX_SYMBOLS = 5000;
const CHROME_EXECUTABLE = process.env.CHROME_EXECUTABLE ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const MAX_BROWSER_SESSIONS = 4;
const MAX_LOCAL_PROCESSES = 4;

const ROOT_VIEWS: RootView[] = [
  { alias: path.basename(DEFAULT_PROJECT_ROOT), root: DEFAULT_PROJECT_ROOT },
  { alias: path.basename(SECONDARY_PROJECT_ROOT), root: SECONDARY_PROJECT_ROOT }
];

function toPosixRelative(root: string, absolutePath: string): string {
  return path.relative(root, absolutePath).split(path.sep).join('/');
}

function isInsideRoot(root: string, absolutePath: string): boolean {
  const relative = path.relative(root, absolutePath);
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
}

function getRootViewForAbsolute(absolutePath: string): RootView | null {
  return ROOT_VIEWS.find(view => isInsideRoot(view.root, absolutePath)) ?? null;
}

function toVirtualPath(absolutePath: string): string {
  const view = getRootViewForAbsolute(absolutePath);
  if (!view) {
    return toPosixRelative(PROJECT_ROOT, absolutePath);
  }

  const relative = path.relative(view.root, absolutePath).split(path.sep).join('/');
  return relative ? `${view.alias}/${relative}` : view.alias;
}

function parseRgMatch(line: string): SearchMatch | null {
  const match = line.match(/^(.*?):(\d+):(\d+):(.*)$/);
  if (!match) {
    return null;
  }

  const [, filePath, lineNumber, columnNumber, text] = match;
  return {
    path: filePath,
    line: Number(lineNumber),
    column: Number(columnNumber),
    text: text ?? ''
  };
}

function isSensitiveRelativePath(relativePath: string): boolean {
  const normalized = relativePath.split(path.sep).join('/');
  return SECRET_NAME_RE.test(normalized);
}

function isInsideAllowedRoot(absolutePath: string): boolean {
  return ALLOWED_ROOTS.some(root => isInsideRoot(root, absolutePath));
}

function getAllowedRootForAbsolute(absolutePath: string): string | null {
  return ROOT_VIEWS.find(view => isInsideRoot(view.root, absolutePath))?.root ?? null;
}

function getDiagnosticsRootForAbsolute(absolutePath: string): string {
  const root = getAllowedRootForAbsolute(absolutePath);
  if (root === SECONDARY_PROJECT_ROOT) {
    return SECONDARY_PROJECT_ROOT;
  }
  return PRIMARY_PACKAGE_ROOT;
}

function normalizeRelativePath(candidate: string | undefined): string {
  if (!candidate || candidate === '.') {
    return '.';
  }

  const cleaned = candidate.replace(/^~\//, '');
  const resolved = path.posix.normalize(cleaned.split(path.sep).join('/'));
  if (path.isAbsolute(resolved)) {
    throw new Error(`Path escapes project root: ${candidate}`);
  }
  return resolved === '' ? '.' : resolved;
}

async function resolveExistingPath(candidate: string): Promise<string> {
  const absolute = await resolveAllowedPath(candidate, false);
  const stat = await fs.lstat(absolute);
  if (stat.isSymbolicLink()) {
    const real = await fs.realpath(absolute);
    if (!isInsideAllowedRoot(real)) {
      throw new Error(`Path escapes project root via symlink: ${candidate}`);
    }
    return real;
  }
  return absolute;
}

async function resolveMaybeMissingPath(candidate: string): Promise<string> {
  return await resolveAllowedPath(candidate, true);
}

async function assertReadablePath(candidate: string): Promise<string> {
  const absolute = await resolveExistingPath(candidate);
  const relative = toVirtualPath(absolute);
  if (isSensitiveRelativePath(relative)) {
    throw new Error(`Access denied for sensitive file: ${relative}`);
  }
  const stat = await fs.stat(absolute);
  if (!stat.isFile()) {
    throw new Error(`Not a file: ${relative}`);
  }
  return absolute;
}

function getCandidateRoot(candidate: string): string {
  if (path.isAbsolute(candidate)) {
    return candidate;
  }

  const normalized = candidate.replace(/^~\//, '').split(path.sep).join('/');

  const segments = normalized.split('/').filter(Boolean);
  if (segments.length >= 2) {
    const firstView = ROOT_VIEWS.find(view => view.alias === segments[0]);
    const secondView = ROOT_VIEWS.find(view => view.alias === segments[1]);
    if (firstView && secondView && firstView.alias !== secondView.alias) {
      return path.resolve(secondView.root, segments.slice(2).join('/'));
    }
  }

  for (const view of ROOT_VIEWS) {
    if (normalized === view.alias || normalized.startsWith(`${view.alias}/`)) {
      const relative = normalized.slice(view.alias.length).replace(/^\//, '');
      return path.resolve(view.root, relative);
    }

    const legacyPrefixPattern = new RegExp(`^(?:\\.\\./)+${view.alias}(?:/(.*))?$`);
    const legacyMatch = normalized.match(legacyPrefixPattern);
    if (legacyMatch) {
      return path.resolve(view.root, legacyMatch[1] ?? '');
    }
  }

  return path.resolve(DEFAULT_PROJECT_ROOT, normalized);
}

async function resolveAllowedPath(candidate: string, allowMissing: boolean): Promise<string> {
  const absolute = getCandidateRoot(candidate);
  if (!isInsideAllowedRoot(absolute)) {
    throw new Error(`Path escapes allowed roots: ${candidate}`);
  }

  if (!allowMissing) {
    await fs.lstat(absolute);
  }

  return absolute;
}

function formatBytes(bytes: number): string {
  if (bytes < 1024) {
    return `${bytes} B`;
  }
  if (bytes < 1024 * 1024) {
    return `${(bytes / 1024).toFixed(1)} KiB`;
  }
  return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
}

function isPackageManagerPnpm(root = DEFAULT_PROJECT_ROOT): boolean {
  return existsSync(path.join(root, 'pnpm-lock.yaml'));
}

function readPackageJson(root = DEFAULT_PROJECT_ROOT): Record<string, unknown> {
  const filePath = path.join(root, 'package.json');
  return JSON.parse(readFileSync(filePath, 'utf8'));
}

function getPackageScripts(root = DEFAULT_PROJECT_ROOT): Record<string, string> {
  try {
    const pkg = readPackageJson(root);
    const scripts = pkg.scripts;
    if (scripts && typeof scripts === 'object' && !Array.isArray(scripts)) {
      return scripts as Record<string, string>;
    }
  } catch {
    // Ignore parse errors and fall back to an empty script map.
  }
  return {};
}

function hasScript(name: string, root = DEFAULT_PROJECT_ROOT): boolean {
  return Object.prototype.hasOwnProperty.call(getPackageScripts(root), name);
}

function treeLine(node: FileNode, depth: number): string {
  const indent = '  '.repeat(depth);
  const prefix = node.kind === 'directory' ? '[dir]' : '[file]';
  return `${indent}${prefix} ${node.name}`;
}

function getRequestPathname(req: IncomingMessage): string {
  return new URL(req.url ?? '/', 'http://localhost').pathname;
}

async function buildTree(dirAbsolute: string, maxDepth: number, maxEntries: number, includeHidden: boolean): Promise<{ lines: string[]; count: number; entries: ListEntry[] }> {
  const lines: string[] = [];
  const entries: ListEntry[] = [];
  let count = 0;

  async function visit(currentDir: string, depth: number): Promise<FileNode[]> {
    if (count >= maxEntries) {
      return [];
    }
    const dirents = await fs.readdir(currentDir, { withFileTypes: true });
    dirents.sort((a, b) => a.name.localeCompare(b.name));
    const children: FileNode[] = [];

    for (const entry of dirents) {
      if (count >= maxEntries) {
        break;
      }
      if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.nuxt' || entry.name === 'dist') {
        continue;
      }
      if (!includeHidden && entry.name.startsWith('.')) {
        continue;
      }
      const absolute = path.join(currentDir, entry.name);
      const relative = toVirtualPath(absolute);
      if (isSensitiveRelativePath(relative)) {
        continue;
      }
      if (entry.isSymbolicLink()) {
        continue;
      }
      if (entry.isDirectory()) {
        const node: FileNode = { name: entry.name, path: relative, kind: 'directory' };
        children.push(node);
        lines.push(treeLine(node, depth));
        entries.push({
          path: toVirtualPath(absolute),
          type: 'dir'
        });
        count += 1;
        if (depth < maxDepth) {
          const nested = await visit(absolute, depth + 1);
          node.children = nested;
        }
        continue;
      }

      if (entry.isFile()) {
        const node: FileNode = { name: entry.name, path: relative, kind: 'file' };
        children.push(node);
        lines.push(treeLine(node, depth));
        entries.push({
          path: toVirtualPath(absolute),
          type: 'file'
        });
        count += 1;
      }
    }

    return children;
  }

  await visit(dirAbsolute, 0);
  return { lines, count, entries };
}

async function runProcess(
  command: string,
  args: string[],
  cwd: string,
  extraEnv: NodeJS.ProcessEnv = {},
  input?: string
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
  return await new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      cwd,
      env: {
        ...process.env,
        ...extraEnv,
        PATH: process.env.PATH ?? ''
      },
      stdio: input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe']
    });

    let stdout = '';
    let stderr = '';

    const stdoutStream = child.stdout;
    const stderrStream = child.stderr;
    if (!stdoutStream || !stderrStream) {
      reject(new Error('Failed to open child process pipes.'));
      return;
    }

    stdoutStream.setEncoding('utf8');
    stderrStream.setEncoding('utf8');
    stdoutStream.on('data', chunk => {
      stdout += String(chunk);
    });
    stderrStream.on('data', chunk => {
      stderr += String(chunk);
    });
    if (input !== undefined && child.stdin) {
      child.stdin.setDefaultEncoding('utf8');
      child.stdin.end(input);
    }
    child.on('error', reject);
    child.on('close', code => {
      resolve({ exitCode: code ?? 0, stdout, stderr });
    });
  });
}

const BLOCKED_NODE_SCRIPT_PATTERNS: RegExp[] = [
  /\bprocess\.(?:env|exit|chdir|kill|binding)\b/i,
  /\b(?:global|globalThis|window|document)\b/i,
  /\b(?:eval|Function)\s*\(/i,
  /\b(?:spawn|execFile?|fork)\s*\(/i,
  /\brequire\s*\(\s*['"](?:child_process|fs|node:child_process|node:fs|net|http|https|dns|tls|vm|worker_threads|inspector|module|repl|readline)['"]\s*\)/i,
  /\bimport\s*\(\s*['"]node:(?:child_process|fs|net|http|https|dns|tls|vm|worker_threads|inspector|module|repl|readline)['"]\s*\)/i,
  /\b(?:fs|fsPromises)\.(?:write|append|unlink|rm|rename|copyFile|chmod|chown|symlink|mkdir|mkdtemp|createWriteStream|createReadStream)\b/i
];

function validateNodeScript(script: string): void {
  if (!script.trim()) {
    throw new Error('Script is empty.');
  }

  for (const pattern of BLOCKED_NODE_SCRIPT_PATTERNS) {
    if (pattern.test(script)) {
      throw new Error(`Node script contains a blocked operation: ${pattern.source}`);
    }
  }
}

async function runNodeScript(script: string, cwd = PRIMARY_PACKAGE_ROOT): Promise<{ exitCode: number; stdout: string; stderr: string }> {
  validateNodeScript(script);
  return await runProcess(process.execPath, ['-'], cwd, {}, script);
}

function validateCommandName(input: RunCommandInput, root = DEFAULT_PROJECT_ROOT): void {
  const { command, args = [] } = input;

  if (command === 'git') {
    const joined = args.join(' ');
    if (
      joined === 'status --short --branch' ||
      joined === 'diff' ||
      joined.startsWith('diff --') ||
      joined.startsWith('status --short --branch --') ||
      joined.startsWith('diff --no-ext-diff')
    ) {
      return;
    }
    throw new Error('Only safe git status/diff forms are allowed.');
  }

  if (command === 'rg') {
    return;
  }

  if (command === 'npm') {
    if (args[0] !== 'run' || typeof args[1] !== 'string') {
      throw new Error('Only npm run scripts are allowed.');
    }
    const script = args[1];
    if (!['build', 'generate', 'lint', 'typecheck', 'sync:site-content'].includes(script)) {
      throw new Error(`npm run ${script} is not allowed.`);
    }
    if (!hasScript(script, root)) {
      throw new Error(`npm script "${script}" does not exist in ${path.join(root, 'package.json')}.`);
    }
    return;
  }

  if (command === 'pnpm') {
    if (!isPackageManagerPnpm(root)) {
      throw new Error('pnpm is not allowed because this project uses npm lockfiles.');
    }
    if (args[0] !== 'run' || typeof args[1] !== 'string') {
      throw new Error('Only pnpm run scripts are allowed.');
    }
    const script = args[1];
    if (!['build', 'generate', 'lint', 'typecheck', 'sync:site-content'].includes(script)) {
      throw new Error(`pnpm run ${script} is not allowed.`);
    }
    if (!hasScript(script, root)) {
      throw new Error(`pnpm script "${script}" does not exist in ${path.join(root, 'package.json')}.`);
    }
    return;
  }

  throw new Error(`Command "${command}" is not allowlisted.`);
}

function buildCommandEnv(command: string, args: string[]): NodeJS.ProcessEnv {
  if (command === 'npm' && args[0] === 'run' && args[1] === 'build') {
    return { NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --max-old-space-size=8192`.trim() };
  }

  if (command === 'pnpm' && args[0] === 'run' && args[1] === 'build') {
    return { NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --max-old-space-size=8192`.trim() };
  }

  return {};
}

function filterSensitiveStatusLines(text: string): string {
  return text
    .split('\n')
    .filter(line => {
      const trimmed = line.trim();
      if (!trimmed) {
        return true;
      }
      const parts = trimmed.split(/\s+/);
      const candidate = parts[parts.length - 1] ?? '';
      return !isSensitiveRelativePath(candidate);
    })
    .join('\n');
}

function redactSensitiveDiff(text: string): string {
  const lines = text.split('\n');
  const out: string[] = [];
  let currentFile: string | null = null;
  let skipCurrent = false;

  for (const line of lines) {
    if (line.startsWith('diff --git ')) {
      const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
      currentFile = match?.[2] ?? null;
      skipCurrent = currentFile ? isSensitiveRelativePath(currentFile) : false;
      if (!skipCurrent) {
        out.push(line);
      } else {
        out.push('diff --git [redacted sensitive path]');
      }
      continue;
    }

    if (skipCurrent) {
      continue;
    }
    out.push(line);
  }

  return out.join('\n');
}

function clampToolText(text: string, maxChars = MAX_GIT_DIFF_CHARS): { text: string; truncated: boolean } {
  if (text.length <= maxChars) {
    return { text, truncated: false };
  }

  const headChars = Math.max(0, Math.floor(maxChars * 0.75));
  const tailChars = Math.max(0, maxChars - headChars);
  const head = text.slice(0, headChars);
  const tail = tailChars > 0 ? text.slice(-tailChars) : '';
  return {
    text: `${head}\n\n[... truncated ${text.length - maxChars} chars ...]\n\n${tail}`,
    truncated: true
  };
}

function assertSafePatchPaths(patchText: string): string[] {
  const paths = new Set<string>();
  const lines = patchText.split(/\r?\n/);

  for (const line of lines) {
    if (line.startsWith('diff --git ')) {
      const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
      if (!match) {
        throw new Error(`Invalid diff header: ${line}`);
      }
      if (match[1] !== '/dev/null') {
        paths.add(match[1]);
      }
      if (match[2] !== '/dev/null') {
        paths.add(match[2]);
      }
    }
    if (line.startsWith('+++ ') || line.startsWith('--- ')) {
      const candidate = line.slice(4).trim().replace(/^a\//, '').replace(/^b\//, '');
      if (candidate !== '/dev/null') {
        paths.add(candidate);
      }
    }
  }

  const accepted: string[] = [];
  for (const candidate of paths) {
    const normalized = normalizeRelativePath(candidate);
    if (normalized.startsWith('..')) {
      throw new Error(`Patch path escapes root: ${candidate}`);
    }
    if (normalized !== '.' && isSensitiveRelativePath(normalized)) {
      throw new Error(`Patch touches a sensitive file: ${normalized}`);
    }
    accepted.push(normalized);
  }

  return accepted;
}

function isNotFoundError(error: unknown): boolean {
  return Boolean(error && typeof error === 'object' && 'code' in error && (error as { code?: string }).code === 'ENOENT');
}

function shellQuote(value: string): string {
  return `'${value.replace(/'/g, `'\\''`)}'`;
}

function assertHubBackendLivePath(absolutePath: string): LivePhpSyncFile {
  if (!isInsideRoot(SECONDARY_PROJECT_ROOT, absolutePath)) {
    throw new Error(`Live PHP sync supports only ${path.basename(SECONDARY_PROJECT_ROOT)} backend paths.`);
  }

  const secondaryRelative = toPosixRelative(SECONDARY_PROJECT_ROOT, absolutePath);
  if (!secondaryRelative.startsWith('backend/')) {
    throw new Error(`Live PHP sync path must start with hub.bent.ge/backend/: ${toVirtualPath(absolutePath)}`);
  }
  if (!secondaryRelative.endsWith('.php')) {
    throw new Error(`Live PHP sync only accepts .php files: ${toVirtualPath(absolutePath)}`);
  }

  const backendRelativePath = secondaryRelative.slice('backend/'.length);
  const [liveTopLevel] = backendRelativePath.split('/');
  if (!liveTopLevel || !HUB_BACKEND_LIVE_DIRS.includes(liveTopLevel)) {
    throw new Error(`Live PHP sync cannot map backend/${backendRelativePath} into the live Laravel root.`);
  }
  if (isSensitiveRelativePath(secondaryRelative)) {
    throw new Error(`Access denied for sensitive file: ${toVirtualPath(absolutePath)}`);
  }

  return {
    localPath: toVirtualPath(absolutePath),
    absolutePath,
    backendRelativePath,
    remotePath: `${HUB_LIVE_ROOT}/${backendRelativePath}`
  };
}

async function collectChangedHubPhpFiles(): Promise<string[]> {
  const result = await runProcess('git', ['status', '--porcelain', '-z', '--', 'backend'], SECONDARY_PROJECT_ROOT);
  if (result.exitCode !== 0) {
    throw new Error(result.stderr.trim() || result.stdout.trim() || 'Unable to collect changed hub backend files.');
  }

  const entries = result.stdout.split('\0').filter(Boolean);
  const changed = new Set<string>();
  for (let index = 0; index < entries.length; index += 1) {
    const entry = entries[index] ?? '';
    const status = entry.slice(0, 2);
    let filePath = entry.slice(3);

    if (status.includes('D')) {
      continue;
    }
    if (status[0] === 'R' || status[0] === 'C') {
      filePath = entries[index + 1] ?? filePath;
      index += 1;
    }
    const backendRelativePath = filePath.startsWith('backend/') ? filePath.slice('backend/'.length) : '';
    const [liveTopLevel] = backendRelativePath.split('/');
    if (filePath.startsWith('backend/') && filePath.endsWith('.php') && liveTopLevel && HUB_BACKEND_LIVE_DIRS.includes(liveTopLevel)) {
      changed.add(`hub.bent.ge/${filePath}`);
    }
  }

  return [...changed].sort();
}

async function prepareLivePhpSyncFiles(paths: string[] | undefined): Promise<LivePhpSyncFile[]> {
  const candidates = paths && paths.length > 0 ? paths : await collectChangedHubPhpFiles();
  if (candidates.length === 0) {
    throw new Error('No changed PHP files found under hub.bent.ge/backend.');
  }

  const files: LivePhpSyncFile[] = [];
  const seen = new Set<string>();
  for (const candidate of candidates) {
    const absolute = await resolveExistingPath(candidate);
    const stat = await fs.lstat(absolute);
    if (stat.isSymbolicLink()) {
      throw new Error(`Refusing to sync symlink: ${toVirtualPath(absolute)}`);
    }
    if (!stat.isFile()) {
      throw new Error(`Live PHP sync accepts files only: ${toVirtualPath(absolute)}`);
    }

    const file = assertHubBackendLivePath(absolute);
    if (!seen.has(file.absolutePath)) {
      seen.add(file.absolutePath);
      files.push(file);
    }
  }

  return files;
}

async function verifyHubLiveRoot(): Promise<void> {
  const checks = ['app', 'bootstrap', 'routes'].map(dir => `test -d ${shellQuote(`${HUB_LIVE_ROOT}/${dir}`)}`).join(' && ');
  const result = await runProcess(
    'ssh',
    ['-o', 'BatchMode=yes', '-o', `ConnectTimeout=${HUB_LIVE_SSH_CONNECT_TIMEOUT_SECONDS}`, HUB_LIVE_REMOTE_HOST, checks],
    DEFAULT_PROJECT_ROOT
  );
  if (result.exitCode !== 0) {
    const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
    throw new Error(output || `Live Laravel root verification failed for ${HUB_LIVE_REMOTE_HOST}:${HUB_LIVE_ROOT}.`);
  }
}

async function syncLivePhpFile(file: LivePhpSyncFile, dryRun: boolean): Promise<{ stdout: string; stderr: string; command: string[] }> {
  const remoteDir = path.posix.dirname(file.remotePath);
  const mkdirResult = await runProcess(
    'ssh',
    ['-o', 'BatchMode=yes', '-o', `ConnectTimeout=${HUB_LIVE_SSH_CONNECT_TIMEOUT_SECONDS}`, HUB_LIVE_REMOTE_HOST, `mkdir -p ${shellQuote(remoteDir)}`],
    DEFAULT_PROJECT_ROOT
  );
  if (mkdirResult.exitCode !== 0) {
    const output = [mkdirResult.stdout.trim(), mkdirResult.stderr.trim()].filter(Boolean).join('\n');
    throw new Error(output || `Unable to create live directory ${remoteDir}.`);
  }

  const args = [
    '-az',
    '--checksum',
    '--itemize-changes',
    '-e',
    `ssh -o BatchMode=yes -o ConnectTimeout=${HUB_LIVE_SSH_CONNECT_TIMEOUT_SECONDS}`,
    ...(dryRun ? ['--dry-run'] : []),
    file.absolutePath,
    `${HUB_LIVE_REMOTE_HOST}:${file.remotePath}`
  ];
  const result = await runProcess('rsync', args, DEFAULT_PROJECT_ROOT);
  if (result.exitCode !== 0) {
    const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
    throw new Error(output || `rsync failed for ${file.localPath}.`);
  }

  return {
    stdout: result.stdout,
    stderr: result.stderr,
    command: ['rsync', ...args]
  };
}

function isReadOnlySql(sql: string): boolean {
  const statements = sql
    .split(';')
    .map(statement => statement.trim())
    .filter(Boolean);
  if (statements.length === 0) {
    return false;
  }

  return statements.every(statement => /^(?:WITH|SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)\b/i.test(statement));
}

async function runBentDbSql(sql: string): Promise<BentDbSqlResult> {
  const helperScript = '/Users/lacbook/.codex/skills/bent-db-content-ops/scripts/bent_db_mysql.sh';
  if (!existsSync(helperScript)) {
    throw new Error(`Missing bent DB helper script: ${helperScript}`);
  }

  const sqlFile = path.join(tmpdir(), `bent-db-${randomUUID()}.sql`);
  await fs.writeFile(sqlFile, sql, 'utf8');
  try {
    const result = await runProcess(helperScript, ['--file', sqlFile], DEFAULT_PROJECT_ROOT);
    return {
      stdout: result.stdout,
      stderr: result.stderr,
      exitCode: result.exitCode,
      command: [helperScript, '--file', sqlFile],
      sqlFile
    };
  } finally {
    try {
      await fs.unlink(sqlFile);
    } catch {
      // ignore
    }
  }
}

async function runLaravelArtisan(args: string[]): Promise<LaravelArtisanResult> {
  const phpArgs = ['artisan', ...args];
  if (phpArgs.length < 2) {
    throw new Error('Laravel artisan requires at least one command argument.');
  }

  const remoteCommand = [
    'ssh',
    '-o',
    'ClearAllForwardings=yes',
    '-o',
    'LogLevel=ERROR',
    '-o',
    `ConnectTimeout=${HUB_LIVE_ARTISAN_SSH_CONNECT_TIMEOUT_SECONDS}`,
    HUB_LIVE_REMOTE_HOST,
    `cd ${shellQuote(HUB_LIVE_ROOT)} && php ${phpArgs.map(shellQuote).join(' ')}`
  ];

  const result = await runProcess(
    'ssh',
    [
      '-o',
      'ClearAllForwardings=yes',
      '-o',
      'LogLevel=ERROR',
      '-o',
      `ConnectTimeout=${HUB_LIVE_ARTISAN_SSH_CONNECT_TIMEOUT_SECONDS}`,
      HUB_LIVE_REMOTE_HOST,
      `cd ${shellQuote(HUB_LIVE_ROOT)} && php ${phpArgs.map(shellQuote).join(' ')}`
    ],
    DEFAULT_PROJECT_ROOT
  );

  return {
    stdout: result.stdout,
    stderr: result.stderr,
    exitCode: result.exitCode,
    command: remoteCommand,
    cwd: HUB_LIVE_ROOT
  };
}

function getProjectBinaryPath(name: string, root = DEFAULT_PROJECT_ROOT): string {
  return path.join(root, 'node_modules', '.bin', name);
}

function getServerBinaryPath(name: string): string {
  return path.join(MCP_SERVER_ROOT, 'node_modules', '.bin', name);
}

function getPackageManagerName(root = DEFAULT_PROJECT_ROOT): 'npm' | 'pnpm' {
  return isPackageManagerPnpm(root) ? 'pnpm' : 'npm';
}

async function probeBinaryVersion(command: string, args: string[], cwd: string): Promise<string | null> {
  try {
    const result = await runProcess(command, args, cwd);
    const value = `${result.stdout}\n${result.stderr}`.trim();
    return result.exitCode === 0 && value ? value.split('\n')[0]?.trim() ?? null : null;
  } catch {
    return null;
  }
}

async function resolveWritableDirectory(candidate: string): Promise<string> {
  const relative = normalizeRelativePath(candidate);
  if (relative === '.' || relative === '') {
    return DEFAULT_PROJECT_ROOT;
  }

  const absolute = getCandidateRoot(relative);
  try {
    const real = await fs.realpath(absolute);
    if (!isInsideAllowedRoot(real)) {
      throw new Error(`Path escapes allowed roots via symlink: ${candidate}`);
    }
    const stat = await fs.stat(real);
    if (!stat.isDirectory()) {
      throw new Error(`Not a directory: ${candidate}`);
    }
    return real;
  } catch (error) {
    if (!isNotFoundError(error)) {
      throw error;
    }

    const parentRelative = path.posix.dirname(relative);
    const parentAbsolute = await resolveWritableDirectory(parentRelative);
    const directory = path.resolve(parentAbsolute, path.posix.basename(relative));
    await fs.mkdir(directory);
    return directory;
  }
}

async function resolveWritableFile(candidate: string): Promise<string> {
  const relative = normalizeRelativePath(candidate);
  if (relative === '.' || relative === '') {
    throw new Error('Expected a file path, not the project root.');
  }
  if (isSensitiveRelativePath(relative)) {
    throw new Error(`Access denied for sensitive file: ${relative}`);
  }

  const parentRelative = path.posix.dirname(relative);
  const parentAbsolute = await resolveWritableDirectory(parentRelative);
  const absolute = path.resolve(parentAbsolute, path.posix.basename(relative));

  try {
    const existing = await fs.lstat(absolute);
    if (existing.isSymbolicLink()) {
      throw new Error(`Refusing to operate on symlink: ${relative}`);
    }
  } catch (error) {
    if (!isNotFoundError(error)) {
      throw error;
    }
  }

  return absolute;
}

async function readTextFileRange(absolutePath: string, startLine: number, endLine: number): Promise<{ text: string; totalLines: number }> {
  const stream = createReadStream(absolutePath, { encoding: 'utf8' });
  const reader = createInterface({ input: stream, crlfDelay: Infinity });
  const lines: string[] = [];
  let totalLines = 0;

  try {
    for await (const line of reader) {
      totalLines += 1;
      if (totalLines >= startLine && totalLines <= endLine) {
        lines.push(line);
      }
      if (totalLines > endLine) {
        break;
      }
    }
  } finally {
    reader.close();
    stream.destroy();
  }

  return {
    text: lines.join('\n'),
    totalLines
  };
}

async function readTextFileNumbered(
  absolutePath: string,
  startLine: number,
  endLine: number
): Promise<{ text: string; totalLines: number; numberedLines: string[] }> {
  const { text, totalLines } = await readTextFileRange(absolutePath, startLine, endLine);
  const lines = text ? text.split('\n') : [];
  const numberedLines = lines.map((line, index) => `${String(startLine + index).padStart(6, ' ')}\t${line}`);
  return {
    text,
    totalLines,
    numberedLines
  };
}

async function applySafeSedSubstitution(
  absolutePath: string,
  search: string,
  replace: string,
  allOccurrences = false
): Promise<{ occurrences: number; updatedText: string }> {
  const original = await fs.readFile(absolutePath, 'utf8');
  const occurrences = original.split(search).length - 1;

  if (occurrences === 0) {
    throw new Error(`Text not found in ${toVirtualPath(absolutePath)}.`);
  }
  if (!allOccurrences && occurrences > 1) {
    throw new Error(`Search text occurs ${occurrences} times. Set allOccurrences=true to replace every match.`);
  }

  return {
    occurrences,
    updatedText: allOccurrences ? original.split(search).join(replace) : original.replace(search, replace)
  };
}

async function writeTextFileAtomically(absolutePath: string, content: string): Promise<void> {
  const tempPath = path.join(path.dirname(absolutePath), `.mcp-write-${randomUUID()}.tmp`);
  await fs.writeFile(tempPath, content, 'utf8');
  await fs.rename(tempPath, absolutePath);
}

async function runSafeProjectTask(task: 'build' | 'generate' | 'lint' | 'typecheck' | 'test' 
   | 'sync:site-content', cwd = PRIMARY_PACKAGE_ROOT): Promise<{ exitCode: number; stdout: string; stderr: string; command: string; args: string[] }> {
  const packageManager = getPackageManagerName(cwd);

  if (task === 'typecheck' && !hasScript('typecheck', cwd)) {
    const nuxiBinary = getProjectBinaryPath('nuxi', cwd);
    if (!existsSync(nuxiBinary)) {
      throw new Error('Typecheck is unavailable because nuxi is not installed in the target project.');
    }
    const args = ['typecheck'];
    const result = await runProcess(nuxiBinary, args, cwd);
    return { ...result, command: nuxiBinary, args };
  }

  if (task === 'test' && !hasScript('test', cwd)) {
    throw new Error('No test script exists in package.json.');
  }

  if (!hasScript(task, cwd)) {
    throw new Error(`npm script "${task}" does not exist in ${path.join(cwd, 'package.json')}.`);
  }

  const args = ['run', task];
  validateCommandName({ command: packageManager, args }, cwd);
  const result = await runProcess(packageManager, args, cwd, buildCommandEnv(packageManager, args));
  return { ...result, command: packageManager, args };
}

async function formatWithPrettierOrEslint(absolutePath: string, cwd = DEFAULT_PROJECT_ROOT): Promise<{ command: string; args: string[]; stdout: string; stderr: string }> {
  const prettierBinary = getServerBinaryPath('prettier');
  if (existsSync(prettierBinary)) {
    const prettierArgs = ['--write', absolutePath];
    const result = await runProcess(prettierBinary, prettierArgs, cwd);
    if (result.exitCode === 0) {
      return { command: prettierBinary, args: prettierArgs, stdout: result.stdout, stderr: result.stderr };
    }
  }

  const eslintBinary = getProjectBinaryPath('eslint', cwd);
  if (existsSync(eslintBinary)) {
    const eslintArgs = ['--fix', absolutePath];
    const result = await runProcess(eslintBinary, eslintArgs, cwd);
    if (result.exitCode === 0) {
      return { command: eslintBinary, args: eslintArgs, stdout: result.stdout, stderr: result.stderr };
    }
  }

  throw new Error('No formatter available. Install prettier or eslint in the MCP server or frontend project.');
}

async function getFileMetadata(candidate: string): Promise<Record<string, unknown>> {
  const relative = normalizeRelativePath(candidate);
  if (isSensitiveRelativePath(relative)) {
    throw new Error(`Access denied for sensitive file: ${relative}`);
  }

  const absolute = getCandidateRoot(relative);
  try {
    const stat = await fs.lstat(absolute);
    if (stat.isSymbolicLink()) {
      const real = await fs.realpath(absolute);
      if (!isInsideAllowedRoot(real)) {
        throw new Error(`Path escapes project root via symlink: ${candidate}`);
      }
      const realStat = await fs.stat(real);
      return {
        path: relative,
        exists: true,
        isDirectory: realStat.isDirectory(),
        isFile: realStat.isFile(),
        isSymbolicLink: true,
        size: realStat.size,
        mtimeMs: realStat.mtimeMs,
        ctimeMs: realStat.ctimeMs,
        resolvedPath: toVirtualPath(real)
      };
    }

    return {
      path: relative,
      exists: true,
      isDirectory: stat.isDirectory(),
      isFile: stat.isFile(),
      isSymbolicLink: false,
      size: stat.size,
      mtimeMs: stat.mtimeMs,
      ctimeMs: stat.ctimeMs
    };
  } catch (error) {
    if (!isNotFoundError(error)) {
      throw error;
    }

    return {
      path: relative,
      exists: false,
      isDirectory: false,
      isFile: false,
      isSymbolicLink: false,
      size: null,
      mtimeMs: null,
      ctimeMs: null
    };
  }
}

function getLanguageForPath(absolutePath: string): string {
  const ext = path.extname(absolutePath).toLowerCase();
  switch (ext) {
    case '.ts':
    case '.tsx':
      return 'ts';
    case '.js':
    case '.jsx':
    case '.mjs':
    case '.cjs':
      return 'js';
    case '.vue':
      return 'vue';
    case '.php':
      return 'php';
    case '.json':
      return 'json';
    default:
      return 'text';
  }
}

function isIndexablePath(absolutePath: string): boolean {
  return INDEXABLE_EXTENSIONS.has(path.extname(absolutePath).toLowerCase());
}

function toLineColumn(text: string, offset: number): { line: number; column: number } {
  const prefix = text.slice(0, Math.max(0, offset));
  const lines = prefix.split(/\r?\n/);
  return {
    line: lines.length,
    column: (lines[lines.length - 1]?.length ?? 0) + 1
  };
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function collectBindingNames(name: ts.BindingName): string[] {
  if (ts.isIdentifier(name)) {
    return [name.text];
  }

  const names: string[] = [];
  for (const element of name.elements) {
    if (ts.isOmittedExpression(element)) {
      continue;
    }
    names.push(...collectBindingNames(element.name));
  }
  return names;
}

function getNodePosition(sourceFile: ts.SourceFile, node: ts.Node, lineOffset = 0): { line: number; column: number } {
  const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
  return {
    line: position.line + 1 + lineOffset,
    column: position.character + 1
  };
}

function createSymbol(
  sourceFile: ts.SourceFile,
  node: ts.Node,
  name: string,
  kind: string,
  language: string,
  lineOffset = 0,
  detail?: string,
  exported = false
): ProjectSymbol {
  const position = getNodePosition(sourceFile, node, lineOffset);
  return {
    name,
    kind,
    path: '',
    line: position.line,
    column: position.column,
    language,
    detail,
    exported
  };
}

function isExportedNode(node: ts.Node): boolean {
  return Boolean(
    ts.canHaveModifiers(node) &&
      ts.getModifiers(node)?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
  );
}

function extractVueBlocks(text: string): Array<{ content: string; startOffset: number; language: string; setup: boolean }> {
  const blocks: Array<{ content: string; startOffset: number; language: string; setup: boolean }> = [];
  const scriptBlockPattern = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
  let match: RegExpExecArray | null;

  while ((match = scriptBlockPattern.exec(text)) !== null) {
    const attrs = match[1] ?? '';
    const content = match[2] ?? '';
    const setup = /\bsetup\b/i.test(attrs);
    const language = (attrs.match(/\blang=["']([^"']+)["']/i)?.[1] ?? 'ts').toLowerCase();
    const contentOffset = (match.index ?? 0) + match[0].indexOf(content);
    blocks.push({
      content,
      startOffset: contentOffset,
      language,
      setup
    });
  }

  return blocks;
}

function collectTypeMembers(
  sourceFile: ts.SourceFile,
  typeNode: ts.TypeNode | undefined,
  language: string,
  lineOffset = 0
): ProjectSymbol[] {
  if (!typeNode || !ts.isTypeLiteralNode(typeNode)) {
    return [];
  }

  const symbols: ProjectSymbol[] = [];
  for (const member of typeNode.members) {
    if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
      symbols.push(createSymbol(sourceFile, member.name, member.name.text, 'prop', language, lineOffset));
    }
    if (ts.isCallSignatureDeclaration(member) && member.parameters.length > 0) {
      const firstParam = member.parameters[0];
      if (firstParam.name && ts.isIdentifier(firstParam.name)) {
        symbols.push(createSymbol(sourceFile, firstParam.name, firstParam.name.text, 'emit', language, lineOffset));
      }
    }
  }

  return symbols;
}

function collectObjectMembers(
  sourceFile: ts.SourceFile,
  objectLiteral: ts.ObjectLiteralExpression,
  language: string,
  lineOffset = 0,
  kind: string
): ProjectSymbol[] {
  const symbols: ProjectSymbol[] = [];

  for (const property of objectLiteral.properties) {
    if (ts.isPropertyAssignment(property)) {
      const name = property.name;
      if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
        symbols.push(createSymbol(sourceFile, name, name.text, kind, language, lineOffset));
      }
    } else if (ts.isShorthandPropertyAssignment(property)) {
      symbols.push(createSymbol(sourceFile, property.name, property.name.text, kind, language, lineOffset));
    }
  }

  return symbols;
}

function collectTsSymbolsFromSourceText(
  absolutePath: string,
  text: string,
  language: string,
  lineOffset = 0
): ProjectSymbol[] {
  const scriptKind = language === 'js' ? ts.ScriptKind.JS : ts.ScriptKind.TS;
  const sourceFile = ts.createSourceFile(absolutePath, text, ts.ScriptTarget.Latest, true, scriptKind);
  const symbols: ProjectSymbol[] = [];
  const baseName = path.basename(absolutePath, path.extname(absolutePath));
  const virtualPath = toVirtualPath(absolutePath);

  if (language === 'vue') {
    symbols.push({
      name: baseName,
      kind: 'component',
      path: virtualPath,
      line: lineOffset + 1,
      column: 1,
      language: 'vue',
      exported: true
    });
  }

  function recordNamedNode(node: ts.Node, name: string, kind: string, detail?: string): void {
    symbols.push(createSymbol(sourceFile, node, name, kind, language, lineOffset, detail, isExportedNode(node)));
  }

  function visit(node: ts.Node): void {
    if (ts.isFunctionDeclaration(node) && node.name) {
      recordNamedNode(node.name, node.name.text, 'function');
    } else if (ts.isClassDeclaration(node) && node.name) {
      recordNamedNode(node.name, node.name.text, 'class');
    } else if (ts.isInterfaceDeclaration(node) && node.name) {
      recordNamedNode(node.name, node.name.text, 'interface');
    } else if (ts.isTypeAliasDeclaration(node) && node.name) {
      recordNamedNode(node.name, node.name.text, 'type');
    } else if (ts.isEnumDeclaration(node) && node.name) {
      recordNamedNode(node.name, node.name.text, 'enum');
    } else if (ts.isModuleDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
      recordNamedNode(node.name, node.name.text, 'namespace');
    } else if (ts.isVariableStatement(node)) {
      const kind =
        node.declarationList.flags & ts.NodeFlags.Const
          ? 'const'
          : node.declarationList.flags & ts.NodeFlags.Let
            ? 'let'
            : 'var';

      for (const declaration of node.declarationList.declarations) {
        for (const name of collectBindingNames(declaration.name)) {
          recordNamedNode(declaration.name, name, kind);
        }
      }
    } else if (ts.isCallExpression(node)) {
      const callee = node.expression;
      if (ts.isIdentifier(callee) && callee.text === 'defineProps') {
        const typeArgument = node.typeArguments?.[0];
        symbols.push(...collectTypeMembers(sourceFile, typeArgument, language, lineOffset));
        const firstArg = node.arguments[0];
        if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
          symbols.push(...collectObjectMembers(sourceFile, firstArg, language, lineOffset, 'prop'));
        }
      }

      if (ts.isIdentifier(callee) && callee.text === 'defineEmits') {
        const typeArgument = node.typeArguments?.[0];
        symbols.push(...collectTypeMembers(sourceFile, typeArgument, language, lineOffset));
        const firstArg = node.arguments[0];
        if (firstArg && ts.isArrayLiteralExpression(firstArg)) {
          for (const element of firstArg.elements) {
            if (ts.isStringLiteral(element) || ts.isNoSubstitutionTemplateLiteral(element)) {
              symbols.push(createSymbol(sourceFile, element, element.text, 'emit', language, lineOffset));
            }
          }
        }
      }
    }

    ts.forEachChild(node, visit);
  }

  visit(sourceFile);
  return symbols.map(symbol => ({
    ...symbol,
    path: symbol.path || virtualPath
  }));
}

function collectPhpSymbolsFromText(absolutePath: string, text: string): ProjectSymbol[] {
  const symbols: ProjectSymbol[] = [];
  const patterns: Array<{ kind: string; regex: RegExp }> = [
    { kind: 'namespace', regex: /\bnamespace\s+([A-Za-z0-9_\\]+)/g },
    { kind: 'class', regex: /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b/g },
    { kind: 'interface', regex: /\binterface\s+([A-Za-z_][A-Za-z0-9_]*)\b/g },
    { kind: 'trait', regex: /\btrait\s+([A-Za-z_][A-Za-z0-9_]*)\b/g },
    { kind: 'function', regex: /\bfunction\s+([A-Za-z_][A-Za-z0-9_]*)\b/g },
    { kind: 'const', regex: /\bconst\s+([A-Za-z_][A-Za-z0-9_]*)\b/g },
    { kind: 'route', regex: /Route::(get|post|put|patch|delete|match|any)\(\s*['"]([^'"]+)['"]/g }
  ];

  for (const pattern of patterns) {
    let match: RegExpExecArray | null;
    while ((match = pattern.regex.exec(text)) !== null) {
      const value = pattern.kind === 'route' ? match[2] : match[1];
      if (!value) {
        continue;
      }
      const position = toLineColumn(text, match.index ?? 0);
      symbols.push({
        name: value,
        kind: pattern.kind,
        path: toVirtualPath(absolutePath),
        line: position.line,
        column: position.column,
        language: 'php',
        exported: pattern.kind === 'class' || pattern.kind === 'function' || pattern.kind === 'interface'
      });
    }
  }

  return symbols;
}

async function collectProjectFiles(roots: string[]): Promise<ProjectFileEntry[]> {
  const files: ProjectFileEntry[] = [];

  async function visit(currentDir: string): Promise<void> {
    const dirents = await fs.readdir(currentDir, { withFileTypes: true });
    dirents.sort((a, b) => a.name.localeCompare(b.name));

    for (const entry of dirents) {
      if (INDEX_IGNORE_DIRS.has(entry.name)) {
        continue;
      }
      if (!entry.isDirectory() && !entry.isFile()) {
        continue;
      }
      if (entry.name.startsWith('.') && entry.name !== '.htaccess') {
        continue;
      }

      const absolute = path.join(currentDir, entry.name);
      const relative = toVirtualPath(absolute);
      if (isSensitiveRelativePath(relative)) {
        continue;
      }

      if (entry.isDirectory()) {
        await visit(absolute);
        continue;
      }

      if (!isIndexablePath(absolute)) {
        continue;
      }

      const stat = await fs.stat(absolute);
      files.push({
        path: relative,
        absolutePath: absolute,
        size: stat.size,
        mtimeMs: stat.mtimeMs,
        language: getLanguageForPath(absolute)
      });
    }
  }

  for (const root of roots) {
    await visit(root);
  }

  return files;
}

async function resolveProjectIndexRoots(scope?: string): Promise<string[]> {
  if (!scope || scope === '.') {
    return ROOT_VIEWS.map(view => view.root);
  }

  const resolved = await resolveExistingPath(scope);
  const stat = await fs.stat(resolved);
  return [stat.isDirectory() ? resolved : path.dirname(resolved)];
}

async function buildProjectIndex(scope?: string, forceRefresh = false): Promise<ProjectIndex> {
  const scopeRoots = await resolveProjectIndexRoots(scope);
  const cacheKey = scopeRoots.join('|');
  const files = await collectProjectFiles(scopeRoots);
  const signature = createHash('sha1')
    .update(
      files
        .map(file => `${file.path}:${file.size}:${file.mtimeMs}`)
        .sort()
        .join('\n')
    )
    .digest('hex');

  const cached = PROJECT_INDEX_CACHE.get(cacheKey);
  if (!forceRefresh && cached && cached.signature === signature) {
    return cached.index;
  }

  const symbols: ProjectSymbol[] = [];
  const languages: Record<string, number> = {};

  for (const file of files) {
    languages[file.language] = (languages[file.language] ?? 0) + 1;
    try {
      const stat = await fs.stat(file.absolutePath);
      if (stat.size > MAX_INDEX_FILE_BYTES) {
        continue;
      }

      const text = await fs.readFile(file.absolutePath, 'utf8');
      if (file.language === 'php') {
        symbols.push(...collectPhpSymbolsFromText(file.absolutePath, text));
        continue;
      }

      if (file.language === 'vue') {
        const componentName = path.basename(file.absolutePath, path.extname(file.absolutePath));
        symbols.push({
          name: componentName,
          kind: 'component',
          path: file.path,
          line: 1,
          column: 1,
          language: 'vue',
          exported: true
        });

        const blocks = extractVueBlocks(text);
        for (const block of blocks) {
          const blockLineOffset = toLineColumn(text, block.startOffset).line - 1;
          symbols.push(...collectTsSymbolsFromSourceText(file.absolutePath, block.content, block.language === 'js' ? 'js' : 'ts', blockLineOffset));
        }
        continue;
      }

      if (file.language === 'js' || file.language === 'ts') {
        symbols.push(...collectTsSymbolsFromSourceText(file.absolutePath, text, file.language));
      }
    } catch {
      // Skip unreadable or binary-looking files without failing the full index.
    }
  }

  const index: ProjectIndex = {
    scope: scope ?? '__workspace__',
    builtAt: Date.now(),
    roots: ROOT_VIEWS,
    files,
    symbols: symbols.slice(0, MAX_INDEX_SYMBOLS),
    languages
  };

  PROJECT_INDEX_CACHE.set(cacheKey, {
    signature,
    builtAt: index.builtAt,
    index
  });

  return index;
}

function findSymbolsInIndex(index: ProjectIndex, query: string, maxResults: number, kind?: string): ProjectSymbol[] {
  const normalized = query.toLowerCase();
  const exact = index.symbols.filter(symbol => symbol.name.toLowerCase() === normalized);
  const pool = exact.length > 0 ? exact : index.symbols.filter(symbol => symbol.name.toLowerCase().includes(normalized));
  return pool.filter(symbol => (kind ? symbol.kind === kind : true)).slice(0, maxResults);
}

async function guessSymbolAtLocation(candidatePath: string, line: number, column?: number): Promise<string | null> {
  const absolute = await resolveExistingPath(candidatePath);
  const text = await fs.readFile(absolute, 'utf8');
  const lines = text.split(/\r?\n/);
  const lineText = lines[line - 1] ?? '';
  const tokenPattern = /[A-Za-z_$][A-Za-z0-9_$]*/g;
  const tokens: Array<{ value: string; start: number; end: number }> = [];
  let match: RegExpExecArray | null;
  while ((match = tokenPattern.exec(lineText)) !== null) {
    tokens.push({
      value: match[0] ?? '',
      start: match.index ?? 0,
      end: (match.index ?? 0) + (match[0]?.length ?? 0)
    });
  }
  if (tokens.length === 0) {
    return null;
  }

  if (typeof column === 'number') {
    const adjusted = Math.max(1, column) - 1;
    const containing = tokens.find(token => adjusted >= token.start && adjusted <= token.end);
    if (containing) {
      return containing.value;
    }
  }

  return tokens[0]?.value ?? null;
}

async function searchReferences(symbol: string, scope?: string, maxResults = 100): Promise<SearchMatch[]> {
  const roots = await resolveProjectIndexRoots(scope);
  const results: SearchMatch[] = [];
  const query = symbol.trim();
  if (!query) {
    return [];
  }

  for (const root of roots) {
    const args = [
      '--line-number',
      '--column',
      '--no-heading',
      '--color',
      'never',
      '--fixed-strings',
      '--max-count',
      String(Math.max(1, maxResults)),
      query,
      '.'
    ];

    const result = await runProcess('rg', args, root);
    const output = result.stdout.trim();
    const lines = output ? output.split('\n') : [];
    for (const line of lines) {
      const match = parseRgMatch(line);
      if (!match) {
        continue;
      }
      const absolute = path.resolve(root, match.path);
      const virtualPath = toVirtualPath(absolute);
      if (isSensitiveRelativePath(virtualPath)) {
        continue;
      }
      results.push({
        ...match,
        path: virtualPath
      });
      if (results.length >= maxResults) {
        return results;
      }
    }
  }

  return results;
}

async function resolvePathInfo(candidate: string): Promise<Record<string, unknown>> {
  const relative = normalizeRelativePath(candidate);
  if (isSensitiveRelativePath(relative)) {
    throw new Error(`Access denied for sensitive file: ${relative}`);
  }
  const absolute = getCandidateRoot(relative);
  const exists = await fs
    .lstat(absolute)
    .then(() => true)
    .catch(() => false);

  if (!exists) {
    return {
      input: candidate,
      virtualPath: relative,
      absolutePath: absolute,
      exists: false
    };
  }

  const stat = await fs.lstat(absolute);
  const root = getRootViewForAbsolute(absolute);
  return {
    input: candidate,
    virtualPath: toVirtualPath(absolute),
    absolutePath: absolute,
    root: root?.alias ?? null,
    exists: true,
    isDirectory: stat.isDirectory(),
    isFile: stat.isFile(),
    isSymbolicLink: stat.isSymbolicLink(),
    size: stat.size,
    mtimeMs: stat.mtimeMs,
    ctimeMs: stat.ctimeMs
  };
}

async function getGoToDefinitionResult(params: {
  symbol?: string;
  path?: string;
  line?: number;
  column?: number;
  scope?: string;
}): Promise<Record<string, unknown>> {
  let query = params.symbol?.trim() ?? '';
  if (!query && params.path && typeof params.line === 'number') {
    query = (await guessSymbolAtLocation(params.path, params.line, params.column)) ?? '';
  }
  if (!query) {
    throw new Error('Provide either symbol or path+line.');
  }

  const index = await buildProjectIndex(params.scope);
  const matches = findSymbolsInIndex(index, query, 25);
  const definitions = await Promise.all(
    matches.map(async symbol => {
      const absolute = await resolveExistingPath(symbol.path);
      const startLine = Math.max(1, symbol.line - 3);
      const endLine = symbol.line + 4;
      const excerpt = await readTextFileRange(absolute, startLine, endLine);
      return {
        ...symbol,
        snippet: excerpt.text,
        snippetStartLine: startLine,
        snippetEndLine: endLine
      };
    })
  );

  return {
    query,
    matches: definitions.length,
    definitions
  };
}

async function getFindReferencesResult(params: {
  symbol?: string;
  path?: string;
  line?: number;
  column?: number;
  scope?: string;
  maxResults?: number;
}): Promise<Record<string, unknown>> {
  let query = params.symbol?.trim() ?? '';
  if (!query && params.path && typeof params.line === 'number') {
    query = (await guessSymbolAtLocation(params.path, params.line, params.column)) ?? '';
  }
  if (!query) {
    throw new Error('Provide either symbol or path+line.');
  }

  const results = await searchReferences(query, params.scope, params.maxResults ?? 100);
  return {
    query,
    matches: results.length,
    results
  };
}

async function getWorkspaceSnapshot(forceRefresh = false): Promise<Record<string, unknown>> {
  const index = await buildProjectIndex(undefined, forceRefresh);
  const status = await runProcess('git', ['status', '--short', '--branch'], DEFAULT_PROJECT_ROOT);
  const packageManager = getPackageManagerName(PRIMARY_PACKAGE_ROOT);
  const scripts = getPackageScripts(PRIMARY_PACKAGE_ROOT);
  return {
    roots: ROOT_VIEWS.map(view => ({
      alias: view.alias,
      root: view.root
    })),
    packageManager,
    scripts,
    gitStatus: filterSensitiveStatusLines(status.stdout.trim()) || '(clean)',
    indexedFiles: index.files.length,
    indexedSymbols: index.symbols.length,
    languages: index.languages,
    builtAt: index.builtAt
  };
}

function isSafeRuntimeUrl(candidate: string): boolean {
  try {
    const url = new URL(candidate);
    if (url.protocol !== 'http:' && url.protocol !== 'https:') {
      return false;
    }
    return ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) || url.hostname.endsWith('.local');
  } catch {
    return false;
  }
}

function buildLocalUrl(input: { url?: string; host?: string; port?: number; path?: string }): string {
  if (input.url) {
    return input.url;
  }
  const host = input.host?.trim() || '127.0.0.1';
  const port = input.port ?? 3000;
  const pathname = input.path?.startsWith('/') ? input.path : input.path ? `/${input.path}` : '/';
  return `http://${host}:${port}${pathname}`;
}

function isBrowserSessionRecord(value: unknown): value is BrowserSessionRecord {
  return Boolean(value && typeof value === 'object' && 'browser' in value && 'context' in value && 'page' in value);
}

async function createBrowserSession(captureConsole = true): Promise<BrowserSessionRecord> {
  if (BROWSER_SESSIONS.size >= MAX_BROWSER_SESSIONS) {
    const oldestKey = [...BROWSER_SESSIONS.entries()].sort((a, b) => a[1].updatedAt - b[1].updatedAt)[0]?.[0];
    if (oldestKey) {
      const oldest = BROWSER_SESSIONS.get(oldestKey);
      BROWSER_SESSIONS.delete(oldestKey);
      if (oldest) {
        await oldest.context.close().catch(() => {});
        await oldest.browser.close().catch(() => {});
      }
    }
  }

  const browser = await chromium.launch({
    headless: true,
    executablePath: CHROME_EXECUTABLE,
    args: ['--no-sandbox']
  });
  const context = await browser.newContext();
  const page = await context.newPage();
  const record: BrowserSessionRecord = {
    browser,
    context,
    page,
    url: 'about:blank',
    logs: [],
    createdAt: Date.now(),
    updatedAt: Date.now(),
    captureConsole
  };

  if (captureConsole) {
    page.on('console', message => {
      record.logs.push(`[${message.type()}] ${message.text()}`);
      record.updatedAt = Date.now();
    });
    page.on('pageerror', error => {
      record.logs.push(`[pageerror] ${error.message}`);
      record.updatedAt = Date.now();
    });
    page.on('requestfailed', request => {
      record.logs.push(`[requestfailed] ${request.method()} ${request.url()} -> ${request.failure()?.errorText ?? 'unknown'}`);
      record.updatedAt = Date.now();
    });
  }

  return record;
}

async function getBrowserSession(sessionId: string): Promise<BrowserSessionRecord> {
  const record = BROWSER_SESSIONS.get(sessionId);
  if (!record || record.page.isClosed()) {
    throw new Error(`Unknown browser session: ${sessionId}`);
  }
  record.updatedAt = Date.now();
  return record;
}

async function closeBrowserSession(sessionId: string): Promise<boolean> {
  const record = BROWSER_SESSIONS.get(sessionId);
  if (!record) {
    return false;
  }
  BROWSER_SESSIONS.delete(sessionId);
  await record.context.close().catch(() => {});
  await record.browser.close().catch(() => {});
  return true;
}

function isSafeProcessCommand(command: string, args: string[]): boolean {
  if (command === 'npm') {
    return args[0] === 'run' && ['dev', 'preview', 'start', 'build', 'generate', 'typecheck'].includes(args[1] ?? '');
  }
  if (command === 'pnpm') {
    return args[0] === 'run' && ['dev', 'preview', 'start', 'build', 'generate', 'typecheck'].includes(args[1] ?? '');
  }
  return false;
}

function buildProcessUrl(host: string, port: number, pathName = '/'): string {
  return `http://${host}:${port}${pathName.startsWith('/') ? pathName : `/${pathName}`}`;
}

function safeProcessEnv(port: number, extraEnv: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
  return {
    ...extraEnv,
    PORT: String(port),
    HOST: extraEnv.HOST?.toString() ?? '127.0.0.1'
  };
}

async function closeLocalProcess(name: string): Promise<boolean> {
  const record = LOCAL_PROCESSES.get(name);
  if (!record) {
    return false;
  }

  LOCAL_PROCESSES.delete(name);
  try {
    record.child.kill('SIGTERM');
  } catch {
    // ignore
  }

  await new Promise(resolve => {
    const timeout = setTimeout(() => resolve(null), 3000);
    record.child.once('close', () => {
      clearTimeout(timeout);
      resolve(null);
    });
  });

  return true;
}

function localProcessSnapshot(record: LocalProcessRecord): Record<string, unknown> {
  return {
    name: record.name,
    command: record.command,
    args: record.args,
    cwd: toVirtualPath(record.cwd),
    host: record.host,
    port: record.port,
    url: record.url,
    startedAt: record.startedAt,
    updatedAt: record.updatedAt,
    exitCode: record.exitCode,
    running: record.exitCode === null,
    logs: record.logs.slice(-100)
  };
}

async function startLocalProcess(params: {
  name: string;
  command: string;
  args: string[];
  cwd: string;
  host?: string;
  port?: number;
  extraEnv?: NodeJS.ProcessEnv;
}): Promise<Record<string, unknown>> {
  const name = params.name.trim();
  if (!name) {
    throw new Error('Process name is required.');
  }
  if (LOCAL_PROCESSES.has(name)) {
    throw new Error(`Process already running: ${name}`);
  }

  const cwd = await resolveExistingPath(params.cwd);
  const stat = await fs.stat(cwd);
  if (!stat.isDirectory()) {
    throw new Error(`Command cwd is not a directory: ${params.cwd}`);
  }

  if (!isSafeProcessCommand(params.command, params.args)) {
    throw new Error(`Command "${params.command} ${params.args.join(' ')}" is not allowlisted for persistent processes.`);
  }

  const host = params.host?.trim() || '127.0.0.1';
  const port = params.port ?? 3000;
  const child = spawn(params.command, params.args, {
    cwd,
    env: {
      ...process.env,
      ...safeProcessEnv(port, params.extraEnv),
      PATH: process.env.PATH ?? ''
    },
    stdio: ['ignore', 'pipe', 'pipe']
  });

  const record: LocalProcessRecord = {
    name,
    command: params.command,
    args: params.args,
    cwd,
    host,
    port,
    url: buildProcessUrl(host, port),
    child,
    logs: [],
    startedAt: Date.now(),
    updatedAt: Date.now(),
    exitCode: null
  };

  const pushLog = (prefix: string, text: string) => {
    for (const line of text.split(/\r?\n/)) {
      if (!line.trim()) {
        continue;
      }
      record.logs.push(`${prefix} ${line}`);
    }
    record.updatedAt = Date.now();
  };

  child.stdout.setEncoding('utf8');
  child.stderr.setEncoding('utf8');
  child.stdout.on('data', chunk => pushLog('[stdout]', String(chunk)));
  child.stderr.on('data', chunk => pushLog('[stderr]', String(chunk)));
  child.on('error', error => {
    pushLog('[error]', error.message);
    record.exitCode = -1;
  });
  child.on('close', code => {
    record.exitCode = code ?? 0;
    record.updatedAt = Date.now();
    if (record.exitCode !== null) {
      pushLog('[exit]', `code=${record.exitCode}`);
    }
  });

  LOCAL_PROCESSES.set(name, record);
  return localProcessSnapshot(record);
}

async function restartLocalProcess(params: {
  name: string;
  command?: string;
  args?: string[];
  cwd?: string;
  host?: string;
  port?: number;
  extraEnv?: NodeJS.ProcessEnv;
}): Promise<Record<string, unknown>> {
  const existing = LOCAL_PROCESSES.get(params.name);
  if (existing) {
    await closeLocalProcess(params.name);
  }
  if (!params.command || !params.args || !params.cwd) {
    throw new Error('restart requires command, args, and cwd when the process is not already running.');
  }
  return await startLocalProcess({
    name: params.name,
    command: params.command,
    args: params.args,
    cwd: params.cwd,
    host: params.host,
    port: params.port,
    extraEnv: params.extraEnv
  });
}

function browserSnapshot(record: BrowserSessionRecord, selector?: string): Record<string, unknown> {
  const page = record.page;
  const text = record.logs.slice(-100);
  return {
    sessionId: [...BROWSER_SESSIONS.entries()].find(([, value]) => value === record)?.[0] ?? null,
    url: record.url,
    createdAt: record.createdAt,
    updatedAt: record.updatedAt,
    console: text,
    captureConsole: record.captureConsole,
    selector: selector ?? null
  };
}

async function inspectRuntime(params: {
  url: string;
  selector?: string;
  waitFor?: string;
  timeoutMs?: number;
  captureConsole?: boolean;
  script?: string;
}): Promise<RuntimeInspectResult> {
  if (!isSafeRuntimeUrl(params.url)) {
    throw new Error('inspect_runtime only allows localhost or local dev URLs.');
  }

  const timeoutMs = Math.max(1000, Math.min(params.timeoutMs ?? 10_000, 60_000));
  const browser = await chromium.launch({
    headless: true,
    executablePath: CHROME_EXECUTABLE,
    args: ['--no-sandbox']
  });
  const page = await browser.newPage();
  const consoleMessages: string[] = [];

  if (params.captureConsole !== false) {
    page.on('console', message => {
      consoleMessages.push(`[${message.type()}] ${message.text()}`);
    });
    page.on('pageerror', error => {
      consoleMessages.push(`[pageerror] ${error.message}`);
    });
  }

  try {
    await page.goto(params.url, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
    if (params.waitFor) {
      await page.waitForSelector(params.waitFor, { timeout: timeoutMs });
    }

    const selector = params.selector?.trim() || '';
    const locator = selector ? page.locator(selector) : page.locator('body');
    const count = selector ? await locator.count() : 1;
    const element = count > 0 ? await locator.first().evaluate((node: Element) => {
      const elementNode = node as HTMLElement & {
        __vueParentComponent?: {
          props?: Record<string, unknown>;
          proxy?: Record<string, unknown>;
        };
      };

      const attributes: Record<string, string> = {};
      for (const attr of Array.from(elementNode.attributes ?? [])) {
        attributes[attr.name] = attr.value;
      }

      const vueProps =
        elementNode.__vueParentComponent?.props ??
        (elementNode.__vueParentComponent?.proxy as Record<string, unknown> | undefined)?.$props ??
        null;
      const reactPropKey = Object.keys(elementNode).find(key => key.startsWith('__reactProps$')) ?? '';
      const reactProps = reactPropKey ? ((elementNode as unknown as Record<string, unknown>)[reactPropKey] as Record<string, unknown> | undefined) ?? null : null;

      return {
        tagName: elementNode.tagName,
        text: elementNode.textContent ?? '',
        html: elementNode.outerHTML ?? '',
        attributes,
        dataset: Object.fromEntries(Object.entries(elementNode.dataset ?? {}).filter(([, value]) => value !== undefined)) as Record<string, string>,
        vueProps,
        reactProps
      };
    }) : null;

    const evalResult = params.script?.trim()
      ? await page.evaluate(source => {
          // eslint-disable-next-line no-eval
          return eval(source);
        }, params.script)
      : undefined;

    return {
      url: page.url(),
      title: await page.title(),
      readyState: await page.evaluate(() => document.readyState),
      selector: selector || undefined,
      console: consoleMessages,
      element:
        element === null
          ? null
          : {
              tagName: element.tagName,
              text: element.text.slice(0, 10_000),
              html: element.html.slice(0, 10_000),
              attributes: element.attributes,
              dataset: element.dataset,
              vueProps: element.vueProps as Record<string, unknown> | null,
              reactProps: element.reactProps as Record<string, unknown> | null
            },
      pageText: await page.evaluate(() => document.body?.innerText?.slice(0, 10_000) ?? ''),
      evalResult
    };
  } finally {
    await browser.close();
  }
}

async function openBrowserSession(params: {
  url?: string;
  host?: string;
  port?: number;
  path?: string;
  captureConsole?: boolean;
  waitFor?: string;
  timeoutMs?: number;
}): Promise<Record<string, unknown>> {
  const url = buildLocalUrl(params);
  if (!isSafeRuntimeUrl(url)) {
    throw new Error('browser_open only allows localhost or local dev URLs.');
  }

  const record = await createBrowserSession(params.captureConsole !== false);
  const sessionId = randomUUID();
  BROWSER_SESSIONS.set(sessionId, record);
  const timeoutMs = Math.max(1000, Math.min(params.timeoutMs ?? 10_000, 60_000));

  await record.page.goto(url, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
  if (params.waitFor) {
    await record.page.waitForSelector(params.waitFor, { timeout: timeoutMs });
  }
  record.url = record.page.url();
  record.updatedAt = Date.now();

  const resolved = new URL(record.url);
  const effectiveHost = params.host?.trim() || resolved.hostname;
  const effectivePort = params.port ?? (resolved.port ? Number(resolved.port) : resolved.protocol === 'https:' ? 443 : 80);
  const effectivePath = params.path?.trim() || `${resolved.pathname}${resolved.search}${resolved.hash}` || '/';

  return {
    sessionId,
    url: record.url,
    host: effectiveHost,
    port: effectivePort,
    path: effectivePath,
    title: await record.page.title(),
    readyState: await record.page.evaluate(() => document.readyState),
    console: record.logs.slice(-20)
  };
}

async function browserSessionSnapshot(params: { sessionId: string; selector?: string }): Promise<Record<string, unknown>> {
  const record = await getBrowserSession(params.sessionId);
  const selector = params.selector?.trim() || '';
  const page = record.page;
  const element = selector
    ? await page.locator(selector).first().evaluate((node: Element) => {
        const elementNode = node as HTMLElement;
        const attributes: Record<string, string> = {};
        for (const attr of Array.from(elementNode.attributes ?? [])) {
          attributes[attr.name] = attr.value;
        }
        return {
          tagName: elementNode.tagName,
          text: elementNode.textContent ?? '',
          html: elementNode.outerHTML ?? '',
          attributes,
          dataset: Object.fromEntries(Object.entries(elementNode.dataset ?? {}).filter(([, value]) => value !== undefined)) as Record<string, string>
        };
      })
    : null;

  return {
    sessionId: params.sessionId,
    url: record.url,
    title: await page.title(),
    readyState: await page.evaluate(() => document.readyState),
    console: record.logs.slice(-100),
    element,
    pageText: await page.evaluate(() => document.body?.innerText?.slice(0, 10_000) ?? '')
  };
}

async function browserClick(params: { sessionId: string; selector: string; timeoutMs?: number }): Promise<Record<string, unknown>> {
  const record = await getBrowserSession(params.sessionId);
  const timeoutMs = Math.max(1000, Math.min(params.timeoutMs ?? 10_000, 60_000));
  await record.page.locator(params.selector).first().click({ timeout: timeoutMs });
  record.updatedAt = Date.now();
  return {
    sessionId: params.sessionId,
    url: record.page.url()
  };
}

async function browserType(params: { sessionId: string; selector: string; text: string; clear?: boolean; timeoutMs?: number }): Promise<Record<string, unknown>> {
  const record = await getBrowserSession(params.sessionId);
  const timeoutMs = Math.max(1000, Math.min(params.timeoutMs ?? 10_000, 60_000));
  const locator = record.page.locator(params.selector).first();
  if (params.clear !== false) {
    await locator.fill('', { timeout: timeoutMs });
  }
  await locator.fill(params.text, { timeout: timeoutMs });
  record.updatedAt = Date.now();
  return {
    sessionId: params.sessionId,
    url: record.page.url(),
    textLength: params.text.length
  };
}

async function browserPress(params: { sessionId: string; selector: string; key: string; timeoutMs?: number }): Promise<Record<string, unknown>> {
  const record = await getBrowserSession(params.sessionId);
  const timeoutMs = Math.max(1000, Math.min(params.timeoutMs ?? 10_000, 60_000));
  await record.page.locator(params.selector).first().press(params.key, { timeout: timeoutMs });
  record.updatedAt = Date.now();
  return {
    sessionId: params.sessionId,
    url: record.page.url(),
    key: params.key
  };
}

async function browserWait(params: { sessionId: string; selector?: string; timeoutMs?: number }): Promise<Record<string, unknown>> {
  const record = await getBrowserSession(params.sessionId);
  const timeoutMs = Math.max(1000, Math.min(params.timeoutMs ?? 10_000, 60_000));
  if (params.selector) {
    await record.page.waitForSelector(params.selector, { timeout: timeoutMs });
  } else {
    await record.page.waitForLoadState('networkidle', { timeout: timeoutMs }).catch(() => {});
  }
  record.updatedAt = Date.now();
  return {
    sessionId: params.sessionId,
    url: record.page.url(),
    readyState: await record.page.evaluate(() => document.readyState)
  };
}

async function browserConsole(params: { sessionId: string; limit?: number; clear?: boolean }): Promise<Record<string, unknown>> {
  const record = await getBrowserSession(params.sessionId);
  const limit = Math.max(1, Math.min(params.limit ?? 50, 500));
  const logs = record.logs.slice(-limit);
  if (params.clear) {
    record.logs.length = 0;
  }
  return {
    sessionId: params.sessionId,
    count: logs.length,
    logs
  };
}

async function browserEval(params: { sessionId: string; script: string }): Promise<Record<string, unknown>> {
  const record = await getBrowserSession(params.sessionId);
  const result = await record.page.evaluate(source => {
    // eslint-disable-next-line no-eval
    return eval(source);
  }, params.script);
  record.updatedAt = Date.now();
  return {
    sessionId: params.sessionId,
    url: record.page.url(),
    result
  };
}

async function browserScreenshot(params: {
  sessionId: string;
  confirmed: true;
  selector?: string;
  fullPage?: boolean;
  quality?: number;
  timeoutMs?: number;
}): Promise<BrowserScreenshotResult & { imageData: string }> {
  const record = await getBrowserSession(params.sessionId);
  const timeoutMs = Math.max(1000, Math.min(params.timeoutMs ?? 10_000, 60_000));
  const fullPage = params.fullPage === true;
  const quality = Math.max(30, Math.min(params.quality ?? 70, 95));
  const selector = params.selector?.trim() || '';
  const buffer = selector
    ? await record.page.locator(selector).first().screenshot({ timeout: timeoutMs, type: 'jpeg', quality })
    : await record.page.screenshot({ timeout: timeoutMs, fullPage, type: 'jpeg', quality });
  const imageData = buffer.toString('base64');
  record.updatedAt = Date.now();

  return {
    sessionId: params.sessionId,
    url: record.page.url(),
    fullPage,
    selector: selector || undefined,
    mimeType: 'image/jpeg',
    bytes: buffer.length,
    imageData
  };
}

async function browserClose(params: { sessionId: string }): Promise<Record<string, unknown>> {
  const closed = await closeBrowserSession(params.sessionId);
  return {
    sessionId: params.sessionId,
    closed
  };
}

async function main(): Promise<void> {
  const packageScripts = getPackageScripts(PRIMARY_PACKAGE_ROOT);
  const server = new McpServer(
    {
      name: 'bent-frontend-mcp-server',
      version: '1.0.0'
    },
    {
      instructions:
        `You are a project-scoped file and git helper for ${DEFAULT_PROJECT_ROOT} and ${SECONDARY_PROJECT_ROOT}. ` +
        'Always stay inside the allowed root. Prefer list_files before read_file on unfamiliar paths. ' +
        'Treat .env, keys, tokens, and secrets as off-limits. Use search_files for discovery, git_status before git_diff, ' +
        'Use write_file/create_file/replace_in_file/sed for direct file edits, read_file_range and nl for large files, ' +
        'project_index, find_symbol, go_to_definition, find_references, resolve_path, and workspace_snapshot for IDE-style context, ' +
        'run_node_script for inline Node snippets that should behave like node - with heredoc input, ' +
        'run_tests for build/lint/typecheck, package_info for scripts and versions, get_diagnostics for typecheck output, ' +
        'inspect_runtime and browser_* tools for local browser/runtime inspection, local_process_* tools for starting and stopping local dev servers, ' +
        'db_select for read-only bent-db SQL, db_query for full-access bent-db SQL, laravel_artisan for php artisan commands on the live backend, ' +
        'and sync_live_php to upload changed hub backend PHP files to the live api.hub.bent.ge Laravel runtime path. ' +
        'For database work, prefer db_select first to inspect schema and rows, then use db_query only when a write or multi-statement batch is actually needed. ' +
        'Do not replace direct DB tools with shell dumps, Laravel migrations, or manual PHP scripts when a DB tool can answer the question directly. ' +
        'Never invent paths outside the project root.'
    }
  );

  server.registerTool(
    'list_files',
    {
      title: 'List Files',
      description: 'Use this when you need a project tree or directory listing inside the allowed root.',
      inputSchema: z.object({
        path: z.string().optional().describe('Directory path relative to the project root. Defaults to the project root.'),
        maxDepth: z.number().int().min(0).max(10).optional().describe('Maximum recursion depth.'),
        maxEntries: z.number().int().min(1).max(MAX_TREE_ENTRIES).optional().describe('Maximum number of entries to return.'),
        includeHidden: z.boolean().optional().describe('Include dotfiles and dot-directories.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ path: relativePath, maxDepth = 4, maxEntries = 1000, includeHidden = false }): Promise<ToolSuccess> => {
      if (!relativePath || relativePath === '.') {
        const roots = ROOT_VIEWS.map(view => ({
          path: view.alias,
          type: 'dir' as const
        }));
        return {
          content: [
            {
              type: 'text',
              text: ['Workspace roots', ...roots.map(entry => `[dir] ${entry.path}`)].join('\n')
            }
          ],
          structuredContent: {
            root: '.',
            count: roots.length,
            entries: roots,
            maxDepth,
            maxEntries
          }
        };
      }

      const target = await resolveExistingPath(relativePath ?? '.');
      const stat = await fs.stat(target);
      if (!stat.isDirectory()) {
        throw new Error(`Not a directory: ${relativePath ?? '.'}`);
      }

      const { lines, count, entries } = await buildTree(target, maxDepth, maxEntries, includeHidden);
      const header = toVirtualPath(target);
      const text = [`Tree for ${header}`, ...lines].join('\n');
      return {
        content: [
          {
            type: 'text',
            text: text || 'No entries found.'
          }
        ],
        structuredContent: {
          root: header,
          count,
          entries,
          maxDepth,
          maxEntries
        }
      };
    }
  );

  server.registerTool(
    'read_file',
    {
      title: 'Read File',
      description: 'Use this when you need the exact contents of one text file inside the allowed root.',
      inputSchema: z.object({
        path: z.string().describe('File path relative to the project root.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ path: relativePath }): Promise<ToolSuccess> => {
      const absolute = await assertReadablePath(relativePath);
      const stat = await fs.stat(absolute);
      if (stat.size > MAX_FILE_BYTES) {
        throw new Error(`File too large to read safely (${formatBytes(stat.size)}).`);
      }
      const text = await fs.readFile(absolute, 'utf8');
      return {
        content: [{ type: 'text', text }],
        structuredContent: {
          path: toVirtualPath(absolute),
          bytes: stat.size,
          content: text
        }
      };
    }
  );

  server.registerTool(
    'read_file_range',
    {
      title: 'Read File Range',
      description: 'Use this when you only need a slice of a large file by line numbers.',
      inputSchema: z.object({
        path: z.string().describe('File path relative to the project root.'),
        startLine: z.number().int().min(1).describe('First line to include, 1-based and inclusive.'),
        endLine: z.number().int().min(1).max(MAX_RANGE_LINES).describe('Last line to include, 1-based and inclusive.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ path: relativePath, startLine, endLine }): Promise<ToolSuccess> => {
      if (endLine < startLine) {
        throw new Error('endLine must be greater than or equal to startLine.');
      }

      const absolute = await assertReadablePath(relativePath);
      const { text, totalLines } = await readTextFileRange(absolute, startLine, endLine);
      const lines = text ? text.split('\n') : [];
      return {
        content: [
          {
            type: 'text',
            text: text || '(empty range)'
          }
        ],
        structuredContent: {
          path: toVirtualPath(absolute),
          startLine,
          endLine,
          totalLines,
          returnedLines: lines.length,
          lines,
          content: text
        }
      };
    }
  );

  server.registerTool(
    'nl',
    {
      title: 'Numbered Lines',
      description: 'Use this to read file lines with `nl -ba` style numbering.',
      inputSchema: z.object({
        path: z.string().describe('File path relative to the project root.'),
        startLine: z.number().int().min(1).optional().describe('First line to include, 1-based and inclusive.'),
        endLine: z.number().int().min(1).max(MAX_RANGE_LINES).optional().describe('Last line to include, 1-based and inclusive.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ path: relativePath, startLine = 1, endLine = MAX_RANGE_LINES }): Promise<ToolSuccess> => {
      if (endLine < startLine) {
        throw new Error('endLine must be greater than or equal to startLine.');
      }

      const absolute = await assertReadablePath(relativePath);
      const { numberedLines, totalLines } = await readTextFileNumbered(absolute, startLine, endLine);
      const text = numberedLines.join('\n');
      return {
        content: [
          {
            type: 'text',
            text: text || '(empty range)'
          }
        ],
        structuredContent: {
          path: toVirtualPath(absolute),
          startLine,
          endLine,
          totalLines,
          returnedLines: numberedLines.length,
          lines: numberedLines
        }
      };
    }
  );

  server.registerTool(
    'sed',
    {
      title: 'Safe Sed',
      description: 'Use this for a limited sed-style read or exact substitution inside one file.',
      inputSchema: z.object({
        path: z.string().describe('File path relative to the project root.'),
        startLine: z.number().int().min(1).optional().describe('Optional first line to print, 1-based and inclusive.'),
        endLine: z.number().int().min(1).max(MAX_RANGE_LINES).optional().describe('Optional last line to print, 1-based and inclusive.'),
        search: z.string().optional().describe('Optional exact text to find for substitution.'),
        replace: z.string().optional().describe('Optional replacement text.'),
        allOccurrences: z.boolean().optional().describe('Replace every occurrence instead of just the first one.'),
        apply: z.boolean().optional().describe('When true, write the substitution back to disk.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async input => {
      const {
        path: relativePath,
        startLine,
        endLine,
        search,
        replace = '',
        allOccurrences = false,
        apply = false
      } = input;
      const absolute = await assertReadablePath(relativePath);
      const hasSearch = Object.prototype.hasOwnProperty.call(input, 'search');
      const hasReplace = Object.prototype.hasOwnProperty.call(input, 'replace');
      const hasApply = Object.prototype.hasOwnProperty.call(input, 'apply');

      if (hasSearch || hasReplace || hasApply) {
        if (!hasSearch || !hasReplace || !search) {
          throw new Error('sed substitution requires search and replace.');
        }

        const { occurrences, updatedText } = await applySafeSedSubstitution(absolute, search, replace, allOccurrences);
        if (apply) {
          const writable = await resolveWritableFile(relativePath);
          await writeTextFileAtomically(writable, updatedText);
        }

        return {
          content: [
            {
              type: 'text',
              text: apply ? `Applied sed substitution in ${toVirtualPath(absolute)} (${occurrences} occurrence(s)).` : updatedText
            }
          ],
          structuredContent: {
            path: toVirtualPath(absolute),
            mode: 'substitute',
            occurrences,
            applied: apply,
            allOccurrences,
            content: updatedText
          }
        };
      }

      const fromLine = startLine ?? 1;
      const toLine = endLine ?? Math.min(fromLine + MAX_RANGE_LINES - 1, MAX_RANGE_LINES);
      if (toLine < fromLine) {
        throw new Error('endLine must be greater than or equal to startLine.');
      }
      const { text, totalLines } = await readTextFileRange(absolute, fromLine, toLine);
      return {
        content: [
          {
            type: 'text',
            text: text || '(empty range)'
          }
        ],
        structuredContent: {
          path: toVirtualPath(absolute),
          mode: 'print',
          startLine: fromLine,
          endLine: toLine,
          totalLines,
          content: text
        }
      };
    }
  );

  server.registerTool(
    'get_file_metadata',
    {
      title: 'Get File Metadata',
      description: 'Use this when you need size, timestamps, and type information for one path.',
      inputSchema: z.object({
        path: z.string().describe('Path relative to the project root.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ path: relativePath }): Promise<ToolSuccess> => {
      const metadata = await getFileMetadata(relativePath);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(metadata, null, 2)
          }
        ],
        structuredContent: metadata
      };
    }
  );

  server.registerTool(
    'search_files',
    {
      title: 'Search Files',
      description: 'Use this when you need ripgrep-style search across the allowed root.',
      inputSchema: z.object({
        query: z.string().min(1).describe('Search term or regular expression.'),
        path: z.string().optional().describe('Directory path relative to the project root to scope the search.'),
        glob: z.array(z.string()).optional().describe('Optional include glob patterns for rg.'),
        maxResults: z.number().int().min(1).max(MAX_SEARCH_RESULTS).optional().describe('Maximum number of matches to return.'),
        caseSensitive: z.boolean().optional().describe('Force case-sensitive search when true.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ query, path: scopePath, glob = [], maxResults = 50, caseSensitive = false }): Promise<ToolSuccess> => {
      const cwd = scopePath ? await resolveExistingPath(scopePath) : DEFAULT_PROJECT_ROOT;
      const cwdStat = await fs.stat(cwd);
      if (!cwdStat.isDirectory()) {
        throw new Error(`Search scope is not a directory: ${scopePath}`);
      }
      const relativeScope = toVirtualPath(cwd);
      if (isSensitiveRelativePath(relativeScope)) {
        throw new Error(`Search scope is sensitive: ${relativeScope}`);
      }

      const args = [
        '--line-number',
        '--column',
        '--no-heading',
        '--color',
        'never',
        '--smart-case',
        '--max-count',
        String(maxResults)
      ];

      if (!caseSensitive) {
        args.push('--ignore-case');
      }

      for (const pattern of glob) {
        args.push('--glob', pattern);
      }

      args.push(query, '.');

      const result = await runProcess('rg', args, cwd);
      const output = result.stdout.trim();
      const lines = output ? output.split('\n') : [];
      const matches = lines
        .map(parseRgMatch)
        .filter((match): match is SearchMatch => Boolean(match))
        .map(match => ({
          ...match,
          path: toVirtualPath(path.resolve(cwd, match.path))
        }))
        .filter(match => !isSensitiveRelativePath(match.path));

      return {
        content: [
          {
            type: 'text',
            text:
              matches.length > 0
                ? matches.map(match => `${match.path}:${match.line}:${match.column}:${match.text}`).join('\n')
                : 'No matches found.'
          }
        ],
        structuredContent: {
          cwd: relativeScope,
          query,
          maxResults,
          matches: matches.length,
          results: matches
        }
      };
    }
  );

  server.registerTool(
    'write_file',
    {
      title: 'Write File',
      description: 'Use this to create or overwrite a file inside the allowed root.',
      inputSchema: z.object({
        path: z.string().min(1).describe('Target file path relative to the project root.'),
        content: z.string().describe('Full file content to write.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ path: relativePath, content }): Promise<ToolSuccess> => {
      const absolute = await resolveWritableFile(relativePath);
      await writeTextFileAtomically(absolute, content);
      return {
        content: [{ type: 'text', text: `Wrote ${toVirtualPath(absolute)}` }],
        structuredContent: {
          path: toVirtualPath(absolute),
          bytes: Buffer.byteLength(content, 'utf8')
        }
      };
    }
  );

  server.registerTool(
    'create_file',
    {
      title: 'Create File',
      description: 'Use this to create a new file without overwriting an existing one.',
      inputSchema: z.object({
        path: z.string().min(1).describe('Target file path relative to the project root.'),
        content: z.string().describe('Initial file content.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ path: relativePath, content }): Promise<ToolSuccess> => {
      const absolute = await resolveWritableFile(relativePath);
      try {
        await fs.lstat(absolute);
        throw new Error(`File already exists: ${toVirtualPath(absolute)}`);
      } catch (error) {
        if (!isNotFoundError(error)) {
          throw error;
        }
      }

      await writeTextFileAtomically(absolute, content);
      return {
        content: [{ type: 'text', text: `Created ${toVirtualPath(absolute)}` }],
        structuredContent: {
          path: toVirtualPath(absolute),
          bytes: Buffer.byteLength(content, 'utf8')
        }
      };
    }
  );

  server.registerTool(
    'replace_in_file',
    {
      title: 'Replace in File',
      description: 'Use this for exact search/replace without sending a diff.',
      inputSchema: z.object({
        path: z.string().min(1).describe('Target file path relative to the project root.'),
        search: z.string().min(1).describe('Exact text to find.'),
        replace: z.string().describe('Replacement text.'),
        allOccurrences: z.boolean().optional().describe('Replace every occurrence instead of just the first one.')
      })
    },
    async ({ path: relativePath, search, replace, allOccurrences = false }): Promise<ToolSuccess> => {
      const absolute = await assertReadablePath(relativePath);
      const original = await fs.readFile(absolute, 'utf8');
      const occurrences = original.split(search).length - 1;

      if (occurrences === 0) {
        throw new Error(`Text not found in ${toVirtualPath(absolute)}.`);
      }
      if (!allOccurrences && occurrences > 1) {
        throw new Error(`Search text occurs ${occurrences} times. Set allOccurrences=true to replace every match.`);
      }

      const updated = allOccurrences ? original.split(search).join(replace) : original.replace(search, replace);
      const writable = await resolveWritableFile(relativePath);
      await writeTextFileAtomically(writable, updated);

      return {
        content: [
          {
            type: 'text',
            text: `Replaced ${allOccurrences ? occurrences : 1} occurrence(s) in ${toVirtualPath(absolute)}`
          }
        ],
        structuredContent: {
          path: toVirtualPath(absolute),
          occurrences,
          replaced: allOccurrences ? occurrences : 1,
          allOccurrences
        }
      };
    }
  );

  server.registerTool(
    'delete_file',
    {
      title: 'Delete File',
      description: 'Use this to delete a file or empty directory inside the allowed root.',
      inputSchema: z.object({
        path: z.string().min(1).describe('Target path relative to the project root.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ path: relativePath }): Promise<ToolSuccess> => {
      const absolute = await resolveExistingPath(relativePath);
      const stat = await fs.lstat(absolute);
      if (stat.isSymbolicLink()) {
        throw new Error(`Refusing to delete symlink: ${toVirtualPath(absolute)}`);
      }
      const relative = toVirtualPath(absolute);
      if (isSensitiveRelativePath(relative)) {
        throw new Error(`Access denied for sensitive file: ${relative}`);
      }
      await fs.rm(absolute, { recursive: false, force: false });
      return {
        content: [{ type: 'text', text: `Deleted ${relative}` }],
        structuredContent: {
          path: relative,
          deleted: true
        }
      };
    }
  );

  server.registerTool(
    'move_file',
    {
      title: 'Move File',
      description: 'Use this to rename or move a file or directory within the allowed root.',
      inputSchema: z.object({
        from: z.string().min(1).describe('Existing source path relative to the project root.'),
        to: z.string().min(1).describe('Destination path relative to the project root.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ from, to }): Promise<ToolSuccess> => {
      const source = await resolveExistingPath(from);
      const sourceRelative = toVirtualPath(source);
      if (isSensitiveRelativePath(sourceRelative)) {
        throw new Error(`Access denied for sensitive file: ${sourceRelative}`);
      }
      const sourceStat = await fs.lstat(source);
      if (sourceStat.isSymbolicLink()) {
        throw new Error(`Refusing to move symlink: ${sourceRelative}`);
      }

      const target = await resolveWritableFile(to);
      try {
        await fs.lstat(target);
        throw new Error(`Destination already exists: ${toVirtualPath(target)}`);
      } catch (error) {
        if (!isNotFoundError(error)) {
          throw error;
        }
      }

      await fs.rename(source, target);
      return {
        content: [
          {
            type: 'text',
            text: `Moved ${sourceRelative} -> ${toVirtualPath(target)}`
          }
        ],
        structuredContent: {
          from: sourceRelative,
          to: toVirtualPath(target)
        }
      };
    }
  );

  server.registerTool(
    'format_file',
    {
      title: 'Format File',
      description: 'Use this to format one file with local prettier or eslint fix.',
      inputSchema: z.object({
        path: z.string().min(1).describe('Target file path relative to the project root.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ path: relativePath }): Promise<ToolSuccess> => {
      const absolute = await resolveExistingPath(relativePath);
      const stat = await fs.lstat(absolute);
      if (stat.isSymbolicLink()) {
        throw new Error(`Refusing to format symlink: ${toVirtualPath(absolute)}`);
      }
      if (stat.isDirectory()) {
        throw new Error(`Cannot format a directory: ${toVirtualPath(absolute)}`);
      }

      const root = getAllowedRootForAbsolute(absolute) ?? DEFAULT_PROJECT_ROOT;
      const result = await formatWithPrettierOrEslint(absolute, root);
      const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
      return {
        content: [
          {
            type: 'text',
            text: text || `Formatted ${toVirtualPath(absolute)}`
          }
        ],
        structuredContent: {
          path: toVirtualPath(absolute),
          command: result.command,
          args: result.args
        }
      };
    }
  );

  server.registerTool(
    'apply_patch',
    {
      title: 'Apply Patch',
      description: 'Use this when you need to apply a small unified diff inside the allowed root.',
      inputSchema: z.object({
        patch: z.string().min(1).describe('Unified diff text.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ patch }): Promise<ToolSuccess> => {
      const paths = assertSafePatchPaths(patch);
      const tempDir = await fs.mkdtemp(path.join(process.platform === 'win32' ? process.env.TEMP ?? '/tmp' : '/tmp', 'mcp-patch-'));
      const patchPath = path.join(tempDir, 'patch.diff');
      await fs.writeFile(patchPath, patch, 'utf8');

      try {
        const result = await runProcess('git', ['apply', '--whitespace=nowarn', patchPath], DEFAULT_PROJECT_ROOT);
        if (result.exitCode !== 0) {
          throw new Error(result.stderr.trim() || result.stdout.trim() || 'git apply failed.');
        }

        return {
          content: [
            {
              type: 'text',
              text: `Patch applied to ${paths.length} path(s):\n${paths.join('\n')}`
            }
          ],
          structuredContent: {
            appliedPaths: paths
          }
        };
      } finally {
        await fs.rm(tempDir, { recursive: true, force: true });
      }
    }
  );

  server.registerTool(
    'run_command',
    {
      title: 'Run Command',
      description: 'Use this when you need one allowlisted, safe command run inside the project root.',
      inputSchema: z.object({
        command: z.string().min(1).describe('Executable name, for example git, rg, npm, or pnpm.'),
        args: z.array(z.string()).default([]).describe('Command arguments.'),
        cwd: z.string().optional().describe('Optional working directory relative to the project root.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async (input): Promise<ToolSuccess> => {
      const args = input.args ?? [];
      const cwd = input.cwd ? await resolveExistingPath(input.cwd) : DEFAULT_PROJECT_ROOT;
      const stat = await fs.stat(cwd);
      if (!stat.isDirectory()) {
        throw new Error(`Command cwd is not a directory: ${input.cwd}`);
      }
      validateCommandName({ command: input.command, args }, cwd);
      const result = await runProcess(input.command, args, cwd, buildCommandEnv(input.command, args));
      const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
      return {
        content: [
          {
            type: 'text',
            text:
              text ||
              (result.exitCode === 0
                ? '(no output)'
                : `Command exited with code ${result.exitCode}.`)
          }
        ],
        structuredContent: {
          command: input.command,
          args,
          cwd: toVirtualPath(cwd),
          exitCode: result.exitCode,
          stdout: result.stdout,
          stderr: result.stderr
        }
      };
    }
  );

  server.registerTool(
    'run_node_script',
    {
      title: 'Run Node Script',
      description:
        'Use this for small inline Node.js snippets that need to run inside a project cwd without shell heredocs. ' +
        'The script is checked for unsafe operations and runs with stdin as the provided source.',
      inputSchema: z.object({
        script: z.string().min(1).describe('Inline Node.js source code to execute, for example a heredoc body.'),
        cwd: z.string().optional().describe('Optional working directory relative to the project root.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ script, cwd: relativeCwd }): Promise<ToolSuccess> => {
      const cwd = relativeCwd ? await resolveExistingPath(relativeCwd) : PRIMARY_PACKAGE_ROOT;
      const stat = await fs.stat(cwd);
      if (!stat.isDirectory()) {
        throw new Error(`Command cwd is not a directory: ${relativeCwd}`);
      }
      const result = await runNodeScript(script, cwd);
      const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
      return {
        content: [
          {
            type: 'text',
            text:
              text ||
              (result.exitCode === 0
                ? '(no output)'
                : `Script exited with code ${result.exitCode}.`)
          }
        ],
        structuredContent: {
          cwd: toVirtualPath(cwd),
          exitCode: result.exitCode,
          stdout: result.stdout,
          stderr: result.stderr
        }
      };
    }
  );

  server.registerTool(
    'git_status',
    {
      title: 'Git Status',
      description: 'Use this when you need a safe git status summary for the allowed root.',
      inputSchema: z.object({}).strict(),
      annotations: {
        readOnlyHint: true
      }
    },
    async (): Promise<ToolSuccess> => {
      const result = await runProcess('git', ['status', '--short', '--branch'], DEFAULT_PROJECT_ROOT);
      const text = filterSensitiveStatusLines(result.stdout.trim());
      return {
        content: [{ type: 'text', text: text || '(clean)' }],
        structuredContent: {
          root: path.basename(DEFAULT_PROJECT_ROOT)
        }
      };
    }
  );

  server.registerTool(
    'git_diff',
    {
      title: 'Git Diff',
      description: 'Use this when you need a safe diff from the allowed root. Secret-like files are redacted.',
      inputSchema: z.object({
        cached: z.boolean().optional().describe('Show staged changes instead of working-tree changes.'),
        paths: z.array(z.string()).optional().describe('Optional file paths relative to the project root.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ cached = false, paths = [] }): Promise<ToolSuccess> => {
      const validatedPaths: string[] = [];
      for (const candidate of paths) {
        const absolute = await resolveMaybeMissingPath(candidate);
        const relative = toVirtualPath(absolute);
        if (isSensitiveRelativePath(relative)) {
          throw new Error(`Diff path is sensitive: ${relative}`);
        }
        validatedPaths.push(relative);
      }

      const args = ['diff', '--no-ext-diff'];
      if (cached) {
        args.push('--cached');
      }
      if (validatedPaths.length > 0) {
        args.push('--', ...validatedPaths);
      }

      const result = await runProcess('git', args, DEFAULT_PROJECT_ROOT);
      const diffText = redactSensitiveDiff(result.stdout.trim());
      const clamped = clampToolText(diffText);
      return {
        content: [
          {
            type: 'text',
            text:
              clamped.text ||
              (result.exitCode === 0 ? '(no diff)' : `git diff exited with code ${result.exitCode}.`)
          }
        ],
        structuredContent: {
          cached,
          paths: validatedPaths,
          truncated: clamped.truncated,
          exitCode: result.exitCode
        }
      };
    }
  );

  server.registerTool(
    'package_info',
    {
      title: 'Package Info',
      description: 'Use this when you need package manager, scripts, and local tool versions.',
      inputSchema: z.object({}).strict(),
      annotations: {
        readOnlyHint: true
      }
    },
    async (): Promise<ToolSuccess> => {
      const packageManager = getPackageManagerName(PRIMARY_PACKAGE_ROOT);
      const npmVersion = await probeBinaryVersion('npm', ['-v'], PRIMARY_PACKAGE_ROOT);
      const pnpmVersion = await probeBinaryVersion('pnpm', ['-v'], PRIMARY_PACKAGE_ROOT);
      const payload = {
        packageManager,
        nodeVersion: process.version,
        npmVersion,
        pnpmVersion,
        scripts: getPackageScripts(PRIMARY_PACKAGE_ROOT),
        lockfiles: {
          packageLock: existsSync(path.join(PRIMARY_PACKAGE_ROOT, 'package-lock.json')),
          pnpmLock: existsSync(path.join(PRIMARY_PACKAGE_ROOT, 'pnpm-lock.yaml'))
        }
      };
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'run_tests',
    {
      title: 'Run Tests',
      description: 'Use this for safe package tasks like build, lint, typecheck, generate, or test.',
      inputSchema: z.object({
        task: z.enum(['build', 'lint', 'typecheck', 'generate', 'test', 'sync:site-content']).describe('Safe package task to run.'),
        cwd: z.string().optional().describe('Optional working directory relative to the project root.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ task, cwd: relativeCwd }): Promise<ToolSuccess> => {
      const cwd = relativeCwd ? await resolveExistingPath(relativeCwd) : PRIMARY_PACKAGE_ROOT;
      const stat = await fs.stat(cwd);
      if (!stat.isDirectory()) {
        throw new Error(`Command cwd is not a directory: ${relativeCwd}`);
      }

      const result = await runSafeProjectTask(task, cwd);
      const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
      return {
        content: [
          {
            type: 'text',
            text:
              text ||
              (result.exitCode === 0
                ? '(no output)'
                : `Task exited with code ${result.exitCode}.`)
          }
        ],
        structuredContent: {
          task,
          cwd: toVirtualPath(cwd),
          command: result.command,
          args: result.args,
          exitCode: result.exitCode,
          stdout: result.stdout,
          stderr: result.stderr
        }
      };
    }
  );

  server.registerTool(
    'sync_live_php',
    {
      title: 'Sync Live PHP',
      description:
        'Use this to upload changed hub backend PHP files to the live api.hub.bent.ge Laravel runtime path. ' +
        'Local hub.bent.ge/backend/app/Foo.php maps to live /home/ontripge/domains/api.hub.bent.ge/app/Foo.php.',
      inputSchema: z.object({
        paths: z
          .array(z.string().min(1))
          .optional()
          .describe('Optional virtual paths to PHP files, for example hub.bent.ge/backend/app/Http/Controllers/Api/Foo.php. If omitted, changed PHP files are detected from git status.'),
        dryRun: z.boolean().optional().describe('When true, verify and show what would sync without uploading files.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ paths, dryRun = false }): Promise<ToolSuccess> => {
      const files = await prepareLivePhpSyncFiles(paths);
      await verifyHubLiveRoot();

      const synced: Array<{ localPath: string; remotePath: string; stdout: string; stderr: string; dryRun: boolean }> = [];
      for (const file of files) {
        const result = await syncLivePhpFile(file, dryRun);
        synced.push({
          localPath: file.localPath,
          remotePath: `${HUB_LIVE_REMOTE_HOST}:${file.remotePath}`,
          stdout: result.stdout.trim(),
          stderr: result.stderr.trim(),
          dryRun
        });
      }

      const text = synced
        .map(file => {
          const output = [file.stdout, file.stderr].filter(Boolean).join('\n');
          return `${file.dryRun ? '[dry-run] ' : ''}${file.localPath} -> ${file.remotePath}${output ? `\n${output}` : ''}`;
        })
        .join('\n\n');

      return {
        content: [
          {
            type: 'text',
            text: text || 'No files synced.'
          }
        ],
        structuredContent: {
          dryRun,
          remoteHost: HUB_LIVE_REMOTE_HOST,
          liveRoot: HUB_LIVE_ROOT,
          count: synced.length,
          files: synced
        }
      };
    }
  );

  server.registerTool(
    'db_select',
    {
      title: 'DB Select',
      description: 'Use this to run read-only SQL against the live bent-db helper and inspect rows or schema.',
      inputSchema: z.object({
        sql: z.string().min(1).describe('Read-only SQL statement, for example SELECT, SHOW, DESCRIBE, or EXPLAIN.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ sql }): Promise<ToolSuccess> => {
      if (!isReadOnlySql(sql)) {
        throw new Error('db_select only allows read-only SQL statements.');
      }
      const result = await runBentDbSql(sql);
      const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
      return {
        content: [
          {
            type: 'text',
            text: text || '(no output)'
          }
        ],
        structuredContent: {
          sql,
          command: result.command,
          exitCode: result.exitCode,
          stdout: result.stdout,
          stderr: result.stderr
        }
      };
    }
  );

  server.registerTool(
    'db_query',
    {
      title: 'DB Query',
      description: 'Use this to run full-access SQL against the live bent-db helper for reads and writes.',
      inputSchema: z.object({
        sql: z.string().min(1).describe('Any SQL statement or multi-statement batch to run against the live database.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ sql }): Promise<ToolSuccess> => {
      const result = await runBentDbSql(sql);
      const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
      return {
        content: [
          {
            type: 'text',
            text: text || '(no output)'
          }
        ],
        structuredContent: {
          sql,
          command: result.command,
          exitCode: result.exitCode,
          stdout: result.stdout,
          stderr: result.stderr
        }
      };
    }
  );

  server.registerTool(
    'laravel_artisan',
    {
      title: 'Laravel Artisan',
      description: 'Use this to run php artisan commands on the live hub backend through bent-db.',
      inputSchema: z.object({
        args: z.array(z.string().min(1)).min(1).describe('Artisan arguments, for example ["migrate", "--force"] or ["route:list"].')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ args }): Promise<ToolSuccess> => {
      const result = await runLaravelArtisan(args);
      const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
      return {
        content: [
          {
            type: 'text',
            text: text || '(no output)'
          }
        ],
        structuredContent: {
          command: result.command,
          cwd: result.cwd,
          exitCode: result.exitCode,
          stdout: result.stdout,
          stderr: result.stderr
        }
      };
    }
  );

  server.registerTool(
    'get_diagnostics',
    {
      title: 'Get Diagnostics',
      description: 'Use this to get typecheck diagnostics for one file or a scoped directory.',
      inputSchema: z.object({
        path: z.string().min(1).describe('File or directory path relative to the project root.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ path: targetPath }): Promise<ToolSuccess> => {
      const absolute = await resolveExistingPath(targetPath);
      const stat = await fs.stat(absolute);
      const relativeTarget = toVirtualPath(absolute);
      const root = getDiagnosticsRootForAbsolute(absolute);
      const result = await runSafeProjectTask('typecheck', root);
      const output = [result.stdout, result.stderr].join('\n').trim();
      const lines = output ? output.split('\n') : [];
      const filtered = lines.filter(line => line.includes(relativeTarget) || line.includes(targetPath));
      return {
        content: [
          {
            type: 'text',
            text: filtered.length > 0 ? filtered.join('\n') : `No diagnostics found for ${relativeTarget}.`
          }
        ],
        structuredContent: {
          path: relativeTarget,
          isDirectory: stat.isDirectory(),
          matchedLines: filtered.length,
          command: result.command,
          args: result.args,
          exitCode: result.exitCode
        }
      };
    }
  );

  server.registerTool(
    'project_index',
    {
      title: 'Project Index',
      description: 'Use this when you need a compact project-wide index of files, symbols, and languages.',
      inputSchema: z.object({
        scope: z.string().optional().describe('Optional directory or file path to scope the index. Defaults to both workspace roots.'),
        forceRefresh: z.boolean().optional().describe('Rebuild the index even if a cached copy exists.'),
        maxSymbols: z.number().int().min(1).max(500).optional().describe('Maximum symbols to return in the response sample.'),
        maxFiles: z.number().int().min(1).max(500).optional().describe('Maximum files to return in the response sample.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ scope, forceRefresh = false, maxSymbols = 200, maxFiles = 200 }): Promise<ToolSuccess> => {
      const index = await buildProjectIndex(scope, forceRefresh);
      const fileSample = index.files.slice(0, maxFiles).map(file => ({
        path: file.path,
        size: file.size,
        mtimeMs: file.mtimeMs,
        language: file.language
      }));
      const symbolSample = index.symbols.slice(0, maxSymbols);
      const summary = {
        scope: index.scope,
        builtAt: index.builtAt,
        rootCount: index.roots.length,
        fileCount: index.files.length,
        symbolCount: index.symbols.length,
        languages: index.languages,
        roots: index.roots.map(root => ({
          alias: root.alias,
          root: root.root
        }))
      };
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                ...summary,
                files: fileSample,
                symbols: symbolSample
              },
              null,
              2
            )
          }
        ],
        structuredContent: {
          ...summary,
          files: fileSample,
          symbols: symbolSample
        }
      };
    }
  );

  server.registerTool(
    'find_symbol',
    {
      title: 'Find Symbol',
      description: 'Use this when you need to search for a symbol name across the indexed workspace.',
      inputSchema: z.object({
        query: z.string().min(1).describe('Symbol name or partial name.'),
        scope: z.string().optional().describe('Optional scope to limit the search.'),
        maxResults: z.number().int().min(1).max(200).optional().describe('Maximum matches to return.'),
        kind: z.string().optional().describe('Optional symbol kind filter, for example component, class, function, prop, emit, or route.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ query, scope, maxResults = 50, kind }): Promise<ToolSuccess> => {
      const index = await buildProjectIndex(scope);
      const matches = findSymbolsInIndex(index, query, maxResults, kind);
      return {
        content: [
          {
            type: 'text',
            text:
              matches.length > 0
                ? matches.map(match => `${match.name} [${match.kind}] ${match.path}:${match.line}:${match.column}`).join('\n')
                : 'No symbol matches found.'
          }
        ],
        structuredContent: {
          query,
          kind: kind ?? null,
          matches: matches.length,
          results: matches
        }
      };
    }
  );

  server.registerTool(
    'go_to_definition',
    {
      title: 'Go To Definition',
      description: 'Use this when you need the declaration site for a symbol or a cursor location.',
      inputSchema: z
        .object({
          symbol: z.string().optional().describe('Explicit symbol name.'),
          path: z.string().optional().describe('Path that contains the symbol under the cursor.'),
          line: z.number().int().min(1).optional().describe('1-based line number for the cursor location.'),
          column: z.number().int().min(1).optional().describe('1-based column number for the cursor location.'),
          scope: z.string().optional().describe('Optional scope to limit the search.')
        })
        .refine(value => Boolean(value.symbol || (value.path && value.line)), {
          message: 'Provide symbol or path+line.'
        }),
      annotations: {
        readOnlyHint: true
      }
    },
    async input => {
      const payload = await getGoToDefinitionResult(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'find_references',
    {
      title: 'Find References',
      description: 'Use this when you need all known usages of a symbol across the indexed workspace.',
      inputSchema: z
        .object({
          symbol: z.string().optional().describe('Explicit symbol name.'),
          path: z.string().optional().describe('Path that contains the symbol under the cursor.'),
          line: z.number().int().min(1).optional().describe('1-based line number for the cursor location.'),
          column: z.number().int().min(1).optional().describe('1-based column number for the cursor location.'),
          scope: z.string().optional().describe('Optional scope to limit the search.'),
          maxResults: z.number().int().min(1).max(200).optional().describe('Maximum references to return.')
        })
        .refine(value => Boolean(value.symbol || (value.path && value.line)), {
          message: 'Provide symbol or path+line.'
        }),
      annotations: {
        readOnlyHint: true
      }
    },
    async input => {
      const payload = await getFindReferencesResult(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'resolve_path',
    {
      title: 'Resolve Path',
      description: 'Use this to inspect how a virtual path maps to an allowed root and absolute path.',
      inputSchema: z.object({
        path: z.string().min(1).describe('Virtual or relative path.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ path: targetPath }): Promise<ToolSuccess> => {
      const payload = await resolvePathInfo(targetPath);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'workspace_snapshot',
    {
      title: 'Workspace Snapshot',
      description: 'Use this when you need a compact view of roots, scripts, git status, and index size.',
      inputSchema: z
        .object({
          forceRefresh: z.boolean().optional().describe('Rebuild the index before taking the snapshot.')
        })
        .strict(),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ forceRefresh = false }): Promise<ToolSuccess> => {
      const payload = await getWorkspaceSnapshot(forceRefresh);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'inspect_runtime',
    {
      title: 'Inspect Runtime',
      description: 'Use this to inspect a local dev page, capture console logs, and read DOM element props.',
      inputSchema: z.object({
        url: z.string().url().describe('Local dev URL to inspect.'),
        selector: z.string().optional().describe('Optional CSS selector to inspect.'),
        waitFor: z.string().optional().describe('Optional CSS selector to wait for before inspection.'),
        timeoutMs: z.number().int().min(1000).max(60000).optional().describe('Timeout in milliseconds.'),
        captureConsole: z.boolean().optional().describe('Capture console and page errors when true.'),
        script: z.string().optional().describe('Optional JavaScript expression to evaluate in the page context.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async input => {
      const payload = await inspectRuntime(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_open',
    {
      title: 'Browser Open',
      description: 'Use this to open a local dev URL in a persistent browser session.',
      inputSchema: z.object({
        url: z.string().url().optional().describe('Local dev URL to open.'),
        host: z.string().optional().describe('Local host name, default 127.0.0.1.'),
        port: z.number().int().min(1).max(65535).optional().describe('Local port, default 3000.'),
        path: z.string().optional().describe('Optional path for host/port form.'),
        captureConsole: z.boolean().optional().describe('Capture console and page errors when true.'),
        waitFor: z.string().optional().describe('Optional selector to wait for.'),
        timeoutMs: z.number().int().min(1000).max(60000).optional().describe('Timeout in milliseconds.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async input => {
      const payload = await openBrowserSession(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_snapshot',
    {
      title: 'Browser Snapshot',
      description: 'Use this to inspect the current browser session, DOM element props, and console logs.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.'),
        selector: z.string().optional().describe('Optional CSS selector to inspect.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async input => {
      const payload = await browserSessionSnapshot(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_click',
    {
      title: 'Browser Click',
      description: 'Use this to click an element in a persistent browser session.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.'),
        selector: z.string().min(1).describe('CSS selector to click.'),
        timeoutMs: z.number().int().min(1000).max(60000).optional().describe('Timeout in milliseconds.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async input => {
      const payload = await browserClick(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_type',
    {
      title: 'Browser Type',
      description: 'Use this to type text into an element in a persistent browser session.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.'),
        selector: z.string().min(1).describe('CSS selector to type into.'),
        text: z.string().describe('Text to type.'),
        clear: z.boolean().optional().describe('Clear existing text first.'),
        timeoutMs: z.number().int().min(1000).max(60000).optional().describe('Timeout in milliseconds.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async input => {
      const payload = await browserType(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_press',
    {
      title: 'Browser Press',
      description: 'Use this to press a key in a persistent browser session.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.'),
        selector: z.string().min(1).describe('CSS selector to press a key on.'),
        key: z.string().min(1).describe('Keyboard key, for example Enter, Escape, Tab.'),
        timeoutMs: z.number().int().min(1000).max(60000).optional().describe('Timeout in milliseconds.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async input => {
      const payload = await browserPress(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_wait',
    {
      title: 'Browser Wait',
      description: 'Use this to wait for a browser session to settle or for one selector to appear.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.'),
        selector: z.string().optional().describe('Optional selector to wait for.'),
        timeoutMs: z.number().int().min(1000).max(60000).optional().describe('Timeout in milliseconds.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async input => {
      const payload = await browserWait(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_console',
    {
      title: 'Browser Console',
      description: 'Use this to read captured console and page error logs from a persistent browser session.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.'),
        limit: z.number().int().min(1).max(500).optional().describe('Maximum log lines to return.'),
        clear: z.boolean().optional().describe('Clear logs after reading them.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async input => {
      const payload = await browserConsole(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_eval',
    {
      title: 'Browser Eval',
      description: 'Use this to evaluate JavaScript in a persistent browser session.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.'),
        script: z.string().min(1).describe('JavaScript to evaluate in the page context.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async input => {
      const payload = await browserEval(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'browser_screenshot',
    {
      title: 'Browser Screenshot',
      description: 'Use this to capture a screenshot from a persistent browser session. Requires explicit confirmation.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.'),
        confirmed: z.literal(true).describe('Must be true to capture a screenshot.'),
        selector: z.string().optional().describe('Optional CSS selector to capture instead of the full page.'),
        fullPage: z.boolean().optional().describe('When true, capture the full page. Defaults to viewport-only.'),
        quality: z.number().int().min(30).max(95).optional().describe('JPEG quality between 30 and 95. Defaults to 70.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async input => {
      const payload = await browserScreenshot(input);
      const { imageData, ...metadata } = payload;
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(metadata, null, 2)
          },
          {
            type: 'image',
            data: imageData,
            mimeType: 'image/jpeg'
          }
        ],
        structuredContent: metadata
      };
    }
  );

  server.registerTool(
    'browser_close',
    {
      title: 'Browser Close',
      description: 'Use this to close and release a persistent browser session.',
      inputSchema: z.object({
        sessionId: z.string().min(1).describe('Persistent browser session identifier.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async input => {
      const payload = await browserClose(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'local_process_start',
    {
      title: 'Local Process Start',
      description: 'Use this to start a safe local dev process such as a Nuxt frontend server.',
      inputSchema: z.object({
        name: z.string().min(1).describe('Process name.'),
        command: z.enum(['npm', 'pnpm']).describe('Package manager to launch.'),
        args: z.array(z.string()).min(1).describe('Allowed command arguments.'),
        cwd: z.string().min(1).describe('Working directory relative to the project root.'),
        host: z.string().optional().describe('Host to advertise for browser access, default 127.0.0.1.'),
        port: z.number().int().min(1).max(65535).optional().describe('Port to advertise for browser access, default 3000.'),
        extraEnv: z.record(z.string(), z.string()).optional().describe('Optional extra environment variables.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async input => {
      const payload = await startLocalProcess(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'local_process_restart',
    {
      title: 'Local Process Restart',
      description: 'Use this to restart a safe local dev process.',
      inputSchema: z.object({
        name: z.string().min(1).describe('Process name.'),
        command: z.enum(['npm', 'pnpm']).optional().describe('Package manager to launch if process is not already running.'),
        args: z.array(z.string()).optional().describe('Allowed command arguments if process is not already running.'),
        cwd: z.string().optional().describe('Working directory relative to the project root if process is not already running.'),
        host: z.string().optional().describe('Host to advertise for browser access, default 127.0.0.1.'),
        port: z.number().int().min(1).max(65535).optional().describe('Port to advertise for browser access, default 3000.'),
        extraEnv: z.record(z.string(), z.string()).optional().describe('Optional extra environment variables.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async input => {
      const payload = await restartLocalProcess(input);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(payload, null, 2)
          }
        ],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'local_process_stop',
    {
      title: 'Local Process Stop',
      description: 'Use this to stop a safe local dev process.',
      inputSchema: z.object({
        name: z.string().min(1).describe('Process name.')
      }),
      annotations: {
        destructiveHint: true
      }
    },
    async ({ name }): Promise<ToolSuccess> => {
      const closed = await closeLocalProcess(name);
      return {
        content: [{ type: 'text', text: JSON.stringify({ name, closed }, null, 2) }],
        structuredContent: {
          name,
          closed
        }
      };
    }
  );

  server.registerTool(
    'local_process_status',
    {
      title: 'Local Process Status',
      description: 'Use this to inspect one local dev process.',
      inputSchema: z.object({
        name: z.string().min(1).describe('Process name.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ name }): Promise<ToolSuccess> => {
      const record = LOCAL_PROCESSES.get(name);
      if (!record) {
        return {
          content: [{ type: 'text', text: JSON.stringify({ name, running: false }, null, 2) }],
          structuredContent: { name, running: false }
        };
      }
      const payload = localProcessSnapshot(record);
      return {
        content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
        structuredContent: payload
      };
    }
  );

  server.registerTool(
    'local_process_logs',
    {
      title: 'Local Process Logs',
      description: 'Use this to read recent stdout/stderr from a local dev process.',
      inputSchema: z.object({
        name: z.string().min(1).describe('Process name.'),
        limit: z.number().int().min(1).max(500).optional().describe('Maximum number of log lines to return.'),
        clear: z.boolean().optional().describe('Clear logs after reading them.')
      }),
      annotations: {
        readOnlyHint: true
      }
    },
    async ({ name, limit = 100, clear = false }): Promise<ToolSuccess> => {
      const record = LOCAL_PROCESSES.get(name);
      if (!record) {
        throw new Error(`Unknown local process: ${name}`);
      }
      const logs = record.logs.slice(-limit);
      if (clear) {
        record.logs.length = 0;
      }
      const payload = {
        name,
        count: logs.length,
        logs
      };
      return {
        content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
        structuredContent: payload
      };
    }
  );

  const transport = new NodeStreamableHTTPServerTransport({
    sessionIdGenerator: process.env.MCP_SESSION_MODE === 'stateful' ? () => randomUUID() : undefined
  });

  await server.connect(transport);

  const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
    if (!isAllowedRequest(req)) {
      res.writeHead(403, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ error: 'Forbidden host' }));
      return;
    }

    const pathname = getRequestPathname(req);

    if (pathname === '/healthz') {
      res.writeHead(200, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ ok: true }));
      return;
    }

    if (req.method === 'OPTIONS') {
      res.writeHead(204, {
        'access-control-allow-origin': '*',
        'access-control-allow-methods': 'GET,POST,OPTIONS',
        'access-control-allow-headers': 'content-type,authorization,mcp-session-id',
        'access-control-max-age': '86400'
      });
      res.end();
      return;
    }

    if (await oauth.handleWellKnownRequest(req, res)) {
      return;
    }

    if (await oauth.handleOAuthRequest(req, res)) {
      return;
    }

    if (pathname !== '/mcp') {
      res.writeHead(404, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ error: 'Not found' }));
      return;
    }

    console.log(
      '[mcp] request',
      JSON.stringify({
        method: req.method,
        hasAuth: Boolean(req.headers.authorization),
        hasSession: Boolean(req.headers['mcp-session-id']),
        accept: req.headers.accept ?? '',
        path: pathname
      })
    );

    if (req.method !== 'GET' && req.method !== 'POST') {
      res.writeHead(405, { 'content-type': 'application/json' });
      res.end(JSON.stringify({ error: 'Method not allowed' }));
      return;
    }

    try {
      const authInfo = oauth.authenticateRequest(req);
      if (!authInfo) {
        console.log(
          '[mcp] unauthorized',
          JSON.stringify({
            method: req.method,
            host: req.headers.host ?? '',
            resourceExpected: `${new URL('/mcp', `https://${String(req.headers.host ?? '')}`).toString()}`
          })
        );
        oauth.sendUnauthorized(req, res);
        return;
      }

      const authedReq = req as AuthenticatedIncomingMessage;
      authedReq.auth = authInfo;

      const maybePromise = transport.handleRequest(authedReq, res);
      if (maybePromise && typeof (maybePromise as Promise<unknown>).catch === 'function') {
        void (maybePromise as Promise<unknown>).catch(error => {
          if (!res.headersSent) {
            res.writeHead(500, { 'content-type': 'application/json' });
          }
          if (!res.writableEnded) {
            res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
          }
        });
      }
    } catch (error) {
      if (!res.headersSent) {
        res.writeHead(500, { 'content-type': 'application/json' });
      }
      if (!res.writableEnded) {
        res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
      }
    }
  });

  httpServer.listen(PORT, HOST, () => {
    console.log(`Bent frontend MCP server listening on http://${HOST}:${PORT}/mcp`);
    console.log(`Project root: ${PROJECT_ROOT}`);
    console.log(
      `Available npm scripts: ${Object.keys(packageScripts).length > 0 ? Object.keys(packageScripts).join(', ') : '(none found)'}`
    );
  });

  const shutdown = async () => {
    try {
      await transport.close();
    } catch {
      // Ignore transport shutdown errors.
    }
    httpServer.close(() => {
      process.exit(0);
    });
  };

  process.on('SIGINT', () => {
    void shutdown();
  });
  process.on('SIGTERM', () => {
    void shutdown();
  });
}

function isAllowedRequest(req: IncomingMessage): boolean {
  const hostHeader = String(req.headers.host ?? '').toLowerCase();
  if (!hostHeader) {
    return true;
  }

  const hostOnly = hostHeader.split(':')[0];
  if (['127.0.0.1', 'localhost', '::1'].includes(hostOnly)) {
    return true;
  }

  if (MCP_ALLOWED_HOSTS.length === 0) {
    return true;
  }

  return MCP_ALLOWED_HOSTS.some(pattern => {
    if (pattern === '*') {
      return true;
    }
    if (pattern.startsWith('*.')) {
      const suffix = pattern.slice(1);
      return hostHeader.endsWith(suffix) || hostOnly.endsWith(suffix);
    }
    return hostHeader === pattern || hostOnly === pattern;
  });
}

void main().catch(error => {
  console.error(error);
  process.exit(1);
});
