feat: stdio MCP wrapper + Windows Run-key autostart fallback (v1.0.1)
Two production-readiness fixes so colleagues can install cleanly: 1. Plugin's MCP server now spawns `claude-mailbox mcp-stdio`, a small stdio MCP wrapper that proxies tool calls to the daemon's REST API. Claude Code does not support env-var substitution in HTTP MCP `url` fields (issue #46889), so the wrapper is the only way to make the daemon URL configurable per machine via CLAUDE_MAILBOX_URL. 2. Windows `install-autostart` now falls back from `schtasks /Create` to an HKCU\Software\Microsoft\Windows\CurrentVersion\Run entry when Group Policy blocks the Scheduled Task path. Both modes are per-user, no admin, persist across logoffs. The chosen mode is recorded in ~/.claude-mailbox/autostart-mode so status/start/stop/ uninstall-autostart pick the right cleanup path. Also bumps the npm version to 1.0.1 to align with the published 1.0.0 plus this patch.
This commit is contained in:
@@ -127,7 +127,7 @@ claude-mailbox uninstall-autostart [--purge]
|
|||||||
|
|
||||||
| Platform | Default mechanism | `--service` mechanism |
|
| Platform | Default mechanism | `--service` mechanism |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Windows | Scheduled Task at logon (no admin) | Windows Service (admin, via `node-windows`) |
|
| Windows | Scheduled Task at logon (no admin); falls back to HKCU Run-key if Group Policy blocks schtasks | Windows Service (admin, via `node-windows`) |
|
||||||
| macOS | launchd LaunchAgent in `~/Library/LaunchAgents/` | n/a |
|
| macOS | launchd LaunchAgent in `~/Library/LaunchAgents/` | n/a |
|
||||||
| Linux | systemd `--user` unit in `~/.config/systemd/user/` | n/a |
|
| Linux | systemd `--user` unit in `~/.config/systemd/user/` | n/a |
|
||||||
|
|
||||||
|
|||||||
4
node/package-lock.json
generated
4
node/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@kuns/claude-mailbox",
|
"name": "@kuns/claude-mailbox",
|
||||||
"version": "0.1.0",
|
"version": "1.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@kuns/claude-mailbox",
|
"name": "@kuns/claude-mailbox",
|
||||||
"version": "0.1.0",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kuns/claude-mailbox",
|
"name": "@kuns/claude-mailbox",
|
||||||
"version": "0.1.0",
|
"version": "1.0.1",
|
||||||
"description": "Standalone MCP mail server that lets parallel Claude sessions coordinate with each other.",
|
"description": "Standalone MCP mail server that lets parallel Claude sessions coordinate with each other.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,10 +1,40 @@
|
|||||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { run, cliEntry, type AutostartManager, type AutostartInstallOpts } from "./index.js";
|
import { run, cliEntry, type AutostartManager, type AutostartInstallOpts } from "./index.js";
|
||||||
import { userConfigPath } from "../config.js";
|
import { userConfigPath } from "../config.js";
|
||||||
|
|
||||||
|
function markerPath(): string {
|
||||||
|
return join(dirname(userConfigPath()), MARKER_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readActiveMode(): "task" | "run-key" | null {
|
||||||
|
const path = markerPath();
|
||||||
|
if (!existsSync(path)) return null;
|
||||||
|
const raw = readFileSync(path, "utf8").trim();
|
||||||
|
if (raw === "task" || raw === "run-key") return raw;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeActiveMode(mode: "task" | "run-key"): void {
|
||||||
|
const path = markerPath();
|
||||||
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
|
writeFileSync(path, mode, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearActiveMode(): void {
|
||||||
|
const path = markerPath();
|
||||||
|
if (existsSync(path)) rmSync(path, { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAccessDenied(stderr: string): boolean {
|
||||||
|
return /access is denied|0x80070005/i.test(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
const TASK_NAME = "ClaudeMailbox";
|
const TASK_NAME = "ClaudeMailbox";
|
||||||
const SERVICE_NAME = "ClaudeMailbox";
|
const SERVICE_NAME = "ClaudeMailbox";
|
||||||
|
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
||||||
|
const RUN_VALUE = "ClaudeMailbox";
|
||||||
|
const MARKER_FILE = "autostart-mode";
|
||||||
|
|
||||||
function ensureConfigSeeded(opts: AutostartInstallOpts): string {
|
function ensureConfigSeeded(opts: AutostartInstallOpts): string {
|
||||||
const path = userConfigPath();
|
const path = userConfigPath();
|
||||||
@@ -24,10 +54,14 @@ function buildServeCommand(): { node: string; script: string; configPath: string
|
|||||||
return { node, script, configPath: userConfigPath() };
|
return { node, script, configPath: userConfigPath() };
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskInstall(opts: AutostartInstallOpts): void {
|
function buildServeCommandString(configPath: string): string {
|
||||||
const configPath = ensureConfigSeeded(opts);
|
|
||||||
const { node, script } = buildServeCommand();
|
const { node, script } = buildServeCommand();
|
||||||
const tr = `"${node}" "${script}" serve --config "${configPath}"`;
|
return `"${node}" "${script}" serve --config "${configPath}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryScheduledTaskInstall(opts: AutostartInstallOpts): { ok: boolean; stderr: string } {
|
||||||
|
const configPath = ensureConfigSeeded(opts);
|
||||||
|
const tr = buildServeCommandString(configPath);
|
||||||
const r = run("schtasks.exe", [
|
const r = run("schtasks.exe", [
|
||||||
"/Create",
|
"/Create",
|
||||||
"/SC",
|
"/SC",
|
||||||
@@ -40,28 +74,110 @@ function scheduledTaskInstall(opts: AutostartInstallOpts): void {
|
|||||||
"LIMITED",
|
"LIMITED",
|
||||||
"/F",
|
"/F",
|
||||||
]);
|
]);
|
||||||
|
if (r.status !== 0) return { ok: false, stderr: r.stderr || r.stdout };
|
||||||
|
run("schtasks.exe", ["/Run", "/TN", TASK_NAME]);
|
||||||
|
return { ok: true, stderr: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function runKeyInstall(opts: AutostartInstallOpts): void {
|
||||||
|
const configPath = ensureConfigSeeded(opts);
|
||||||
|
const cmd = buildServeCommandString(configPath);
|
||||||
|
const r = run("reg.exe", [
|
||||||
|
"add",
|
||||||
|
RUN_KEY,
|
||||||
|
"/v",
|
||||||
|
RUN_VALUE,
|
||||||
|
"/t",
|
||||||
|
"REG_SZ",
|
||||||
|
"/d",
|
||||||
|
cmd,
|
||||||
|
"/f",
|
||||||
|
]);
|
||||||
if (r.status !== 0) {
|
if (r.status !== 0) {
|
||||||
throw new Error(`schtasks /Create failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
throw new Error(`reg add (HKCU Run) failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
||||||
}
|
}
|
||||||
const start = run("schtasks.exe", ["/Run", "/TN", TASK_NAME]);
|
spawnRunKeyDaemon(configPath);
|
||||||
if (start.status !== 0) {
|
}
|
||||||
console.warn(`Task created but /Run returned ${start.status}: ${start.stderr.trim()}`);
|
|
||||||
|
function spawnRunKeyDaemon(configPath: string): void {
|
||||||
|
if (runKeyDaemonRunning()) return;
|
||||||
|
const { node, script } = buildServeCommand();
|
||||||
|
const ps = `Start-Process -WindowStyle Hidden -FilePath "${node}" -ArgumentList @('"${script}"','serve','--config','"${configPath}"')`;
|
||||||
|
run("powershell.exe", ["-NoProfile", "-Command", ps]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runKeyDaemonRunning(): boolean {
|
||||||
|
const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*claude-mailbox*serve*' } | Select-Object -First 1 -ExpandProperty ProcessId`;
|
||||||
|
const r = run("powershell.exe", ["-NoProfile", "-Command", ps]);
|
||||||
|
return r.status === 0 && r.stdout.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function killRunKeyDaemon(): void {
|
||||||
|
const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*claude-mailbox*serve*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }`;
|
||||||
|
run("powershell.exe", ["-NoProfile", "-Command", ps]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runKeyUninstall(): void {
|
||||||
|
const r = run("reg.exe", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"]);
|
||||||
|
if (r.status !== 0 && !/unable to find/i.test(r.stderr)) {
|
||||||
|
throw new Error(`reg delete failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
||||||
}
|
}
|
||||||
|
killRunKeyDaemon();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduledTaskInstall(opts: AutostartInstallOpts): void {
|
||||||
|
const attempt = tryScheduledTaskInstall(opts);
|
||||||
|
if (attempt.ok) {
|
||||||
|
writeActiveMode("task");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isAccessDenied(attempt.stderr)) {
|
||||||
|
console.warn(
|
||||||
|
"schtasks /Create denied by Windows policy — falling back to HKCU Run-key autostart (per-user, no admin).",
|
||||||
|
);
|
||||||
|
runKeyInstall(opts);
|
||||||
|
writeActiveMode("run-key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`schtasks /Create failed: ${attempt.stderr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskUninstall(purge: boolean): void {
|
function scheduledTaskUninstall(purge: boolean): void {
|
||||||
|
const mode = readActiveMode();
|
||||||
|
if (mode === "run-key") {
|
||||||
|
runKeyUninstall();
|
||||||
|
clearActiveMode();
|
||||||
|
if (purge) purgeData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Default to task uninstall, also clean up Run-key in case of mixed state
|
||||||
run("schtasks.exe", ["/End", "/TN", TASK_NAME]);
|
run("schtasks.exe", ["/End", "/TN", TASK_NAME]);
|
||||||
const r = run("schtasks.exe", ["/Delete", "/TN", TASK_NAME, "/F"]);
|
const r = run("schtasks.exe", ["/Delete", "/TN", TASK_NAME, "/F"]);
|
||||||
if (r.status !== 0 && !/cannot find/i.test(r.stderr)) {
|
if (r.status !== 0 && !/cannot find/i.test(r.stderr) && !/does not exist/i.test(r.stderr)) {
|
||||||
throw new Error(`schtasks /Delete failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
// Fall through — try Run-key cleanup anyway
|
||||||
}
|
}
|
||||||
|
// Best-effort Run-key cleanup
|
||||||
|
run("reg.exe", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"]);
|
||||||
|
killRunKeyDaemon();
|
||||||
|
clearActiveMode();
|
||||||
if (purge) purgeData();
|
if (purge) purgeData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskStatus(): "Running" | "Stopped" | "NotInstalled" {
|
function scheduledTaskStatus(): "Running" | "Stopped" | "NotInstalled" {
|
||||||
|
const mode = readActiveMode();
|
||||||
|
if (mode === "run-key") {
|
||||||
|
const r = run("reg.exe", ["query", RUN_KEY, "/v", RUN_VALUE]);
|
||||||
|
if (r.status !== 0) return "NotInstalled";
|
||||||
|
return runKeyDaemonRunning() ? "Running" : "Stopped";
|
||||||
|
}
|
||||||
const r = run("schtasks.exe", ["/Query", "/TN", TASK_NAME, "/FO", "LIST", "/V"]);
|
const r = run("schtasks.exe", ["/Query", "/TN", TASK_NAME, "/FO", "LIST", "/V"]);
|
||||||
if (r.status !== 0) {
|
if (r.status !== 0) {
|
||||||
if (/cannot find/i.test(r.stderr) || /does not exist/i.test(r.stderr)) return "NotInstalled";
|
if (/cannot find/i.test(r.stderr) || /does not exist/i.test(r.stderr)) {
|
||||||
|
// Maybe a Run-key install happened without a marker (legacy / manual). Check reg.
|
||||||
|
const reg = run("reg.exe", ["query", RUN_KEY, "/v", RUN_VALUE]);
|
||||||
|
if (reg.status === 0) return runKeyDaemonRunning() ? "Running" : "Stopped";
|
||||||
|
return "NotInstalled";
|
||||||
|
}
|
||||||
return "Stopped";
|
return "Stopped";
|
||||||
}
|
}
|
||||||
if (/Status:\s*Running/i.test(r.stdout)) return "Running";
|
if (/Status:\s*Running/i.test(r.stdout)) return "Running";
|
||||||
@@ -69,11 +185,22 @@ function scheduledTaskStatus(): "Running" | "Stopped" | "NotInstalled" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskRun(): void {
|
function scheduledTaskRun(): void {
|
||||||
|
const mode = readActiveMode();
|
||||||
|
if (mode === "run-key") {
|
||||||
|
const cfgPath = userConfigPath();
|
||||||
|
spawnRunKeyDaemon(cfgPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const r = run("schtasks.exe", ["/Run", "/TN", TASK_NAME]);
|
const r = run("schtasks.exe", ["/Run", "/TN", TASK_NAME]);
|
||||||
if (r.status !== 0) throw new Error(`schtasks /Run failed: ${r.stderr || r.stdout}`);
|
if (r.status !== 0) throw new Error(`schtasks /Run failed: ${r.stderr || r.stdout}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskEnd(): void {
|
function scheduledTaskEnd(): void {
|
||||||
|
const mode = readActiveMode();
|
||||||
|
if (mode === "run-key") {
|
||||||
|
killRunKeyDaemon();
|
||||||
|
return;
|
||||||
|
}
|
||||||
run("schtasks.exe", ["/End", "/TN", TASK_NAME]);
|
run("schtasks.exe", ["/End", "/TN", TASK_NAME]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { resolveConfig, baseUrl, DEFAULT_PORT } from "./config.js";
|
import { resolveConfig, baseUrl, DEFAULT_PORT } from "./config.js";
|
||||||
import { startServer } from "./server.js";
|
import { startServer } from "./server.js";
|
||||||
import { autostartManager } from "./autostart/index.js";
|
import { autostartManager } from "./autostart/index.js";
|
||||||
|
import { runStdioMcp } from "./mcp-stdio.js";
|
||||||
import {
|
import {
|
||||||
applyInstall,
|
applyInstall,
|
||||||
applyUninstall,
|
applyUninstall,
|
||||||
@@ -267,6 +268,20 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("mcp-stdio")
|
||||||
|
.description(
|
||||||
|
"Run a stdio MCP server that proxies tool calls to the local daemon's REST API. The daemon URL comes from $CLAUDE_MAILBOX_URL (default http://127.0.0.1:47822). Used by the Claude Code plugin's .mcp.json so the URL is configurable per machine without env-substitution in the URL field.",
|
||||||
|
)
|
||||||
|
.action(async () => {
|
||||||
|
try {
|
||||||
|
await runStdioMcp();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err instanceof Error ? err.message : String(err));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("install-hook")
|
.command("install-hook")
|
||||||
.description(
|
.description(
|
||||||
|
|||||||
165
node/src/mcp-stdio.ts
Normal file
165
node/src/mcp-stdio.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { DEFAULT_PORT } from "./config.js";
|
||||||
|
|
||||||
|
const HARDCODED_DEFAULT_URL = `http://127.0.0.1:${DEFAULT_PORT}`;
|
||||||
|
|
||||||
|
function resolveDaemonUrl(): string {
|
||||||
|
const env = (process.env["CLAUDE_MAILBOX_URL"] ?? "").trim();
|
||||||
|
if (!env || env.includes("${")) return HARDCODED_DEFAULT_URL;
|
||||||
|
return env.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireIdentity(value: string | undefined, argName: "from" | "name"): string {
|
||||||
|
const v = (value ?? "").trim();
|
||||||
|
if (!v) {
|
||||||
|
throw new Error(
|
||||||
|
`Pass \`${argName}\` (your mailbox name from the SessionStart announcement).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rest(
|
||||||
|
method: "GET" | "POST",
|
||||||
|
url: string,
|
||||||
|
init: { headers?: Record<string, string>; body?: unknown } = {},
|
||||||
|
): Promise<unknown> {
|
||||||
|
const headers: Record<string, string> = { Accept: "application/json", ...(init.headers ?? {}) };
|
||||||
|
let body: string | undefined;
|
||||||
|
if (init.body !== undefined) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
body = JSON.stringify(init.body);
|
||||||
|
}
|
||||||
|
const res = await fetch(url, { method, headers, body });
|
||||||
|
const text = await res.text();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`${method} ${url} → ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
return text.length ? JSON.parse(text) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStdioMcpServer(daemonUrl: string = resolveDaemonUrl()): McpServer {
|
||||||
|
const server = new McpServer({ name: "claude-mailbox", version: "1.0.0" });
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"send",
|
||||||
|
{
|
||||||
|
title: "Send mail",
|
||||||
|
description:
|
||||||
|
"Send a message to another mailbox. Pass `from` with your own mailbox name (see the SessionStart announcement).",
|
||||||
|
inputSchema: {
|
||||||
|
to: z.string().describe("Name of the recipient mailbox."),
|
||||||
|
body: z.string().describe("Message body (plain text or markdown)."),
|
||||||
|
from: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Your mailbox name. Take it from the SessionStart announcement.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ to, body, from }) => {
|
||||||
|
const sender = requireIdentity(from, "from");
|
||||||
|
const out = (await rest("POST", `${daemonUrl}/v1/send`, {
|
||||||
|
headers: { "X-Mailbox": sender },
|
||||||
|
body: { to, body },
|
||||||
|
})) as { id: number; queuedAt: string };
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(out) }],
|
||||||
|
structuredContent: out,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"check_inbox",
|
||||||
|
{
|
||||||
|
title: "Check inbox",
|
||||||
|
description:
|
||||||
|
"Pull all undelivered messages for your mailbox and mark them delivered. Pass `name` with your own mailbox name.",
|
||||||
|
inputSchema: {
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Your mailbox name. Take it from the SessionStart announcement.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ name }) => {
|
||||||
|
const me = requireIdentity(name, "name");
|
||||||
|
const messages = (await rest(
|
||||||
|
"POST",
|
||||||
|
`${daemonUrl}/v1/check-inbox?name=${encodeURIComponent(me)}`,
|
||||||
|
{ headers: { "X-Mailbox": me } },
|
||||||
|
)) as { id: number; from: string; body: string; sentAt: string }[];
|
||||||
|
const arr = Array.isArray(messages) ? messages : [];
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(arr) }],
|
||||||
|
structuredContent: { messages: arr },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"peek_inbox",
|
||||||
|
{
|
||||||
|
title: "Peek inbox",
|
||||||
|
description:
|
||||||
|
"Non-consuming check of your mailbox. Returns pending count and oldest pending timestamp. Pass `name` with your own mailbox name.",
|
||||||
|
inputSchema: {
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Your mailbox name. Take it from the SessionStart announcement.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ name }) => {
|
||||||
|
const me = requireIdentity(name, "name");
|
||||||
|
const out = (await rest("GET", `${daemonUrl}/v1/peek?name=${encodeURIComponent(me)}`)) as {
|
||||||
|
pending: number;
|
||||||
|
oldestAt: string | null;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(out) }],
|
||||||
|
structuredContent: out,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"list_mailboxes",
|
||||||
|
{
|
||||||
|
title: "List mailboxes",
|
||||||
|
description:
|
||||||
|
"Discover known mailboxes and how many messages each has waiting for you. Pass `name` with your own mailbox name.",
|
||||||
|
inputSchema: {
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Your mailbox name. Take it from the SessionStart announcement.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ name }) => {
|
||||||
|
const me = requireIdentity(name, "name");
|
||||||
|
const list = (await rest("GET", `${daemonUrl}/v1/list`, {
|
||||||
|
headers: { "X-Mailbox": me },
|
||||||
|
})) as { name: string; lastSeenAt: string; pendingForYou: number }[];
|
||||||
|
const arr = Array.isArray(list) ? list : [];
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(arr) }],
|
||||||
|
structuredContent: { mailboxes: arr },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runStdioMcp(): Promise<void> {
|
||||||
|
const server = buildStdioMcpServer();
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mailbox": {
|
"mailbox": {
|
||||||
"type": "http",
|
"type": "stdio",
|
||||||
"url": "http://127.0.0.1:47822/mcp"
|
"command": "claude-mailbox",
|
||||||
|
"args": ["mcp-stdio"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ Cost: one local HTTP round-trip per prompt + Node coldstart (~100ms on Windows).
|
|||||||
|
|
||||||
## MCP tools
|
## MCP tools
|
||||||
|
|
||||||
The plugin also ships a `.mcp.json` so Claude has direct access to the mailbox via tool calls. Because the X-Mailbox header would be the same for two parallel sessions sharing one `.mcp.json`, **each MCP tool takes the caller's mailbox name as an explicit argument** (from the SessionStart announcement):
|
The plugin ships a `.mcp.json` that spawns a **stdio MCP wrapper** (`claude-mailbox mcp-stdio`) so the daemon URL is configurable per machine via the `CLAUDE_MAILBOX_URL` env var (Claude Code doesn't yet support env substitution in HTTP MCP URLs — see issue #46889). The wrapper proxies tool calls to the daemon's REST API.
|
||||||
|
|
||||||
|
Each MCP tool takes the caller's mailbox name as an explicit argument (from the SessionStart announcement):
|
||||||
|
|
||||||
| Tool | Required args | Purpose |
|
| Tool | Required args | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
@@ -59,11 +59,9 @@ Run: `claude-mailbox status`
|
|||||||
- `Stopped` → `claude-mailbox start`, re-check.
|
- `Stopped` → `claude-mailbox start`, re-check.
|
||||||
- `NotInstalled` → `claude-mailbox install-autostart`, then `claude-mailbox start`, re-check.
|
- `NotInstalled` → `claude-mailbox install-autostart`, then `claude-mailbox start`, re-check.
|
||||||
|
|
||||||
**If `install-autostart` fails with "Access is denied" on Windows:** Group Policy may block non-admin `schtasks /Create`. Two fallbacks:
|
**Behavior on `install-autostart`:** The CLI tries a Scheduled Task first (`schtasks /RL LIMITED`, no admin). If Windows Group Policy returns "Access is denied", it falls back transparently to an `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` registry entry plus a hidden `node serve` process — same per-user persistence, no admin needed. The chosen mechanism is recorded in `~/.claude-mailbox/autostart-mode` and respected by `status`/`start`/`stop`/`uninstall-autostart`.
|
||||||
1. Tell the user to run `claude-mailbox install-autostart` from an elevated PowerShell themselves (one-time).
|
|
||||||
2. For this session, run `claude-mailbox serve` as a background process so the rest of the doctor's checks can pass — the daemon won't survive logoff, but that's fine for verification.
|
|
||||||
|
|
||||||
If status doesn't reach `Running` after the fallback, stop and report.
|
If `install-autostart` still fails after both attempts (very rare — would mean both `schtasks` and `reg add` are blocked), stop and report what `status` and `start` printed.
|
||||||
|
|
||||||
## Step 4 — health probe
|
## Step 4 — health probe
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user