Add headless puppet bridge: control socket, renderer handlers, MCP daemon

This commit is contained in:
agent
2026-09-11 12:59:29 +02:00
parent 24447f5d93
commit 7e4fb7aa34
18 changed files with 2180 additions and 38 deletions
+143
View File
@@ -0,0 +1,143 @@
/*
* Veskslop Phase 0 spike — MCP control bridge.
*
* Exposes MCP over Streamable HTTP and forwards tool calls to the Vesktop
* control socket, where the real client performs them. Every command is paced
* by the shared queue (300-800 ms), reads are coalesced, and state reads are
* cache-only.
*
* Usage:
* pnpm control:bridge
* VESKSLOP_SOCKET=/tmp/veskslop-mock.sock MCP_PORT=8788 pnpm control:bridge
*/
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import * as z from "zod/v4";
import { dedupe, getQueryCount, paced } from "./pacing.js";
import { sendVesktopCommand, socketPath } from "./vesktop-socket.js";
const HOST = process.env.MCP_HOST ?? "127.0.0.1";
const PORT = Number(process.env.MCP_PORT ?? 8788);
function jsonText(value: unknown) {
return { content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }] };
}
function command<T>(name: string, data?: unknown): Promise<T> {
return paced(() => sendVesktopCommand<T>(name, data));
}
function buildServer(): McpServer {
const server = new McpServer({ name: "veskslop-control", version: "0.0.0" });
server.registerTool(
"get_state",
{
description: "Get the logged-in user, guilds and channels. Cache-only, no Discord queries."
},
async () => jsonText(await command("state:get"))
);
server.registerTool(
"send_message",
{
description: "Send a message as the logged-in user through the real client. Query-paced.",
inputSchema: {
channelId: z.string().describe("Target channel ID"),
content: z.string().min(1).describe("Message content")
}
},
async ({ channelId, content }) => {
const result = await command<Record<string, unknown>>("message:send", { channelId, content });
return jsonText({ ...result, queriesUsed: getQueryCount() });
}
);
server.registerTool(
"read_messages",
{
description: "Read messages from the client's cache. Identical concurrent reads are coalesced.",
inputSchema: {
channelId: z.string().describe("Target channel ID"),
limit: z.number().int().min(1).max(100).default(20).describe("How many messages")
}
},
async ({ channelId, limit }) => {
const result = await dedupe(`read:${channelId}:${limit}`, () =>
command<{ messages: unknown[]; cached: number; channelId: string }>("message:read", { channelId, limit })
);
return jsonText({ ...result, queriesUsed: getQueryCount() });
}
);
server.registerTool(
"send_dm",
{
description: "Send a DM as the logged-in user. Sanctioned live-test target: user zzib.",
inputSchema: {
userId: z.string().describe("Recipient user ID"),
content: z.string().min(1).describe("Message content")
}
},
async ({ userId, content }) => {
const result = await command<Record<string, unknown>>("dm:send", { userId, content });
return jsonText({ ...result, queriesUsed: getQueryCount() });
}
);
return server;
}
const app = createMcpExpressApp({ host: HOST });
app.post("/mcp", async (req, res) => {
const server = buildServer();
try {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling MCP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null
});
}
}
});
app.get("/mcp", (_req, res) => {
res.writeHead(405).end(
JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null })
);
});
app.delete("/mcp", (_req, res) => {
res.writeHead(405).end(
JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null })
);
});
app.get("/health", (_req, res) => {
res.json({ ok: true, socketPath, queries: getQueryCount() });
});
app.listen(PORT, HOST, (error?: Error) => {
if (error) {
console.error("Failed to start control bridge:", error);
process.exit(1);
}
console.log(`[control] MCP listening on http://${HOST}:${PORT}/mcp`);
console.log(`[control] Vesktop socket: ${socketPath}`);
});
process.on("SIGINT", () => process.exit(0));