3 Commits

3 changed files with 249 additions and 240 deletions

View File

@@ -0,0 +1,7 @@
{
"access_token": "ya29.a0AT3oNZ9Db5cbvOq43MccPAw3bB6dmyKxfHkvLlHvvAnMGd-fQqyHYa0raFy5Tx0G0iJysQz_Hb9A-POPTp3_KJ7-6KpovEJPsgoTxy_jKlt81aa2CKKFWZlAClrvldH4I5XTJt6hyrDwu8fFvfBrpoqbVe0FK9yJWIMua3GAfChFjEebPMIqGF7VCwGjhXNMdOikNp_5SQaCgYKAagSARMSFQHGX2Miikn-iDccI4e7x_vlkScaaw0209",
"refresh_token": "1//0fD27YPvUVJODCgYIARAAGA8SNwF-L9IrSeT9ypacVvfAmW0K7J6M7PS4o4i4LBBLKwgm54lvNs1-VyIWnYav_88jWcP3Ugb_7Xw",
"scope": "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/gmail.modify",
"token_type": "Bearer",
"expiry_date": 1783036437865
}

View File

@@ -2,7 +2,7 @@ You are an expert Executive Assistant AI specialized in email management and pro
I have been applying for jobs, which generates a large volume of responses. I have been applying for jobs, which generates a large volume of responses.
Review, in reverse chronoloical order, the first 150 emails in my inbox. Apply the following instructions to those emails. Review the first 200 emails in my inbox. Apply the following instructions to those emails.
STEP 1: Constraints STEP 1: Constraints
- Do not make up information. - Do not make up information.
@@ -25,7 +25,9 @@ STEP 3: DELETE emails classified as Category A and Category C.
STEP 4: ("Anti step") - NEVER DELETE Category B emails. STEP 4: ("Anti step") - NEVER DELETE Category B emails.
STEP 5: SUMMARIZE emails classified as Category B and Category D. For each, include: STEP 5: Confirm via natrual lanugage output in the cat, the total number of emails fetched for review.
STEP 6: SUMMARIZE emails classified as Category B and Category D. For each, include:
1. Sender name and email address 1. Sender name and email address
2. Email subject line 2. Email subject line
3. Date and time received 3. Date and time received

View File

