mcpbrowser 0.3.34 → 0.3.35
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 +3 -1
- package/src/actions/execute-javascript.js +3 -1
- package/src/actions/fetch-page.js +3 -1
- package/src/actions/get-current-html.js +3 -1
- 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 +323 -0
- package/src/mcp-browser.js +34 -2
- package/src/plugins/_example/index.js +139 -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 +194 -0
- package/src/plugins/gmail/selectors.js +82 -0
- package/src/plugins.json +3 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read-email.js — Open and read a specific email thread.
|
|
3
|
+
*
|
|
4
|
+
* Tier usage:
|
|
5
|
+
* T1: gmailNavigate to thread by ID hash
|
|
6
|
+
* T2: keyboard shortcut 'o' to open selected row
|
|
7
|
+
* T3: span[email] for sender extraction
|
|
8
|
+
* T4: MESSAGE_CONTAINER, MSG_BODY, MSG_DATE, THREAD_SUBJECT, ATTACHMENT_* selectors
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ErrorResponse } from '../../../core/responses.js';
|
|
12
|
+
import {
|
|
13
|
+
checkPrecondition,
|
|
14
|
+
gmailNavigate,
|
|
15
|
+
checkKeyboardShortcuts,
|
|
16
|
+
selectEmailRow,
|
|
17
|
+
waitForGmail,
|
|
18
|
+
GmailActionResponse
|
|
19
|
+
} from '../helpers.js';
|
|
20
|
+
import {
|
|
21
|
+
THREAD_SUBJECT,
|
|
22
|
+
MESSAGE_CONTAINER,
|
|
23
|
+
MSG_BODY,
|
|
24
|
+
MSG_DATE,
|
|
25
|
+
ATTACHMENT_AREA,
|
|
26
|
+
ATTACHMENT_NAME,
|
|
27
|
+
ATTACHMENT_SIZE
|
|
28
|
+
} from '../selectors.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Open and read a specific email by ID or list index.
|
|
32
|
+
* @param {object} opts
|
|
33
|
+
* @param {import('puppeteer-core').Page} opts.page
|
|
34
|
+
* @param {object} opts.params
|
|
35
|
+
* @param {string} [opts.params.id] - Gmail thread/message ID
|
|
36
|
+
* @param {number} [opts.params.index] - Positional index in current list view
|
|
37
|
+
* @returns {Promise<GmailActionResponse|ErrorResponse>}
|
|
38
|
+
*/
|
|
39
|
+
export async function readEmail({ page, params }) {
|
|
40
|
+
if (params.id == null && params.index == null) {
|
|
41
|
+
return new ErrorResponse(
|
|
42
|
+
'Either id or index is required to read an email.',
|
|
43
|
+
[
|
|
44
|
+
'Use list_emails to see available emails and their indices',
|
|
45
|
+
'Use search_emails to find a specific email by keyword'
|
|
46
|
+
]
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Precondition: must be on Gmail
|
|
51
|
+
const pre = await checkPrecondition(page, 'on_gmail');
|
|
52
|
+
if (!pre.met) {
|
|
53
|
+
return new ErrorResponse(pre.error, [
|
|
54
|
+
pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (params.id) {
|
|
59
|
+
// T1: Navigate directly to thread by ID
|
|
60
|
+
await gmailNavigate(page, '#inbox/' + params.id);
|
|
61
|
+
} else {
|
|
62
|
+
// T2: Select row by index, then open with keyboard shortcut
|
|
63
|
+
const kb = await checkKeyboardShortcuts(page);
|
|
64
|
+
if (!kb.enabled) {
|
|
65
|
+
return new ErrorResponse(
|
|
66
|
+
kb.error || 'Keyboard shortcuts are not enabled in Gmail.',
|
|
67
|
+
['Enable keyboard shortcuts in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.']
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sel = await selectEmailRow(page, { index: params.index });
|
|
72
|
+
if (!sel.selected) {
|
|
73
|
+
return new ErrorResponse(
|
|
74
|
+
sel.error || `Could not select email at index ${params.index}.`,
|
|
75
|
+
['Use list_emails to check available email indices']
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// T2: Press 'o' to open the selected email
|
|
80
|
+
await page.keyboard.press('o');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Wait for thread content to load
|
|
84
|
+
await waitForGmail(page, THREAD_SUBJECT);
|
|
85
|
+
|
|
86
|
+
// T3+T4: Extract thread data from DOM
|
|
87
|
+
const thread = await page.evaluate((selectors) => {
|
|
88
|
+
const subjectEl = document.querySelector(selectors.threadSubject);
|
|
89
|
+
const subject = subjectEl?.textContent?.trim() || '';
|
|
90
|
+
|
|
91
|
+
const containers = document.querySelectorAll(selectors.messageContainer);
|
|
92
|
+
const messages = [];
|
|
93
|
+
|
|
94
|
+
for (const container of containers) {
|
|
95
|
+
// T3: span[email] for sender info
|
|
96
|
+
const senderEl = container.querySelector('span[email]');
|
|
97
|
+
const sender = senderEl?.getAttribute('name') || senderEl?.textContent?.trim() || '';
|
|
98
|
+
const senderEmail = senderEl?.getAttribute('email') || '';
|
|
99
|
+
|
|
100
|
+
// T4: CSS selectors for body, date, attachments
|
|
101
|
+
const bodyEl = container.querySelector(selectors.msgBody);
|
|
102
|
+
const body = bodyEl?.textContent?.trim() || '';
|
|
103
|
+
|
|
104
|
+
const dateEl = container.querySelector(selectors.msgDate);
|
|
105
|
+
const date = dateEl?.getAttribute('title') || dateEl?.textContent?.trim() || '';
|
|
106
|
+
|
|
107
|
+
const attachments = [];
|
|
108
|
+
const attachArea = container.querySelector(selectors.attachmentArea);
|
|
109
|
+
if (attachArea) {
|
|
110
|
+
const nameEls = attachArea.querySelectorAll(selectors.attachmentName);
|
|
111
|
+
const sizeEls = attachArea.querySelectorAll(selectors.attachmentSize);
|
|
112
|
+
for (let i = 0; i < nameEls.length; i++) {
|
|
113
|
+
attachments.push({
|
|
114
|
+
name: nameEls[i]?.textContent?.trim() || '',
|
|
115
|
+
size: sizeEls[i]?.textContent?.trim() || ''
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
messages.push({ sender, senderEmail, date, body, attachments });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Attempt to extract thread ID from URL hash
|
|
124
|
+
const hash = window.location.hash || '';
|
|
125
|
+
const idMatch = hash.match(/\/([A-Za-z0-9]+)$/);
|
|
126
|
+
const id = idMatch ? idMatch[1] : undefined;
|
|
127
|
+
|
|
128
|
+
return { id, subject, messageCount: messages.length, messages };
|
|
129
|
+
}, {
|
|
130
|
+
threadSubject: THREAD_SUBJECT,
|
|
131
|
+
messageContainer: MESSAGE_CONTAINER,
|
|
132
|
+
msgBody: MSG_BODY,
|
|
133
|
+
msgDate: MSG_DATE,
|
|
134
|
+
attachmentArea: ATTACHMENT_AREA,
|
|
135
|
+
attachmentName: ATTACHMENT_NAME,
|
|
136
|
+
attachmentSize: ATTACHMENT_SIZE
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return new GmailActionResponse(
|
|
140
|
+
{ thread },
|
|
141
|
+
`Email: "${thread.subject}" — ${thread.messageCount} message(s).`,
|
|
142
|
+
[
|
|
143
|
+
'Use reply_email to respond',
|
|
144
|
+
'Use forward_email to forward',
|
|
145
|
+
'Use archive_email to archive',
|
|
146
|
+
'Use list_emails to return to inbox'
|
|
147
|
+
]
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reply-email.js — Reply (or reply-all) to the currently open email thread.
|
|
3
|
+
*
|
|
4
|
+
* Tier usage:
|
|
5
|
+
* T2: 'r' for reply, 'a' for reply-all, Ctrl+Enter to send
|
|
6
|
+
* T3: div[aria-label="Message Body"] for reply editor
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ErrorResponse } from '../../../core/responses.js';
|
|
10
|
+
import logger from '../../../core/logger.js';
|
|
11
|
+
import {
|
|
12
|
+
checkPrecondition,
|
|
13
|
+
checkKeyboardShortcuts,
|
|
14
|
+
waitForGmail,
|
|
15
|
+
GmailActionResponse
|
|
16
|
+
} from '../helpers.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reply to the currently open thread.
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {import('puppeteer-core').Page} opts.page
|
|
22
|
+
* @param {object} opts.params
|
|
23
|
+
* @param {string} [opts.params.body] - Reply body text
|
|
24
|
+
* @param {boolean} [opts.params.replyAll] - If true, reply-all
|
|
25
|
+
* @param {boolean} [opts.params.send] - If true, send immediately
|
|
26
|
+
* @returns {Promise<GmailActionResponse|ErrorResponse>}
|
|
27
|
+
*/
|
|
28
|
+
export async function replyEmail({ page, params }) {
|
|
29
|
+
// Precondition: must be on Gmail
|
|
30
|
+
const pre = await checkPrecondition(page, 'on_gmail');
|
|
31
|
+
if (!pre.met) {
|
|
32
|
+
return new ErrorResponse(pre.error, [
|
|
33
|
+
pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Precondition: must have a thread open
|
|
38
|
+
const threadPre = await checkPrecondition(page, 'thread_open');
|
|
39
|
+
if (!threadPre.met) {
|
|
40
|
+
return new ErrorResponse(threadPre.error, [
|
|
41
|
+
threadPre.suggestion || "Use read_email to open an email thread first."
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Verify keyboard shortcuts are enabled
|
|
46
|
+
const kb = await checkKeyboardShortcuts(page);
|
|
47
|
+
if (!kb.enabled) {
|
|
48
|
+
return new ErrorResponse(kb.error, [
|
|
49
|
+
'Enable keyboard shortcuts in Gmail Settings → General → Keyboard shortcuts → ON, then reload Gmail.'
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// T2: Press 'a' for reply-all or 'r' for reply
|
|
54
|
+
const key = params.replyAll ? 'a' : 'r';
|
|
55
|
+
await page.keyboard.press(key);
|
|
56
|
+
logger.debug(`replyEmail: pressed '${key}' for ${params.replyAll ? 'reply-all' : 'reply'}`);
|
|
57
|
+
|
|
58
|
+
// Wait for reply editor
|
|
59
|
+
await waitForGmail(page, 'div[aria-label="Message Body"]');
|
|
60
|
+
|
|
61
|
+
// Fill reply body
|
|
62
|
+
if (params.body) {
|
|
63
|
+
await page.type('div[aria-label="Message Body"]', params.body);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Send if requested
|
|
67
|
+
if (params.send) {
|
|
68
|
+
await page.keyboard.down('Control');
|
|
69
|
+
await page.keyboard.press('Enter');
|
|
70
|
+
await page.keyboard.up('Control');
|
|
71
|
+
logger.debug('replyEmail: sent via Ctrl+Enter');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const status = params.send ? 'sent' : 'draft';
|
|
75
|
+
const action = params.replyAll ? 'Reply-all' : 'Reply';
|
|
76
|
+
const summary = params.send
|
|
77
|
+
? `${action} sent.`
|
|
78
|
+
: `${action} draft created. Review and send manually in Gmail.`;
|
|
79
|
+
|
|
80
|
+
return new GmailActionResponse(
|
|
81
|
+
{ status, replyAll: !!params.replyAll },
|
|
82
|
+
summary,
|
|
83
|
+
params.send
|
|
84
|
+
? ['Use list_emails to return to inbox']
|
|
85
|
+
: ['Review and send the reply draft manually in Gmail']
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search-emails.js — Search emails using Gmail's search functionality.
|
|
3
|
+
*
|
|
4
|
+
* Tier usage:
|
|
5
|
+
* T1: gmailNavigate to #search/<query>
|
|
6
|
+
* T4: EMAIL_ROW + NO_RESULTS selectors for result detection
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ErrorResponse } from '../../../core/responses.js';
|
|
10
|
+
import {
|
|
11
|
+
checkPrecondition,
|
|
12
|
+
gmailNavigate,
|
|
13
|
+
waitForGmail,
|
|
14
|
+
extractEmailRows,
|
|
15
|
+
GmailActionResponse
|
|
16
|
+
} from '../helpers.js';
|
|
17
|
+
import { EMAIL_ROW, NO_RESULTS } from '../selectors.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Search emails by query string.
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {import('puppeteer-core').Page} opts.page
|
|
23
|
+
* @param {object} opts.params
|
|
24
|
+
* @param {string} opts.params.query - Gmail search query
|
|
25
|
+
* @param {number} [opts.params.limit=25] - Maximum results to return
|
|
26
|
+
* @returns {Promise<GmailActionResponse|ErrorResponse>}
|
|
27
|
+
*/
|
|
28
|
+
export async function searchEmails({ page, params }) {
|
|
29
|
+
// Validate query
|
|
30
|
+
if (!params.query || !params.query.trim()) {
|
|
31
|
+
return new ErrorResponse(
|
|
32
|
+
'Search query is required.',
|
|
33
|
+
[
|
|
34
|
+
'Provide a query string, e.g. search_emails({ query: "from:boss subject:urgent" })',
|
|
35
|
+
'Use list_emails to browse without a search query'
|
|
36
|
+
]
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Precondition: must be on Gmail
|
|
41
|
+
const pre = await checkPrecondition(page, 'on_gmail');
|
|
42
|
+
if (!pre.met) {
|
|
43
|
+
return new ErrorResponse(pre.error, [
|
|
44
|
+
pre.suggestion || "Use fetch_webpage({ url: 'https://mail.google.com' }) to open Gmail first."
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const query = params.query.trim();
|
|
49
|
+
const limit = params.limit || 25;
|
|
50
|
+
|
|
51
|
+
// T1: Navigate to search results
|
|
52
|
+
await gmailNavigate(page, '#search/' + encodeURIComponent(query));
|
|
53
|
+
|
|
54
|
+
// Wait for either email rows or no-results indicator
|
|
55
|
+
let hasResults = true;
|
|
56
|
+
try {
|
|
57
|
+
await waitForGmail(page, EMAIL_ROW);
|
|
58
|
+
} catch {
|
|
59
|
+
// Check if no-results indicator is present instead
|
|
60
|
+
const noResults = await page.$(NO_RESULTS);
|
|
61
|
+
if (noResults) {
|
|
62
|
+
hasResults = false;
|
|
63
|
+
} else {
|
|
64
|
+
// Neither rows nor no-results — re-throw the timeout
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Gmail search did not return results within the timeout. ` +
|
|
67
|
+
`Query: "${query}". The page may still be loading.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!hasResults) {
|
|
73
|
+
return new GmailActionResponse(
|
|
74
|
+
{ emails: [], query, resultCount: 0 },
|
|
75
|
+
`No results found for "${query}".`,
|
|
76
|
+
[
|
|
77
|
+
'Try a different or broader search query',
|
|
78
|
+
'Use list_emails to browse the inbox instead'
|
|
79
|
+
]
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// T3+T4: Extract search results
|
|
84
|
+
const emails = await extractEmailRows(page, limit);
|
|
85
|
+
|
|
86
|
+
return new GmailActionResponse(
|
|
87
|
+
{ emails, query, resultCount: emails.length },
|
|
88
|
+
`Found ${emails.length} result(s) for "${query}".`,
|
|
89
|
+
[
|
|
90
|
+
'Use read_email to open a specific result',
|
|
91
|
+
'Refine your search with Gmail search operators (from:, to:, subject:, has:attachment)',
|
|
92
|
+
'Use list_emails to return to the inbox'
|
|
93
|
+
]
|
|
94
|
+
);
|
|
95
|
+
}
|