wifi-densepose/tools/ruview-mcp/src/index.ts

309 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* @ruv/ruview-mcp — RuView MCP Server
*
* Exposes RuView's WiFi-DensePose sensing capabilities as Model Context Protocol
* (MCP) tools that Claude Code, Cursor, Codex, and other MCP-compatible agents
* can call directly.
*
* Tools exposed:
* ruview_csi_latest — pull the latest CSI window from the sensing-server
* ruview_pose_infer — single-shot 17-keypoint pose estimation
* ruview_count_infer — single-shot person count with confidence interval
* ruview_registry_list — list cogs from the Cognitum edge registry (ADR-102)
* ruview_train_count — kick off a count-cog training run (returns job ID)
* ruview_job_status — poll a background training job
*
* Usage:
* node dist/index.js # stdio transport (default)
* RUVIEW_SENSING_SERVER_URL=http://cognitum-v0:3000 node dist/index.js
*
* To register with Claude Code:
* claude mcp add ruview -- node /path/to/tools/ruview-mcp/dist/index.js
*
* See ADR-104 for the full design rationale and security model.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { loadConfig } from "./config.js";
import { csiLatestSchema, csiLatest } from "./tools/csi-latest.js";
import { poseInferSchema, poseInfer } from "./tools/pose-infer.js";
import { countInferSchema, countInfer } from "./tools/count-infer.js";
import { registryListSchema, registryList } from "./tools/registry-list.js";
import {
trainCountSchema,
trainCount,
jobStatusSchema,
jobStatus,
} from "./tools/train-count.js";
const PACKAGE_VERSION = "0.0.1";
const SERVER_NAME = "ruview";
// ── Tool registry ──────────────────────────────────────────────────────────
const TOOLS = [
{
name: "ruview_csi_latest",
description:
"Pull the latest CSI window from a running wifi-densepose-sensing-server. " +
"Returns 56-subcarrier × 20-frame amplitude/phase arrays suitable for " +
"downstream inference or research analysis.",
inputSchema: {
type: "object" as const,
properties: {
sensing_server_url: {
type: "string",
description:
"Base URL of the sensing-server (default: RUVIEW_SENSING_SERVER_URL or http://localhost:3000).",
},
},
},
handler: async (args: unknown, config: ReturnType<typeof loadConfig>) => {
const input = csiLatestSchema.parse(args);
return csiLatest(input, config);
},
},
{
name: "ruview_pose_infer",
description:
"Run a single-shot 17-keypoint COCO pose estimation inference using the " +
"cog-pose-estimation Cog binary (ADR-101). Accepts a CSI window JSON file " +
"or uses the live sensing-server if no window is provided. " +
"Returns [{keypoints: [[x,y]×17], confidence}] per detected person.",
inputSchema: {
type: "object" as const,
properties: {
window_path: {
type: "string",
description: "Path to a CSI window JSON file. Omit to use the live sensing-server.",
},
cog_binary: {
type: "string",
description: "Path to cog-pose-estimation binary.",
},
},
},
handler: async (args: unknown, config: ReturnType<typeof loadConfig>) => {
const input = poseInferSchema.parse(args);
return poseInfer(input, config);
},
},
{
name: "ruview_count_infer",
description:
"Run a single-shot person-count inference using the cog-person-count Cog " +
"binary (ADR-103). Returns {count, confidence, count_p95_low, count_p95_high} " +
"with a Stoer-Wagner multi-node fusion upper bound when multiple nodes are active.",
inputSchema: {
type: "object" as const,
properties: {
window_path: {
type: "string",
description: "Path to a CSI window JSON file. Omit to use the live sensing-server.",
},
cog_binary: {
type: "string",
description: "Path to cog-person-count binary.",
},
max_persons: {
type: "integer",
minimum: 1,
maximum: 7,
description: "Upper bound on person count (17). Default: 7.",
},
},
},
handler: async (args: unknown, config: ReturnType<typeof loadConfig>) => {
const input = countInferSchema.parse(args);
return countInfer(input, config);
},
},
{
name: "ruview_registry_list",
description:
"List cogs from the Cognitum edge module registry (ADR-102). " +
"Fetches /api/v1/edge/registry from the sensing-server, which proxies the " +
"canonical GCS catalog (105 cogs, 11 categories). Supports category filter and search.",
inputSchema: {
type: "object" as const,
properties: {
category: {
type: "string",
description:
"Filter by category: health, security, building, retail, industrial, " +
"research, ai, swarm, signal, network, developer.",
},
search: {
type: "string",
description: "Search substring matched against cog id and name (case-insensitive).",
},
refresh: {
type: "boolean",
description: "Bypass the 1-hour registry cache.",
},
sensing_server_url: {
type: "string",
description: "Override the sensing-server URL.",
},
},
},
handler: async (args: unknown, config: ReturnType<typeof loadConfig>) => {
const input = registryListSchema.parse(args);
return registryList(input, config);
},
},
{
name: "ruview_train_count",
description:
"Kick off a cog-person-count training run using the Candle GPU trainer " +
"(ADR-103). The paired JSONL file provides CSI windows + camera-derived " +
"person-count labels. Returns a job_id to poll with ruview_job_status.",
inputSchema: {
type: "object" as const,
required: ["paired_jsonl"],
properties: {
paired_jsonl: {
type: "string",
description:
"Path to the paired JSONL training file (produced by scripts/align-ground-truth.js).",
},
epochs: {
type: "integer",
minimum: 1,
maximum: 10000,
description: "Training epochs (default: 400).",
},
learning_rate: {
type: "number",
description: "Initial learning rate (default: 0.001).",
},
output_dir: {
type: "string",
description:
"Directory for model artifacts (default: v2/crates/cog-person-count/cog/artifacts/).",
},
},
},
handler: async (args: unknown, config: ReturnType<typeof loadConfig>) => {
const input = trainCountSchema.parse(args);
return trainCount(input, config);
},
},
{
name: "ruview_job_status",
description:
"Poll the status of a background training job started by ruview_train_count. " +
"Returns {status, epochs_done, epochs_total, recent_log} for the given job_id.",
inputSchema: {
type: "object" as const,
required: ["job_id"],
properties: {
job_id: {
type: "string",
description: "UUID returned by ruview_train_count.",
},
},
},
handler: async (args: unknown, config: ReturnType<typeof loadConfig>) => {
const input = jobStatusSchema.parse(args);
return jobStatus(input, config);
},
},
] as const;
// ── Server bootstrap ────────────────────────────────────────────────────────
async function main(): Promise<void> {
const config = loadConfig();
const server = new Server(
{
name: SERVER_NAME,
version: PACKAGE_VERSION,
},
{
capabilities: {
tools: {},
},
}
);
// List tools handler.
server.setRequestHandler(ListToolsRequestSchema, () => ({
tools: TOOLS.map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
})),
}));
// Call tool handler.
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = TOOLS.find((t) => t.name === name);
if (!tool) {
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
ok: false,
error: `Unknown tool "${name}". Available tools: ${TOOLS.map((t) => t.name).join(", ")}`,
}),
},
],
isError: true,
};
}
try {
const result = await tool.handler(args ?? {}, config);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
},
],
};
} catch (e: unknown) {
const message = e instanceof Error ? e.message : String(e);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
ok: false,
error: message,
}),
},
],
isError: true,
};
}
});
// Wire up stdio transport.
const transport = new StdioServerTransport();
await server.connect(transport);
// Log to stderr so it doesn't interfere with the MCP stdio protocol.
process.stderr.write(
`[ruview-mcp] Server v${PACKAGE_VERSION} started. ` +
`Sensing server: ${config.sensingServerUrl}\n`
);
}
main().catch((e) => {
process.stderr.write(`[ruview-mcp] Fatal: ${String(e)}\n`);
process.exit(1);
});