/**
 * cesium-mcp-runtime — MCP Server for Cesium
 *
 * 通过标准 MCP 协议暴露 Cesium 操控工具，
 * 通过 WebSocket 桥接到浏览器中的 cesium-mcp-bridge 执行。
 *
 * 架构：
 *   AI Agent ←→ MCP Server (stdio) ←→ WebSocket ←→ Browser (cesium-mcp-bridge)
 *   Backend  ←→ HTTP POST /api/command  ←→ WebSocket ←→ Browser (cesium-mcp-bridge)
 */

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { z } from 'zod'
import { WebSocketServer, WebSocket, type RawData } from 'ws'
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
import { AsyncLocalStorage } from 'node:async_hooks'
import { readFileSync, existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { toolDescriptions as _enToolDesc, paramDescriptions as _enParamDesc } from './locales/en.js'
import { toolDescriptions as _zhToolDesc, paramDescriptions as _zhParamDesc } from './locales/zh-CN.js'

// ==================== WebSocket Bridge ====================

const WS_PORT = parseInt(process.env.CESIUM_WS_PORT ?? '9100')
const MAX_PORT_RETRIES = 10

/** 按 sessionId 管理已连接的浏览器 */
const browserClients = new Map<string, WebSocket>()

/** 等待浏览器响应的 pending requests */
const pendingRequests = new Map<string, {
  resolve: (result: unknown) => void
  reject: (err: Error) => void
  timer: ReturnType<typeof setTimeout>
}>()

let requestIdCounter = 0
let _relayPort = 0 // >0 means relay mode: forward commands to an existing instance

const DEFAULT_SESSION_ID = process.env.DEFAULT_SESSION_ID ?? 'default'

/** URL-level session context for MCP HTTP requests (e.g. /mcp?session=xxx) */
const _httpSessionStore = new AsyncLocalStorage<string>()

function getDefaultBrowser(): WebSocket | null {
  if (browserClients.size === 0) return null
  // 优先返回绑定的 session 连接
  const preferred = browserClients.get(DEFAULT_SESSION_ID)
  if (preferred && preferred.readyState === WebSocket.OPEN) return preferred
  // fallback: 返回第一个可用连接
  return browserClients.values().next().value ?? null
}

function sendToBrowser(action: string, params: Record<string, unknown>, timeoutMs = 30000): Promise<unknown> {
  // Extract sessionId from params for multi-browser routing (transparent to tool handlers)
  const { sessionId: paramSessionId, ...cleanParams } = params as { sessionId?: string; [k: string]: unknown }
  // Priority: tool param > URL query (?session=xxx) > default
  const sessionId = paramSessionId ?? _httpSessionStore.getStore()
  if (_relayPort > 0) return _sendViaRelay(action, cleanParams, timeoutMs, sessionId)
  return new Promise((resolve, reject) => {
    const ws = sessionId
      ? (browserClients.get(sessionId) ?? getDefaultBrowser())
      : getDefaultBrowser()
    if (!ws || ws.readyState !== WebSocket.OPEN) {
      reject(new Error('无浏览器连接。请在浏览器中打开包含 CesiumJS 的页面并连接 WebSocket。示例：http://localhost:9100/demo/'))
      return
    }

    const reqId = `req_${++requestIdCounter}`
    const timer = setTimeout(() => {
      pendingRequests.delete(reqId)
      reject(new Error(`浏览器响应超时（${timeoutMs}ms）`))
    }, timeoutMs)

    pendingRequests.set(reqId, { resolve, reject, timer })

    ws.send(JSON.stringify({
      jsonrpc: '2.0',
      id: reqId,
      method: action,
      params: cleanParams,
    }))
  })
}

/** 将命令推送到指定 session 的浏览器（fire-and-forget，不等待响应） */
function pushToBrowser(sessionId: string, command: { action: string; params: Record<string, unknown> }): boolean {
  if (_relayPort > 0) {
    _pushViaRelay(sessionId, command)
    return true
  }
  const ws = browserClients.get(sessionId) ?? getDefaultBrowser()
  if (!ws || ws.readyState !== WebSocket.OPEN) return false
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: `push_${++requestIdCounter}`,
    method: command.action,
    params: command.params,
  }))
  return true
}

/** Relay mode: forward sendToBrowser via HTTP POST to existing instance */
async function _sendViaRelay(action: string, params: Record<string, unknown>, timeoutMs: number, sessionId?: string): Promise<unknown> {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeoutMs)
  try {
    const resp = await fetch(`http://127.0.0.1:${_relayPort}/api/relay`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action, params, sessionId }),
      signal: controller.signal,
    })
    const data = await resp.json() as { ok: boolean; result?: unknown; error?: string }
    if (!data.ok) throw new Error(data.error ?? 'Relay failed')
    return data.result
  } catch (err: unknown) {
    if (err instanceof DOMException && err.name === 'AbortError') {
      throw new Error(`浏览器响应超时（${timeoutMs}ms, via relay）`)
    }
    throw err
  } finally {
    clearTimeout(timer)
  }
}

/** Relay mode: forward pushToBrowser via HTTP POST to existing instance */
function _pushViaRelay(sessionId: string, command: { action: string; params: Record<string, unknown> }) {
  fetch(`http://127.0.0.1:${_relayPort}/api/command`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ sessionId, command }),
  }).catch(() => { /* fire-and-forget */ })
}

/** Convert Zod shape object to JSON Schema (simplified) */
function zodShapeToJsonSchema(shape: Record<string, z.ZodTypeAny>): Record<string, unknown> {
  const properties: Record<string, unknown> = {}
  const required: string[] = []

  for (const [key, zodType] of Object.entries(shape)) {
    const prop: Record<string, unknown> = {}
    let innerType = zodType

    // Unwrap defaults and optionals
    let isOptional = false
    let defaultValue: unknown = undefined
    while (innerType) {
      if (innerType._def?.typeName === 'ZodDefault') {
        defaultValue = innerType._def.defaultValue()
        innerType = innerType._def.innerType
      } else if (innerType._def?.typeName === 'ZodOptional') {
        isOptional = true
        innerType = innerType._def.innerType
      } else if (innerType._def?.typeName === 'ZodEffects') {
        innerType = innerType._def.schema
      } else {
        break
      }
    }

    const typeName = innerType?._def?.typeName ?? ''
    switch (typeName) {
      case 'ZodNumber': prop.type = 'number'; break
      case 'ZodString': prop.type = 'string'; break
      case 'ZodBoolean': prop.type = 'boolean'; break
      case 'ZodEnum': prop.type = 'string'; prop.enum = innerType._def.values; break
      case 'ZodArray': prop.type = 'array'; break
      case 'ZodObject': prop.type = 'object'; break
      case 'ZodRecord': prop.type = 'object'; break
      default: prop.type = 'string'; break
    }

    if (defaultValue !== undefined) prop.default = defaultValue
    if (zodType.description) prop.description = zodType.description

    properties[key] = prop
    if (!isOptional && defaultValue === undefined) required.push(key)
  }

  return { type: 'object', properties, required: required.length ? required : undefined }
}

// Server-side tools: handlers run on Node.js, NOT forwarded to browser bridge
const SERVER_SIDE_TOOLS = new Set(['geocode'])

/** Invoke a server-side tool handler from _toolDefs, return parsed result */
async function _invokeServerSideTool(action: string, params: Record<string, unknown>): Promise<unknown> {
  const def = _toolDefs.get(action)
  if (!def) throw new Error(`Server-side tool "${action}" not found`)
  const handler = def[def.length - 1] as (params: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }>
  const mcpResult = await handler(params)
  // Unwrap MCP content format to raw result for HTTP API compatibility
  const text = mcpResult?.content?.[0]?.text
  if (text) {
    try { return JSON.parse(text) } catch { return text }
  }
  return mcpResult
}

/** HTTP 请求处理：POST /api/command */
async function handleHttpRequest(req: IncomingMessage, res: ServerResponse) {
  // CORS
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

  if (req.method === 'OPTIONS') {
    res.writeHead(204)
    res.end()
    return
  }

  if (req.method === 'POST' && req.url?.startsWith('/api/command')) {
    let body = ''
    req.on('data', (chunk: Buffer) => { body += chunk.toString() })
    req.on('end', () => {
      try {
        const payload = JSON.parse(body)
        const sessionId: string = payload.sessionId ?? 'default'
        const commands: Array<{ action: string; params: Record<string, unknown> }> =
          Array.isArray(payload.commands) ? payload.commands : [payload.command]

        let sent = 0
        for (const cmd of commands) {
          if (!cmd) continue
          if (SERVER_SIDE_TOOLS.has(cmd.action) && _toolDefs.has(cmd.action)) {
            // Server-side tool: invoke handler directly (fire-and-forget)
            _invokeServerSideTool(cmd.action, cmd.params ?? {}).catch(() => {})
            sent++
          } else if (pushToBrowser(sessionId, cmd)) {
            sent++
          }
        }

        res.writeHead(200, { 'Content-Type': 'application/json' })
        res.end(JSON.stringify({ ok: true, sent, total: commands.length }))
      } catch {
        res.writeHead(400, { 'Content-Type': 'application/json' })
        res.end(JSON.stringify({ ok: false, error: 'Invalid JSON' }))
      }
    })
    return
  }

  // POST /api/relay — request-response command relay (used by secondary instances)
  if (req.method === 'POST' && req.url?.startsWith('/api/relay')) {
    let body = ''
    req.on('data', (chunk: Buffer) => { body += chunk.toString() })
    req.on('end', async () => {
      try {
        const { action, params, sessionId } = JSON.parse(body) as { action: string; params: Record<string, unknown>; sessionId?: string }
        let result: unknown
        if (SERVER_SIDE_TOOLS.has(action) && _toolDefs.has(action)) {
          // Server-side tool: invoke handler directly
          result = await _invokeServerSideTool(action, params ?? {})
        } else {
          // Bridge tool: forward to browser
          const routedParams = sessionId ? { ...params, sessionId } : params
          result = await sendToBrowser(action, routedParams)
        }
        res.writeHead(200, { 'Content-Type': 'application/json' })
        res.end(JSON.stringify({ ok: true, result }))
      } catch (err) {
        res.writeHead(200, { 'Content-Type': 'application/json' })
        res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }))
      }
    })
    return
  }

  // GET /api/status — 连接状态
  if (req.method === 'GET' && req.url?.startsWith('/api/status')) {
    const sessions = Array.from(browserClients.keys())
    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ ok: true, server: 'cesium-mcp-runtime', sessions, connections: sessions.length }))
    return
  }

  // GET /api/tools — list enabled tools with JSON Schema
  if (req.method === 'GET' && req.url?.startsWith('/api/tools')) {
    // Support ?toolsets=view,layer filtering
    const parsedToolsUrl = new URL(req.url, 'http://localhost')
    const tsParam = parsedToolsUrl.searchParams.get('toolsets')?.trim()
    const allowedTools = tsParam
      ? new Set(tsParam.split(',').flatMap(s => TOOLSETS[s.trim()] ?? []))
      : null // null = no filter, show all enabled
    const tools: Array<{ name: string; description: string; inputSchema: Record<string, unknown>; _meta?: Record<string, unknown> }> = []
    for (const [name, args] of _toolDefs.entries()) {
      if (allowedTools ? !allowedTools.has(name) : !_enabledTools.has(name)) continue
      const description = args[1] as string
      const zodShape = args[2] as Record<string, z.ZodTypeAny> | undefined
      const jsonSchema = zodShape ? zodShapeToJsonSchema(zodShape) : { type: 'object', properties: {} }
      const toolset = TOOL_TO_TOOLSET.get(name)
      tools.push({ name, description, inputSchema: jsonSchema, ...(toolset ? { _meta: { toolset } } : {}) })
    }
    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ ok: true, tools }))
    return
  }

  // GET /proxy?url=... — CORS proxy for local resources
  if (req.method === 'GET' && req.url?.startsWith('/proxy')) {
    const parsed = new URL(req.url, `http://localhost:${WS_PORT}`)
    const targetUrl = parsed.searchParams.get('url')
    if (!targetUrl) {
      res.writeHead(400, { 'Content-Type': 'application/json' })
      res.end(JSON.stringify({ error: 'Missing ?url= parameter' }))
      return
    }
    try {
      const proxyResp = await fetch(targetUrl)
      const contentType = proxyResp.headers.get('content-type') || 'application/octet-stream'
      const buffer = Buffer.from(await proxyResp.arrayBuffer())
      res.writeHead(proxyResp.status, {
        'Content-Type': contentType,
        'Access-Control-Allow-Origin': '*',
      })
      res.end(buffer)
    } catch (err) {
      res.writeHead(502, { 'Content-Type': 'application/json' })
      res.end(JSON.stringify({ error: `Proxy failed: ${err instanceof Error ? err.message : String(err)}` }))
    }
    return
  }

  // GET /bridge.js — serve the locally bundled cesium-mcp-bridge (so the viewer always
  // runs the dist shipped alongside this runtime, not a stale unpkg@latest)
  if (req.method === 'GET' && req.url === '/bridge.js') {
    const bundle = _findLocalBridgeBundle()
    if (bundle) {
      res.writeHead(200, {
        'Content-Type': 'application/javascript; charset=utf-8',
        'Access-Control-Allow-Origin': '*',
      })
      res.end(bundle)
      return
    }
    res.writeHead(404)
    res.end('// local bridge bundle not found')
    return
  }

  // GET / — serve built-in viewer page
  if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
    const token = process.env.CESIUM_ION_TOKEN || ''
    const html = _getViewerHtml(token, WS_PORT)
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
    res.end(html)
    return
  }

  res.writeHead(404)
  res.end('Not Found')
}

