183 lines
6.1 KiB
TypeScript
183 lines
6.1 KiB
TypeScript
/**
|
|
* MCP tools for rInbox (email mailboxes & threads).
|
|
*
|
|
* ALL tools use forceAuth=true — always requires token+member
|
|
* regardless of space visibility (email content is sensitive).
|
|
*
|
|
* Tools: rinbox_list_mailboxes, rinbox_list_threads,
|
|
* rinbox_get_thread, rinbox_list_approvals
|
|
*/
|
|
|
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { z } from "zod";
|
|
import type { SyncServer } from "../local-first/sync-server";
|
|
import { mailboxDocId } from "../../modules/rinbox/schemas";
|
|
import type { MailboxDoc } from "../../modules/rinbox/schemas";
|
|
import { resolveAccess, accessDeniedResponse } from "./_auth";
|
|
|
|
const MAILBOX_PREFIX = ":inbox:mailboxes:";
|
|
|
|
/** Find all mailbox docIds for a space. */
|
|
function findMailboxDocIds(syncServer: SyncServer, space: string): string[] {
|
|
const prefix = `${space}${MAILBOX_PREFIX}`;
|
|
return syncServer.getDocIds().filter(id => id.startsWith(prefix));
|
|
}
|
|
|
|
export function registerInboxTools(server: McpServer, syncServer: SyncServer) {
|
|
server.tool(
|
|
"rinbox_list_mailboxes",
|
|
"List mailboxes in a space (requires auth + membership — email content is sensitive)",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
},
|
|
async ({ space, token }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const docIds = findMailboxDocIds(syncServer, space);
|
|
const mailboxes = [];
|
|
for (const docId of docIds) {
|
|
const doc = syncServer.getDoc<MailboxDoc>(docId);
|
|
if (!doc?.mailbox) continue;
|
|
mailboxes.push({
|
|
id: doc.mailbox.id,
|
|
name: doc.mailbox.name,
|
|
slug: doc.mailbox.slug,
|
|
email: doc.mailbox.email,
|
|
threadCount: Object.keys(doc.threads || {}).length,
|
|
approvalCount: Object.keys(doc.approvals || {}).length,
|
|
});
|
|
}
|
|
|
|
return { content: [{ type: "text", text: JSON.stringify(mailboxes, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"rinbox_list_threads",
|
|
"List email threads in a mailbox (subjects only, no body content)",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
mailbox_slug: z.string().optional().describe("Filter by mailbox slug (searches all if omitted)"),
|
|
status: z.string().optional().describe("Filter by status"),
|
|
search: z.string().optional().describe("Search in subject/from"),
|
|
limit: z.number().optional().describe("Max results (default 50)"),
|
|
},
|
|
async ({ space, token, mailbox_slug, status, search, limit }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const docIds = mailbox_slug
|
|
? findMailboxDocIds(syncServer, space).filter(id => id.endsWith(`:${mailbox_slug}`))
|
|
: findMailboxDocIds(syncServer, space);
|
|
|
|
let threads: Array<{
|
|
id: string; mailboxId: string; subject: string;
|
|
fromAddress: string | null; fromName: string | null;
|
|
status: string; isRead: boolean; isStarred: boolean;
|
|
receivedAt: number;
|
|
}> = [];
|
|
|
|
for (const docId of docIds) {
|
|
const doc = syncServer.getDoc<MailboxDoc>(docId);
|
|
if (!doc?.threads) continue;
|
|
for (const t of Object.values(doc.threads)) {
|
|
threads.push({
|
|
id: t.id,
|
|
mailboxId: t.mailboxId,
|
|
subject: t.subject,
|
|
fromAddress: t.fromAddress,
|
|
fromName: t.fromName,
|
|
status: t.status,
|
|
isRead: t.isRead,
|
|
isStarred: t.isStarred,
|
|
receivedAt: t.createdAt,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (status) threads = threads.filter(t => t.status === status);
|
|
if (search) {
|
|
const q = search.toLowerCase();
|
|
threads = threads.filter(t =>
|
|
t.subject.toLowerCase().includes(q) ||
|
|
(t.fromAddress && t.fromAddress.toLowerCase().includes(q)) ||
|
|
(t.fromName && t.fromName.toLowerCase().includes(q)),
|
|
);
|
|
}
|
|
|
|
threads.sort((a, b) => b.receivedAt - a.receivedAt);
|
|
threads = threads.slice(0, limit || 50);
|
|
|
|
return { content: [{ type: "text", text: JSON.stringify(threads, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"rinbox_get_thread",
|
|
"Get a full email thread including body text (omits bodyHtml for size)",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
thread_id: z.string().describe("Thread ID"),
|
|
},
|
|
async ({ space, token, thread_id }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
for (const docId of findMailboxDocIds(syncServer, space)) {
|
|
const doc = syncServer.getDoc<MailboxDoc>(docId);
|
|
const thread = doc?.threads?.[thread_id];
|
|
if (thread) {
|
|
// Omit bodyHtml to reduce payload size
|
|
const { bodyHtml, ...safe } = thread;
|
|
return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] };
|
|
}
|
|
}
|
|
|
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Thread not found" }) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"rinbox_list_approvals",
|
|
"List pending email approvals (draft reviews awaiting signatures)",
|
|
{
|
|
space: z.string().describe("Space slug"),
|
|
token: z.string().describe("JWT auth token"),
|
|
mailbox_slug: z.string().optional().describe("Filter by mailbox slug"),
|
|
},
|
|
async ({ space, token, mailbox_slug }) => {
|
|
const access = await resolveAccess(token, space, false, true);
|
|
if (!access.allowed) return accessDeniedResponse(access.reason!);
|
|
|
|
const docIds = mailbox_slug
|
|
? findMailboxDocIds(syncServer, space).filter(id => id.endsWith(`:${mailbox_slug}`))
|
|
: findMailboxDocIds(syncServer, space);
|
|
|
|
const approvals = [];
|
|
for (const docId of docIds) {
|
|
const doc = syncServer.getDoc<MailboxDoc>(docId);
|
|
if (!doc?.approvals) continue;
|
|
for (const a of Object.values(doc.approvals)) {
|
|
if (a.status !== "pending") continue;
|
|
approvals.push({
|
|
id: a.id,
|
|
mailboxId: a.mailboxId,
|
|
subject: a.subject,
|
|
toAddresses: a.toAddresses,
|
|
authorId: a.authorId,
|
|
requiredSignatures: a.requiredSignatures,
|
|
currentSignatures: a.signatures?.length ?? 0,
|
|
createdAt: a.createdAt,
|
|
});
|
|
}
|
|
}
|
|
|
|
return { content: [{ type: "text", text: JSON.stringify(approvals, null, 2) }] };
|
|
},
|
|
);
|
|
}
|