mirror of
https://github.com/spartanz51/tutabridge.git
synced 2026-06-24 10:54:32 +02:00
Read-only MCP server (HTTP, GUI-controlled)
Expose the mailbox to an LLM client (Claude Desktop / Code) over an in-process MCP server, so the bridge itself hosts it and the GUI controls it live. Strictly read-only: there is no tool that sends, moves, deletes or mutates mail — by design and asserted in tests. Transport: Streamable HTTP (MCP 2025-06-18) on a single POST /mcp endpoint bound to 127.0.0.1, answering each JSON-RPC request with application/json (no SSE — the server never pushes). Auth is a bearer token (the bridge password); the Origin header is validated to block DNS-rebinding. Permission tiers (config.McpPermission, default Disabled = server off): - Metadata — folders, metadata search (subject/sender/date), headers only. - Full — the above plus full-text body search and message body text. Tools: list_folders, search_messages, list_unread, get_message. Search combines subject/sender always and the encrypted FTS body index under Full; get_message returns headers always and body only under Full. Wiring: spawned in-process by both the CLI (main.rs) and the GUI bridge task (bridge.rs); a Disabled tier makes serve() a no-op, and it is kept out of the select! so it never triggers teardown. GUI gains an MCP section (tier selector, port, full-read warning, "copy client config" button) and a get_mcp_client_config command that emits the ready-to-paste client snippet. Validated live on a ~19k-message mailbox: initialize / tools/list / tools/call all conform; 401 without the bearer token, 403 on a foreign Origin, 202 on notifications; list_folders, body search and get_message (HTML stripped to text) all return correctly. 240 unit tests.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Config, BridgeStatus } from "../types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Config, BridgeStatus, McpPermission } from "../types";
|
||||
|
||||
interface Props {
|
||||
config: Config | null;
|
||||
@@ -16,6 +17,9 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
|
||||
const [apiUrl, setApiUrl] = useState("https://app.tuta.com");
|
||||
const [syncLimit, setSyncLimit] = useState(500);
|
||||
const [fetchAll, setFetchAll] = useState(false);
|
||||
const [mcpPermission, setMcpPermission] = useState<McpPermission>("disabled");
|
||||
const [mcpPort, setMcpPort] = useState(1944);
|
||||
const [mcpCopied, setMcpCopied] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -26,6 +30,8 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
|
||||
setApiUrl(config.api_url);
|
||||
setFetchAll(config.sync_limit === 0);
|
||||
setSyncLimit(config.sync_limit === 0 ? 500 : config.sync_limit);
|
||||
setMcpPermission(config.mcp_permission ?? "disabled");
|
||||
setMcpPort(config.mcp_port ?? 1944);
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
@@ -38,11 +44,24 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
|
||||
smtp_port: smtpPort,
|
||||
api_url: apiUrl,
|
||||
sync_limit: fetchAll ? 0 : syncLimit,
|
||||
mcp_permission: mcpPermission,
|
||||
mcp_port: mcpPort,
|
||||
});
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
|
||||
const handleCopyMcpConfig = async () => {
|
||||
try {
|
||||
const snippet = await invoke<string>("get_mcp_client_config");
|
||||
await navigator.clipboard.writeText(snippet);
|
||||
setMcpCopied(true);
|
||||
setTimeout(() => setMcpCopied(false), 2000);
|
||||
} catch {
|
||||
/* clipboard denied — ignore */
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>Configuration</h2>
|
||||
@@ -110,6 +129,51 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>AI access (MCP server)</label>
|
||||
<small className="field-hint">
|
||||
Lets an LLM client (Claude Desktop / Code) <strong>read</strong> this
|
||||
mailbox over a local MCP server. Strictly read-only — it can never
|
||||
send, move or delete mail.
|
||||
</small>
|
||||
<select
|
||||
value={mcpPermission}
|
||||
onChange={(e) => setMcpPermission(e.target.value as McpPermission)}
|
||||
>
|
||||
<option value="disabled">Disabled (off)</option>
|
||||
<option value="metadata">
|
||||
Metadata only — folders, search, headers (no body)
|
||||
</option>
|
||||
<option value="full">Full read — also message bodies</option>
|
||||
</select>
|
||||
{mcpPermission === "full" && (
|
||||
<small className="field-hint">
|
||||
⚠️ The connected LLM can read full message content. Body text is
|
||||
untrusted — a malicious email could try to mislead the model. Only
|
||||
enable with a client you trust.
|
||||
</small>
|
||||
)}
|
||||
{mcpPermission !== "disabled" && (
|
||||
<>
|
||||
<input
|
||||
type="number"
|
||||
value={mcpPort}
|
||||
onChange={(e) => setMcpPort(Number(e.target.value))}
|
||||
placeholder="MCP port (127.0.0.1)"
|
||||
/>
|
||||
<button type="button" onClick={handleCopyMcpConfig}>
|
||||
{mcpCopied ? "Copied!" : "Copy client config"}
|
||||
</button>
|
||||
<small className="field-hint">
|
||||
Save first, then paste the copied snippet into your MCP client.
|
||||
The server listens on 127.0.0.1 and requires the bridge password
|
||||
as a bearer token.
|
||||
</small>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isRunning && (
|
||||
<small className="field-hint">Changes apply after a restart.</small>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/** Read-only MCP server access tier. `disabled` = server off. */
|
||||
export type McpPermission = "disabled" | "metadata" | "full";
|
||||
|
||||
export interface Config {
|
||||
email: string;
|
||||
imap_port: number;
|
||||
@@ -5,6 +8,10 @@ export interface Config {
|
||||
api_url: string;
|
||||
/** Max mails synced per folder; 0 = fetch all. */
|
||||
sync_limit: number;
|
||||
/** Read-only MCP server permission tier. */
|
||||
mcp_permission: McpPermission;
|
||||
/** Port the read-only MCP HTTP server listens on (127.0.0.1). */
|
||||
mcp_port: number;
|
||||
}
|
||||
|
||||
export type BridgeStatus = "Stopped" | "Starting" | "Running" | { Error: string };
|
||||
|
||||
Reference in New Issue
Block a user