mcpbrowser 0.3.34 → 0.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/actions/click-element.js +8 -3
- package/src/actions/execute-javascript.js +8 -3
- package/src/actions/fetch-page.js +9 -3
- package/src/actions/get-current-html.js +9 -3
- package/src/actions/plugin-action.js +180 -0
- package/src/actions/plugin-info.js +170 -0
- package/src/core/logger.js +3 -7
- package/src/core/plugin-loader.js +344 -0
- package/src/mcp-browser.js +34 -2
- package/src/plugins/_example/index.js +140 -0
- package/src/plugins/gcal/actions/check-availability.js +185 -0
- package/src/plugins/gcal/actions/create-event.js +238 -0
- package/src/plugins/gcal/actions/delete-event.js +138 -0
- package/src/plugins/gcal/actions/edit-event.js +244 -0
- package/src/plugins/gcal/actions/list-events.js +96 -0
- package/src/plugins/gcal/actions/read-event.js +174 -0
- package/src/plugins/gcal/actions/rsvp-event.js +149 -0
- package/src/plugins/gcal/actions/search-events.js +121 -0
- package/src/plugins/gcal/helpers.js +415 -0
- package/src/plugins/gcal/index.js +148 -0
- package/src/plugins/gcal/selectors.js +54 -0
- package/src/plugins/gmail/actions/archive-email.js +65 -0
- package/src/plugins/gmail/actions/compose-email.js +116 -0
- package/src/plugins/gmail/actions/delete-email.js +65 -0
- package/src/plugins/gmail/actions/forward-email.js +95 -0
- package/src/plugins/gmail/actions/label-email.js +107 -0
- package/src/plugins/gmail/actions/list-emails.js +61 -0
- package/src/plugins/gmail/actions/mark-read.js +71 -0
- package/src/plugins/gmail/actions/mark-unread.js +71 -0
- package/src/plugins/gmail/actions/read-email.js +149 -0
- package/src/plugins/gmail/actions/reply-email.js +87 -0
- package/src/plugins/gmail/actions/search-emails.js +95 -0
- package/src/plugins/gmail/helpers.js +419 -0
- package/src/plugins/gmail/index.js +195 -0
- package/src/plugins/gmail/selectors.js +82 -0
- package/src/plugins.json +4 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* helpers.js — Shared tiered utilities for the Gmail plugin.
|
|
3
|
+
*
|
|
4
|
+
* Provides URL-based navigation (T1), keyboard shortcut verification (T2),
|
|
5
|
+
* ARIA/data-attr DOM utilities (T3), and CSS-selector data extraction (T4).
|
|
6
|
+
*
|
|
7
|
+
* All helpers are stateless — they inspect the page at invocation time
|
|
8
|
+
* per FR-017. No internal state is maintained between calls.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { MCPResponse } from '../../core/responses.js';
|
|
12
|
+
import * as sel from './selectors.js';
|
|
13
|
+
import logger from '../../core/logger.js';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// CONSTANTS
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/** Default timeout for waiting on Gmail content (FR-012) */
|
|
20
|
+
export const DEFAULT_TIMEOUT = 10_000;
|
|
21
|
+
|
|
22
|
+
/** Gmail view states detected from URL hash + DOM (FR-024) */
|
|
23
|
+
export const VIEW = Object.freeze({
|
|
24
|
+
EMAIL_LIST: 'email_list',
|
|
25
|
+
THREAD: 'thread',
|
|
26
|
+
COMPOSE: 'compose',
|
|
27
|
+
SEARCH_RESULTS: 'search_results',
|
|
28
|
+
LOADING: 'loading',
|
|
29
|
+
NOT_GMAIL: 'not_gmail',
|
|
30
|
+
NOT_READY: 'not_ready'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Standard Gmail folder hashes */
|
|
34
|
+
const STANDARD_FOLDERS = new Set([
|
|
35
|
+
'inbox', 'sent', 'drafts', 'trash', 'spam', 'starred', 'all', 'important'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// T1: URL-BASED NAVIGATION (FR-020)
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract the Google account index from a Gmail URL.
|
|
44
|
+
* Handles /u/0/, /u/1/, /u/2/, etc. Defaults to '0' if not found.
|
|
45
|
+
* @param {string} url - Current page URL
|
|
46
|
+
* @returns {string} Account index (e.g., '0', '1', '2')
|
|
47
|
+
*/
|
|
48
|
+
export function getAccountIndex(url) {
|
|
49
|
+
const match = url.match(/\/u\/(\d+)\//);
|
|
50
|
+
return match ? match[1] : '0';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Navigate to a Gmail URL hash, preserving the current account index.
|
|
55
|
+
* @param {import('puppeteer-core').Page} page
|
|
56
|
+
* @param {string} hash - Hash fragment (e.g., '#inbox', '#search/query', '#inbox/threadId')
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
export async function gmailNavigate(page, hash) {
|
|
60
|
+
const currentUrl = page.url();
|
|
61
|
+
const accountIndex = getAccountIndex(currentUrl);
|
|
62
|
+
const targetUrl = `https://mail.google.com/mail/u/${accountIndex}/${hash}`;
|
|
63
|
+
logger.debug(`gmailNavigate: ${targetUrl}`);
|
|
64
|
+
await page.goto(targetUrl, { waitUntil: 'networkidle2', timeout: DEFAULT_TIMEOUT });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a Gmail URL hash for a folder name.
|
|
69
|
+
* Standard folders use #folder, custom labels use #label/Name.
|
|
70
|
+
* @param {string} folder - Folder name (inbox, sent, etc.) or label name
|
|
71
|
+
* @returns {string} Hash fragment
|
|
72
|
+
*/
|
|
73
|
+
export function folderToHash(folder) {
|
|
74
|
+
const lower = folder.toLowerCase();
|
|
75
|
+
if (STANDARD_FOLDERS.has(lower)) {
|
|
76
|
+
return `#${lower}`;
|
|
77
|
+
}
|
|
78
|
+
return `#label/${encodeURIComponent(folder)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// VIEW DETECTION (FR-024 — URL hash primary, DOM fallback)
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detect the current Gmail view by inspecting the URL hash first (T1),
|
|
87
|
+
* then falling back to DOM inspection for ambiguous cases (T3).
|
|
88
|
+
* Also detects CAPTCHA/interstitial states returning NOT_READY.
|
|
89
|
+
* @param {import('puppeteer-core').Page} page
|
|
90
|
+
* @returns {Promise<string>} One of VIEW enum values
|
|
91
|
+
*/
|
|
92
|
+
export async function detectView(page) {
|
|
93
|
+
const url = page.url();
|
|
94
|
+
|
|
95
|
+
// Not Gmail at all
|
|
96
|
+
if (!url.includes('mail.google.com')) {
|
|
97
|
+
logger.debug('detectView: not Gmail');
|
|
98
|
+
return VIEW.NOT_GMAIL;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for CAPTCHA / security interstitial (edge case)
|
|
102
|
+
const hasInterstitial = await page.evaluate(() => {
|
|
103
|
+
const body = document.body?.textContent || '';
|
|
104
|
+
return body.includes('Confirm it') ||
|
|
105
|
+
!!document.querySelector('iframe[src*="accounts.google.com"]') ||
|
|
106
|
+
!!document.querySelector('#captcha');
|
|
107
|
+
});
|
|
108
|
+
if (hasInterstitial) {
|
|
109
|
+
logger.debug('detectView: not_ready (interstitial/CAPTCHA)');
|
|
110
|
+
return VIEW.NOT_READY;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Parse URL hash
|
|
114
|
+
const hashMatch = url.match(/#(.+)/);
|
|
115
|
+
const hash = hashMatch ? hashMatch[1] : '';
|
|
116
|
+
|
|
117
|
+
// Check for compose overlay via DOM (compose can appear over any view)
|
|
118
|
+
const hasComposeDialog = await page.evaluate(() =>
|
|
119
|
+
!!document.querySelector('div[role="dialog"]')
|
|
120
|
+
);
|
|
121
|
+
if (hasComposeDialog) {
|
|
122
|
+
logger.debug('detectView: compose (dialog overlay)');
|
|
123
|
+
return VIEW.COMPOSE;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Search results
|
|
127
|
+
if (hash.startsWith('search/')) {
|
|
128
|
+
logger.debug('detectView: search_results');
|
|
129
|
+
return VIEW.SEARCH_RESULTS;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Thread view — hash contains folder/threadId pattern
|
|
133
|
+
if (/^(inbox|sent|all|drafts|trash|spam|starred|important|label\/[^/]+)\/[A-Za-z0-9]+/.test(hash)) {
|
|
134
|
+
logger.debug('detectView: thread');
|
|
135
|
+
return VIEW.THREAD;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Standard folder or label view
|
|
139
|
+
if (hash === '' || hash === 'inbox' || STANDARD_FOLDERS.has(hash) || hash.startsWith('label/')) {
|
|
140
|
+
// Check if content has loaded
|
|
141
|
+
const hasMain = await page.evaluate(() =>
|
|
142
|
+
!!document.querySelector('div[role="main"]')
|
|
143
|
+
);
|
|
144
|
+
if (!hasMain) {
|
|
145
|
+
logger.debug('detectView: loading');
|
|
146
|
+
return VIEW.LOADING;
|
|
147
|
+
}
|
|
148
|
+
logger.debug('detectView: email_list');
|
|
149
|
+
return VIEW.EMAIL_LIST;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fallback — check if main content exists
|
|
153
|
+
const hasMain = await page.evaluate(() =>
|
|
154
|
+
!!document.querySelector('div[role="main"]')
|
|
155
|
+
);
|
|
156
|
+
if (hasMain) {
|
|
157
|
+
logger.debug('detectView: email_list (fallback)');
|
|
158
|
+
return VIEW.EMAIL_LIST;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
logger.debug('detectView: loading (no main container)');
|
|
162
|
+
return VIEW.LOADING;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// T2: KEYBOARD SHORTCUT VERIFICATION (FR-019)
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check whether Gmail keyboard shortcuts are enabled.
|
|
171
|
+
* Sends '?' to trigger the shortcuts help dialog, then detects it.
|
|
172
|
+
* @param {import('puppeteer-core').Page} page
|
|
173
|
+
* @returns {Promise<{enabled: boolean, error?: string}>}
|
|
174
|
+
*/
|
|
175
|
+
export async function checkKeyboardShortcuts(page) {
|
|
176
|
+
try {
|
|
177
|
+
// Ensure no input is focused before sending shortcut
|
|
178
|
+
await page.evaluate(() => {
|
|
179
|
+
if (document.activeElement && document.activeElement.tagName !== 'BODY') {
|
|
180
|
+
document.activeElement.blur();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Send ? (Shift+/) to open shortcuts help
|
|
185
|
+
await page.keyboard.down('Shift');
|
|
186
|
+
await page.keyboard.press('/');
|
|
187
|
+
await page.keyboard.up('Shift');
|
|
188
|
+
|
|
189
|
+
// Wait briefly for the dialog
|
|
190
|
+
const dialog = await page.waitForSelector(
|
|
191
|
+
'div[role="dialog"]',
|
|
192
|
+
{ timeout: 2000 }
|
|
193
|
+
).catch(() => null);
|
|
194
|
+
|
|
195
|
+
if (dialog) {
|
|
196
|
+
// Close the help dialog
|
|
197
|
+
await page.keyboard.press('Escape');
|
|
198
|
+
return { enabled: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
enabled: false,
|
|
203
|
+
error: 'Gmail keyboard shortcuts are disabled. Enable them in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.'
|
|
204
|
+
};
|
|
205
|
+
} catch {
|
|
206
|
+
return {
|
|
207
|
+
enabled: false,
|
|
208
|
+
error: 'Could not verify Gmail keyboard shortcuts. Ensure Gmail is fully loaded.'
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// PRECONDITION CHECKING (FR-025)
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validate a precondition before sending a keyboard shortcut.
|
|
219
|
+
* Fails early with an actionable error if the precondition is not met.
|
|
220
|
+
* @param {import('puppeteer-core').Page} page
|
|
221
|
+
* @param {'on_gmail'|'thread_open'|'list_view'} requirement
|
|
222
|
+
* @returns {Promise<{met: boolean, error?: string, suggestion?: string}>}
|
|
223
|
+
*/
|
|
224
|
+
export async function checkPrecondition(page, requirement) {
|
|
225
|
+
const url = page.url();
|
|
226
|
+
|
|
227
|
+
switch (requirement) {
|
|
228
|
+
case 'on_gmail': {
|
|
229
|
+
if (!url.includes('mail.google.com')) {
|
|
230
|
+
return {
|
|
231
|
+
met: false,
|
|
232
|
+
error: 'Gmail is not the active page.',
|
|
233
|
+
suggestion: "Use fetch_webpage({ url: 'https://mail.google.com' }) to navigate to Gmail first."
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return { met: true };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'thread_open': {
|
|
240
|
+
const hash = url.match(/#(.+)/)?.[1] || '';
|
|
241
|
+
const isThread = /^(inbox|sent|all|drafts|trash|spam|starred|important|label\/[^/]+)\/[A-Za-z0-9]+/.test(hash);
|
|
242
|
+
if (!isThread) {
|
|
243
|
+
return {
|
|
244
|
+
met: false,
|
|
245
|
+
error: 'No email thread is currently open.',
|
|
246
|
+
suggestion: "Use plugin_action({ plugin: 'gmail', action: 'read_email', params: { index: 0 } }) to open an email first."
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { met: true };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'list_view': {
|
|
253
|
+
const view = await detectView(page);
|
|
254
|
+
if (view !== VIEW.EMAIL_LIST && view !== VIEW.SEARCH_RESULTS) {
|
|
255
|
+
return {
|
|
256
|
+
met: false,
|
|
257
|
+
error: 'Not in email list view.',
|
|
258
|
+
suggestion: "Use plugin_action({ plugin: 'gmail', action: 'list_emails' }) to return to the email list."
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return { met: true };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
default:
|
|
265
|
+
return { met: false, error: `Unknown precondition: ${requirement}` };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// CONTENT WAITING (FR-012 — 10s timeout with diagnostics)
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Wait for a Gmail element to appear, with timeout and diagnostic error.
|
|
275
|
+
* Timeout errors include the selector name per Constitution IV.
|
|
276
|
+
* @param {import('puppeteer-core').Page} page
|
|
277
|
+
* @param {string} selector - CSS selector to wait for
|
|
278
|
+
* @param {number} [timeout=DEFAULT_TIMEOUT]
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
* @throws {Error} With selector name and tier in message on timeout
|
|
281
|
+
*/
|
|
282
|
+
export async function waitForGmail(page, selector, timeout = DEFAULT_TIMEOUT) {
|
|
283
|
+
try {
|
|
284
|
+
await page.waitForSelector(selector, { timeout });
|
|
285
|
+
} catch {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Gmail content did not load within ${timeout}ms. ` +
|
|
288
|
+
`Selector that failed: "${selector}". ` +
|
|
289
|
+
`The page may still be loading or Gmail's UI may have changed.`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ============================================================================
|
|
295
|
+
// EMAIL ROW SELECTION — Hybrid DOM+keyboard (FR-016)
|
|
296
|
+
// ============================================================================
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Locate and select an email row by ID or positional index.
|
|
300
|
+
* Uses T3 (data-legacy-message-id, role="checkbox") with T4 fallback.
|
|
301
|
+
* Clicks the row's checkbox to select it for keyboard actions.
|
|
302
|
+
* @param {import('puppeteer-core').Page} page
|
|
303
|
+
* @param {{ index?: number, id?: string }} target
|
|
304
|
+
* @returns {Promise<{selected: boolean, error?: string}>}
|
|
305
|
+
*/
|
|
306
|
+
export async function selectEmailRow(page, { index, id } = {}) {
|
|
307
|
+
if (id !== undefined && id !== null) {
|
|
308
|
+
// T3: Prefer ID-based selection via data attribute
|
|
309
|
+
const row = await page.$(`tr[data-legacy-message-id="${id}"]`);
|
|
310
|
+
if (row) {
|
|
311
|
+
const checkbox = await row.$('div[role="checkbox"]');
|
|
312
|
+
if (checkbox) {
|
|
313
|
+
await checkbox.click();
|
|
314
|
+
logger.debug(`selectEmailRow: selected by ID "${id}"`);
|
|
315
|
+
return { selected: true };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
logger.debug(`selectEmailRow: ID "${id}" not found, trying index fallback`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (index !== undefined && index !== null) {
|
|
322
|
+
// T4: Positional index via CSS class selector
|
|
323
|
+
const rows = await page.$$(sel.EMAIL_ROW);
|
|
324
|
+
if (index >= 0 && index < rows.length) {
|
|
325
|
+
const checkbox = await rows[index].$('div[role="checkbox"]');
|
|
326
|
+
if (checkbox) {
|
|
327
|
+
await checkbox.click();
|
|
328
|
+
logger.debug(`selectEmailRow: selected by index ${index}`);
|
|
329
|
+
return { selected: true };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
selected: false,
|
|
334
|
+
error: `Email index ${index} is out of range. There are ${(await page.$$(sel.EMAIL_ROW)).length} visible emails.`
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { selected: false, error: 'No index or id provided for email selection.' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ============================================================================
|
|
342
|
+
// EMAIL ROW EXTRACTION — T3+T4 data extraction
|
|
343
|
+
// ============================================================================
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Extract structured email summary data from visible email rows.
|
|
347
|
+
* Uses T3 (span[email], [data-legacy-message-id]) and T4 (CSS selectors).
|
|
348
|
+
* @param {import('puppeteer-core').Page} page
|
|
349
|
+
* @param {number} [limit=25]
|
|
350
|
+
* @returns {Promise<Array>} Array of EmailSummary objects per data-model.md
|
|
351
|
+
*/
|
|
352
|
+
export async function extractEmailRows(page, limit = 25) {
|
|
353
|
+
return page.evaluate((selectors, lim) => {
|
|
354
|
+
const rows = document.querySelectorAll(selectors.emailRow);
|
|
355
|
+
const results = [];
|
|
356
|
+
const count = Math.min(rows.length, lim);
|
|
357
|
+
|
|
358
|
+
for (let i = 0; i < count; i++) {
|
|
359
|
+
const row = rows[i];
|
|
360
|
+
// T3: span[email] for sender data
|
|
361
|
+
const senderEl = row.querySelector('span[email]');
|
|
362
|
+
// T4: CSS class selectors for subject, snippet, date
|
|
363
|
+
const subjectEl = row.querySelector(selectors.subjectSpan);
|
|
364
|
+
const snippetEl = row.querySelector(selectors.snippetSpan);
|
|
365
|
+
const dateEl = row.querySelector(selectors.dateCell);
|
|
366
|
+
// T4: Unread detection via class
|
|
367
|
+
const isUnread = row.classList.contains(selectors.unreadClass);
|
|
368
|
+
// T3: data attribute for ID
|
|
369
|
+
const id = row.getAttribute('data-legacy-message-id') || undefined;
|
|
370
|
+
|
|
371
|
+
results.push({
|
|
372
|
+
index: i,
|
|
373
|
+
id,
|
|
374
|
+
sender: senderEl?.getAttribute('name') || senderEl?.textContent?.trim() || '',
|
|
375
|
+
senderEmail: senderEl?.getAttribute('email') || '',
|
|
376
|
+
subject: subjectEl?.textContent?.trim() || '',
|
|
377
|
+
snippet: snippetEl?.textContent?.trim() || '',
|
|
378
|
+
date: dateEl?.getAttribute('title') || dateEl?.textContent?.trim() || '',
|
|
379
|
+
isUnread
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return results;
|
|
383
|
+
}, {
|
|
384
|
+
emailRow: sel.EMAIL_ROW,
|
|
385
|
+
subjectSpan: sel.SUBJECT_SPAN,
|
|
386
|
+
snippetSpan: sel.SNIPPET_SPAN,
|
|
387
|
+
dateCell: sel.DATE_CELL,
|
|
388
|
+
unreadClass: sel.EMAIL_ROW_UNREAD
|
|
389
|
+
}, limit);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ============================================================================
|
|
393
|
+
// RESPONSE CLASS
|
|
394
|
+
// ============================================================================
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Gmail-specific response class extending MCPResponse.
|
|
398
|
+
* Carries structured email data alongside nextSteps.
|
|
399
|
+
*/
|
|
400
|
+
export class GmailActionResponse extends MCPResponse {
|
|
401
|
+
/**
|
|
402
|
+
* @param {object} data - Structured data payload
|
|
403
|
+
* @param {string} summary - Human-readable summary text
|
|
404
|
+
* @param {string[]} nextSteps - Suggested next actions
|
|
405
|
+
*/
|
|
406
|
+
constructor(data, summary, nextSteps) {
|
|
407
|
+
super(nextSteps);
|
|
408
|
+
this.data = data;
|
|
409
|
+
this._summary = summary;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_getAdditionalFields() {
|
|
413
|
+
return { ...this.data };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
getTextSummary() {
|
|
417
|
+
return this._summary;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail plugin — Site-specific automation for Gmail (mail.google.com).
|
|
3
|
+
* Implements the MCPBrowser plugin interface (interfaceVersion 1).
|
|
4
|
+
*
|
|
5
|
+
* Uses a tiered interaction strategy (FR-011):
|
|
6
|
+
* T1: URL hash navigation for folders/search
|
|
7
|
+
* T2: Keyboard shortcuts for actions (compose, reply, archive, etc.)
|
|
8
|
+
* T3: ARIA / data attributes / name attrs for data extraction and form filling
|
|
9
|
+
* T4: CSS class selectors (last resort, centralized in selectors.js)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { listEmails } from './actions/list-emails.js';
|
|
13
|
+
import { readEmail } from './actions/read-email.js';
|
|
14
|
+
import { searchEmails } from './actions/search-emails.js';
|
|
15
|
+
import { composeEmail } from './actions/compose-email.js';
|
|
16
|
+
import { replyEmail } from './actions/reply-email.js';
|
|
17
|
+
import { forwardEmail } from './actions/forward-email.js';
|
|
18
|
+
import { archiveEmail } from './actions/archive-email.js';
|
|
19
|
+
import { deleteEmail } from './actions/delete-email.js';
|
|
20
|
+
import { labelEmail } from './actions/label-email.js';
|
|
21
|
+
import { markRead } from './actions/mark-read.js';
|
|
22
|
+
import { markUnread } from './actions/mark-unread.js';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// MANIFEST
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export const manifest = {
|
|
29
|
+
name: "gmail",
|
|
30
|
+
version: "1.0.0",
|
|
31
|
+
description: "Gmail plugin for MCPBrowser — email management with hybrid UI resilience (URL navigation, keyboard shortcuts, ARIA selectors, CSS fallback)",
|
|
32
|
+
interfaceVersion: 1,
|
|
33
|
+
urlPatterns: ["mail.google.com"],
|
|
34
|
+
domPatterns: ["div[data-ogsr-up]", ".aH2"]
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// DETECTION
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Detect whether this plugin is applicable for the given page.
|
|
43
|
+
* @param {string} url - Current page URL
|
|
44
|
+
* @param {string} html - Extracted page HTML
|
|
45
|
+
* @returns {{ matched: boolean, confidence?: number }}
|
|
46
|
+
*/
|
|
47
|
+
export function matchesPage(url, html) {
|
|
48
|
+
try {
|
|
49
|
+
if (url && url.includes('mail.google.com')) {
|
|
50
|
+
return { matched: true, confidence: 1.0 };
|
|
51
|
+
}
|
|
52
|
+
if (html && (html.includes('data-ogsr-up') || html.includes('aH2'))) {
|
|
53
|
+
return { matched: true, confidence: 0.8 };
|
|
54
|
+
}
|
|
55
|
+
return { matched: false };
|
|
56
|
+
} catch {
|
|
57
|
+
return { matched: false };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// ACTIONS
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the complete list of actions this plugin provides.
|
|
67
|
+
* @returns {Array} ActionDescriptor[] per plugin interface contract
|
|
68
|
+
*/
|
|
69
|
+
export function getActions() {
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
name: "list_emails",
|
|
73
|
+
description: "List visible emails from the current Gmail folder/view",
|
|
74
|
+
params: [
|
|
75
|
+
{ name: "folder", type: "string", description: "Gmail folder: inbox, sent, drafts, trash, spam, or a label name", required: false },
|
|
76
|
+
{ name: "limit", type: "number", description: "Maximum number of emails to return (default: 25)", required: false, default: 25 }
|
|
77
|
+
],
|
|
78
|
+
execute: listEmails
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "read_email",
|
|
82
|
+
description: "Open and read a specific email by index or ID",
|
|
83
|
+
params: [
|
|
84
|
+
{ name: "index", type: "number", description: "0-based position in current email list", required: false },
|
|
85
|
+
{ name: "id", type: "string", description: "Gmail internal message/thread ID", required: false }
|
|
86
|
+
],
|
|
87
|
+
execute: readEmail
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "search_emails",
|
|
91
|
+
description: "Search Gmail using URL hash navigation and return matching emails",
|
|
92
|
+
params: [
|
|
93
|
+
{ name: "query", type: "string", description: "Gmail search query (supports from:, to:, subject:, has:attachment, etc.)", required: true },
|
|
94
|
+
{ name: "limit", type: "number", description: "Maximum results to return (default: 25)", required: false, default: 25 }
|
|
95
|
+
],
|
|
96
|
+
execute: searchEmails
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "compose_email",
|
|
100
|
+
description: "Open Gmail compose window via keyboard shortcut and fill in email fields",
|
|
101
|
+
params: [
|
|
102
|
+
{ name: "to", type: "string", description: "Recipient email address", required: true },
|
|
103
|
+
{ name: "subject", type: "string", description: "Email subject", required: false, default: "" },
|
|
104
|
+
{ name: "body", type: "string", description: "Email body text", required: false, default: "" },
|
|
105
|
+
{ name: "cc", type: "string", description: "CC recipient email address", required: false },
|
|
106
|
+
{ name: "send", type: "boolean", description: "If true, send immediately via Ctrl+Enter. Default: leave as draft", required: false, default: false }
|
|
107
|
+
],
|
|
108
|
+
execute: composeEmail
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "reply_email",
|
|
112
|
+
description: "Reply to the currently open email thread via keyboard shortcut",
|
|
113
|
+
params: [
|
|
114
|
+
{ name: "body", type: "string", description: "Reply body text", required: true },
|
|
115
|
+
{ name: "replyAll", type: "boolean", description: "If true, reply to all participants (keyboard 'a')", required: false, default: false },
|
|
116
|
+
{ name: "send", type: "boolean", description: "If true, send immediately via Ctrl+Enter. Default: leave as draft", required: false, default: false }
|
|
117
|
+
],
|
|
118
|
+
execute: replyEmail
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "forward_email",
|
|
122
|
+
description: "Forward the currently open email thread via keyboard shortcut",
|
|
123
|
+
params: [
|
|
124
|
+
{ name: "to", type: "string", description: "Recipient email address to forward to", required: true },
|
|
125
|
+
{ name: "body", type: "string", description: "Additional body text above forwarded content", required: false, default: "" },
|
|
126
|
+
{ name: "send", type: "boolean", description: "If true, send immediately via Ctrl+Enter. Default: leave as draft", required: false, default: false }
|
|
127
|
+
],
|
|
128
|
+
execute: forwardEmail
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "archive_email",
|
|
132
|
+
description: "Archive an email via keyboard shortcut (remove from inbox, keep in All Mail)",
|
|
133
|
+
params: [
|
|
134
|
+
{ name: "index", type: "number", description: "0-based index in email list", required: false },
|
|
135
|
+
{ name: "id", type: "string", description: "Gmail message/thread ID", required: false }
|
|
136
|
+
],
|
|
137
|
+
execute: archiveEmail
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "delete_email",
|
|
141
|
+
description: "Move an email to Trash via keyboard shortcut",
|
|
142
|
+
params: [
|
|
143
|
+
{ name: "index", type: "number", description: "0-based index in email list", required: false },
|
|
144
|
+
{ name: "id", type: "string", description: "Gmail message/thread ID", required: false }
|
|
145
|
+
],
|
|
146
|
+
execute: deleteEmail
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "label_email",
|
|
150
|
+
description: "Apply a Gmail label to an email via keyboard shortcut",
|
|
151
|
+
params: [
|
|
152
|
+
{ name: "index", type: "number", description: "0-based index in email list", required: false },
|
|
153
|
+
{ name: "id", type: "string", description: "Gmail message/thread ID", required: false },
|
|
154
|
+
{ name: "label", type: "string", description: "Label name to apply", required: true }
|
|
155
|
+
],
|
|
156
|
+
execute: labelEmail
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "mark_read",
|
|
160
|
+
description: "Mark an email as read via keyboard shortcut (Shift+i)",
|
|
161
|
+
params: [
|
|
162
|
+
{ name: "index", type: "number", description: "0-based index in email list", required: false },
|
|
163
|
+
{ name: "id", type: "string", description: "Gmail message/thread ID", required: false }
|
|
164
|
+
],
|
|
165
|
+
execute: markRead
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "mark_unread",
|
|
169
|
+
description: "Mark an email as unread via keyboard shortcut (Shift+u)",
|
|
170
|
+
params: [
|
|
171
|
+
{ name: "index", type: "number", description: "0-based index in email list", required: false },
|
|
172
|
+
{ name: "id", type: "string", description: "Gmail message/thread ID", required: false }
|
|
173
|
+
],
|
|
174
|
+
execute: markUnread
|
|
175
|
+
}
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// INFO
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Return high-level plugin context for the AI agent.
|
|
185
|
+
* @returns {object} PluginInfo per plugin interface contract
|
|
186
|
+
*/
|
|
187
|
+
export function getInfo() {
|
|
188
|
+
return {
|
|
189
|
+
recommendation: "Manage Gmail emails — list, read, search, compose, reply, forward, archive, delete, label, and mark as read/unread.",
|
|
190
|
+
description: "Gmail email management with hybrid UI resilience — list, read, search, compose, reply, forward, archive, delete, label, and mark emails using URL navigation (T1), keyboard shortcuts (T2), ARIA selectors (T3), and CSS fallback (T4).",
|
|
191
|
+
targetPages: ["Gmail inbox (mail.google.com)"],
|
|
192
|
+
authFlow: "User must be signed into Gmail in the browser before using plugin actions. The plugin does not handle Google account authentication.",
|
|
193
|
+
actions: getActions().map(({ name, description, params }) => ({ name, description, params }))
|
|
194
|
+
};
|
|
195
|
+
}
|