/** Locate the bundled bridge IIFE shipped alongside this runtime (monorepo sibling or npm dep). Cached. */
let _bridgeBundleCache: string | null | undefined
function _findLocalBridgeBundle(): string | null {
  if (_bridgeBundleCache !== undefined) return _bridgeBundleCache
  const here = dirname(fileURLToPath(import.meta.url))
  const file = 'cesium-mcp-bridge.browser.global.js'
  const candidates = [
    // monorepo: runtime/dist → ../../cesium-mcp-bridge/dist
    join(here, '..', '..', 'cesium-mcp-bridge', 'dist', file),
    // npm install: node_modules/cesium-mcp-runtime/dist → node_modules/cesium-mcp-bridge/dist
    join(here, '..', '..', '..', 'cesium-mcp-bridge', 'dist', file),
    join(here, '..', 'node_modules', 'cesium-mcp-bridge', 'dist', file),
  ]
  for (const c of candidates) {
    if (existsSync(c)) {
      _bridgeBundleCache = readFileSync(c, 'utf-8')
      return _bridgeBundleCache
    }
  }
  _bridgeBundleCache = null
  return null
}

/** Built-in viewer HTML served at GET / */
function _getViewerHtml(token: string, wsPort: number): string {
  return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cesium MCP Viewer</title>
<script src="https://cesium.com/downloads/cesiumjs/releases/1.142/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.142/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<script src="/bridge.js" onerror="var s=document.createElement('script');s.src='https://unpkg.com/cesium-mcp-bridge@latest/dist/cesium-mcp-bridge.browser.global.js';document.head.appendChild(s)"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body,#c{width:100%;height:100%;overflow:hidden}
#s{position:fixed;top:12px;right:12px;z-index:999;padding:6px 12px;border-radius:6px;font:13px/1.4 -apple-system,sans-serif;backdrop-filter:blur(8px);transition:all .3s}
.c0{background:rgba(255,170,0,.85);color:#333}
.c1{background:rgba(0,180,80,.85);color:#fff}
.c2{background:rgba(220,50,50,.85);color:#fff}
</style>
</head>
<body>
<div id="c"></div><div id="s" class="c0">Connecting...</div>
<script>
var WS=${wsPort},TOK='${token}',SE=new URLSearchParams(location.search).get('session')||'default';
if(TOK)Cesium.Ion.defaultAccessToken=TOK;
var v=new Cesium.Viewer('c',{terrain:Cesium.Terrain.fromWorldTerrain(),baseLayerPicker:!0,geocoder:!0,animation:!0,timeline:!0});
var b=new CesiumMcpBridge.CesiumBridge(v),el=document.getElementById('s');
function conn(){var ws=new WebSocket('ws://localhost:'+WS+'?session='+SE);
ws.onopen=function(){el.className='c1';el.textContent='Connected'};
ws.onmessage=function(e){var m=JSON.parse(e.data);b.execute({action:m.method,params:m.params||{}}).then(function(r){if(m.id)ws.send(JSON.stringify({id:m.id,result:r||{success:!0}}))})};
ws.onclose=function(){el.className='c2';el.textContent='Disconnected';setTimeout(conn,3000)};
ws.onerror=function(){ws.close()}}
conn();
</script>
</body></html>`
}

/** Probe a port to check if a cesium-mcp-runtime instance is already running */
async function _probeExistingInstance(port: number): Promise<boolean> {
  try {
    const resp = await fetch(`http://127.0.0.1:${port}/api/status`, { signal: AbortSignal.timeout(1500) })
    const data = await resp.json() as { server?: string }
    return data.server === 'cesium-mcp-runtime'
  } catch {
    return false
  }
}

/** Try to bind HTTP server to a specific port. Returns true on success. */
function _tryListen(httpServer: ReturnType<typeof createServer>, port: number): Promise<boolean> {
  return new Promise(resolve => {
    const onError = (err: NodeJS.ErrnoException) => {
      httpServer.removeListener('listening', onListening)
      if (err.code === 'EADDRINUSE') resolve(false)
      else { console.error('[cesium-mcp-runtime] HTTP server error:', err.message); resolve(false) }
    }
    const onListening = () => {
      httpServer.removeListener('error', onError)
      resolve(true)
    }
    httpServer.once('error', onError)
    httpServer.once('listening', onListening)
    httpServer.listen(port)
  })
}

async function startServer() {
  // Phase 1: check if target port is available
  const httpServer = createServer(handleHttpRequest)
  const wss = new WebSocketServer({ server: httpServer, noServer: false })

  // Prevent unhandled error crash when httpServer fails to bind
  wss.on('error', () => { /* handled by httpServer error listener in _tryListen */ })

  _setupWss(wss)

  if (await _tryListen(httpServer, WS_PORT)) {
    console.error(`[cesium-mcp-runtime] HTTP + WebSocket server on http://localhost:${WS_PORT}`)
    console.error('[cesium-mcp-runtime] POST /api/command — 推送地图命令')
    console.error('[cesium-mcp-runtime] POST /api/relay   — 命令中继（request-response）')
    console.error('[cesium-mcp-runtime] GET  /api/status  — 连接状态')
    return
  }

  // Phase 2: port in use — check if it's another cesium-mcp-runtime
  httpServer.close()
  if (await _probeExistingInstance(WS_PORT)) {
    _relayPort = WS_PORT
    console.error(`[cesium-mcp-runtime] Port ${WS_PORT} occupied by existing cesium-mcp-runtime — relay mode enabled`)
    console.error(`[cesium-mcp-runtime] Commands will be forwarded to http://127.0.0.1:${WS_PORT}`)
    return
  }

  // Phase 3: port occupied by other service — try incremental ports
  for (let offset = 1; offset <= MAX_PORT_RETRIES; offset++) {
    const tryPort = WS_PORT + offset
    const altServer = createServer(handleHttpRequest)
    const altWss = new WebSocketServer({ server: altServer })
    altWss.on('error', () => { /* handled by altServer error listener */ })
    _setupWss(altWss)
    if (await _tryListen(altServer, tryPort)) {
      console.error(`[cesium-mcp-runtime] Port ${WS_PORT} occupied by another service, using port ${tryPort}`)
      console.error(`[cesium-mcp-runtime] HTTP + WebSocket server on http://localhost:${tryPort}`)
      return
    }
    altServer.close()
  }

  console.error(`[cesium-mcp-runtime] Could not find available port (tried ${WS_PORT}-${WS_PORT + MAX_PORT_RETRIES}), WebSocket server disabled`)
}

function _setupWss(wss: WebSocketServer) {
  wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
    const sessionId = new URL(req.url ?? '/', 'http://localhost').searchParams.get('session') ?? 'default'
    const oldWs = browserClients.get(sessionId)
    if (oldWs && oldWs.readyState === WebSocket.OPEN) {
      console.error(`[ws] 同名 session=${sessionId} 已存在，关闭旧连接`)
      oldWs.removeAllListeners('close')
      oldWs.close(1000, 'replaced by new connection')
    }
    console.error(`[ws] 浏览器连接: session=${sessionId}`)
    browserClients.set(sessionId, ws)

    ws.on('message', (raw: RawData) => {
      try {
        const msg = JSON.parse(raw.toString())
        if (msg.id && pendingRequests.has(msg.id)) {
          const pending = pendingRequests.get(msg.id)!
          pendingRequests.delete(msg.id)
          clearTimeout(pending.timer)
          if (msg.error) {
            pending.reject(new Error(msg.error.message ?? JSON.stringify(msg.error)))
          } else {
            pending.resolve(msg.result)
          }
        }
      } catch { /* ignore parse errors */ }
    })

    ws.on('close', () => {
      console.error(`[ws] 浏览器断开: session=${sessionId}`)
      browserClients.delete(sessionId)
    })
  })
}

// ==================== MCP Server ====================

declare const __VERSION__: string

const server = new McpServer({
  name: 'cesium-mcp-runtime',
  version: __VERSION__,
  title: 'Cesium MCP Runtime',
  description: 'AI-powered 3D globe control via MCP — camera, layers, entities, animation, and interaction with CesiumJS.',
  websiteUrl: 'https://github.com/gaopengbin/cesium-mcp',
}, {
  instructions: 'Cesium MCP Runtime provides tools for controlling a CesiumJS 3D globe via AI. A browser with cesium-mcp-bridge must be connected via WebSocket for command execution. Use view tools (flyTo, setView) to navigate, entity tools to add markers/polygons/models, layer tools to manage GeoJSON/3D Tiles, and animation tools for time-based animations.',
})

// ==================== Resources ====================

server.resource(
  'camera',
  'cesium://scene/camera',
  { description: '当前相机状态（经纬度、高度、角度）', mimeType: 'application/json' },
  async () => {
    try {
      const result = await sendToBrowser('getView', {})
      return { contents: [{ uri: 'cesium://scene/camera', text: JSON.stringify(result), mimeType: 'application/json' }] }
    } catch {
      return { contents: [{ uri: 'cesium://scene/camera', text: '{"error":"no browser connected"}', mimeType: 'application/json' }] }
    }
  },
)

