From f85ce9aa43e4a0fb0f207f751959aba4e6decc9c Mon Sep 17 00:00:00 2001 From: LukeMackin Date: Wed, 22 Jul 2026 17:20:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20P0=20=E2=80=94=20MCP=20Tool=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B(36)=20+=20Server=E6=A1=86=E6=9E=B6=20+=207=E4=B8=AA?= =?UTF-8?q?=E6=A0=B8=E5=BF=83Tool=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S-001: packages/shared/src/mcp/tools.ts — 36 Tool接口定义(10模块) S-002: packages/shared/src/mcp/config.ts — DeviceConfig/Gateway类型 C-001: packages/server/src/index.ts — MCP Server框架+SSE传输 C-002~011: packages/server/src/tools.ts — 7个P0核心Tool - file_list, file_read, file_write, file_move - exec, process_list, sysinfo packages/server/src/main.ts — 入口, 环境变量配置 --- packages/server/src/index.ts | 84 +++++++ packages/server/src/main.ts | 66 ++++++ packages/server/src/tools.ts | 148 ++++++++++++ packages/shared/src/index.ts | 5 +- packages/shared/src/mcp/config.ts | 47 ++++ packages/shared/src/mcp/index.ts | 2 + packages/shared/src/mcp/tools.ts | 375 ++++++++++++++++++++++++++++++ 7 files changed, 725 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/index.ts create mode 100644 packages/server/src/main.ts create mode 100644 packages/server/src/tools.ts create mode 100644 packages/shared/src/mcp/config.ts create mode 100644 packages/shared/src/mcp/index.ts create mode 100644 packages/shared/src/mcp/tools.ts diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000..0c03301 --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,84 @@ +/** + * C-001: MCP Server Framework + * + * Bootstrap the MCP server with SSE transport using @modelcontextprotocol/sdk. + * Registers all 36 tools from @yuzu-gca/shared and exposes lifecycle hooks. + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { ALL_TOOL_DEFS, type ToolDef, type DeviceConfig } from "@yuzu-gca/shared"; + +export interface ServerOptions { + config: DeviceConfig; + toolHandlers: Map Promise>; +} + +/** + * Creates and returns an MCP Server instance with all tools registered. + * Call `server.connect(transport)` to start handling requests. + */ +export function createServer(opts: ServerOptions): McpServer { + const server = new McpServer({ + name: "yuzu-gca-device", + version: opts.config.identity.id, + }); + + // Register every tool from the shared type definitions + for (const toolDef of ALL_TOOL_DEFS) { + const handler = opts.toolHandlers.get(toolDef.name); + if (handler) { + server.tool( + toolDef.name, + toolDef.description, + toolDef.inputSchema.properties, + async (args: any) => { + const result = await handler(args); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + }, + ); + } else { + // Register placeholder for unimplemented tools + server.tool( + toolDef.name, + toolDef.description, + toolDef.inputSchema.properties, + async () => ({ + content: [{ type: "text", text: JSON.stringify({ error: "Tool not yet implemented", tool: toolDef.name }) }], + }), + ); + } + } + + return server; +} + +/** + * Start the server with SSE transport on a given port. + */ +export async function startServer( + server: McpServer, + port: number = 3001, +): Promise { + const transport = new SSEServerTransport({ + port, + endpoint: "/sse", + }); + + await server.connect(transport); + console.log(`[Yuzu-GCA] MCP Server listening on :${port}/sse`); +} + +/** + * Convenience — create + start in one call. + */ +export async function bootstrap( + opts: ServerOptions, + port?: number, +): Promise { + const server = createServer(opts); + await startServer(server, port); + return server; +} diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts new file mode 100644 index 0000000..0428db2 --- /dev/null +++ b/packages/server/src/main.ts @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/** + * Yuzu-GCA MCP Server Entry Point + * + * Usage: node dist/main.js + * Or: npx tsx src/main.ts + */ + +import { bootstrap, type ServerOptions } from "./index.js"; +import * as tools from "./tools.js"; +import type { DeviceConfig, GatewayConfig } from "@yuzu-gca/shared"; +import * as os from "node:os"; +import { randomUUID } from "node:crypto"; + +// Build device config from environment +const config: DeviceConfig = { + identity: { + id: process.env.GCA_DEVICE_ID ?? randomUUID(), + name: process.env.GCA_DEVICE_NAME ?? os.hostname(), + platform: os.platform(), + arch: os.arch(), + hostname: os.hostname(), + osVersion: os.release(), + }, + gateway: { + url: process.env.GCA_GATEWAY_URL ?? "http://localhost:3000", + transport: (process.env.GCA_TRANSPORT as any) ?? "sse", + reconnectDelay: 1000, + maxReconnectDelay: 30000, + heartbeatInterval: 15000, + }, + capabilities: [ + "device_info", "device_status", + "exec", "process_list", + "file_list", "file_read", "file_write", "file_move", + "sysinfo", + ], + logLevel: (process.env.GCA_LOG_LEVEL as any) ?? "info", +}; + +// Build tool handlers map +const toolHandlers = new Map Promise>([ + ["file_list", tools.file_list], + ["file_read", tools.file_read], + ["file_write", tools.file_write], + ["file_move", tools.file_move], + ["exec", tools.exec], + ["process_list",tools.process_list], + ["sysinfo", tools.sysinfo], +]); + +const opts: ServerOptions = { config, toolHandlers }; +const port = Number(process.env.GCA_PORT ?? 3001); + +console.log(`[Yuzu-GCA] Starting MCP Server...`); +console.log(` Device: ${config.identity.name} (${config.identity.platform}/${config.identity.arch})`); +console.log(` Gateway: ${config.gateway.url}`); +console.log(` Tools: ${toolHandlers.size} implemented`); +console.log(` Port: ${port}`); + +bootstrap(opts, port).then(() => { + console.log(`[Yuzu-GCA] Ready.`); +}).catch(err => { + console.error(`[Yuzu-GCA] Fatal:`, err); + process.exit(1); +}); diff --git a/packages/server/src/tools.ts b/packages/server/src/tools.ts new file mode 100644 index 0000000..ee7dbdd --- /dev/null +++ b/packages/server/src/tools.ts @@ -0,0 +1,148 @@ +/** + * P0 Core Tool Implementations + * + * file_list, file_read, file_write, file_move, exec, process_list, sysinfo. + * These map to C-002 through C-011 in the backlog. + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import { execSync, spawn } from "node:child_process"; +import * as os from "node:os"; + +// ── C-002: file_list ──────────────────────────────────────── +export async function file_list(args: { + path: string; + recursive?: boolean; +}): Promise { + const dir = args.path || "."; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const result = entries.map(e => ({ + name: e.name, + type: e.isDirectory() ? "dir" : e.isFile() ? "file" : "other", + size: e.isFile() ? fs.statSync(path.join(dir, e.name)).size : undefined, + })); + return { path: dir, entries: result, count: result.length }; +} + +// ── C-003: file_read ──────────────────────────────────────── +export async function file_read(args: { + path: string; + encoding?: string; + offset?: number; + limit?: number; +}): Promise { + const encoding = (args.encoding === "base64" ? "base64" : "utf8") as BufferEncoding; + let content = fs.readFileSync(args.path, encoding); + if (encoding === "utf8") { + const lines = (content as string).split("\n"); + const start = args.offset ?? 0; + const end = args.limit ? start + args.limit : lines.length; + content = lines.slice(start, end).join("\n"); + } + return { path: args.path, content, size: fs.statSync(args.path).size }; +} + +// ── C-004: file_write ─────────────────────────────────────── +export async function file_write(args: { + path: string; + content: string; + encoding?: string; +}): Promise { + const encoding = args.encoding === "base64" ? "base64" : "utf8"; + fs.mkdirSync(path.dirname(args.path), { recursive: true }); + fs.writeFileSync(args.path, args.content, encoding as BufferEncoding); + return { ok: true, path: args.path, size: fs.statSync(args.path).size }; +} + +// ── C-005: file_move ──────────────────────────────────────── +export async function file_move(args: { + from: string; + to: string; +}): Promise { + fs.mkdirSync(path.dirname(args.to), { recursive: true }); + fs.renameSync(args.from, args.to); + return { ok: true, from: args.from, to: args.to }; +} + +// ── C-008: exec ───────────────────────────────────────────── +export async function exec(args: { + command: string; + cwd?: string; + timeout?: number; +}): Promise { + const timeout = args.timeout ?? 30000; + try { + const stdout = execSync(args.command, { + cwd: args.cwd ?? process.cwd(), + timeout, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, // 10MB + stdio: ["pipe", "pipe", "pipe"], + }); + return { ok: true, stdout: stdout.toString(), stderr: "" }; + } catch (err: any) { + return { + ok: false, + stdout: err.stdout?.toString() ?? "", + stderr: err.stderr?.toString() ?? "", + exitCode: err.status ?? -1, + signal: err.signal, + }; + } +} + +// ── C-010: process_list ───────────────────────────────────── +export async function process_list(args: { + filter?: string; +}): Promise { + let stdout: string; + try { + if (process.platform === "win32") { + stdout = execSync("tasklist /FO CSV /NH", { encoding: "utf8" }).toString(); + } else { + stdout = execSync("ps aux --no-headers", { encoding: "utf8" }).toString(); + } + } catch { + return { error: "Failed to list processes" }; + } + + const processes = stdout.trim().split("\n").map(line => { + if (process.platform === "win32") { + const [name, pid, , mem] = line.replace(/"/g, "").split(","); + return { name: name?.trim(), pid: Number(pid), mem: mem?.trim() }; + } + const parts = line.trim().split(/\s+/); + return { user: parts[0], pid: Number(parts[1]), cpu: parts[2], mem: parts[3], name: parts[10] }; + }); + + const filtered = args.filter + ? processes.filter(p => p.name?.toLowerCase().includes(args.filter!.toLowerCase())) + : processes.slice(0, 50); + + return { count: filtered.length, processes: filtered }; +} + +// ── C-011: sysinfo ────────────────────────────────────────── +export async function sysinfo(_args: {}): Promise { + const cpuUsage = os.loadavg(); + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const uptime = os.uptime(); + + return { + hostname: os.hostname(), + platform: os.platform(), + arch: os.arch(), + cpus: os.cpus().length, + loadavg: cpuUsage, + memory: { + total: totalMem, + free: freeMem, + used: totalMem - freeMem, + percent: Math.round(((totalMem - freeMem) / totalMem) * 100), + }, + uptime, + node: process.version, + }; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 00434e8..7a3573c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,2 +1,3 @@ -export { parseVersion, compareVersion, checkUpdate, semverGt } from './updater/index'; -export type { GcaVersion, Manifest, ManifestBuild } from './updater/index'; +export { parseVersion, compareVersion, checkUpdate, semverGt } from './updater/index.js'; +export type { GcaVersion, Manifest, ManifestBuild } from './updater/index.js'; +export * from './mcp/index.js'; diff --git a/packages/shared/src/mcp/config.ts b/packages/shared/src/mcp/config.ts new file mode 100644 index 0000000..c31b2bb --- /dev/null +++ b/packages/shared/src/mcp/config.ts @@ -0,0 +1,47 @@ +/** + * S-002: Device Configuration Types + */ + +/** Connection mode for the Gateway */ +export type TransportMode = "sse" | "websocket" | "stdio"; + +export interface GatewayConfig { + url: string; + token?: string; + transport: TransportMode; + reconnectDelay: number; // ms, default 1000 + maxReconnectDelay: number; // ms, default 30000 + heartbeatInterval: number; // ms, default 15000 +} + +export interface DeviceIdentity { + id: string; // unique device ID (UUID) + name: string; // human-readable name + platform: NodeJS.Platform; + arch: string; + hostname: string; + osVersion: string; +} + +export interface ProxyConfig { + enabled: boolean; + host: string; + port: number; + auth?: { username: string; password: string }; +} + +export interface DeviceConfig { + identity: DeviceIdentity; + gateway: GatewayConfig; + proxy?: ProxyConfig; + capabilities: string[]; // enabled MCP tool names + logLevel: "debug" | "info" | "warn" | "error"; +} + +export const DEFAULT_GATEWAY_CONFIG: GatewayConfig = { + url: "http://localhost:3000", + transport: "sse", + reconnectDelay: 1000, + maxReconnectDelay: 30000, + heartbeatInterval: 15000, +}; diff --git a/packages/shared/src/mcp/index.ts b/packages/shared/src/mcp/index.ts new file mode 100644 index 0000000..edecf95 --- /dev/null +++ b/packages/shared/src/mcp/index.ts @@ -0,0 +1,2 @@ +export * from "./tools.js"; +export * from "./config.js"; diff --git a/packages/shared/src/mcp/tools.ts b/packages/shared/src/mcp/tools.ts new file mode 100644 index 0000000..4ec8dbd --- /dev/null +++ b/packages/shared/src/mcp/tools.ts @@ -0,0 +1,375 @@ +/** + * S-001: MCP Tool Types — 36 Tool definitions for Yuzu-GCA + * + * Each Tool follows the @modelcontextprotocol/sdk shape: + * { name, description, inputSchema } + * + * Organized by module per docs/backlog: + * Module 1: Device Management (5 tools) + * Module 2: Command Execution (4 tools) + * Module 3: File Operations (6 tools) + * Module 4: System Control (4 tools) + * Module 5: Remote Desktop (4 tools) + * Module 6: AI Manipulation (4 tools) + * Module 7: Network (3 tools) + * Module 8: Hardware (3 tools) + * Module 9: Developer Tools (2 tools) + * Module 10: Media (1 tool) + */ + +import type { z } from "zod"; + +// ── JSON Schema primitive aliases ────────────────────────── +const str = (desc: string) => ({ type: "string" as const, description: desc }); +const num = (desc: string) => ({ type: "number" as const, description: desc }); +const bool = (desc: string) => ({ type: "boolean" as const, description: desc }); +const arr = (items: any, desc: string) => ({ type: "array" as const, items, description: desc }); +const obj = (props: Record, required: string[], desc: string) => ({ + type: "object" as const, properties: props, required, description: desc, +}); + +// ── Tool definition type ─────────────────────────────────── +export interface ToolDef { + name: string; + description: string; + inputSchema: { + type: "object"; + properties: Record; + required?: string[]; + }; +} + +export interface ToolCategory { + module: number; + name: string; + tools: ToolDef[]; +} + +// ── Module 1: Device Management (5 tools) ────────────────── +export const DEVICE_TOOLS: ToolDef[] = [ + { + name: "device_info", + description: "Get device hardware/OS information (CPU, RAM, hostname, platform, arch)", + inputSchema: obj({}, [], "No input required"), + }, + { + name: "device_list", + description: "List all connected devices known to the Gateway", + inputSchema: obj({}, [], "No input required"), + }, + { + name: "device_status", + description: "Get current connection status and health of a specific device", + inputSchema: obj({ deviceId: str("Target device ID (default: self)") }, [], ""), + }, + { + name: "device_register", + description: "Register this device with the Gateway with capabilities and metadata", + inputSchema: obj({ + name: str("Human-readable device name"), + capabilities: arr(str("capability"), "List of capability strings"), + }, ["name"], ""), + }, + { + name: "device_unregister", + description: "Unregister from Gateway (graceful disconnect)", + inputSchema: obj({}, [], "No input required"), + }, +]; + +// ── Module 2: Command Execution (4 tools) ────────────────── +export const EXEC_TOOLS: ToolDef[] = [ + { + name: "exec", + description: "Execute a shell command and return stdout/stderr. 30s timeout.", + inputSchema: obj({ + command: str("Shell command to execute"), + cwd: str("Working directory (optional)"), + timeout: num("Timeout in ms (default 30000)"), + }, ["command"], ""), + }, + { + name: "exec_background", + description: "Start a long-running command and return a process ID for later control", + inputSchema: obj({ + command: str("Shell command to execute"), + cwd: str("Working directory (optional)"), + }, ["command"], ""), + }, + { + name: "process_list", + description: "List running processes with PID, name, CPU%, memory", + inputSchema: obj({ + filter: str("Optional name filter (substring match)"), + }, [], ""), + }, + { + name: "process_kill", + description: "Kill a process by PID", + inputSchema: obj({ + pid: num("Process ID to kill"), + signal: str("Signal (default: SIGTERM)"), + }, ["pid"], ""), + }, +]; + +// ── Module 3: File Operations (6 tools) ──────────────────── +export const FILE_TOOLS: ToolDef[] = [ + { + name: "file_list", + description: "List files and directories at a given path", + inputSchema: obj({ + path: str("Directory path to list"), + recursive: bool("Recursive listing (default false)"), + }, ["path"], ""), + }, + { + name: "file_read", + description: "Read contents of a file (text or binary as base64)", + inputSchema: obj({ + path: str("File path to read"), + encoding: str("utf8 | base64 (default utf8)"), + offset: num("Line offset (0-based, optional)"), + limit: num("Max lines to return (optional)"), + }, ["path"], ""), + }, + { + name: "file_write", + description: "Write content to a file (create or overwrite)", + inputSchema: obj({ + path: str("File path to write"), + content: str("Content to write (text or base64)"), + encoding: str("utf8 | base64 (default utf8)"), + }, ["path", "content"], ""), + }, + { + name: "file_move", + description: "Move or rename a file/directory", + inputSchema: obj({ + from: str("Source path"), + to: str("Destination path"), + }, ["from", "to"], ""), + }, + { + name: "file_delete", + description: "Delete a file or directory (recursive for dirs)", + inputSchema: obj({ + path: str("Path to delete"), + recursive: bool("Recursive delete for directories (default false)"), + }, ["path"], ""), + }, + { + name: "file_stat", + description: "Get file metadata (size, permissions, mtime)", + inputSchema: obj({ + path: str("Path to stat"), + }, ["path"], ""), + }, +]; + +// ── Module 4: System Control (4 tools) ───────────────────── +export const SYSTEM_TOOLS: ToolDef[] = [ + { + name: "sysinfo", + description: "Get system resource usage (CPU%, RAM, disk, uptime, network IO)", + inputSchema: obj({}, [], "No input required"), + }, + { + name: "power", + description: "Power management: shutdown, reboot, sleep, lock", + inputSchema: obj({ + action: str("shutdown | reboot | sleep | lock"), + }, ["action"], ""), + }, + { + name: "service", + description: "Manage system services/daemons (start/stop/restart/status)", + inputSchema: obj({ + name: str("Service name"), + action: str("start | stop | restart | status | enable | disable"), + }, ["name", "action"], ""), + }, + { + name: "notify_send", + description: "Send a desktop notification to the device", + inputSchema: obj({ + title: str("Notification title"), + message: str("Notification body"), + urgency: str("low | normal | critical (default normal)"), + }, ["title", "message"], ""), + }, +]; + +// ── Module 5: Remote Desktop (4 tools) ───────────────────── +export const REMOTE_TOOLS: ToolDef[] = [ + { + name: "screenshot", + description: "Capture a screenshot of the device display", + inputSchema: obj({ + display: num("Display index (default 0)"), + format: str("png | jpg (default png)"), + quality: num("JPEG quality 1-100 (default 80)"), + }, [], ""), + }, + { + name: "remote_input", + description: "Inject keyboard/mouse input events on the remote device", + inputSchema: obj({ + type: str("key | mouse | text"), + action: str("key: press/release; mouse: move/click/scroll; text: input string"), + value: str("Key name, coordinates, or text content"), + }, ["type", "action", "value"], ""), + }, + { + name: "clipboard_sync", + description: "Read or write the device clipboard", + inputSchema: obj({ + action: str("read | write"), + content: str("Content to write (required for write action)"), + }, ["action"], ""), + }, + { + name: "remote_stream", + description: "Start/stop a low-latency screen streaming session", + inputSchema: obj({ + action: str("start | stop"), + fps: num("Frames per second (default 15)"), + quality: num("Quality 1-100 (default 60)"), + }, ["action"], ""), + }, +]; + +// ── Module 6: AI Manipulation (4 tools) ──────────────────── +export const AI_TOOLS: ToolDef[] = [ + { + name: "ui_find", + description: "Find UI elements by description (uses OCR/accessibility)", + inputSchema: obj({ + query: str("Text or description to search for on screen"), + confidence: num("Minimum confidence 0-1 (default 0.7)"), + }, ["query"], ""), + }, + { + name: "ui_act", + description: "Click, type, or scroll at a UI element found by ui_find", + inputSchema: obj({ + element: str("Element descriptor from ui_find result"), + action: str("click | dblclick | type | scroll | hover"), + value: str("Text to type or scroll direction"), + }, ["element", "action"], ""), + }, + { + name: "browser_open", + description: "Open a URL in the default browser and optionally extract content", + inputSchema: obj({ + url: str("URL to open"), + extract: bool("Extract page text content after load (default false)"), + }, ["url"], ""), + }, + { + name: "ocr_screen", + description: "Run OCR on a screenshot region and return text results", + inputSchema: obj({ + x: num("Region left (0-1 fraction of width, default 0)"), + y: num("Region top (default 0)"), + w: num("Region width (default 1)"), + h: num("Region height (default 1)"), + lang: str("Language code (default: eng)"), + }, [], ""), + }, +]; + +// ── Module 7: Network (3 tools) ───────────────────────────── +export const NETWORK_TOOLS: ToolDef[] = [ + { + name: "net_info", + description: "Get network interfaces, IPs, MAC addresses", + inputSchema: obj({}, [], "No input required"), + }, + { + name: "net_ping", + description: "Ping a host and return latency/packet loss", + inputSchema: obj({ + host: str("Hostname or IP to ping"), + count: num("Number of pings (default 4)"), + }, ["host"], ""), + }, + { + name: "net_speedtest", + description: "Run a basic network speed test (download/upload)", + inputSchema: obj({}, [], "No input required"), + }, +]; + +// ── Module 8: Hardware (3 tools) ──────────────────────────── +export const HARDWARE_TOOLS: ToolDef[] = [ + { + name: "hw_temperature", + description: "Read CPU/GPU temperature sensors", + inputSchema: obj({}, [], "No input required"), + }, + { + name: "hw_battery", + description: "Get battery status (level, charging, health)", + inputSchema: obj({}, [], "No input required"), + }, + { + name: "hw_sensors", + description: "Read hardware sensors (fan speed, voltage, etc.)", + inputSchema: obj({ + type: str("Filter by sensor type (optional)"), + }, [], ""), + }, +]; + +// ── Module 9: Developer Tools (2 tools) ───────────────────── +export const DEV_TOOLS: ToolDef[] = [ + { + name: "dev_shell", + description: "Open an interactive shell session (persistent, stateful)", + inputSchema: obj({ + command: str("Shell command (empty to start session)"), + }, ["command"], ""), + }, + { + name: "dev_eval", + description: "Execute JavaScript/Python code snippet on the device and return result", + inputSchema: obj({ + code: str("Code to execute"), + lang: str("js | python (default js)"), + }, ["code"], ""), + }, +]; + +// ── Module 10: Media (1 tool) ────────────────────────────── +export const MEDIA_TOOLS: ToolDef[] = [ + { + name: "media_control", + description: "Control media playback (play/pause/next/prev/volume)", + inputSchema: obj({ + action: str("play | pause | next | prev | volume_up | volume_down | mute"), + }, ["action"], ""), + }, +]; + +// ── Aggregate ─────────────────────────────────────────────── +export const ALL_TOOLS: ToolCategory[] = [ + { module: 1, name: "Device Management", tools: DEVICE_TOOLS }, + { module: 2, name: "Command Execution", tools: EXEC_TOOLS }, + { module: 3, name: "File Operations", tools: FILE_TOOLS }, + { module: 4, name: "System Control", tools: SYSTEM_TOOLS }, + { module: 5, name: "Remote Desktop", tools: REMOTE_TOOLS }, + { module: 6, name: "AI Manipulation", tools: AI_TOOLS }, + { module: 7, name: "Network", tools: NETWORK_TOOLS }, + { module: 8, name: "Hardware", tools: HARDWARE_TOOLS }, + { module: 9, name: "Developer Tools", tools: DEV_TOOLS }, + { module: 10, name: "Media", tools: MEDIA_TOOLS }, +]; + +export const ALL_TOOL_DEFS: ToolDef[] = ALL_TOOLS.flatMap(c => c.tools); +export const TOOL_COUNT = ALL_TOOL_DEFS.length; // 36 + +/** Look up a tool by name */ +export function getTool(name: string): ToolDef | undefined { + return ALL_TOOL_DEFS.find(t => t.name === name); +}