src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { invokeTool } from "./core/qemuMcpClient.js";
import { globalConfigSchematics } from "./config.js";
/**
* Tool provider for the qemu-mcp LM Studio plugin.
*
* Each tool is a thin, validated wrapper around a tool exposed by the upstream
* `qemu-mcp-server` Rust binary (see `scripts/setup.cjs`). The Rust source
* tree lives in `./qemu-mcp-server` and is never modified here.
*/
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const config = ctl.getPluginConfig(globalConfigSchematics);
const qmpSocketPath = config.get("qmpSocketPath");
const serialSocketPath = config.get("serialSocketPath");
const executeQmpTool = tool({
name: "execute_qmp",
description:
"Execute a QMP command on the QEMU instance (e.g. `query-status`, `stop`, `cont`, `eject`). " +
"Any other QMP command name is passed through to QEMU as-is. " +
"Pass raw QMP arguments as a JSON string (use `{}` when no arguments are needed).",
parameters: {
qmp_command: z.string().describe("QMP command name, e.g. `query-status`, `stop`, `cont`, `eject`."),
qmp_arguments: z.string().describe("JSON object with the command's arguments (may be `{}`)."),
},
implementation: async (
{ qmp_command, qmp_arguments },
{ signal, status }
) => {
status("Sending QMP command via qemu-mcp-server...");
try {
const result = await invokeTool(
"execute_qmp",
{ qmp_command, qmp_arguments },
{ qmpSocketPath, serialSocketPath, abortSignal: signal }
);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message.includes("Operation aborted")) {
return "Error: QMP command was aborted by the user.";
}
return "Error executing QMP command: " + message;
}
},
});
const readSerialTool = tool({
name: "read_serial",
description:
"Read pending output from the guest's serial console. The server keeps the " +
"serial socket connected in the background and buffers everything the guest " +
"prints between calls, so nothing is lost while no read is in flight. " +
"Combine with `write_serial` and `wait_for` to drive interactive guest programs.",
parameters: {
wait_for: z
.string()
.optional()
.describe(
"Optional substring to wait for in the output. The call keeps polling until this " +
"substring appears or `timeout_ms` is reached."
),
timeout_ms: z
.number()
.int()
.positive()
.default(5000)
.describe("Give up waiting after this many milliseconds (default 5000)."),
max_chars: z
.number()
.int()
.positive()
.optional()
.describe("Return at most this many characters from the end of the buffer."),
clear: z
.boolean()
.default(false)
.describe("Discard the buffered output after returning it (default false)."),
},
implementation: async (
{ wait_for, timeout_ms, max_chars, clear },
{ signal, status }
) => {
status("Reading guest serial console...");
try {
const result = await invokeTool(
"read_serial",
{ wait_for, timeout_ms, max_chars, clear },
{ qmpSocketPath, serialSocketPath, abortSignal: signal }
);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message.includes("Operation aborted")) {
return "Error: serial read was aborted by the user.";
}
return "Error reading serial console: " + message;
}
},
});
const writeSerialTool = tool({
name: "write_serial",
description:
"Write data to the guest's serial console, i.e. simulate typing into its UART. " +
"Combine with `read_serial` (using `wait_for` on an expected prompt) to drive " +
"interactive guest programs such as bootloaders or shell prompts.",
parameters: {
data: z.string().describe("Bytes to send to the guest serial port."),
newline: z
.boolean()
.default(true)
.describe("Append a newline after `data` (default true)."),
},
implementation: async (
{ data, newline },
{ signal, status }
) => {
status("Writing to guest serial console...");
try {
const result = await invokeTool(
"write_serial",
{ data, newline },
{ qmpSocketPath, serialSocketPath, abortSignal: signal }
);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message.includes("Operation aborted")) {
return "Error: serial write was aborted by the user.";
}
return "Error writing to serial console: " + message;
}
},
});
return [executeQmpTool, readSerialTool, writeSerialTool];
}
src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { invokeTool } from "./core/qemuMcpClient.js";
import { globalConfigSchematics } from "./config.js";
/**
* Tool provider for the qemu-mcp LM Studio plugin.
*
* Each tool is a thin, validated wrapper around a tool exposed by the upstream
* `qemu-mcp-server` Rust binary (see `scripts/setup.cjs`). The Rust source
* tree lives in `./qemu-mcp-server` and is never modified here.
*/
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const config = ctl.getPluginConfig(globalConfigSchematics);
const qmpSocketPath = config.get("qmpSocketPath");
const serialSocketPath = config.get("serialSocketPath");
const executeQmpTool = tool({
name: "execute_qmp",
description:
"Execute a QMP command on the QEMU instance (e.g. `query-status`, `stop`, `cont`, `eject`). " +
"Any other QMP command name is passed through to QEMU as-is. " +
"Pass raw QMP arguments as a JSON string (use `{}` when no arguments are needed).",
parameters: {
qmp_command: z.string().describe("QMP command name, e.g. `query-status`, `stop`, `cont`, `eject`."),
qmp_arguments: z.string().describe("JSON object with the command's arguments (may be `{}`)."),
},
implementation: async (
{ qmp_command, qmp_arguments },
{ signal, status }
) => {
status("Sending QMP command via qemu-mcp-server...");
try {
const result = await invokeTool(
"execute_qmp",
{ qmp_command, qmp_arguments },
{ qmpSocketPath, serialSocketPath, abortSignal: signal }
);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message.includes("Operation aborted")) {
return "Error: QMP command was aborted by the user.";
}
return "Error executing QMP command: " + message;
}
},
});
const readSerialTool = tool({
name: "read_serial",
description:
"Read pending output from the guest's serial console. The server keeps the " +
"serial socket connected in the background and buffers everything the guest " +
"prints between calls, so nothing is lost while no read is in flight. " +
"Combine with `write_serial` and `wait_for` to drive interactive guest programs.",
parameters: {
wait_for: z
.string()
.optional()
.describe(
"Optional substring to wait for in the output. The call keeps polling until this " +
"substring appears or `timeout_ms` is reached."
),
timeout_ms: z
.number()
.int()
.positive()
.default(5000)
.describe("Give up waiting after this many milliseconds (default 5000)."),
max_chars: z
.number()
.int()
.positive()
.optional()
.describe("Return at most this many characters from the end of the buffer."),
clear: z
.boolean()
.default(false)
.describe("Discard the buffered output after returning it (default false)."),
},
implementation: async (
{ wait_for, timeout_ms, max_chars, clear },
{ signal, status }
) => {
status("Reading guest serial console...");
try {
const result = await invokeTool(
"read_serial",
{ wait_for, timeout_ms, max_chars, clear },
{ qmpSocketPath, serialSocketPath, abortSignal: signal }
);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message.includes("Operation aborted")) {
return "Error: serial read was aborted by the user.";
}
return "Error reading serial console: " + message;
}
},
});
const writeSerialTool = tool({
name: "write_serial",
description:
"Write data to the guest's serial console, i.e. simulate typing into its UART. " +
"Combine with `read_serial` (using `wait_for` on an expected prompt) to drive " +
"interactive guest programs such as bootloaders or shell prompts.",
parameters: {
data: z.string().describe("Bytes to send to the guest serial port."),
newline: z
.boolean()
.default(true)
.describe("Append a newline after `data` (default true)."),
},
implementation: async (
{ data, newline },
{ signal, status }
) => {
status("Writing to guest serial console...");
try {
const result = await invokeTool(
"write_serial",
{ data, newline },
{ qmpSocketPath, serialSocketPath, abortSignal: signal }
);
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message.includes("Operation aborted")) {
return "Error: serial write was aborted by the user.";
}
return "Error writing to serial console: " + message;
}
},
});
return [executeQmpTool, readSerialTool, writeSerialTool];
}