@@ -3,303 +3,303 @@ import { gmail_v1 } from "googleapis";
import fs from "fs"; import fs from "fs";
import { server } from "../McpServer.js"; import { server } from "../McpServer.js";
import { import {
accountSchema, accountSchema,
getGmailClient, getGmailClient,
getSummaryPath, getSummaryPath,
loadClassificationPrompt, loadClassificationPrompt,
loadActionPrompt, loadActionPrompt,
} from "../loaders/prompt-config-loaders.js"; } from "../loaders/prompt-config-loaders.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Email parsing helpers // Email parsing helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const getHeader = ( export const getHeader = (
headers: gmail_v1.Schema$MessagePartHeader[] | undefined, headers: gmail_v1.Schema$MessagePartHeader[] | undefined,
name: string name: string
): string => { ): string => {
if (!headers) return ""; if (!headers) return "";
const header = headers.find( const header = headers.find(
(h) => h.name?.toLowerCase() === name.toLowerCase() (h) => h.name?.toLowerCase() === name.toLowerCase()
); );
return header?.value ?? ""; return header?.value ?? "";
}; };
export const decodeBody = (message: gmail_v1.Schema$Message): string => { export const decodeBody = (message: gmail_v1.Schema$Message): string => {
const parts = message.payload?.parts; const parts = message.payload?.parts;
let encoded = ""; let encoded = "";
if (parts) { if (parts) {
const textPart = parts.find((p) => p.mimeType === "text/plain"); const textPart = parts.find((p) => p.mimeType === "text/plain");
encoded = textPart?.body?.data ?? ""; encoded = textPart?.body?.data ?? "";
if (!encoded) {
const htmlPart = parts.find((p) => p.mimeType === "text/html");
encoded = htmlPart?.body?.data ?? "";
}
} else {
encoded = message.payload?.body?.data ?? "";
}
if (!encoded) { if (!encoded) {
const htmlPart = parts.find((p) => p.mimeType === "text/html"); return message.snippet ?? "";
encoded = htmlPart?.body?.data ?? "";
} }
} else {
encoded = message.payload?.body?.data ?? "";
}
if (!encoded) { return Buffer.from(encoded, "base64url").toString("utf-8");
return message.snippet ?? "";
}
return Buffer.from(encoded, "base64url").toString("utf-8");
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tool: fetch_new_emails // Tool: fetch_new_emails
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
server.registerTool( server.registerTool(
"fetch_new_emails", "fetch_new_emails",
{ {
description: description:
"Fetch unread emails from a Gmail inbox. Returns sender, date, " + "Fetch unread emails from a Gmail inbox. Returns sender, date, " +
"subject, message ID, and body text for each message. The message IDs " + "subject, message ID, and body text for each message. The message IDs " +
"can be passed to delete_emails later. Specify which account to fetch from.", "can be passed to delete_emails later. Specify which account to fetch from.",
inputSchema: { inputSchema: {
account: accountSchema, account: accountSchema,
maxResults: z maxResults: z
.number() .number()
.min(1) .min(1)
.max(100) .max(200)
.describe("Maximum number of unread emails to fetch (1-100)"), .describe("Maximum number of unread emails to fetch (1-200)"),
},
}, },
}, async ({ account, maxResults }) => {
async ({ account, maxResults }) => { try {
try { const gmail = getGmailClient(account);
const gmail = getGmailClient(account);
const listResponse = await gmail.users.messages.list({ const listResponse = await gmail.users.messages.list({
userId: "me", userId: "me",
q: "is:unread", q: "is:unread",
maxResults, maxResults,
}); });
const messageIds = listResponse.data.messages ?? []; const messageIds = listResponse.data.messages ?? [];
if (messageIds.length === 0) { if (messageIds.length === 0) {
return { return {
content: [{ type: "text" as const, text: "No unread emails found." }], content: [{ type: "text" as const, text: "No unread emails found." }],
}; };
} }
const emails: string[] = []; const emails: string[] = [];
for (const msg of messageIds) { for (const msg of messageIds) {
const detail = await gmail.users.messages.get({ const detail = await gmail.users.messages.get({
userId: "me", userId: "me",
id: msg.id!, id: msg.id!,
format: "full", format: "full",
}); });
const headers = detail.data.payload?.headers; const headers = detail.data.payload?.headers;
const from = getHeader(headers, "From"); const from = getHeader(headers, "From");
const subject = getHeader(headers, "Subject"); const subject = getHeader(headers, "Subject");
const date = getHeader(headers, "Date"); const date = getHeader(headers, "Date");
const body = decodeBody(detail.data); const body = decodeBody(detail.data);
const truncatedBody = const truncatedBody =
body.length > 2000 ? body.substring(0, 2000) + "\n[...truncated]" : body; body.length > 2000 ? body.substring(0, 2000) + "\n[...truncated]" : body;
emails.push( emails.push(
[ [
`MESSAGE_ID: ${msg.id}`, `MESSAGE_ID: ${msg.id}`,
`FROM: ${from}`, `FROM: ${from}`,
`DATE: ${date}`, `DATE: ${date}`,
`SUBJECT: ${subject}`, `SUBJECT: ${subject}`,
`BODY:\n${truncatedBody}`, `BODY:\n${truncatedBody}`,
].join("\n") ].join("\n")
); );
} }
const classificationInstructions = loadClassificationPrompt(); const classificationInstructions = loadClassificationPrompt();
const actionInstructions = loadActionPrompt(); const actionInstructions = loadActionPrompt();
const instructionsBlock = const instructionsBlock =
(classificationInstructions (classificationInstructions
? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS (PHASE 1):\n${"=".repeat(60)}\n${classificationInstructions}` ? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS (PHASE 1):\n${"=".repeat(60)}\n${classificationInstructions}`
: "") + : "") +
(actionInstructions (actionInstructions
? `\n\n${"=".repeat(60)}\nACTION INSTRUCTIONS (PHASE 2):\n${"=".repeat(60)}\n${actionInstructions}` ? `\n\n${"=".repeat(60)}\nACTION INSTRUCTIONS (PHASE 2):\n${"=".repeat(60)}\n${actionInstructions}`
: ""); : "");
return { return {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: text:
`Found ${emails.length} unread email(s):\n\n` + `Found ${emails.length} unread email(s):\n\n` +
`${"=".repeat(60)}\n${emails.join(`\n${"=".repeat(60)}\n`)}` + `${"=".repeat(60)}\n${emails.join(`\n${"=".repeat(60)}\n`)}` +
instructionsBlock, instructionsBlock,
}, },
], ],
}; };
} catch (error) { } catch (error) {
const errMsg = error instanceof Error ? error.message : String(error); const errMsg = error instanceof Error ? error.message : String(error);
return { return {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: `Error fetching emails: ${errMsg}`, text: `Error fetching emails: ${errMsg}`,
}, },
], ],
}; };
}
} }
}
); );
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tool: delete_emails // Tool: delete_emails
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
server.registerTool( server.registerTool(
"delete_emails", "delete_emails",
{ {
description: description:
"Move emails to trash by their Gmail message IDs. Use this for " + "Move emails to trash by their Gmail message IDs. Use this for " +
"category A (acknowledgements) and category C (rejections) emails. " + "category A (acknowledgements) and category C (rejections) emails. " +
"Specify which account the emails belong to.", "Specify which account the emails belong to.",
inputSchema: { inputSchema: {
account: accountSchema, account: accountSchema,
messageIds: z messageIds: z
.array(z.string()) .array(z.string())
.describe("Array of Gmail message IDs to move to trash"), .describe("Array of Gmail message IDs to move to trash"),
},
}, },
}, async ({ account, messageIds }) => {
async ({ account, messageIds }) => {
try {
const gmail = getGmailClient(account);
const results: string[] = [];
for (const id of messageIds) {
try { try {
await gmail.users.messages.trash({ const gmail = getGmailClient(account);
userId: "me", const results: string[] = [];
id,
});
results.push(`Trashed: ${id}`);
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
results.push(`Failed to trash ${id}: ${errMsg}`);
}
}
return { for (const id of messageIds) {
content: [ try {
{ await gmail.users.messages.trash({
type: "text" as const, userId: "me",
text: `Delete results:\n${results.join("\n")}`, id,
}, });
], results.push(`Trashed: ${id}`);
}; } catch (err) {
} catch (error) { const errMsg = err instanceof Error ? err.message : String(err);
const errMsg = error instanceof Error ? error.message : String(error); results.push(`Failed to trash ${id}: ${errMsg}`);
return { }
content: [ }
{
type: "text" as const, return {
text: `Error deleting emails: ${errMsg}`, content: [
}, {
], type: "text" as const,
}; text: `Delete results:\n${results.join("\n")}`,
},
],
};
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: `Error deleting emails: ${errMsg}`,
},
],
};
}
} }
}
); );
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tool: append_to_summary // Tool: append_to_summary
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
interface SummaryEntry { interface SummaryEntry {
senderName: string; senderName: string;
senderEmail: string; senderEmail: string;
dateReceived: string; dateReceived: string;
subject: string; subject: string;
category: "B" | "D"; category: "B" | "D";
addedAt: string; addedAt: string;
} }
server.registerTool( server.registerTool(
"append_to_summary", "append_to_summary",
{ {
description: description:
"Append classified email entries to the local summary file. Use this " + "Append classified email entries to the local summary file. Use this " +
"for category B (advancement to next step) and category D (other) emails. " + "for category B (advancement to next step) and category D (other) emails. " +
"Each entry records the sender, date, subject, and category. " + "Each entry records the sender, date, subject, and category. " +
"Specify which account the emails belong to.", "Specify which account the emails belong to.",
inputSchema: { inputSchema: {
account: accountSchema, account: accountSchema,
entries: z entries: z
.array( .array(
z.object({ z.object({
senderName: z.string().describe("Name of the sender"), senderName: z.string().describe("Name of the sender"),
senderEmail: z.string().describe("Email address of the sender"), senderEmail: z.string().describe("Email address of the sender"),
dateReceived: z dateReceived: z
.string() .string()
.describe("Date and time the email was received"), .describe("Date and time the email was received"),
subject: z.string().describe("Email subject line"), subject: z.string().describe("Email subject line"),
category: z category: z
.enum(["B", "D"]) .enum(["B", "D"])
.describe( .describe(
"Category: B = advancement to next step, D = other/uncategorized" "Category: B = advancement to next step, D = other/uncategorized"
), ),
}) })
) )
.describe("Array of email summary entries to append"), .describe("Array of email summary entries to append"),
},
}, },
}, async ({ account, entries }) => {
async ({ account, entries }) => { try {
try { const summaryPath = getSummaryPath(account);
const summaryPath = getSummaryPath(account);
let summary: SummaryEntry[] = []; let summary: SummaryEntry[] = [];
if (fs.existsSync(summaryPath)) { if (fs.existsSync(summaryPath)) {
summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8")); summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8"));
} }
const now = new Date(); const now = new Date();
const nowIso = now.toISOString(); const nowIso = now.toISOString();
const newEntries: SummaryEntry[] = entries.map((e) => ({ const newEntries: SummaryEntry[] = entries.map((e) => ({
...e, ...e,
addedAt: nowIso, addedAt: nowIso,
})); }));
summary.push(...newEntries); summary.push(...newEntries);
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000; const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
const cutoff = now.getTime() - RETENTION_MS; const cutoff = now.getTime() - RETENTION_MS;
const beforePurge = summary.length; const beforePurge = summary.length;
summary = summary.filter( summary = summary.filter(
(entry) => new Date(entry.addedAt).getTime() >= cutoff (entry) => new Date(entry.addedAt).getTime() >= cutoff
); );
const purged = beforePurge - summary.length; const purged = beforePurge - summary.length;
fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2)); fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
const purgeNote = purged > 0 const purgeNote = purged > 0
? `\nPurged ${purged} entry/entries older than 30 days.` ? `\nPurged ${purged} entry/entries older than 30 days.`
: ""; : "";
return { return {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: text:
`Appended ${newEntries.length} entry/entries to summary.\n` + `Appended ${newEntries.length} entry/entries to summary.\n` +
`Total entries in summary: ${summary.length}\n` + `Total entries in summary: ${summary.length}\n` +
`Summary file: ${summaryPath}` + `Summary file: ${summaryPath}` +
purgeNote, purgeNote,
}, },
], ],
}; };
} catch (error) { } catch (error) {
const errMsg = error instanceof Error ? error.message : String(error); const errMsg = error instanceof Error ? error.message : String(error);
return { return {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: `Error appending to summary: ${errMsg}`, text: `Error appending to summary: ${errMsg}`,
}, },
], ],
}; };
}
} }
}
); );