tests / fakeMcpServer.cjs
"use strict";
/**
* Fake MCP server that mimics the qemu-mcp-server Rust binary's stdio-based
* JSON-RPC 2.0 protocol. Used for unit testing the TypeScript client
* (qemuMcpClient.ts) without requiring a real QEMU instance.
*
* Protocol supported:
* - initialize (id: 1) → returns protocol version + capabilities
* - notifications/initialized → no response
* - tools/call (id: 2) → returns tool result or error
*
* This server reads newline-delimited JSON-RPC messages from stdin and writes
* responses to stdout (also newline-delimited). All logs go to stderr.
*/
// `process` is a Node.js global; no require needed.
const TOOLS = ["execute_qmp", "read_serial", "write_serial"];
/**
* Simulated tool implementations.
*/
function execQmp(args) {
const { qmp_command, qmp_arguments } = args;
if (!qmp_command || typeof qmp_command !== "string") {
return {
content: [
{
type: "text",
text: "Error: qmp_command is required and must be a string.",
},
],
};
}
// Simulate a QEMU response for query-status
if (qmp_command === "query-status") {
return {
content: [
{
type: "text",
text: JSON.stringify({
status: "running",
singlestep: false,
running: true,
guest_paused: false,
}),
},
],
};
}
// Simulate stop
if (qmp_command === "stop") {
return {
content: [
{ type: "text", text: "VM stopped successfully." },
],
};
}
// Simulate cont
if (qmp_command === "cont") {
return {
content: [
{ type: "text", text: "VM continued successfully." },
],
};
}
// Simulate eject
if (qmp_command === "eject") {
const media = qmp_arguments ? JSON.parse(qmp_arguments) : {};
return {
content: [
{
type: "text",
text: `Ejected medium from ${media.device || "default"} drive.`,
},
],
};
}
// Pass-through for unknown commands
return {
content: [
{
type: "text",
text: JSON.stringify({
exec_status: "ok",
command: qmp_command,
arguments: qmp_arguments,
}),
},
],
};
}
function readSerial(args) {
const { wait_for, timeout_ms, max_chars, clear } = args;
let text = "";
if (wait_for) {
text += `[read_serial] waiting for: "${wait_for}" (timeout: ${timeout_ms}ms)`;
} else {
text = `[read_serial] no output pending (timeout: ${timeout_ms}ms)`;
}
if (max_chars) {
text += ` | max_chars: ${max_chars}`;
}
if (clear) {
text += " | buffer cleared after read";
}
return {
content: [{ type: "text", text }],
};
}
function writeSerial(args) {
const { data, newline } = args;
return {
content: [
{
type: "text",
text: `Wrote ${data.length} byte(s) to serial console${newline ? " with newline" : ""}.`,
},
],
};
};
const TOOL_HANDLERS = {
execute_qmp: execQmp,
read_serial: readSerial,
write_serial: writeSerial,
};
/**
* Write a JSON-RPC response to stdout (newline-delimited).
*/
function sendResponse(id, result) {
const response = {
jsonrpc: "2.0",
id,
result,
};
process.stdout.write(JSON.stringify(response) + "\n");
}
/**
* Write a JSON-RPC error response to stdout.
*/
function sendError(id, message) {
const response = {
jsonrpc: "2.0",
id,
error: {
code: -32603,
message,
},
};
process.stdout.write(JSON.stringify(response) + "\n");
}
/**
* Process the MCP protocol.
*/
function main() {
let nextId = 2; // id 1 is reserved for initialize
let initialized = false;
let buffer = "";
// Use process.stdin (from the parent's piped stdin) or process.openStdin()
const stdin = process.stdin || process.openStdin();
if (stdin && typeof stdin.setEncoding === "function") {
stdin.setEncoding("utf8");
}
function onData(chunk) {
buffer += chunk;
let idx;
while ((idx = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, idx).trim();
buffer = buffer.slice(idx + 1);
if (!line) continue;
let parsed;
try {
parsed = JSON.parse(line);
} catch {
// Incomplete message; wait for more data.
continue;
}
handleMessage(parsed);
}
}
if (stdin && typeof stdin.on === "function") {
stdin.on("data", onData);
}
function handleMessage(msg) {
if (!msg || typeof msg !== "object") return;
// Handle initialize (id: 1)
if (msg.method === "initialize") {
const id = msg.id;
sendResponse(id, {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "fake-qemu-mcp-server", version: "1.0.0" },
});
return;
}
// Handle notifications (no id)
if (!msg.id) {
if (msg.method === "notifications/initialized") {
initialized = true;
}
return;
}
// Handle tools/call
if (msg.method === "tools/call") {
const { name, arguments: args } = msg.params || {};
const handler = TOOL_HANDLERS[name];
if (!handler) {
sendError(
msg.id,
`Tool not found: ${name}`
);
return;
}
try {
const result = handler(args || {});
sendResponse(msg.id, result);
} catch (err) {
sendError(msg.id, err.message || "Internal error");
}
return;
}
// Unknown method
sendError(
msg.id,
`Unknown method: ${msg.method}`
);
}
// Keep the process alive.
if (stdin && typeof stdin.resume === "function") {
stdin.resume();
}
}
main();
tests / fakeMcpServer.cjs
"use strict";
/**
* Fake MCP server that mimics the qemu-mcp-server Rust binary's stdio-based
* JSON-RPC 2.0 protocol. Used for unit testing the TypeScript client
* (qemuMcpClient.ts) without requiring a real QEMU instance.
*
* Protocol supported:
* - initialize (id: 1) → returns protocol version + capabilities
* - notifications/initialized → no response
* - tools/call (id: 2) → returns tool result or error
*
* This server reads newline-delimited JSON-RPC messages from stdin and writes
* responses to stdout (also newline-delimited). All logs go to stderr.
*/
// `process` is a Node.js global; no require needed.
const TOOLS = ["execute_qmp", "read_serial", "write_serial"];
/**
* Simulated tool implementations.
*/
function execQmp(args) {
const { qmp_command, qmp_arguments } = args;
if (!qmp_command || typeof qmp_command !== "string") {
return {
content: [
{
type: "text",
text: "Error: qmp_command is required and must be a string.",
},
],
};
}
// Simulate a QEMU response for query-status
if (qmp_command === "query-status") {
return {
content: [
{
type: "text",
text: JSON.stringify({
status: "running",
singlestep: false,
running: true,
guest_paused: false,
}),
},
],
};
}
// Simulate stop
if (qmp_command === "stop") {
return {
content: [
{ type: "text", text: "VM stopped successfully." },
],
};
}
// Simulate cont
if (qmp_command === "cont") {
return {
content: [
{ type: "text", text: "VM continued successfully." },
],
};
}
// Simulate eject
if (qmp_command === "eject") {
const media = qmp_arguments ? JSON.parse(qmp_arguments) : {};
return {
content: [
{
type: "text",
text: `Ejected medium from ${media.device || "default"} drive.`,
},
],
};
}
// Pass-through for unknown commands
return {
content: [
{
type: "text",
text: JSON.stringify({
exec_status: "ok",
command: qmp_command,
arguments: qmp_arguments,
}),
},
],
};
}
function readSerial(args) {
const { wait_for, timeout_ms, max_chars, clear } = args;
let text = "";
if (wait_for) {
text += `[read_serial] waiting for: "${wait_for}" (timeout: ${timeout_ms}ms)`;
} else {
text = `[read_serial] no output pending (timeout: ${timeout_ms}ms)`;
}
if (max_chars) {
text += ` | max_chars: ${max_chars}`;
}
if (clear) {
text += " | buffer cleared after read";
}
return {
content: [{ type: "text", text }],
};
}
function writeSerial(args) {
const { data, newline } = args;
return {
content: [
{
type: "text",
text: `Wrote ${data.length} byte(s) to serial console${newline ? " with newline" : ""}.`,
},
],
};
};
const TOOL_HANDLERS = {
execute_qmp: execQmp,
read_serial: readSerial,
write_serial: writeSerial,
};
/**
* Write a JSON-RPC response to stdout (newline-delimited).
*/
function sendResponse(id, result) {
const response = {
jsonrpc: "2.0",
id,
result,
};
process.stdout.write(JSON.stringify(response) + "\n");
}
/**
* Write a JSON-RPC error response to stdout.
*/
function sendError(id, message) {
const response = {
jsonrpc: "2.0",
id,
error: {
code: -32603,
message,
},
};
process.stdout.write(JSON.stringify(response) + "\n");
}
/**
* Process the MCP protocol.
*/
function main() {
let nextId = 2; // id 1 is reserved for initialize
let initialized = false;
let buffer = "";
// Use process.stdin (from the parent's piped stdin) or process.openStdin()
const stdin = process.stdin || process.openStdin();
if (stdin && typeof stdin.setEncoding === "function") {
stdin.setEncoding("utf8");
}
function onData(chunk) {
buffer += chunk;
let idx;
while ((idx = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, idx).trim();
buffer = buffer.slice(idx + 1);
if (!line) continue;
let parsed;
try {
parsed = JSON.parse(line);
} catch {
// Incomplete message; wait for more data.
continue;
}
handleMessage(parsed);
}
}
if (stdin && typeof stdin.on === "function") {
stdin.on("data", onData);
}
function handleMessage(msg) {
if (!msg || typeof msg !== "object") return;
// Handle initialize (id: 1)
if (msg.method === "initialize") {
const id = msg.id;
sendResponse(id, {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "fake-qemu-mcp-server", version: "1.0.0" },
});
return;
}
// Handle notifications (no id)
if (!msg.id) {
if (msg.method === "notifications/initialized") {
initialized = true;
}
return;
}
// Handle tools/call
if (msg.method === "tools/call") {
const { name, arguments: args } = msg.params || {};
const handler = TOOL_HANDLERS[name];
if (!handler) {
sendError(
msg.id,
`Tool not found: ${name}`
);
return;
}
try {
const result = handler(args || {});
sendResponse(msg.id, result);
} catch (err) {
sendError(msg.id, err.message || "Internal error");
}
return;
}
// Unknown method
sendError(
msg.id,
`Unknown method: ${msg.method}`
);
}
// Keep the process alive.
if (stdin && typeof stdin.resume === "function") {
stdin.resume();
}
}
main();