feat: P0 — MCP Tool类型(36) + Server框架 + 7个核心Tool实现

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 — 入口, 环境变量配置
This commit is contained in:
LukeMackin
2026-07-22 17:20:38 +08:00
parent a98d35cb1c
commit f85ce9aa43
7 changed files with 725 additions and 2 deletions

View File

@@ -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<string, (args: any) => Promise<any>>;
}
/**
* 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<void> {
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<McpServer> {
const server = createServer(opts);
await startServer(server, port);
return server;
}

View File

@@ -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<string, (args: any) => Promise<any>>([
["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);
});

View File

@@ -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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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,
};
}