server.resource(
  'layers',
  'cesium://scene/layers',
  { description: '当前已加载的图层列表（ID、名称、类型、可见性）', mimeType: 'application/json' },
  async () => {
    try {
      const result = await sendToBrowser('listLayers', {})
      return { contents: [{ uri: 'cesium://scene/layers', text: JSON.stringify(result), mimeType: 'application/json' }] }
    } catch {
      return { contents: [{ uri: 'cesium://scene/layers', text: '{"error":"no browser connected"}', mimeType: 'application/json' }] }
    }
  },
)

// ==================== Toolsets (工具分组管理) ====================

const TOOLSETS: Record<string, string[]> = {
  view: ['flyTo', 'setView', 'getView', 'zoomToExtent', 'saveViewpoint', 'loadViewpoint', 'listViewpoints', 'exportScene'],
  entity: ['addMarker', 'addLabel', 'addModel', 'addPolygon', 'addPolyline', 'updateEntity', 'removeEntity', 'batchAddEntities', 'queryEntities', 'getEntityProperties'],
  layer: ['addGeoJsonLayer', 'addGeoJsonPrimitive', 'listLayers', 'getLayerSchema', 'removeLayer', 'clearAll', 'setLayerVisibility', 'updateLayerStyle', 'setBasemap'],
  camera: ['lookAtTransform', 'startOrbit', 'stopOrbit', 'setCameraOptions'],
  'entity-ext': ['addBillboard', 'addBox', 'addCorridor', 'addCylinder', 'addEllipse', 'addRectangle', 'addWall'],
  animation: ['createAnimation', 'controlAnimation', 'removeAnimation', 'listAnimations', 'updateAnimationPath', 'trackEntity', 'controlClock', 'setGlobeLighting'],
  scene: ['setSceneOptions', 'setPostProcess', 'setIonToken'],
  tiles: ['load3dTiles', 'load3dGaussianSplat', 'loadTerrain', 'loadImageryService', 'loadCzml', 'loadKml', 'setEdgeDisplayMode'],
  interaction: ['screenshot', 'highlight', 'measure'],
  trajectory: ['playTrajectory'],
  heatmap: ['addHeatmap'],
  geolocation: ['geocode'],
}

const TOOLSET_DESCRIPTIONS: Record<string, string> = {
  view: 'Camera view controls (flyTo, setView, getView, zoomToExtent), viewpoint bookmarks (save, load, list), and scene export',
  entity: 'Core entity operations (marker, label, model, polygon, polyline, update, remove) plus batch add, query, and property inspection',
  layer: 'Layer management (GeoJSON, list, remove, clear all, visibility, style, basemap)',
  camera: 'Advanced camera controls (lookAt, orbit, camera options)',
  'entity-ext': 'Extended entity types (billboard, box, corridor, cylinder, ellipse, rectangle, wall)',
  animation: 'Animation system (create/control animations, track entities, clock, lighting)',
  scene: 'Scene environment and post-processing (fog, atmosphere, shadows, bloom, SSAO, FXAA)',
  tiles: '3D Tiles, terrain, imagery services, CZML and KML/KMZ data sources',
  interaction: 'User interaction (screenshot, highlight, measure)',
  trajectory: 'Trajectory playback',
  heatmap: 'Heatmap visualization',
  geolocation: 'Geocoding — convert address/place name to coordinates (Nominatim/OSM)',
}

const DEFAULT_TOOLSETS = ['view', 'entity', 'layer', 'interaction']

const _tsEnv = process.env.CESIUM_TOOLSETS?.trim()
const _allMode = _tsEnv === 'all'
const _enabledSets = new Set<string>(
  _allMode
    ? Object.keys(TOOLSETS)
    : _tsEnv
      ? _tsEnv.split(',').map(s => s.trim()).filter(s => s in TOOLSETS)
      : DEFAULT_TOOLSETS,
)

const _enabledTools = new Set<string>()
for (const setName of _enabledSets) {
  for (const tool of TOOLSETS[setName]!) {
    _enabledTools.add(tool)
  }
}

// Reverse map: tool name → toolset name (for _meta injection)
const TOOL_TO_TOOLSET = new Map<string, string>()
for (const [setName, tools] of Object.entries(TOOLSETS)) {
  for (const tool of tools) TOOL_TO_TOOLSET.set(tool, setName)
}

// Store all tool definitions for lazy registration (Dynamic Discovery)
const _toolDefs = new Map<string, unknown[]>()

// i18n: select locale based on CESIUM_LOCALE env var (default: en)
const _localeKey = process.env.CESIUM_LOCALE?.trim().toLowerCase()
const _toolDesc = _localeKey === 'zh-cn' ? _zhToolDesc : _enToolDesc
const _paramDesc = _localeKey === 'zh-cn' ? _zhParamDesc : _enParamDesc

/** Apply a stored tool definition to a McpServer, injecting _meta.toolset via registerTool API */
function _applyToolDef(s: McpServer, args: unknown[]) {
  const name = args[0] as string
  const toolset = TOOL_TO_TOOLSET.get(name)
  if (toolset) {
    // Use registerTool API which supports _meta
    ;(s as any).registerTool(name, {
      description: args[1] as string,
      inputSchema: args[2],
      annotations: args[3],
      _meta: { toolset },
    }, args[4])
  } else {
    ;(s.tool as Function).apply(s, args)
  }
}

/** Register tool only if it belongs to an enabled toolset */
const _registerTool = ((...args: unknown[]) => {
  const name = args[0] as string
  // Apply locale overrides for tool description + param descriptions
  if (_toolDesc[name]) args[1] = _toolDesc[name]
  const paramOverrides = _paramDesc[name]
  if (paramOverrides && typeof args[2] === 'object' && args[2] !== null) {
    const schema = args[2] as Record<string, z.ZodTypeAny>
    for (const [key, desc] of Object.entries(paramOverrides)) {
      if (schema[key]) schema[key] = schema[key].describe(desc)
    }
  }
  // Auto-inject optional sessionId for multi-browser routing
  if (typeof args[2] === 'object' && args[2] !== null) {
    const schema = args[2] as Record<string, z.ZodTypeAny>
    schema.sessionId = z.string().optional().describe(
      _localeKey === 'zh-cn'
        ? '目标浏览器 session ID（多浏览器路由，可选）'
        : 'Target browser session ID for multi-browser routing (optional)',
    )
  }
  _toolDefs.set(name, args)
  if (_enabledTools.has(name)) {
    _applyToolDef(server, args)
  }
}) as typeof server.tool

/** Dynamically enable a toolset — registers its tools lazily */
function _enableToolset(setName: string): string[] {
  const tools = TOOLSETS[setName]
  if (!tools) return []
  const added: string[] = []
  for (const toolName of tools) {
    if (!_enabledTools.has(toolName)) {
      _enabledTools.add(toolName)
      const def = _toolDefs.get(toolName)
      if (def) {
        _applyToolDef(server, def)
        added.push(toolName)
      }
    }
  }
  _enabledSets.add(setName)
  return added
}

// ==================== Tools ====================

