chrome-devtools-mcp-for-extension 0.9.15 → 0.9.17
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/build/src/main.js +2 -0
- package/build/src/tools/chatgpt-web.js +349 -0
- package/package.json +1 -1
package/build/src/main.js
CHANGED
|
@@ -16,6 +16,7 @@ import { McpContext } from './McpContext.js';
|
|
|
16
16
|
import { McpResponse } from './McpResponse.js';
|
|
17
17
|
import { Mutex } from './Mutex.js';
|
|
18
18
|
import * as bookmarkTools from './tools/bookmarks.js';
|
|
19
|
+
import * as chatgptWebTools from './tools/chatgpt-web.js';
|
|
19
20
|
import * as consoleTools from './tools/console.js';
|
|
20
21
|
import * as emulationTools from './tools/emulation.js';
|
|
21
22
|
import * as extensionTools from './tools/extensions.js';
|
|
@@ -133,6 +134,7 @@ function registerTool(tool) {
|
|
|
133
134
|
}
|
|
134
135
|
const tools = [
|
|
135
136
|
...Object.values(bookmarkTools),
|
|
137
|
+
...Object.values(chatgptWebTools),
|
|
136
138
|
...Object.values(consoleTools),
|
|
137
139
|
...Object.values(emulationTools),
|
|
138
140
|
...Object.values(extensionTools),
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import z from 'zod';
|
|
9
|
+
import { ToolCategories } from './categories.js';
|
|
10
|
+
import { defineTool } from './ToolDefinition.js';
|
|
11
|
+
/**
|
|
12
|
+
* Sanitize question to remove sensitive information like passwords
|
|
13
|
+
*/
|
|
14
|
+
function sanitizeQuestion(text) {
|
|
15
|
+
const passwordPatterns = [
|
|
16
|
+
/password\s*[:=]\s*\S+/gi,
|
|
17
|
+
/パスワード\s*[::=]\s*\S+/gi,
|
|
18
|
+
/pwd\s*[:=]\s*\S+/gi,
|
|
19
|
+
/secret\s*[:=]\s*\S+/gi,
|
|
20
|
+
];
|
|
21
|
+
let sanitized = text;
|
|
22
|
+
for (const pattern of passwordPatterns) {
|
|
23
|
+
sanitized = sanitized.replace(pattern, '[パスワードは除外されました]');
|
|
24
|
+
}
|
|
25
|
+
return sanitized;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Save conversation log to docs/ask/chatgpt/
|
|
29
|
+
*/
|
|
30
|
+
async function saveConversationLog(projectName, question, response, metadata) {
|
|
31
|
+
// Generate timestamp in yymmdd_HHMMSS format
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const timestamp = [
|
|
34
|
+
String(now.getFullYear()).slice(2).padStart(2, '0'),
|
|
35
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
36
|
+
String(now.getDate()).padStart(2, '0'),
|
|
37
|
+
'_',
|
|
38
|
+
String(now.getHours()).padStart(2, '0'),
|
|
39
|
+
String(now.getMinutes()).padStart(2, '0'),
|
|
40
|
+
String(now.getSeconds()).padStart(2, '0'),
|
|
41
|
+
].join('');
|
|
42
|
+
// Generate topic slug from first 50 characters
|
|
43
|
+
const topicSlug = question
|
|
44
|
+
.substring(0, 50)
|
|
45
|
+
.replace(/[^a-z0-9\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]+/gi, '-')
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.slice(0, 30);
|
|
48
|
+
const filename = `${timestamp}-${projectName}-${topicSlug}.md`;
|
|
49
|
+
const logDir = 'docs/ask/chatgpt';
|
|
50
|
+
const logPath = path.join(process.cwd(), logDir, filename);
|
|
51
|
+
// Ensure directory exists
|
|
52
|
+
await fs.promises.mkdir(path.dirname(logPath), { recursive: true });
|
|
53
|
+
const content = `# ${topicSlug}
|
|
54
|
+
|
|
55
|
+
## 📅 メタ情報
|
|
56
|
+
- **日時**: ${now.toLocaleString('ja-JP')}
|
|
57
|
+
- **プロジェクト**: ${projectName}
|
|
58
|
+
- **AIモデル**: ${metadata.model || 'ChatGPT'}
|
|
59
|
+
${metadata.thinkingTime ? `- **思考時間**: ${metadata.thinkingTime}s\n` : ''}${metadata.chatUrl ? `- **チャットURL**: ${metadata.chatUrl}\n` : ''}
|
|
60
|
+
## ❓ 質問
|
|
61
|
+
|
|
62
|
+
${question}
|
|
63
|
+
|
|
64
|
+
## 💬 回答
|
|
65
|
+
|
|
66
|
+
${response}
|
|
67
|
+
`;
|
|
68
|
+
await fs.promises.writeFile(logPath, content, 'utf-8');
|
|
69
|
+
return path.relative(process.cwd(), logPath);
|
|
70
|
+
}
|
|
71
|
+
export const askChatGPTWeb = defineTool({
|
|
72
|
+
name: 'ask_chatgpt_web',
|
|
73
|
+
description: `Ask ChatGPT a question via web browser automation. Claude can use this to consult ChatGPT for additional AI perspectives during development. Conversations are organized by project name and logged to docs/ask/chatgpt/.`,
|
|
74
|
+
annotations: {
|
|
75
|
+
category: ToolCategories.NAVIGATION_AUTOMATION,
|
|
76
|
+
readOnlyHint: false,
|
|
77
|
+
},
|
|
78
|
+
schema: {
|
|
79
|
+
question: z
|
|
80
|
+
.string()
|
|
81
|
+
.describe('The question to ask ChatGPT. Should be detailed and well-formed for best results.'),
|
|
82
|
+
projectName: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Project name for organizing conversations. Defaults to current working directory name.'),
|
|
86
|
+
createNewChat: z
|
|
87
|
+
.boolean()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('Force creation of a new chat instead of reusing existing project chat. Default: false'),
|
|
90
|
+
},
|
|
91
|
+
handler: async (request, response, context) => {
|
|
92
|
+
const { question, projectName, createNewChat = false } = request.params;
|
|
93
|
+
// Sanitize question
|
|
94
|
+
const sanitizedQuestion = sanitizeQuestion(question);
|
|
95
|
+
// Determine project name
|
|
96
|
+
const project = projectName || path.basename(process.cwd()) || 'unknown-project';
|
|
97
|
+
const page = context.getSelectedPage();
|
|
98
|
+
try {
|
|
99
|
+
// Step 1: Navigate to ChatGPT
|
|
100
|
+
response.appendResponseLine('ChatGPTに接続中...');
|
|
101
|
+
await page.goto('https://chatgpt.com/', { waitUntil: 'networkidle2' });
|
|
102
|
+
// Check if logged in
|
|
103
|
+
const currentUrl = page.url();
|
|
104
|
+
if (currentUrl.includes('auth') || currentUrl.includes('login')) {
|
|
105
|
+
response.appendResponseLine('❌ ChatGPTにログインが必要です。ブラウザで手動ログインしてください。');
|
|
106
|
+
response.appendResponseLine(`ログインURL: ${currentUrl}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
response.appendResponseLine('✅ ログイン確認完了');
|
|
110
|
+
// Step 2: Search for existing chat or create new one
|
|
111
|
+
if (!createNewChat) {
|
|
112
|
+
response.appendResponseLine(`既存のプロジェクトチャット「[Project: ${project}]」を検索中...`);
|
|
113
|
+
// Open search
|
|
114
|
+
const searchOpened = await page.evaluate(() => {
|
|
115
|
+
const searchButton = Array.from(document.querySelectorAll('div.group.__menu-item.hoverable')).find((elem) => elem.textContent?.includes('チャットを検索'));
|
|
116
|
+
if (searchButton) {
|
|
117
|
+
searchButton.click();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
});
|
|
122
|
+
if (searchOpened) {
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
124
|
+
// Search for project chat
|
|
125
|
+
const chatFound = await page.evaluate((projectName) => {
|
|
126
|
+
const searchInput = document.querySelector('input[placeholder*="チャットを検索"]');
|
|
127
|
+
if (searchInput) {
|
|
128
|
+
searchInput.value = `[Project: ${projectName}]`;
|
|
129
|
+
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}, project);
|
|
134
|
+
if (chatFound) {
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
136
|
+
// Try to find and click the chat
|
|
137
|
+
const existingChat = await page.evaluate((projectName) => {
|
|
138
|
+
const chatLinks = Array.from(document.querySelectorAll('a[href^="/c/"]'));
|
|
139
|
+
const targetChat = chatLinks.find((link) => link.textContent?.includes(`[Project: ${projectName}]`));
|
|
140
|
+
if (targetChat) {
|
|
141
|
+
targetChat.click();
|
|
142
|
+
return {
|
|
143
|
+
found: true,
|
|
144
|
+
href: targetChat.href,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { found: false };
|
|
148
|
+
}, project);
|
|
149
|
+
if (existingChat.found) {
|
|
150
|
+
response.appendResponseLine(`✅ 既存チャットを使用: ${existingChat.href}`);
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
response.appendResponseLine('既存チャットが見つかりませんでした。新規作成します。');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Step 3: Create new chat if needed
|
|
160
|
+
let isNewChat = false;
|
|
161
|
+
if (createNewChat || page.url() === 'https://chatgpt.com/') {
|
|
162
|
+
response.appendResponseLine('新規チャットを作成中...');
|
|
163
|
+
isNewChat = true;
|
|
164
|
+
// Click "新しいチャット"
|
|
165
|
+
await page.evaluate(() => {
|
|
166
|
+
const newChatLink = document.querySelector('a[href="/"]');
|
|
167
|
+
if (newChatLink) {
|
|
168
|
+
newChatLink.click();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
172
|
+
// Turn off temporary chat
|
|
173
|
+
const tempChatDisabled = await page.evaluate(() => {
|
|
174
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
175
|
+
const btn = buttons.find((b) => {
|
|
176
|
+
const label = b.getAttribute('aria-label') || '';
|
|
177
|
+
return label.includes('一時チャットをオフにする');
|
|
178
|
+
});
|
|
179
|
+
if (btn) {
|
|
180
|
+
btn.click();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
});
|
|
185
|
+
if (tempChatDisabled) {
|
|
186
|
+
response.appendResponseLine('✅ 一時チャットを無効化');
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Step 4: Send question
|
|
191
|
+
response.appendResponseLine('質問を送信中...');
|
|
192
|
+
const questionSent = await page.evaluate((questionText) => {
|
|
193
|
+
const prosemirror = document.querySelector('.ProseMirror[contenteditable="true"]');
|
|
194
|
+
if (!prosemirror)
|
|
195
|
+
return false;
|
|
196
|
+
prosemirror.innerHTML = '';
|
|
197
|
+
const p = document.createElement('p');
|
|
198
|
+
p.textContent = questionText;
|
|
199
|
+
prosemirror.appendChild(p);
|
|
200
|
+
prosemirror.dispatchEvent(new Event('input', { bubbles: true }));
|
|
201
|
+
return true;
|
|
202
|
+
}, sanitizedQuestion);
|
|
203
|
+
if (!questionSent) {
|
|
204
|
+
response.appendResponseLine('❌ エディタが見つかりません');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
208
|
+
// Click send button
|
|
209
|
+
const sent = await page.evaluate(() => {
|
|
210
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
211
|
+
const sendButton = buttons.find((btn) => {
|
|
212
|
+
const svg = btn.querySelector('svg');
|
|
213
|
+
return (svg &&
|
|
214
|
+
!btn.disabled &&
|
|
215
|
+
btn.offsetParent !== null);
|
|
216
|
+
});
|
|
217
|
+
if (sendButton) {
|
|
218
|
+
sendButton.click();
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
});
|
|
223
|
+
if (!sent) {
|
|
224
|
+
response.appendResponseLine('❌ 送信ボタンが見つかりません');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
response.appendResponseLine('✅ 質問送信完了');
|
|
228
|
+
// Step 5: Monitor streaming with progress updates
|
|
229
|
+
response.appendResponseLine('ChatGPTの回答を待機中... (10秒ごとに進捗を表示)');
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
let lastText = '';
|
|
232
|
+
while (true) {
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
234
|
+
const status = await page.evaluate(() => {
|
|
235
|
+
// Check if streaming
|
|
236
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
237
|
+
const isStreaming = buttons.some((btn) => btn.textContent?.includes('ストリーミングの停止') ||
|
|
238
|
+
btn.textContent?.includes('停止'));
|
|
239
|
+
if (!isStreaming) {
|
|
240
|
+
// Get final response
|
|
241
|
+
const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
242
|
+
if (assistantMessages.length === 0)
|
|
243
|
+
return { completed: false };
|
|
244
|
+
const latestMessage = assistantMessages[assistantMessages.length - 1];
|
|
245
|
+
const thinkingButton = latestMessage.querySelector('button[aria-label*="思考時間"]');
|
|
246
|
+
const thinkingTime = thinkingButton
|
|
247
|
+
? parseInt((thinkingButton.textContent || '').match(/\d+/)?.[0] || '0')
|
|
248
|
+
: undefined;
|
|
249
|
+
return {
|
|
250
|
+
completed: true,
|
|
251
|
+
text: latestMessage.textContent || '',
|
|
252
|
+
thinkingTime,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// Get current text
|
|
256
|
+
const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
257
|
+
const latestMessage = assistantMessages[assistantMessages.length - 1];
|
|
258
|
+
const currentText = latestMessage
|
|
259
|
+
? latestMessage.textContent?.substring(0, 200)
|
|
260
|
+
: '';
|
|
261
|
+
return {
|
|
262
|
+
completed: false,
|
|
263
|
+
streaming: true,
|
|
264
|
+
currentText,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
if (status.completed) {
|
|
268
|
+
response.appendResponseLine(`\n✅ 回答完了 (所要時間: ${Math.floor((Date.now() - startTime) / 1000)}秒)`);
|
|
269
|
+
if (status.thinkingTime) {
|
|
270
|
+
response.appendResponseLine(`🤔 思考時間: ${status.thinkingTime}秒`);
|
|
271
|
+
}
|
|
272
|
+
// Rename chat if it's a new chat
|
|
273
|
+
if (isNewChat) {
|
|
274
|
+
response.appendResponseLine('チャット名を変更中...');
|
|
275
|
+
// Wait for chat to be created
|
|
276
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
277
|
+
// Click chat menu
|
|
278
|
+
const menuClicked = await page.evaluate(() => {
|
|
279
|
+
const menuButtons = Array.from(document.querySelectorAll('button[aria-label="会話のオプションを開く"]'));
|
|
280
|
+
// Find the first menu button (current chat)
|
|
281
|
+
const btn = menuButtons[0];
|
|
282
|
+
if (btn) {
|
|
283
|
+
btn.click();
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
});
|
|
288
|
+
if (menuClicked) {
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
290
|
+
// Click "名前を変更する"
|
|
291
|
+
const renameClicked = await page.evaluate(() => {
|
|
292
|
+
const menuItems = Array.from(document.querySelectorAll('[role="menuitem"]'));
|
|
293
|
+
const renameItem = menuItems.find((item) => item.textContent?.includes('名前を変更する'));
|
|
294
|
+
if (renameItem) {
|
|
295
|
+
renameItem.click();
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
});
|
|
300
|
+
if (renameClicked) {
|
|
301
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
302
|
+
// Enter new name
|
|
303
|
+
await page.evaluate((projectName) => {
|
|
304
|
+
const textbox = document.querySelector('input[type="text"]');
|
|
305
|
+
if (textbox) {
|
|
306
|
+
textbox.value = `[Project: ${projectName}]`;
|
|
307
|
+
textbox.dispatchEvent(new Event('input', { bubbles: true }));
|
|
308
|
+
textbox.blur();
|
|
309
|
+
}
|
|
310
|
+
}, project);
|
|
311
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
312
|
+
response.appendResponseLine(`✅ チャット名を「[Project: ${project}]」に変更`);
|
|
313
|
+
// Close the menu popup by clicking outside
|
|
314
|
+
await page.evaluate(() => {
|
|
315
|
+
const body = document.body;
|
|
316
|
+
body.click();
|
|
317
|
+
});
|
|
318
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Save conversation log
|
|
323
|
+
const chatUrl = page.url();
|
|
324
|
+
const logPath = await saveConversationLog(project, sanitizedQuestion, status.text || '', {
|
|
325
|
+
thinkingTime: status.thinkingTime,
|
|
326
|
+
chatUrl,
|
|
327
|
+
model: 'ChatGPT 5 Thinking',
|
|
328
|
+
});
|
|
329
|
+
response.appendResponseLine(`📝 会話ログ保存: ${logPath}`);
|
|
330
|
+
response.appendResponseLine(`🔗 チャットURL: ${chatUrl}`);
|
|
331
|
+
response.appendResponseLine('\n' + '='.repeat(60));
|
|
332
|
+
response.appendResponseLine('ChatGPTの回答:\n');
|
|
333
|
+
response.appendResponseLine(status.text || '');
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
// Show progress every 10 seconds
|
|
337
|
+
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
338
|
+
if (elapsedSeconds % 10 === 0 && status.currentText !== lastText) {
|
|
339
|
+
lastText = status.currentText || '';
|
|
340
|
+
response.appendResponseLine(`⏱️ ${elapsedSeconds}秒経過 - 現在のテキスト: ${lastText.substring(0, 100)}...`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
346
|
+
response.appendResponseLine(`❌ エラー: ${errorMessage}`);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.17",
|
|
4
4
|
"description": "MCP server for Chrome extension development with Web Store automation. Fork of chrome-devtools-mcp with extension-specific tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|