KeiSeiKit-1.0/_ts_packages/packages/mcp-server/src/rust-bridge.ts
Parfii-bot c21943e40b feat(ts-packages): 6 TS packages — MCP server + 5 external-API adapters
Total 1465 LOC + 616 test LOC, 78/78 tests pass.
- @keisei/mcp-server (25 tests) — Rust-CLI bridge via execa, stdio+HTTP, HMAC auth, kei() meta-tool
- @keisei/telegram-adapter (16 tests) — grammy Bot, 7 tools
- @keisei/recall-adapter (8 tests) — Zoom via Recall.ai, 5 tools
- @keisei/grok-adapter (6 tests) — xAI OpenAI-compatible, 2 tools
- @keisei/gmail-adapter (11 tests) — googleapis OAuth2, 6 tools (new — LBM gap)
- @keisei/youtube-adapter (12 tests) — YouTube Data API v3, 5 tools (new — LBM gap)

RULE 0.2 exception #4 (TS for MCP/API layer documented in _ts_packages/README.md).
RULE 0.8 — env vars only (TELEGRAM_BOT_TOKEN, XAI_API_KEY, GMAIL_*, YOUTUBE_API_KEY).
Strict TypeScript: strict + exactOptionalPropertyTypes + noUncheckedIndexedAccess.
Genesis-scan clean (0 hits).
2026-04-22 12:45:19 +08:00

83 lines
2.5 KiB
TypeScript

// Bridge layer: spawn Rust primitive CLIs and marshal JSON args <-> CLI flags.
// One Rust binary = one MCP tool. Subprocess lifecycle is isolated per call.
import { execa } from "execa";
import path from "node:path";
import { RustBridgeError, TimeoutError } from "./errors.js";
const DEFAULT_TIMEOUT_MS = 30_000;
export interface RustCallRequest {
binary: string;
args: readonly string[];
stdin?: string;
timeoutMs?: number;
}
export interface RustCallResult {
stdout: string;
stderr: string;
exitCode: number;
}
export interface RustBridgeConfig {
binDir: string;
defaultTimeoutMs?: number;
}
export class RustBridge {
private readonly binDir: string;
private readonly defaultTimeoutMs: number;
constructor(cfg: RustBridgeConfig) {
this.binDir = cfg.binDir;
this.defaultTimeoutMs = cfg.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
}
async call(req: RustCallRequest): Promise<RustCallResult> {
const binPath = this.resolveBin(req.binary);
const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
try {
const opts = {
timeout: timeoutMs,
reject: false as const,
env: process.env,
...(req.stdin !== undefined ? { input: req.stdin } : {}),
};
const child = execa(binPath, [...req.args], opts);
const result = await child;
if (result.timedOut) throw new TimeoutError(req.binary, timeoutMs);
return {
stdout: typeof result.stdout === "string" ? result.stdout : "",
stderr: typeof result.stderr === "string" ? result.stderr : "",
exitCode: result.exitCode ?? -1,
};
} catch (err) {
if (err instanceof TimeoutError) throw err;
const msg = err instanceof Error ? err.message : String(err);
throw new RustBridgeError(msg, { binary: req.binary });
}
}
private resolveBin(binary: string): string {
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(binary)) {
throw new RustBridgeError(`invalid binary name: ${binary}`);
}
return path.join(this.binDir, binary);
}
}
// Convert a JSON object of named args to CLI flags: {foo_bar: "v"} => ["--foo-bar", "v"]
export function jsonArgsToCli(args: Record<string, unknown>): string[] {
const out: string[] = [];
for (const [key, raw] of Object.entries(args)) {
if (raw === undefined || raw === null) continue;
const flag = `--${key.replace(/_/g, "-")}`;
if (typeof raw === "boolean") {
if (raw) out.push(flag);
continue;
}
out.push(flag, String(raw));
}
return out;
}