// — flyTo
_registerTool(
  'flyTo',
  '飞行到指定经纬度位置（带动画过渡）',
  {
    longitude: z.number().describe('经度（-180 ~ 180）'),
    latitude: z.number().describe('纬度（-90 ~ 90）'),
    height: z.number().default(50000).describe('相机高度（米），默认 50000'),
    heading: z.number().default(0).describe('航向角（度），0 为正北'),
    pitch: z.number().default(-45).describe('俯仰角（度），-90 为正下方'),
    duration: z.number().default(2).describe('飞行动画时长（秒）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Fly To Location' },
  async (params) => {
    const result = await sendToBrowser('flyTo', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addGeoJsonLayer
_registerTool(
  'addGeoJsonLayer',
  '添加 GeoJSON 图层到地图（支持 Point/Line/Polygon，可配置颜色/分级/分类渲染）。data 和 url 二选一',
  {
    id: z.string().optional().describe('图层ID（不传则自动生成）'),
    name: z.string().optional().describe('图层显示名称'),
    data: z.record(z.unknown()).optional().describe('GeoJSON FeatureCollection 对象（与 url 二选一）'),
    url: z.string().optional().describe('GeoJSON 文件 URL（与 data 二选一，浏览器端 fetch 加载）'),
    style: z.record(z.unknown()).optional().describe('样式配置（color, opacity, pointSize, choropleth, category）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add GeoJSON Layer' },
  async (params) => {
    const result = await sendToBrowser('addGeoJsonLayer', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addGeoJsonPrimitive
_registerTool(
  'addGeoJsonPrimitive',
  '高性能加载大规模 GeoJSON 数据（10万+ 要素）。绕过 Entity 系统，直接使用 Primitive 渲染，适合海量数据可视化。data 和 url 二选一',
  {
    id: z.string().optional().describe('图层ID（不传则自动生成）'),
    name: z.string().optional().describe('图层显示名称'),
    data: z.any().optional().describe('GeoJSON 对象（与 url 二选一）'),
    url: z.string().optional().describe('GeoJSON 文件 URL（与 data 二选一）'),
    allowPicking: z.boolean().optional().describe('是否允许拾取（默认 true，关闭可提升性能）'),
    show: z.boolean().optional().describe('是否显示（默认 true）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add GeoJSON Primitive' },
  async (params) => {
    const result = await sendToBrowser('addGeoJsonPrimitive', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addLabel
_registerTool(
  'addLabel',
  '为 GeoJSON 要素添加文本标注（显示属性值）',
  {
    data: z.record(z.unknown()).describe('GeoJSON FeatureCollection 对象'),
    field: z.string().describe('用作标注文本的属性字段名（如 "name"、"population"）'),
    style: z.record(z.unknown()).optional().describe('标注样式（font, fillColor, outlineColor, scale 等）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Label' },
  async (params) => {
    const result = await sendToBrowser('addLabel', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addHeatmap
_registerTool(
  'addHeatmap',
  '添加热力图图层（基于 GeoJSON 点数据生成热力可视化，贴图到地面）',
  {
    data: z.record(z.unknown()).describe('GeoJSON Point FeatureCollection'),
    radius: z.number().default(30).describe('热力影响半径（像素）'),
    blur: z.number().default(0.85).describe('热力模糊程度 0-1'),
    maxOpacity: z.number().default(0.8).describe('最大不透明度 0-1'),
    resolution: z.number().default(512).describe('热力图分辨率（像素）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Heatmap' },
  async (params) => {
    const result = await sendToBrowser('addHeatmap', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — removeLayer
_registerTool(
  'removeLayer',
  '从地图上移除指定图层（按图层ID）',
  { id: z.string().describe('要移除的图层ID（可通过 listLayers 获取）') },
  { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, title: 'Remove Layer' },
  async (params) => {
    const result = await sendToBrowser('removeLayer', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — clearAll
_registerTool(
  'clearAll',
  '清除地图上的所有图层、实体、动画和轨迹（一键重置场景）',
  {},
  { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, title: 'Clear All' },
  async () => {
    const result = await sendToBrowser('clearAll', {})
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setBasemap
_registerTool(
  'setBasemap',
  '切换底图风格',
  {
    basemap: z.enum(['dark', 'satellite', 'standard', 'osm', 'arcgis', 'light', 'tianditu_vec', 'tianditu_img', 'amap', 'amap_satellite']).describe('底图类型：dark=暗色, satellite=卫星影像, standard=标准, osm=OpenStreetMap, arcgis=ArcGIS街道, light=浅色, tianditu_vec=天地图矢量, tianditu_img=天地图影像, amap=高德地图, amap_satellite=高德卫星'),
    token: z.string().optional().describe('底图服务令牌（天地图等需要认证的服务必填）'),
    url: z.string().optional().describe('自定义URL模板（{x},{y},{z}占位符），提供时忽略basemap参数'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set Basemap' },
  async (params) => {
    const result = await sendToBrowser('setBasemap', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — screenshot
_registerTool(
  'screenshot',
  '截取当前地图视图（返回 base64 PNG）',
  {},
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Screenshot' },
  async () => {
    const result = await sendToBrowser('screenshot', {})
    const data = result as { dataUrl?: string } | null
    if (data?.dataUrl) {
      return { content: [{ type: 'image' as const, data: data.dataUrl.replace(/^data:image\/\w+;base64,/, ''), mimeType: 'image/png' }] }
    }
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — highlight
_registerTool(
  'highlight',
  '高亮指定图层的要素（支持清除恢复原始样式）',
  {
    layerId: z.string().optional().describe('图层ID（清除所有高亮时可不传）'),
    featureIndex: z.number().optional().describe('要素索引（不传则高亮/清除全部）'),
    color: z.string().default('#FFFF00').describe('高亮颜色（CSS 格式）'),
    clear: z.boolean().optional().describe('传 true 清除高亮、恢复原始样式'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Highlight' },
  async (params) => {
    const result = await sendToBrowser('highlight', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — measure
_registerTool(
  'measure',
  '测量距离或面积（基于坐标计算，可在地图上显示）',
  {
    mode: z.enum(['distance', 'area']).describe('测量模式：distance=距离, area=面积'),
    positions: z.array(z.array(z.number()).min(2).max(3)).min(2).describe('坐标数组 [[lon,lat,alt?], ...]'),
    showOnMap: z.boolean().optional().default(true).describe('是否在地图上显示测量结果'),
    id: z.string().optional().describe('自定义测量实体ID'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Measure' },
  async (params) => {
    const result = await sendToBrowser('measure', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setView
_registerTool(
  'setView',
  '瞬间切换到指定经纬度视角（无动画）',
  {
    longitude: z.number().describe('经度（-180 ~ 180）'),
    latitude: z.number().describe('纬度（-90 ~ 90）'),
    height: z.number().optional().default(50000).describe('高度（米）'),
    heading: z.number().optional().default(0).describe('航向角（度）'),
    pitch: z.number().optional().default(-90).describe('俯仰角（度）'),
    roll: z.number().optional().default(0).describe('翻滚角（度）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set View' },
  async (params) => {
    const result = await sendToBrowser('setView', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — getView
_registerTool(
  'getView',
  '获取当前相机视角信息（经纬度、高度、角度）',
  {},
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Get View' },
  async () => {
    const result = await sendToBrowser('getView', {})
    return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
  },
)

// — zoomToExtent
_registerTool(
  'zoomToExtent',
  '缩放到指定地理范围',
  {
    west: z.number().describe('西边界经度（度）'),
    south: z.number().describe('南边界纬度（度）'),
    east: z.number().describe('东边界经度（度）'),
    north: z.number().describe('北边界纬度（度）'),
    duration: z.number().optional().default(2).describe('动画时长（秒）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Zoom to Extent' },
  async (params) => {
    const result = await sendToBrowser('zoomToExtent', { bbox: [params.west, params.south, params.east, params.north], duration: params.duration })
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addMarker
_registerTool(
  'addMarker',
  '在指定经纬度添加标注点，返回 layerId 供后续操作',
  {
    longitude: z.number().describe('经度（-180 ~ 180）'),
    latitude: z.number().describe('纬度（-90 ~ 90）'),
    label: z.string().optional().describe('标注文本'),
    color: z.string().optional().default('#3B82F6').describe('标注颜色（CSS 格式）'),
    size: z.number().optional().default(12).describe('点大小（像素）'),
    id: z.string().optional().describe('自定义图层ID（不传则自动生成）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Marker' },
  async (params) => {
    const result = await sendToBrowser('addMarker', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addPolyline
_registerTool(
  'addPolyline',
  '在地图上添加折线（路径、线段），返回 entityId',
  {
    coordinates: z.array(z.array(z.number())).describe('折线坐标数组 [[lon, lat, height?], ...]'),
    color: z.string().optional().default('#3B82F6').describe('线条颜色（CSS 格式）'),
    width: z.number().optional().default(3).describe('线条宽度（像素）'),
    clampToGround: z.boolean().optional().default(true).describe('是否贴地'),
    label: z.string().optional().describe('折线标注文本'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Polyline' },
  async (params) => {
    const result = await sendToBrowser('addPolyline', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addPolygon
_registerTool(
  'addPolygon',
  '在地图上添加多边形区域（面积、边界），返回 entityId',
  {
    coordinates: z.array(z.array(z.number())).describe('多边形外环坐标 [[lon, lat, height?], ...]'),
    color: z.string().optional().default('#3B82F6').describe('填充颜色（CSS 格式）'),
    outlineColor: z.string().optional().default('#FFFFFF').describe('描边颜色'),
    opacity: z.number().optional().default(0.6).describe('填充透明度（0~1）'),
    extrudedHeight: z.number().optional().describe('拉伸高度（米），可用于创建立体效果'),
    clampToGround: z.boolean().optional().default(true).describe('是否贴地'),
    label: z.string().optional().describe('多边形标注文本'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Polygon' },
  async (params) => {
    const result = await sendToBrowser('addPolygon', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addModel
_registerTool(
  'addModel',
  '在指定经纬度放置 3D 模型（glTF/GLB），返回 entityId',
  {
    longitude: z.number().describe('经度（-180 ~ 180）'),
    latitude: z.number().describe('纬度（-90 ~ 90）'),
    height: z.number().optional().default(0).describe('放置高度（米）'),
    url: z.string().describe('glTF/GLB 模型文件 URL'),
    scale: z.number().optional().default(1).describe('模型缩放比例'),
    heading: z.number().optional().default(0).describe('航向角（度），0=正北'),
    pitch: z.number().optional().default(0).describe('俯仰角（度）'),
    roll: z.number().optional().default(0).describe('翻滚角（度）'),
    label: z.string().optional().describe('模型标注文本'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Model' },
  async (params) => {
    const result = await sendToBrowser('addModel', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — updateEntity
_registerTool(
  'updateEntity',
  '更新已有实体的属性（位置、颜色、标签、缩放、可见性）',
  {
    entityId: z.string().describe('实体ID（addMarker/addPolyline 等返回的 entityId）'),
    position: z.object({
      longitude: z.number().describe('经度（-180 ~ 180）'),
      latitude: z.number().describe('纬度（-90 ~ 90）'),
      height: z.number().optional().describe('高度（米）'),
    }).optional().describe('新位置坐标'),
    label: z.string().optional().describe('新标注文本'),
    color: z.string().optional().describe('新颜色（CSS 格式）'),
    scale: z.number().optional().describe('新缩放比例'),
    show: z.boolean().optional().describe('是否显示'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Update Entity' },
  async (params) => {
    const result = await sendToBrowser('updateEntity', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — removeEntity
_registerTool(
  'removeEntity',
  '移除单个实体（通过 entityId）',
  {
    entityId: z.string().describe('要移除的实体ID'),
  },
  { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, title: 'Remove Entity' },
  async (params) => {
    const result = await sendToBrowser('removeEntity', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — batchAddEntities
_registerTool(
  'batchAddEntities',
  '批量添加多个实体（一次调用创建多个 marker/polyline/polygon/model 等），返回所有 entityId',
  {
    entities: z.array(z.object({
      type: z.enum(['marker', 'polyline', 'polygon', 'model', 'billboard', 'box', 'cylinder', 'ellipse', 'rectangle', 'wall', 'corridor']).describe('实体类型'),
    }).passthrough()).describe('实体定义数组，每个元素包含 type 字段和该类型所需的参数'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Batch Add Entities' },
  async (params) => {
    const result = await sendToBrowser('batchAddEntities', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — queryEntities
_registerTool(
  'queryEntities',
  '查询已有实体 — 按名称、类型、空间范围过滤，返回 entityId/name/type/position 列表',
  {
    name: z.string().optional().describe('名称模糊匹配（不区分大小写）'),
    type: z.enum(['marker', 'polyline', 'polygon', 'model', 'billboard', 'box', 'cylinder', 'ellipse', 'rectangle', 'wall', 'corridor', 'label', 'unknown']).optional().describe('按实体类型过滤'),
    bbox: z.array(z.number()).length(4).optional().describe('空间范围过滤 [west, south, east, north]（度）'),
  },
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Query Entities' },
  async (params) => {
    const result = await sendToBrowser('queryEntities', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — getEntityProperties
_registerTool(
  'getEntityProperties',
  '获取指定实体的详细属性 — 包括类型、位置、自定义属性和图形属性',
  {
    entityId: z.string().describe('实体ID（可通过 queryEntities 获取）'),
  },
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Get Entity Properties' },
  async (params) => {
    const result = await sendToBrowser('getEntityProperties', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — saveViewpoint
_registerTool(
  'saveViewpoint',
  '保存当前视角为书签（名称 → 视角状态），可通过 loadViewpoint 恢复',
  {
    name: z.string().describe('书签名称（唯一标识，重复则覆盖）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Save Viewpoint' },
  async (params) => {
    const result = await sendToBrowser('saveViewpoint', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — loadViewpoint
_registerTool(
  'loadViewpoint',
  '恢复已保存的视角书签（带飞行动画），返回保存的视角状态',
  {
    name: z.string().describe('书签名称'),
    duration: z.number().optional().default(2).describe('飞行动画时长（秒），0 表示瞬移'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Load Viewpoint' },
  async (params) => {
    const result = await sendToBrowser('loadViewpoint', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — listViewpoints
_registerTool(
  'listViewpoints',
  '列出所有已保存的视角书签',
  {},
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'List Viewpoints' },
  async () => {
    const result = await sendToBrowser('listViewpoints', {})
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — exportScene
_registerTool(
  'exportScene',
  '导出当前场景快照 — 包含视角、图层列表、实体列表和时间戳',
  {},
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Export Scene' },
  async () => {
    const result = await sendToBrowser('exportScene', {})
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setLayerVisibility
_registerTool(
  'setLayerVisibility',
  '设置图层可见性',
  {
    id: z.string().describe('图层ID'),
    visible: z.boolean().describe('是否可见'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set Layer Visibility' },
  async (params) => {
    const result = await sendToBrowser('setLayerVisibility', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — listLayers
_registerTool(
  'listLayers',
  '获取当前所有图层列表（含 ID、名称、类型、可见性）',
  {},
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'List Layers' },
  async () => {
    const result = await sendToBrowser('listLayers', {})
    return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
  },
)

// — getLayerSchema
_registerTool(
  'getLayerSchema',
  '获取图层的属性字段结构 — 返回字段名、类型、示例值，适用于 GeoJSON/CZML/KML/3D Tiles 图层',
  {
    layerId: z.string().describe('图层ID（可通过 listLayers 获取）'),
  },
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Get Layer Schema' },
  async (params) => {
    const result = await sendToBrowser('getLayerSchema', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
  },
)

// — updateLayerStyle
const choroplethStyleSchema = z.object({
  field: z.string().min(1).describe('Property field used for choropleth classification'),
  breaks: z.array(z.number()).min(2).describe('Ascending class break values; colors length must be breaks length minus one'),
  colors: z.array(z.string()).min(1).describe('CSS colors for each choropleth interval'),
}).superRefine((value, ctx) => {
  if (value.colors.length !== value.breaks.length - 1) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      path: ['colors'],
      message: 'colors length must equal breaks length minus one',
    })
  }
  for (let i = 1; i < value.breaks.length; i++) {
    if (value.breaks[i]! <= value.breaks[i - 1]!) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        path: ['breaks', i],
        message: 'breaks must be strictly ascending',
      })
    }
  }
})

const categoryStyleSchema = z.object({
  field: z.string().min(1).describe('Property field used for category styling'),
  colors: z.array(z.string()).min(1).optional().describe('Optional CSS color palette'),
})

const layerStyleSchema = z.object({
  color: z.string().optional().describe('CSS color for entity layer features'),
  opacity: z.number().min(0).max(1).optional().describe('Opacity in range 0-1'),
  strokeWidth: z.number().min(0).optional().describe('Polyline or polygon outline width'),
  pointSize: z.number().min(0).optional().describe('Point or billboard size'),
  randomColor: z.boolean().optional().describe('Apply random colors to original GeoJSON entities'),
  gradient: z.tuple([z.string(), z.string()]).optional().describe('Two CSS colors used as index gradient'),
  choropleth: choroplethStyleSchema.optional().describe('GeoJSON choropleth style'),
  category: categoryStyleSchema.optional().describe('GeoJSON category style'),
}).superRefine((value, ctx) => {
  const enabled = [
    value.choropleth !== undefined,
    value.category !== undefined,
    value.randomColor === true,
    value.gradient !== undefined,
  ].filter(Boolean).length
  if (enabled > 1) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Only one thematic style is allowed: choropleth, category, randomColor, or gradient',
    })
  }
})

const imageryStyleSchema = z.object({
  alpha: z.number().min(0).max(1).optional().describe('Imagery alpha in range 0-1'),
  brightness: z.number().optional().describe('Imagery brightness multiplier'),
  contrast: z.number().optional().describe('Imagery contrast multiplier'),
  hue: z.number().optional().describe('Imagery hue shift in radians'),
  saturation: z.number().optional().describe('Imagery saturation multiplier'),
  gamma: z.number().optional().describe('Imagery gamma correction'),
}).refine(value => Object.values(value).some(v => v !== undefined), {
  message: 'At least one imagery style field is required',
})

const primitiveStyleSchema = z.object({
  color: z.string().optional().describe('CSS fill color for GeoJSON Primitive materials'),
  opacity: z.number().min(0).max(1).optional().describe('Fill alpha in range 0-1'),
  outlineColor: z.string().optional().describe('CSS outline color for GeoJSON Primitive materials'),
  outlineWidth: z.number().min(0).max(255).optional().describe('Outline width in range 0-255'),
  pointSize: z.number().min(0).max(255).optional().describe('Point size in range 0-255'),
  lineWidth: z.number().min(0).max(255).optional().describe('Polyline width in range 0-255'),
}).refine(value => Object.values(value).some(v => v !== undefined), {
  message: 'At least one primitive style field is required',
})

_registerTool(
  'updateLayerStyle',
  '修改已有图层的样式（颜色、透明度、标注样式、3D Tiles 样式等）',
  {
    layerId: z.string().describe('图层ID'),
    labelStyle: z.record(z.unknown()).optional().describe('标注样式（font, fillColor, outlineColor, outlineWidth, scale 等）'),
    layerStyle: layerStyleSchema.optional().describe('Entity layer style. Thematic fields are GeoJSON-only and mutually exclusive.'),
    imageryStyle: imageryStyleSchema.optional().describe('Imagery layer visual style. Visibility is controlled by setLayerVisibility.'),
    primitiveStyle: primitiveStyleSchema.optional().describe('GeoJSON Primitive material style. Visibility is controlled by setLayerVisibility.'),
    tileStyle: z.object({
      color: z.string().optional().describe('3D Tiles 颜色表达式，如 "color(\'red\')" 或条件表达式'),
      show: z.string().optional().describe('3D Tiles 显示条件表达式，如 "${Height} > 50"'),
      pointSize: z.string().optional().describe('3D Tiles 点大小表达式'),
      meta: z.record(z.string()).optional().describe('3D Tiles meta 属性'),
    }).optional().describe('3D Tiles 样式（Cesium3DTileStyle 表达式）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Update Layer Style' },
  async (params) => {
    const result = await sendToBrowser('updateLayerStyle', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — playTrajectory
_registerTool(
  'playTrajectory',
  '播放移动轨迹动画',
  {
    id: z.string().optional().describe('轨迹图层ID'),
    name: z.string().optional().describe('轨迹名称'),
    coordinates: z.array(z.array(z.number())).describe('轨迹坐标数组 [[lon, lat, alt?], ...]'),
    durationSeconds: z.number().optional().default(10).describe('动画时长（秒）'),
    trailSeconds: z.number().optional().default(2).describe('尾迹长度（秒）'),
    label: z.string().optional().describe('移动体标签'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Play Trajectory' },
  async (params) => {
    const result = await sendToBrowser('playTrajectory', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — load3dTiles
_registerTool(
  'load3dTiles',
  '加载 3D Tiles 数据集（支持 URL 或 Cesium Ion 资产 ID）',
  {
    id: z.string().optional().describe('图层ID'),
    name: z.string().optional().describe('图层名称'),
    url: z.string().optional().describe('tileset.json 的 URL（与 ionAssetId 二选一）'),
    ionAssetId: z.number().optional().describe('Cesium Ion 资产 ID（与 url 二选一）'),
    maximumScreenSpaceError: z.number().optional().default(16).describe('最大屏幕空间误差（值越小越精细）'),
    heightOffset: z.number().optional().describe('高度偏移（米）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Load 3D Tiles' },
  async (params) => {
    const result = await sendToBrowser('load3dTiles', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — load3dGaussianSplat
_registerTool(
  'load3dGaussianSplat',
  '加载 3D 高斯泼溅（Gaussian Splat）数据集',
  {
    id: z.string().optional().describe('图层ID'),
    name: z.string().optional().describe('图层名称'),
    url: z.string().describe('高斯泼溅 tileset.json 的 URL'),
    maximumScreenSpaceError: z.number().optional().default(16).describe('最大屏幕空间误差（值越小越精细）'),
    show: z.boolean().optional().default(true).describe('是否显示'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Load 3D Gaussian Splat' },
  async (params) => {
    const result = await sendToBrowser('load3dGaussianSplat', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — loadTerrain
_registerTool(
  'loadTerrain',
  '加载或切换地形（平坦/ArcGIS/CesiumIon/自定义 URL）',
  {
    provider: z.enum(['flat', 'arcgis', 'cesiumion']).describe('地形提供者类型'),
    url: z.string().optional().describe('自定义地形服务 URL'),
    cesiumIonAssetId: z.number().optional().describe('Cesium Ion 资产ID（provider=cesiumion 时需要）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Load Terrain' },
  async (params) => {
    const result = await sendToBrowser('loadTerrain', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — loadImageryService
_registerTool(
  'loadImageryService',
  '加载影像服务图层（WMS/WMTS/XYZ/ArcGIS MapServer/Cesium Ion）',
  {
    id: z.string().optional().describe('图层ID'),
    name: z.string().optional().describe('图层名称'),
    url: z.string().optional().describe('影像服务 URL（与 ionAssetId 二选一）'),
    ionAssetId: z.number().optional().describe('Cesium Ion 影像资产 ID（与 url 二选一）'),
    serviceType: z.enum(['wms', 'wmts', 'xyz', 'arcgis_mapserver', 'ion']).optional().describe('服务类型（使用 ionAssetId 时可不填）'),
    layerName: z.string().optional().describe('WMS/WMTS 图层名'),
    opacity: z.number().optional().default(1.0).describe('透明度（0~1）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Load Imagery Service' },
  async (params) => {
    const result = await sendToBrowser('loadImageryService', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// ==================== Camera Tools (融合官方 Camera Server) ====================

// — loadCzml
_registerTool(
  'loadCzml',
  '加载 CZML 时序数据源（CesiumJS 原生格式，支持时变位置/样式/动画）。data 和 url 二选一',
  {
    id: z.string().optional().describe('图层ID（不传则自动生成）'),
    name: z.string().optional().describe('数据源显示名称'),
    data: z.array(z.unknown()).optional().describe('CZML 数据包数组（与 url 二选一）'),
    url: z.string().optional().describe('CZML 文件 URL（与 data 二选一，浏览器端 fetch 加载）'),
    sourceUri: z.string().optional().describe('CZML 中相对引用的基础 URI'),
    clampToGround: z.boolean().optional().describe('将实体贴地显示'),
    flyTo: z.boolean().optional().describe('加载后自动飞行到数据范围（默认 true）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Load CZML' },
  async (params) => {
    const result = await sendToBrowser('loadCzml', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — loadKml
_registerTool(
  'loadKml',
  '加载 KML/KMZ 数据源（Google Earth 格式）。url 和 data 二选一',
  {
    id: z.string().optional().describe('图层ID（不传则自动生成）'),
    name: z.string().optional().describe('数据源显示名称'),
    url: z.string().optional().describe('KML/KMZ 文件 URL（与 data 二选一，浏览器端 fetch 加载）'),
    data: z.string().optional().describe('KML XML 字符串（与 url 二选一）'),
    sourceUri: z.string().optional().describe('KML 中相对引用的基础 URI'),
    clampToGround: z.boolean().optional().describe('将实体贴地显示'),
    flyTo: z.boolean().optional().describe('加载后自动飞行到数据范围（默认 true）'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Load KML/KMZ' },
  async (params) => {
    const result = await sendToBrowser('loadKml', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setEdgeDisplayMode
_registerTool(
  'setEdgeDisplayMode',
  '设置 3D Tiles 边缘显示模式（仅表面 / 表面+边缘 / 仅边缘线框）',
  {
    tilesetId: z.string().optional().describe('目标图层ID（不传则应用于场景中所有 3D Tiles）'),
    mode: z.enum(['surfaces_only', 'surfaces_and_edges', 'edges_only']).describe('边缘显示模式：surfaces_only=仅表面, surfaces_and_edges=表面+边缘, edges_only=仅线框'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set Edge Display Mode' },
  async (params) => {
    const result = await sendToBrowser('setEdgeDisplayMode', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// ==================== Camera Tools (融合官方 Camera Server) ====================
_registerTool(
  'lookAtTransform',
  'Look at a specific position from a given heading/pitch/range (orbit-style camera)',
  {
    longitude: z.number().describe('Target longitude (degrees)'),
    latitude: z.number().describe('Target latitude (degrees)'),
    height: z.number().optional().default(0).describe('Target height (meters)'),
    heading: z.number().optional().default(0).describe('Camera heading (degrees), 0=North'),
    pitch: z.number().optional().default(-45).describe('Camera pitch (degrees), -90=straight down'),
    range: z.number().optional().default(1000).describe('Distance from target (meters)'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Look At Transform' },
  async (params) => {
    const result = await sendToBrowser('lookAtTransform', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — startOrbit
_registerTool(
  'startOrbit',
  'Start orbiting the camera around the current view center',
  {
    speed: z.number().optional().default(0.005).describe('Rotation speed (radians per tick)'),
    clockwise: z.boolean().optional().default(true).describe('Orbit direction'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Start Orbit' },
  async (params) => {
    const result = await sendToBrowser('startOrbit', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — stopOrbit
_registerTool(
  'stopOrbit',
  'Stop the camera orbit animation',
  {},
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Stop Orbit' },
  async () => {
    const result = await sendToBrowser('stopOrbit', {})
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setCameraOptions
_registerTool(
  'setCameraOptions',
  'Configure camera controller options (enable/disable rotation, zoom, tilt, etc.)',
  {
    enableRotate: z.boolean().optional().describe('Enable camera rotation'),
    enableTranslate: z.boolean().optional().describe('Enable camera translation'),
    enableZoom: z.boolean().optional().describe('Enable camera zoom'),
    enableTilt: z.boolean().optional().describe('Enable camera tilt'),
    enableLook: z.boolean().optional().describe('Enable camera look'),
    minimumZoomDistance: z.number().optional().describe('Minimum zoom distance (meters)'),
    maximumZoomDistance: z.number().optional().describe('Maximum zoom distance (meters)'),
    enableInputs: z.boolean().optional().describe('Enable/disable all camera inputs'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set Camera Options' },
  async (params) => {
    const result = await sendToBrowser('setCameraOptions', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// ==================== Entity Type Tools (融合官方 Entity Server) ====================

const colorSchema = z.union([
  z.string().describe('CSS color string (e.g. "#FF0000", "red")'),
  z.object({ red: z.number().describe('Red channel (0-1)'), green: z.number().describe('Green channel (0-1)'), blue: z.number().describe('Blue channel (0-1)'), alpha: z.number().optional().describe('Alpha channel (0-1)') }).describe('RGBA color object'),
]).optional()

const materialSchema = z.union([
  z.string().describe('CSS color string'),
  z.object({ red: z.number().describe('Red (0-1)'), green: z.number().describe('Green (0-1)'), blue: z.number().describe('Blue (0-1)'), alpha: z.number().optional().describe('Alpha (0-1)') }).describe('RGBA color'),
  z.object({
    type: z.enum(['color', 'image', 'checkerboard', 'stripe', 'grid']).describe('Material type'),
    color: z.union([z.string(), z.object({ red: z.number().describe('Red (0-1)'), green: z.number().describe('Green (0-1)'), blue: z.number().describe('Blue (0-1)'), alpha: z.number().optional().describe('Alpha (0-1)') })]).optional().describe('Base color'),
    image: z.string().optional().describe('Image URL'),
    evenColor: z.union([z.string(), z.object({ red: z.number().describe('Red (0-1)'), green: z.number().describe('Green (0-1)'), blue: z.number().describe('Blue (0-1)'), alpha: z.number().optional().describe('Alpha (0-1)') })]).optional().describe('Even color for checkerboard/stripe'),
    oddColor: z.union([z.string(), z.object({ red: z.number().describe('Red (0-1)'), green: z.number().describe('Green (0-1)'), blue: z.number().describe('Blue (0-1)'), alpha: z.number().optional().describe('Alpha (0-1)') })]).optional().describe('Odd color for checkerboard/stripe'),
    orientation: z.enum(['horizontal', 'vertical']).optional().describe('Stripe orientation'),
    cellAlpha: z.number().optional().describe('Cell alpha for grid material'),
  }).describe('Complex material specification'),
]).optional()

const orientationSchema = z.object({
  heading: z.number().describe('Heading (degrees)'),
  pitch: z.number().describe('Pitch (degrees)'),
  roll: z.number().describe('Roll (degrees)'),
}).optional()

const positionDegreesSchema = z.object({
  longitude: z.number().describe('Longitude (degrees)'),
  latitude: z.number().describe('Latitude (degrees)'),
  height: z.number().optional().describe('Height above ground (meters)'),
})

// — addBillboard
_registerTool(
  'addBillboard',
  'Add a billboard (image icon) at a position on the globe',
  {
    longitude: z.number().describe('Longitude (degrees)'),
    latitude: z.number().describe('Latitude (degrees)'),
    height: z.number().optional().default(0).describe('Height (meters)'),
    name: z.string().optional().describe('Billboard name'),
    image: z.string().describe('Image URL for the billboard'),
    scale: z.number().optional().default(1.0).describe('Scale factor'),
    color: colorSchema.describe('Tint color'),
    pixelOffset: z.object({ x: z.number(), y: z.number() }).optional().describe('Pixel offset from position'),
    horizontalOrigin: z.enum(['CENTER', 'LEFT', 'RIGHT']).optional().describe('Horizontal origin'),
    verticalOrigin: z.enum(['CENTER', 'TOP', 'BOTTOM', 'BASELINE']).optional().describe('Vertical origin'),
    heightReference: z.enum(['NONE', 'CLAMP_TO_GROUND', 'RELATIVE_TO_GROUND']).optional().describe('Height reference'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Billboard' },
  async (params) => {
    const result = await sendToBrowser('addBillboard', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addBox
_registerTool(
  'addBox',
  'Add a 3D box entity at a position',
  {
    longitude: z.number().describe('Longitude (degrees)'),
    latitude: z.number().describe('Latitude (degrees)'),
    height: z.number().optional().default(0).describe('Height (meters)'),
    name: z.string().optional().describe('Box name'),
    dimensions: z.object({
      width: z.number().describe('Width in meters (X)'),
      length: z.number().describe('Length in meters (Y)'),
      height: z.number().describe('Height in meters (Z)'),
    }).describe('Box dimensions'),
    material: materialSchema.describe('Material (color string, RGBA object, or material spec)'),
    outline: z.boolean().optional().default(true).describe('Show outline'),
    outlineColor: colorSchema.describe('Outline color'),
    fill: z.boolean().optional().default(true).describe('Show fill'),
    orientation: orientationSchema.describe('Orientation (heading/pitch/roll in degrees)'),
    heightReference: z.enum(['NONE', 'CLAMP_TO_GROUND', 'RELATIVE_TO_GROUND']).optional().describe('Height reference'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Box' },
  async (params) => {
    const result = await sendToBrowser('addBox', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addCorridor
_registerTool(
  'addCorridor',
  'Add a corridor (path with width) entity',
  {
    name: z.string().optional().describe('Corridor name'),
    positions: z.array(positionDegreesSchema).describe('Array of positions along the corridor'),
    width: z.number().describe('Corridor width in meters'),
    material: materialSchema.describe('Material'),
    cornerType: z.enum(['ROUNDED', 'MITERED', 'BEVELED']).optional().describe('Corner type'),
    height: z.number().optional().describe('Height above ground (meters)'),
    extrudedHeight: z.number().optional().describe('Extruded height (meters)'),
    outline: z.boolean().optional().describe('Show outline'),
    outlineColor: colorSchema.describe('Outline color'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Corridor' },
  async (params) => {
    const result = await sendToBrowser('addCorridor', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addCylinder
_registerTool(
  'addCylinder',
  'Add a cylinder or cone entity at a position',
  {
    longitude: z.number().describe('Longitude (degrees)'),
    latitude: z.number().describe('Latitude (degrees)'),
    height: z.number().optional().default(0).describe('Height (meters)'),
    name: z.string().optional().describe('Cylinder name'),
    length: z.number().describe('Cylinder length/height in meters'),
    topRadius: z.number().describe('Top radius in meters'),
    bottomRadius: z.number().describe('Bottom radius in meters'),
    material: materialSchema.describe('Material'),
    outline: z.boolean().optional().default(true).describe('Show outline'),
    outlineColor: colorSchema.describe('Outline color'),
    fill: z.boolean().optional().default(true).describe('Show fill'),
    orientation: orientationSchema.describe('Orientation (heading/pitch/roll in degrees)'),
    numberOfVerticalLines: z.number().optional().default(16).describe('Number of vertical lines'),
    slices: z.number().optional().default(128).describe('Number of slices'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Cylinder' },
  async (params) => {
    const result = await sendToBrowser('addCylinder', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addEllipse
_registerTool(
  'addEllipse',
  'Add an ellipse (oval) entity at a position',
  {
    longitude: z.number().describe('Center longitude (degrees)'),
    latitude: z.number().describe('Center latitude (degrees)'),
    height: z.number().optional().default(0).describe('Height (meters)'),
    name: z.string().optional().describe('Ellipse name'),
    semiMajorAxis: z.number().describe('Semi-major axis in meters'),
    semiMinorAxis: z.number().describe('Semi-minor axis in meters'),
    material: materialSchema.describe('Material'),
    extrudedHeight: z.number().optional().describe('Extruded height (meters)'),
    rotation: z.number().optional().describe('Rotation (radians)'),
    outline: z.boolean().optional().describe('Show outline'),
    outlineColor: colorSchema.describe('Outline color'),
    fill: z.boolean().optional().default(true).describe('Show fill'),
    stRotation: z.number().optional().describe('Texture rotation (radians)'),
    numberOfVerticalLines: z.number().optional().describe('Number of vertical lines'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Ellipse' },
  async (params) => {
    const result = await sendToBrowser('addEllipse', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addRectangle
_registerTool(
  'addRectangle',
  'Add a rectangle entity defined by geographic bounds',
  {
    name: z.string().optional().describe('Rectangle name'),
    west: z.number().describe('West longitude (degrees)'),
    south: z.number().describe('South latitude (degrees)'),
    east: z.number().describe('East longitude (degrees)'),
    north: z.number().describe('North latitude (degrees)'),
    material: materialSchema.describe('Material'),
    height: z.number().optional().describe('Height (meters)'),
    extrudedHeight: z.number().optional().describe('Extruded height (meters)'),
    rotation: z.number().optional().describe('Rotation (radians)'),
    outline: z.boolean().optional().describe('Show outline'),
    outlineColor: colorSchema.describe('Outline color'),
    fill: z.boolean().optional().default(true).describe('Show fill'),
    stRotation: z.number().optional().describe('Texture rotation (radians)'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Rectangle' },
  async (params) => {
    const result = await sendToBrowser('addRectangle', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — addWall
_registerTool(
  'addWall',
  'Add a wall entity along a series of positions',
  {
    name: z.string().optional().describe('Wall name'),
    positions: z.array(positionDegreesSchema).describe('Array of positions along the wall'),
    minimumHeights: z.array(z.number()).optional().describe('Minimum heights at each position'),
    maximumHeights: z.array(z.number()).optional().describe('Maximum heights at each position'),
    material: materialSchema.describe('Material'),
    outline: z.boolean().optional().describe('Show outline'),
    outlineColor: colorSchema.describe('Outline color'),
    fill: z.boolean().optional().default(true).describe('Show fill'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Add Wall' },
  async (params) => {
    const result = await sendToBrowser('addWall', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// ==================== Animation Tools (融合官方 Animation Server) ====================

// — createAnimation
_registerTool(
  'createAnimation',
  'Create a time-based animation with waypoints (moving entity along a path)',
  {
    name: z.string().optional().describe('Animation name'),
    waypoints: z.array(z.object({
      longitude: z.number().describe('Longitude (degrees)'),
      latitude: z.number().describe('Latitude (degrees)'),
      height: z.number().optional().describe('Height (meters)'),
      time: z.string().describe('ISO 8601 timestamp'),
    })).describe('Array of waypoints with positions and timestamps'),
    modelUri: z.string().optional().describe('glTF/GLB model URL, or preset: cesium_man, cesium_air, ground_vehicle, cesium_drone'),
    showPath: z.boolean().optional().default(true).describe('Show trail path'),
    pathWidth: z.number().optional().default(2).describe('Path width (pixels)'),
    pathColor: z.string().optional().default('#00FF00').describe('Path color (CSS)'),
    pathLeadTime: z.number().optional().default(0).describe('Path lead time (seconds)'),
    pathTrailTime: z.number().optional().default(1e10).describe('Path trail time (seconds)'),
    multiplier: z.number().optional().default(1).describe('Clock speed multiplier'),
    shouldAnimate: z.boolean().optional().default(true).describe('Auto-start animation'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Create Animation' },
  async (params) => {
    const result = await sendToBrowser('createAnimation', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — controlAnimation
_registerTool(
  'controlAnimation',
  'Play or pause the current animation',
  {
    action: z.enum(['play', 'pause']).describe('Play or pause'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Control Animation' },
  async (params) => {
    const result = await sendToBrowser('controlAnimation', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — removeAnimation
_registerTool(
  'removeAnimation',
  'Remove an animation entity',
  {
    entityId: z.string().describe('Entity ID of the animation to remove'),
  },
  { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, title: 'Remove Animation' },
  async (params) => {
    const result = await sendToBrowser('removeAnimation', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — listAnimations
_registerTool(
  'listAnimations',
  'List all active animations',
  {},
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'List Animations' },
  async () => {
    const result = await sendToBrowser('listAnimations', {})
    return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
  },
)

// — updateAnimationPath
_registerTool(
  'updateAnimationPath',
  'Update the visual properties of an animation path',
  {
    entityId: z.string().describe('Entity ID of the animation'),
    width: z.number().optional().describe('New path width (pixels)'),
    color: z.string().optional().describe('New path color (CSS)'),
    leadTime: z.number().optional().describe('New lead time (seconds)'),
    trailTime: z.number().optional().describe('New trail time (seconds)'),
    show: z.boolean().optional().describe('Show/hide path'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Update Animation Path' },
  async (params) => {
    const result = await sendToBrowser('updateAnimationPath', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — trackEntity
_registerTool(
  'trackEntity',
  'Track (follow) an entity with the camera, or stop tracking',
  {
    entityId: z.string().optional().describe('Entity ID to track (omit to stop tracking)'),
    heading: z.number().optional().describe('Camera heading (degrees)'),
    pitch: z.number().optional().default(-30).describe('Camera pitch (degrees)'),
    range: z.number().optional().default(500).describe('Camera distance from entity (meters)'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Track Entity' },
  async (params) => {
    const result = await sendToBrowser('trackEntity', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — controlClock
_registerTool(
  'controlClock',
  'Configure the Cesium clock (time range, speed, animation state)',
  {
    action: z.enum(['configure', 'setTime', 'setMultiplier']).describe('Clock action'),
    startTime: z.string().optional().describe('ISO 8601 start time (for configure)'),
    stopTime: z.string().optional().describe('ISO 8601 stop time (for configure)'),
    currentTime: z.string().optional().describe('ISO 8601 current time (for configure)'),
    time: z.string().optional().describe('ISO 8601 time to jump to (for setTime)'),
    multiplier: z.number().optional().describe('Clock speed multiplier (for configure/setMultiplier)'),
    shouldAnimate: z.boolean().optional().describe('Whether clock should animate (for configure)'),
    clockRange: z.enum(['UNBOUNDED', 'CLAMPED', 'LOOP_STOP']).optional().describe('Clock range mode (for configure)'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, title: 'Control Clock' },
  async (params) => {
    const result = await sendToBrowser('controlClock', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setGlobeLighting
_registerTool(
  'setGlobeLighting',
  'Enable/disable globe lighting and atmospheric effects',
  {
    enableLighting: z.boolean().optional().describe('Enable globe lighting'),
    dynamicAtmosphereLighting: z.boolean().optional().describe('Enable dynamic atmosphere lighting'),
    dynamicAtmosphereLightingFromSun: z.boolean().optional().describe('Use sun position for atmosphere lighting'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set Globe Lighting' },
  async (params) => {
    const result = await sendToBrowser('setGlobeLighting', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setSceneOptions
_registerTool(
  'setSceneOptions',
  'Configure scene environment (fog, atmosphere, shadows, sun, moon, background color, depth testing)',
  {
    fogEnabled: z.boolean().optional().describe('Enable/disable fog'),
    fogDensity: z.number().optional().describe('Fog density (0.0~1.0, default ~0.0002)'),
    fogMinimumBrightness: z.number().optional().describe('Minimum fog brightness (0.0~1.0)'),
    skyAtmosphereShow: z.boolean().optional().describe('Show sky atmosphere'),
    skyAtmosphereHueShift: z.number().optional().describe('Sky hue shift (-1.0~1.0)'),
    skyAtmosphereSaturationShift: z.number().optional().describe('Sky saturation shift (-1.0~1.0)'),
    skyAtmosphereBrightnessShift: z.number().optional().describe('Sky brightness shift (-1.0~1.0)'),
    groundAtmosphereShow: z.boolean().optional().describe('Show ground atmosphere'),
    shadowsEnabled: z.boolean().optional().describe('Enable shadows'),
    shadowsSoftShadows: z.boolean().optional().describe('Use soft shadows'),
    shadowsDarkness: z.number().optional().describe('Shadow darkness (0.0=no shadow, 1.0=fully dark)'),
    sunShow: z.boolean().optional().describe('Show the sun'),
    sunGlowFactor: z.number().optional().describe('Sun glow factor (default 1.0)'),
    moonShow: z.boolean().optional().describe('Show the moon'),
    depthTestAgainstTerrain: z.boolean().optional().describe('Enable depth test against terrain (entities behind terrain are hidden)'),
    backgroundColor: z.string().optional().describe('Scene background color (CSS format, e.g. "#000000")'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set Scene Options' },
  async (params) => {
    const result = await sendToBrowser('setSceneOptions', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setPostProcess
_registerTool(
  'setPostProcess',
  'Configure post-processing effects (bloom glow, ambient occlusion SSAO, anti-aliasing FXAA)',
  {
    bloom: z.boolean().optional().describe('Enable bloom glow effect'),
    bloomContrast: z.number().optional().describe('Bloom contrast (default 128)'),
    bloomBrightness: z.number().optional().describe('Bloom brightness (default -0.3)'),
    bloomDelta: z.number().optional().describe('Bloom delta (default 1.0)'),
    bloomSigma: z.number().optional().describe('Bloom sigma (default 3.78)'),
    bloomStepSize: z.number().optional().describe('Bloom step size (default 5.0)'),
    bloomGlowOnly: z.boolean().optional().describe('Show only the glow (no base scene)'),
    ambientOcclusion: z.boolean().optional().describe('Enable ambient occlusion (SSAO)'),
    aoIntensity: z.number().optional().describe('AO intensity (default 3.0)'),
    aoBias: z.number().optional().describe('AO bias (default 0.1)'),
    aoLengthCap: z.number().optional().describe('AO length cap (default 0.26)'),
    aoStepSize: z.number().optional().describe('AO step size (default 1.95)'),
    fxaa: z.boolean().optional().describe('Enable FXAA anti-aliasing'),
  },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set Post-Processing' },
  async (params) => {
    const result = await sendToBrowser('setPostProcess', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — setIonToken
_registerTool(
  'setIonToken',
  'Set Cesium Ion access token for loading Ion assets (3D Tiles, imagery, terrain). Must be called before loading private Ion resources.',
  { token: z.string().describe('Cesium Ion access token') },
  { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Set Ion Token' },
  async (params: Record<string, unknown>) => {
    const result = await sendToBrowser('setIonToken', params)
    return { content: [{ type: 'text' as const, text: JSON.stringify(result ?? { success: true }) }] }
  },
)

// — geocode (直接 HTTP 请求 Nominatim，不经过 Bridge)
let _lastGeocodeTime = 0
let _proxyDispatcher: object | undefined

// 初始化代理（读取 HTTPS_PROXY / HTTP_PROXY / ALL_PROXY 环境变量）
const _proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.ALL_PROXY
if (_proxyUrl) {
  import('undici').then(({ ProxyAgent }) => {
    _proxyDispatcher = new ProxyAgent(_proxyUrl)
  }).catch(() => { /* undici not available, skip proxy */ })
}

_registerTool(
  'geocode',
  '将地址、地标或地名转换为地理坐标（经纬度）。使用 OpenStreetMap Nominatim 免费服务，无需 API Key。',
  {
    address: z.string().min(1).describe('地址、地标或地名，例如 "故宫"、"Eiffel Tower"、"东京塔"'),
    countryCode: z.string().length(2).optional().describe('两位 ISO 国家代码限制搜索范围（如 "CN"、"US"、"JP"）'),
  },
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, title: 'Geocode Address' },
  async ({ address, countryCode }) => {
    // Rate limiting: Nominatim 要求至少 1 秒间隔
    const now = Date.now()
    const wait = 1100 - (now - _lastGeocodeTime)
    if (wait > 0) await new Promise(r => setTimeout(r, wait))
    _lastGeocodeTime = Date.now()

    const params = new URLSearchParams({
      q: address,
      format: 'json',
      addressdetails: '1',
      limit: '1',
    })
    if (countryCode) params.set('countrycodes', countryCode)

    const ua = process.env.OSM_USER_AGENT || 'cesium-mcp-runtime/1.0'
    const fetchOptions: RequestInit & { dispatcher?: object } = {
      headers: { 'User-Agent': ua },
    }
    if (_proxyDispatcher) fetchOptions.dispatcher = _proxyDispatcher

    const resp = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, fetchOptions)
    if (!resp.ok) {
      return { content: [{ type: 'text' as const, text: JSON.stringify({ success: false, message: `Nominatim API error: ${resp.status}` }) }], isError: true }
    }
    const data = await resp.json() as Array<{
      lat: string; lon: string; display_name: string;
      boundingbox?: [string, string, string, string];
      address?: Record<string, string>;
    }>

    if (!data.length) {
      return { content: [{ type: 'text' as const, text: JSON.stringify({ success: false, message: `No results found for: ${address}` }) }] }
    }

    const item = data[0]!
    const result = {
      success: true,
      longitude: parseFloat(item.lon),
      latitude: parseFloat(item.lat),
      displayName: item.display_name,
      boundingBox: item.boundingbox ? {
        south: parseFloat(item.boundingbox[0]),
        north: parseFloat(item.boundingbox[1]),
        west: parseFloat(item.boundingbox[2]),
        east: parseFloat(item.boundingbox[3]),
      } : undefined,
    }
    return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
  },
)

// ==================== Prompts ====================

server.prompt(
  'cesium-quickstart',
  'Quick reference for using Cesium MCP tools',
  async () => ({
    messages: [{
      role: 'user' as const,
      content: {
        type: 'text' as const,
        text: `Cesium MCP Quick Start Guide:

1. **Camera**: flyTo(lng, lat) to navigate, setView for instant move, getView to read current position
2. **Entities**: addMarker for points, addPolygon/addPolyline for shapes, addModel for 3D models
3. **Layers**: addGeoJsonLayer for vector data, load3dTiles for 3D city models, loadImageryService for WMS/WMTS
4. **Animation**: createAnimation with waypoints for moving entities, controlAnimation to play/pause
5. **Interaction**: screenshot to capture view, highlight to emphasize features
6. **Discovery**: list_toolsets to see available tool groups, enable_toolset to activate more tools

All entity/layer operations return an ID for subsequent updates or removal.`,
      },
    }],
  }),
)

// ==================== Meta-tools (Dynamic Discovery) ====================

if (!_allMode) {
  server.tool(
    'list_toolsets',
    'List all available tool groups and their enabled status. Call this to discover additional capabilities before asking the user to configure anything.',
    {},
    { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'List Toolsets' },
    async () => {
      const groups = Object.entries(TOOLSETS).map(([name, tools]) => ({
        name,
        description: TOOLSET_DESCRIPTIONS[name] ?? '',
        tools: tools.length,
        enabled: _enabledSets.has(name),
        toolNames: tools,
      }))
      return { content: [{ type: 'text' as const, text: JSON.stringify(groups, null, 2) }] }
    },
  )

  server.tool(
    'enable_toolset',
    'Enable a tool group to make its tools available. Call list_toolsets first to see available groups.',
    {
      toolset: z.string().describe('Name of the toolset to enable (e.g. "camera", "animation", "entity-ext")'),
    },
    { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'Enable Toolset' },
    async ({ toolset }) => {
      if (!(toolset in TOOLSETS)) {
        return {
          content: [{ type: 'text' as const, text: `Unknown toolset "${toolset}". Available: ${Object.keys(TOOLSETS).join(', ')}` }],
          isError: true,
        }
      }
      if (_enabledSets.has(toolset)) {
        return { content: [{ type: 'text' as const, text: `Toolset "${toolset}" is already enabled.` }] }
      }
      const added = _enableToolset(toolset)
      server.sendToolListChanged?.()
      return {
        content: [{
          type: 'text' as const,
          text: `Enabled toolset "${toolset}" — ${added.length} new tools available: ${added.join(', ')}`,
        }],
      }
    },
  )
}

// ==================== Session Management ====================

server.tool(
  'listSessions',
  _localeKey === 'zh-cn'
    ? '列出当前所有已连接的浏览器 session（ID 和连接状态），用于多浏览器路由'
    : 'List all connected browser sessions (ID and connection state) for multi-browser routing',
  {},
  { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'List Sessions' },
  async () => {
    const sessions = Array.from(browserClients.entries()).map(([id, ws]) => ({
      sessionId: id,
      connected: ws.readyState === WebSocket.OPEN,
      isDefault: id === DEFAULT_SESSION_ID,
    }))
    return { content: [{ type: 'text' as const, text: JSON.stringify(sessions, null, 2) }] }
  },
)

// ==================== Streamable HTTP Transport ====================

/**
 * Create a fresh McpServer instance with all enabled tools replayed.
 * Used for stateless HTTP transport mode — each request gets its own server.
 */
function _createHttpMcpServer(filterToolsets?: Set<string>): McpServer {
  const s = new McpServer({
    name: 'cesium-mcp-runtime',
    version: __VERSION__,
    title: 'Cesium MCP Runtime',
    description: 'AI-powered 3D globe control via MCP — camera, layers, entities, animation, and interaction with CesiumJS.',
    websiteUrl: 'https://github.com/gaopengbin/cesium-mcp',
  }, {
    instructions: 'Cesium MCP Runtime provides tools for controlling a CesiumJS 3D globe via AI. A browser with cesium-mcp-bridge must be connected via WebSocket for command execution. Use view tools (flyTo, setView) to navigate, entity tools to add markers/polygons/models, layer tools to manage GeoJSON/3D Tiles, and animation tools for time-based animations.',
  })

  // Replay resources
  s.resource(
    'camera', 'cesium://scene/camera',
    { description: '当前相机状态（经纬度、高度、角度）', mimeType: 'application/json' },
    async () => {
      try {
        const result = await sendToBrowser('getView', {})
        return { contents: [{ uri: 'cesium://scene/camera', text: JSON.stringify(result), mimeType: 'application/json' }] }
      } catch {
        return { contents: [{ uri: 'cesium://scene/camera', text: '{"error":"no browser connected"}', mimeType: 'application/json' }] }
      }
    },
  )
  s.resource(
    'layers', 'cesium://scene/layers',
    { description: '当前已加载的图层列表（ID、名称、类型、可见性）', mimeType: 'application/json' },
    async () => {
      try {
        const result = await sendToBrowser('listLayers', {})
        return { contents: [{ uri: 'cesium://scene/layers', text: JSON.stringify(result), mimeType: 'application/json' }] }
      } catch {
        return { contents: [{ uri: 'cesium://scene/layers', text: '{"error":"no browser connected"}', mimeType: 'application/json' }] }
      }
    },
  )

  // Replay tools with _meta.toolset, optionally filtered by toolsets param
  const allowedToolsets = filterToolsets ?? new Set(Object.keys(TOOLSETS))
  const allowedTools = new Set<string>()
  for (const setName of allowedToolsets) {
    if (TOOLSETS[setName]) {
      for (const tool of TOOLSETS[setName]) allowedTools.add(tool)
    }
  }
  for (const [name, args] of _toolDefs.entries()) {
    if (allowedTools.has(name)) {
      _applyToolDef(s, args)
    }
  }

  // Register listSessions for HTTP mode
  s.tool(
    'listSessions',
    _localeKey === 'zh-cn'
      ? '列出当前所有已连接的浏览器 session（ID 和连接状态），用于多浏览器路由'
      : 'List all connected browser sessions (ID and connection state) for multi-browser routing',
    {},
    { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, title: 'List Sessions' },
    async () => {
      const sessions = Array.from(browserClients.entries()).map(([id, ws]) => ({
        sessionId: id,
        connected: ws.readyState === WebSocket.OPEN,
        isDefault: id === DEFAULT_SESSION_ID,
      }))
      return { content: [{ type: 'text' as const, text: JSON.stringify(sessions, null, 2) }] }
    },
  )

  return s
}

/**
 * Handle MCP Streamable HTTP requests (stateless mode).
 * Each POST creates a fresh McpServer + transport, handles the request, then cleans up.
 */
async function _handleMcpRequest(req: IncomingMessage, res: ServerResponse) {
  // CORS
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id')
  res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id')

  if (req.method === 'OPTIONS') {
    res.writeHead(204)
    res.end()
    return
  }

  // Only serve /mcp endpoint
  const parsedUrl = new URL(req.url ?? '/', 'http://localhost')
  if (parsedUrl.pathname !== '/mcp') {
    res.writeHead(404)
    res.end('Not Found — MCP endpoint is POST /mcp')
    return
  }

  // Extract ?session=xxx for browser routing (propagated via AsyncLocalStorage to sendToBrowser)
  const urlSession = parsedUrl.searchParams.get('session') ?? undefined

  // Extract ?toolsets=view,layer for filtering available tools in HTTP mode
  const urlToolsets = parsedUrl.searchParams.get('toolsets')?.trim()
  const filterToolsets = urlToolsets
    ? new Set(urlToolsets.split(',').map(s => s.trim()).filter(s => s in TOOLSETS))
    : undefined

  if (req.method === 'POST') {
    let body = ''
    req.on('data', (chunk: Buffer) => { body += chunk.toString() })
    req.on('end', async () => {
      const run = async () => {
        try {
          const parsedBody = JSON.parse(body)
          const mcpServer = _createHttpMcpServer(filterToolsets)
          const transport = new StreamableHTTPServerTransport({
            sessionIdGenerator: undefined, // stateless
          })
          res.on('close', () => { transport.close().catch(() => {}) })
          await mcpServer.connect(transport)
          await transport.handleRequest(req, res, parsedBody)
        } catch {
          if (!res.headersSent) {
            res.writeHead(400, { 'Content-Type': 'application/json' })
            res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null }))
          }
        }
      }
      if (urlSession) {
        await _httpSessionStore.run(urlSession, run)
      } else {
        await run()
      }
    })
    return
  }

  // GET and DELETE are not supported in stateless mode
  res.writeHead(405, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed in stateless mode' }, id: null }))
}

// ==================== Smithery Sandbox ====================

/**
 * Smithery 扫描时使用的无副作用服务器实例。
 * 返回带有相同工具/资源元数据的独立 McpServer，
 * 不启动 WebSocket，不连接 transport。
 */
export function createSandboxServer() {
  // Register all tools for sandbox scanning
  for (const setName of Object.keys(TOOLSETS)) {
    if (!_enabledSets.has(setName)) _enableToolset(setName)
  }
  return server
}

// ==================== 启动 ====================

export async function main(argv: string[] = []) {
  // Parse CLI arguments
  const transportArg = _parseArg(argv, '--transport') ?? process.env.MCP_TRANSPORT ?? 'stdio'
  const mcpPortArg = parseInt(_parseArg(argv, '--port') ?? process.env.MCP_HTTP_PORT ?? '0')

  await startServer()

  if (transportArg === 'http') {
    // Streamable HTTP transport mode
    const port = mcpPortArg || WS_PORT + 100 // default: WS_PORT + 100 (e.g. 9200)
    const mcpHttpServer = createServer(_handleMcpRequest)
    mcpHttpServer.listen(port, () => {
      const allToolCount = _toolDefs.size
      console.error(`[cesium-mcp-runtime] MCP Server running (Streamable HTTP), ${allToolCount} tools available`)
      console.error(`[cesium-mcp-runtime] MCP endpoint: http://localhost:${port}/mcp`)
      console.error('[cesium-mcp-runtime] All toolsets enabled for HTTP mode')
      if (_relayPort > 0) {
        console.error(`[cesium-mcp-runtime] Relay mode active → commands forwarded to port ${_relayPort}`)
      }
    })
    return
  }

  // Default: stdio transport
  const transport = new StdioServerTransport()
  await server.connect(transport)
  const metaCount = _allMode ? 0 : 2
  console.error(`[cesium-mcp-runtime] MCP Server running (stdio), ${_enabledTools.size + metaCount} tools registered (toolsets: ${[..._enabledSets].join(', ')})`)
  if (_relayPort > 0) {
    console.error(`[cesium-mcp-runtime] Relay mode active → commands forwarded to port ${_relayPort}`)
  }
}

/** Parse a CLI argument value: --key value or --key=value */
function _parseArg(argv: string[], key: string): string | undefined {
  for (let i = 0; i < argv.length; i++) {
    if (argv[i] === key && i + 1 < argv.length) return argv[i + 1]
    if (argv[i]?.startsWith(key + '=')) return argv[i]!.slice(key.length + 1)
  }
  return undefined
}
