(server: McpServer, client: McpClient)
| 5 | import { attachmentsArraySchema } from "./attachments.js"; |
| 6 | |
| 7 | export function registerReviewTools(server: McpServer, client: McpClient): void { |
| 8 | server.registerTool( |
| 9 | "list_pending_messages", |
| 10 | { |
| 11 | title: "List outbound messages awaiting approval", |
| 12 | annotations: { readOnlyHint: true }, |
| 13 | description: |
| 14 | "Use when the user asks what's awaiting approval, or after a `send_message`/`reply_to_message` returned `pending_review` and they want to see the queue. Lists held **outbound** messages (held by the agent's outbound policy or content scan) sorted by soonest-expiring first. Body content is summary-only — call `get_pending_message` for the full draft of one. Read-only; cheap, but don't poll it on a loop. Note: this lists OUTBOUND holds only — a held **inbound** message (screening review) is surfaced by the `email.pending_review` webhook (with its `message_id`), not here, and is resolved with the same `approve_message`/`reject_message` tools.", |
| 15 | inputSchema: strictInputSchema({}), |
| 16 | }, |
| 17 | async () => runTool(async () => ({ messages: await client.listPendingMessages() })), |
| 18 | ); |
| 19 | |
| 20 | server.registerTool( |
| 21 | "get_pending_message", |
| 22 | { |
| 23 | title: "Get a pending-approval message", |
| 24 | annotations: { readOnlyHint: true }, |
| 25 | description: |
| 26 | "Fetch the full draft (subject, recipients, body, attachments) of one outbound message awaiting human approval. Body content is only present while the message is `pending_review` — after a terminal transition the server scrubs it.", |
| 27 | inputSchema: strictInputSchema({ |
| 28 | message_id: z.string().describe("The pending message ID (msg_…)."), |
| 29 | }), |
| 30 | }, |
| 31 | async (args) => runTool(() => client.getPendingMessage(args.message_id)), |
| 32 | ); |
| 33 | |
| 34 | // Map the snake_case approve override args to the SDK's ApproveRequest |
| 35 | // (camelCase body fields). An explicitly-passed empty attachments array |
| 36 | // must survive as a strip override, so map by key-presence. |
| 37 | const mapOverrides = (overrides: { |
| 38 | subject?: string; |
| 39 | body_text?: string; |
| 40 | body_html?: string; |
| 41 | to?: string[]; |
| 42 | cc?: string[]; |
| 43 | bcc?: string[]; |
| 44 | attachments?: Array<{ filename: string; content_type: string; data: string }>; |
| 45 | }) => ({ |
| 46 | ...(overrides.subject !== undefined ? { subject: overrides.subject } : {}), |
| 47 | ...(overrides.body_text !== undefined ? { body: overrides.body_text } : {}), |
| 48 | ...(overrides.body_html !== undefined ? { htmlBody: overrides.body_html } : {}), |
| 49 | ...(overrides.to !== undefined ? { to: overrides.to } : {}), |
| 50 | ...(overrides.cc !== undefined ? { cc: overrides.cc } : {}), |
| 51 | ...(overrides.bcc !== undefined ? { bcc: overrides.bcc } : {}), |
| 52 | ...(overrides.attachments !== undefined |
| 53 | ? { |
| 54 | attachments: overrides.attachments.map((a) => ({ |
| 55 | filename: a.filename, |
| 56 | contentType: a.content_type, |
| 57 | data: a.data, |
| 58 | })), |
| 59 | } |
| 60 | : {}), |
| 61 | }); |
| 62 | |
| 63 | server.registerTool( |
| 64 | "approve_message", |
no test coverage detected