Skip to main content
Glama

MCP Todoist

by kentaroh7777
helper.ts55.4 kB
import { Page, expect } from '@playwright/test'; /** * MCP関連のテストヘルパー関数 */ export class MCPTestHelper { private logs: string[] = []; private errors: string[] = []; private isLoggingInitialized = false; constructor(private page: Page) {} /** * ログ収集を初期化する */ initializeLogging(): void { if (this.isLoggingInitialized) { return; // 既に初期化済み } // ログとエラーを初期化 this.logs = []; this.errors = []; // ブラウザコンソールログを収集 this.page.on('console', (msg) => { const log = `[${msg.type()}] ${msg.text()}`; this.logs.push(log); console.log(`[DEBUG LOG] ${log}`); }); // JavaScript エラーを収集 this.page.on('pageerror', (error) => { const errorMsg = `[PAGE ERROR] ${error.message}`; this.errors.push(errorMsg); console.log(`[DEBUG ERROR] ${errorMsg}`); }); // ネットワークエラーを収集 this.page.on('requestfailed', (request) => { const failMsg = `[NETWORK FAIL] ${request.method()} ${request.url()} - ${request.failure()?.errorText}`; this.errors.push(failMsg); console.log(`[DEBUG NETWORK] ${failMsg}`); }); this.isLoggingInitialized = true; console.log('[HELPER] ログ収集が初期化されました'); } /** * 収集したログを出力する */ outputLogs(maxLogs: number = 20): void { console.log('\n[DEBUG] === 収集されたログ出力 ==='); console.log('[DEBUG] 収集したログ数:', this.logs.length); console.log('[DEBUG] 収集したエラー数:', this.errors.length); // 最新のログを出力 const recentLogs = this.logs.slice(-maxLogs); recentLogs.forEach((log, index) => { console.log(`[DEBUG] Log ${index + 1}: ${log}`); }); // 全エラーを出力 this.errors.forEach((error, index) => { console.log(`[DEBUG] Error ${index + 1}: ${error}`); }); console.log('[DEBUG] =========================\n'); } /** * 特定のキーワードでログをフィルタして出力する */ outputFilteredLogs(keywords: string[], title: string = '関連ログ'): void { const filteredLogs = this.logs.filter(log => keywords.some(keyword => log.toLowerCase().includes(keyword.toLowerCase())) ); console.log(`\n[DEBUG] === ${title} ===`); filteredLogs.forEach((log, index) => { console.log(`[DEBUG] ${title} ${index + 1}: ${log}`); }); console.log('[DEBUG] =========================\n'); } /** * 接続関連のログを出力する */ outputConnectionLogs(): void { this.outputFilteredLogs( ['connect', 'connection', 'MCP', 'session', 'mutation', 'DEBUG'], '接続関連ログ' ); } /** * エラー関連のログを出力する */ outputErrorLogs(): void { this.outputFilteredLogs( ['error', 'ERROR', 'fail', 'FAIL', 'exception', 'Exception'], 'エラー関連ログ' ); } /** * ログをクリアする */ clearLogs(): void { this.logs = []; this.errors = []; console.log('[HELPER] ログがクリアされました'); } /** * MCPテスターページの初期状態を確認する */ async verifyMCPTesterInitialState(): Promise<void> { console.log('[HELPER] MCP Tester初期状態の確認を開始'); // MCP Tester画面の基本確認 await expect(this.page.locator('text=MCP テスター')).toBeVisible(); await expect(this.page.locator('text=未接続')).toBeVisible(); await expect(this.page.locator('text=Convex MCP サーバー')).toBeVisible(); console.log('[HELPER] MCP Tester初期状態の確認完了'); } /** * MCPサーバーに接続する * @param waitForConnection 接続完了まで待機するか(デフォルト: true) */ async connectToMCPServer(waitForConnection: boolean = true): Promise<void> { console.log('[HELPER] MCPサーバーへの接続を開始'); // 接続ボタンの確認(スペースありの"接 続"に対応) const connectButton = this.page.locator('button').filter({ hasText: /接\s*続/ }); await expect(connectButton).toBeVisible(); await expect(connectButton).toBeEnabled(); // 接続ボタンのクリック await connectButton.click(); if (waitForConnection) { // 接続処理の開始確認("未接続"が消える、または"接続中"が表示される) await Promise.race([ expect(this.page.locator('text=未接続')).toBeHidden({ timeout: 5000 }), expect(this.page.locator('text=接続中')).toBeVisible({ timeout: 5000 }) ]); console.log('[HELPER] 接続処理が開始されました。完了まで待機します...'); // 接続完了まで待機(切断ボタンが表示される、または接続済みバッジが表示されるまで) await Promise.race([ expect(this.page.locator('button').filter({ hasText: /切\s*断/ })).toBeVisible({ timeout: 30000 }), expect(this.page.locator('text=接続済み')).toBeVisible({ timeout: 30000 }) ]); // 追加で3秒待機(データのロードとレンダリング完了を待つ) await this.page.waitForTimeout(3000); } console.log('[HELPER] MCPサーバーへの接続処理完了'); } /** * MCPサーバーから切断する */ async disconnectFromMCPServer(): Promise<void> { console.log('[HELPER] MCPサーバーからの切断を開始'); // 切断ボタンの確認 const disconnectButton = this.page.locator('button').filter({ hasText: /切\s*断/ }); await expect(disconnectButton).toBeVisible({ timeout: 10000 }); await expect(disconnectButton).toBeEnabled(); // 切断ボタンのクリック await disconnectButton.click(); // 切断完了の確認 await expect(this.page.locator('text=未接続')).toBeVisible({ timeout: 10000 }); await expect(this.page.locator('button').filter({ hasText: /接\s*続/ })).toBeVisible(); console.log('[HELPER] MCPサーバーからの切断処理完了'); } /** * MCP機能タブの表示を確認する */ async verifyMCPFunctionTabs(): Promise<void> { console.log('[HELPER] MCP機能タブの表示確認を開始'); // MCP機能タブの存在確認(より具体的なセレクターを使用) await expect(this.page.locator('[role="tab"]').filter({ hasText: 'ツール' })).toBeVisible(); await expect(this.page.locator('[role="tab"]').filter({ hasText: 'リソース' })).toBeVisible(); await expect(this.page.locator('[role="tab"]').filter({ hasText: 'プロンプト' })).toBeVisible(); await expect(this.page.locator('[role="tab"]').filter({ hasText: 'デバッグ' })).toBeVisible(); console.log('[HELPER] MCP機能タブの表示確認完了'); } /** * ツールタブに切り替えて内容を確認する */ async switchToToolsTabAndVerify(): Promise<void> { console.log('[HELPER] ツールタブへの切り替えを開始'); // ツールタブをクリック const toolsTab = this.page.locator('[role="tab"]').filter({ hasText: 'ツール' }); await expect(toolsTab).toBeVisible(); await toolsTab.click(); console.log('[HELPER] ツールタブ内容の確認を開始'); // ツールセレクタが表示されることを確認 const toolSelect = this.page.locator('[data-testid="tool-select"]'); await expect(toolSelect).toBeVisible({ timeout: 10000 }); // Selectドロップダウンを開く await toolSelect.click(); // MCPツールの表示確認(ドロップダウン内のオプション) await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'todoist_get_tasks' })).toBeVisible({ timeout: 10000 }); await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'todoist_create_task' })).toBeVisible(); await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'todoist_update_task' })).toBeVisible(); await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'todoist_close_task' })).toBeVisible(); await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'todoist_get_projects' })).toBeVisible(); // ドロップダウンを閉じるためにエスケープキーを押す await this.page.keyboard.press('Escape'); console.log('[HELPER] ツールタブ内容の確認完了'); } /** * リソースタブに切り替えて内容を確認する */ async switchToResourcesTabAndVerify(): Promise<void> { console.log('[HELPER] リソースタブへの切り替えを開始'); // リソースタブをクリック const resourcesTab = this.page.locator('[role="tab"]').filter({ hasText: 'リソース' }); await expect(resourcesTab).toBeVisible(); await resourcesTab.click(); console.log('[HELPER] リソースタブ内容の確認を開始'); // リソースセレクタが表示されることを確認 const resourceSelect = this.page.locator('[data-testid="resource-select"]'); await expect(resourceSelect).toBeVisible({ timeout: 10000 }); // Selectドロップダウンを開く await resourceSelect.click(); // MCPリソースの表示確認(ドロップダウン内のオプション) await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'Todoist Tasks' })).toBeVisible({ timeout: 10000 }); await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'Todoist Projects' })).toBeVisible(); // 最初のリソースを選択してURIを確認 await this.page.locator('.ant-select-item-option').filter({ hasText: 'Todoist Tasks' }).click(); // リソース情報カード内のURIを確認(より具体的なセレクター) await expect(this.page.locator('[data-testid="resources-section"] .ant-card code').filter({ hasText: 'todoist://tasks' })).toBeVisible({ timeout: 5000 }); // 2番目のリソースも確認 await resourceSelect.click(); await this.page.locator('.ant-select-item-option').filter({ hasText: 'Todoist Projects' }).click(); await expect(this.page.locator('[data-testid="resources-section"] .ant-card code').filter({ hasText: 'todoist://projects' })).toBeVisible({ timeout: 5000 }); console.log('[HELPER] リソースタブ内容の確認完了'); } /** * プロンプトタブに切り替えて内容を確認する */ async switchToPromptsTabAndVerify(): Promise<void> { console.log('[HELPER] プロンプトタブへの切り替えを開始'); // プロンプトタブをクリック const promptsTab = this.page.locator('[role="tab"]').filter({ hasText: 'プロンプト' }); await expect(promptsTab).toBeVisible(); await promptsTab.click(); console.log('[HELPER] プロンプトタブ内容の確認を開始'); // プロンプトセレクタが表示されることを確認 const promptSelect = this.page.locator('[data-testid="prompt-select"]'); await expect(promptSelect).toBeVisible({ timeout: 10000 }); // Selectドロップダウンを開く await promptSelect.click(); // MCPプロンプトの表示確認(ドロップダウン内のオプション) await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'task_summary' })).toBeVisible({ timeout: 10000 }); await expect(this.page.locator('.ant-select-item-option').filter({ hasText: 'project_analysis' })).toBeVisible(); // ドロップダウンを閉じるためにエスケープキーを押す await this.page.keyboard.press('Escape'); console.log('[HELPER] プロンプトタブ内容の確認完了'); } /** * MCP接続UIのレイアウトを確認する */ async verifyMCPConnectionUILayout(): Promise<void> { console.log('[HELPER] MCP接続UIレイアウトの確認を開始'); // MCP Tester画面のレイアウト確認 await expect(this.page.locator('text=MCP テスター')).toBeVisible(); // タブの存在確認(具体的なロケーターを使用してstrict mode violationを回避) await expect(this.page.locator('[role="tab"]').filter({ hasText: '接続' })).toBeVisible(); await expect(this.page.locator('[role="tab"]').filter({ hasText: 'ツール' })).toBeVisible(); await expect(this.page.locator('[role="tab"]').filter({ hasText: 'リソース' })).toBeVisible(); await expect(this.page.locator('[role="tab"]').filter({ hasText: 'プロンプト' })).toBeVisible(); await expect(this.page.locator('[role="tab"]').filter({ hasText: 'デバッグ' })).toBeVisible(); // 接続タブがアクティブな状態であることを確認 const activeTab = this.page.locator('[role="tab"][aria-selected="true"]'); await expect(activeTab).toBeVisible(); console.log('[HELPER] MCP接続UIレイアウトの確認完了'); } /** * テストページに移動し、初期状態を確認する */ async navigateToTestPageAndVerify(): Promise<void> { console.log('[HELPER] テストページに移動します'); // テストページに移動 await this.page.goto('/test'); // ページロードを待機 await expect(this.page.locator('text=MCP テスター')).toBeVisible({ timeout: 10000 }); console.log('[HELPER] テストページの移動完了'); } /** * MCPツールを実行し、結果を取得する * @param toolName ツール名 * @param params パラメーター(JSON文字列) * @returns 実行結果文字列 */ async executeMCPTool(toolName: string, params: string = '{}'): Promise<string> { console.log(`[HELPER] MCPツール実行開始: ${toolName}`); // ツール選択 - Ant DesignのSelectコンポーネント用 const toolSelect = this.page.locator('[data-testid="tool-select"]'); await expect(toolSelect).toBeVisible({ timeout: 10000 }); // Selectのドロップダウンをクリック await toolSelect.click(); await this.page.waitForTimeout(500); // ツール名でオプションを選択 const optionSelector = `.ant-select-dropdown .ant-select-item[title*="${toolName}"]`; await this.page.locator(optionSelector).click(); await this.page.waitForTimeout(500); // パラメーター設定 if (params && params !== '{}') { const paramsObj = JSON.parse(params); for (const [key, value] of Object.entries(paramsObj)) { console.log(`[HELPER] パラメーター設定: ${key} = ${value}`); // Ant DesignのForm.Item内の入力要素を特定(複数パターン対応) const inputSelectors = [ `[data-testid="tool-params-form"] .ant-form-item:has(.ant-form-item-label:text("${key}")) input`, `[data-testid="tool-params-form"] .ant-form-item:has(.ant-form-item-label:text("${key}")) textarea`, `[data-testid="tool-params-form"] .ant-form-item:has(.ant-form-item-label:text("${key}")) .ant-select`, `[data-testid="tool-params-form"] input[id*="${key}"]`, `[data-testid="tool-params-form"] textarea[id*="${key}"]` ]; let inputSuccess = false; for (const selector of inputSelectors) { try { const inputElement = this.page.locator(selector).first(); await expect(inputElement).toBeVisible({ timeout: 2000 }); // Selectの場合は特別な処理 if (selector.includes('.ant-select')) { await inputElement.click(); await this.page.waitForTimeout(300); const optionElement = this.page.locator(`.ant-select-dropdown .ant-select-item:has-text("${value}")`).first(); if (await optionElement.isVisible()) { await optionElement.click(); } else { // オプションが見つからない場合は入力形式で試行 const searchInput = this.page.locator('.ant-select-dropdown input').first(); if (await searchInput.isVisible()) { await searchInput.fill(String(value)); await this.page.keyboard.press('Enter'); } } } else { // 通常の入力フィールド await inputElement.fill(String(value)); } console.log(`[HELPER] パラメーター ${key} の入力成功 (セレクタ: ${selector})`); inputSuccess = true; break; } catch (selectorError) { // このセレクタでは見つからない場合は次を試行 continue; } } if (!inputSuccess) { console.log(`[HELPER] パラメーター ${key} の入力に失敗: 全セレクタで要素が見つかりませんでした`); // 入力フィールドが見つからない場合もテストを継続 } } } // 実行ボタンをクリック const executeButton = this.page.locator('[data-testid="execute-tool-button"]'); await expect(executeButton).toBeVisible({ timeout: 5000 }); await expect(executeButton).toBeEnabled({ timeout: 5000 }); console.log(`[HELPER] 実行ボタンをクリックします: ${toolName}`); await executeButton.click(); console.log(`[HELPER] 実行ボタンクリック完了: ${toolName}`); // クリック後の状態変化を少し待機 await this.page.waitForTimeout(1000); // 結果を待機(移動操作はより時間がかかる可能性があるため、タイムアウトを延長) const maxWaitTime = toolName === 'todoist_move_task' ? 90 : 30; console.log(`[HELPER] 結果表示を待機中: ${toolName} (最大 ${maxWaitTime}秒)`); // 結果表示エリアが更新されるまで待機(より寛容な待機方法を試行) try { await expect(this.page.locator('[data-testid="tool-result-display"]')).toBeVisible({ timeout: maxWaitTime * 1000 }); } catch (waitError) { console.log(`[HELPER] 結果表示エリアの待機に失敗: ${waitError}`); // エラーメッセージが表示されていないかチェック const errorAlert = this.page.locator('.ant-alert-error'); if (await errorAlert.isVisible()) { const errorText = await errorAlert.textContent(); console.log(`[HELPER] エラーアラートが検出されました: ${errorText}`); } // ローディング状態が続いているかチェック const loadingElement = this.page.locator('[data-testid="execute-tool-button"][loading]'); if (await loadingElement.isVisible()) { console.log(`[HELPER] 実行ボタンがローディング状態のままです`); } throw waitError; } // 結果を取得 const resultElement = this.page.locator('[data-testid="tool-result-display"]'); const resultText = await resultElement.textContent(); console.log(`[HELPER] MCPツール実行完了: ${toolName}`); return resultText ?? ''; } /** * Todoistタスクを取得する * @param projectId プロジェクトID(オプション) */ async getTodoistTasks(projectId?: string): Promise<string> { console.log('[HELPER] Todoistタスク取得を開始'); let result: string; if (projectId) { const params = JSON.stringify({ project_id: projectId }); result = await this.executeMCPTool('todoist_get_tasks', params); } else { // プロジェクトIDが指定されていない場合はパラメータなしで実行 result = await this.executeMCPTool('todoist_get_tasks'); } console.log('[HELPER] Todoistタスク取得完了'); return result; } /** * Todoistタスクを作成する * @param content タスク内容 * @param projectId プロジェクトID(オプション) * @param description 説明(オプション) */ async createTodoistTask(content: string, projectId?: string, description?: string): Promise<string> { console.log('[HELPER] Todoistタスク作成を開始'); const params: any = { content }; if (projectId) params.project_id = projectId; if (description) params.description = description; const result = await this.executeMCPTool('todoist_create_task', JSON.stringify(params)); console.log('[HELPER] Todoistタスク作成完了'); return result; } /** * Todoistタスクを更新する * @param taskId タスクID * @param content タスク内容(オプション) * @param description 説明(オプション) * @param projectId プロジェクトID(オプション、タスク移動に使用) * @returns 実行結果のテキスト */ async updateTodoistTask(taskId: string, content?: string, description?: string, projectId?: string): Promise<string> { console.log('[HELPER] Todoistタスク更新を開始'); const params: any = { task_id: taskId }; if (content) params.content = content; if (description) params.description = description; if (projectId !== undefined) { // インボックスに移動する場合はnullを設定 params.project_id = projectId === 'inbox' ? null : projectId; } const result = await this.executeMCPTool('todoist_update_task', JSON.stringify(params)); console.log('[HELPER] Todoistタスク更新完了'); return result; } /** * Todoistタスクをクローズする * @param taskId タスクID */ async closeTodoistTask(taskId: string): Promise<string> { console.log('[HELPER] Todoistタスククローズを開始'); const params = { task_id: taskId }; const result = await this.executeMCPTool('todoist_close_task', JSON.stringify(params)); console.log('[HELPER] Todoistタスククローズ完了'); return result; } /** * Todoistプロジェクト一覧を取得する */ async getTodoistProjects(): Promise<string> { console.log('[HELPER] Todoistプロジェクト取得を開始'); const result = await this.executeMCPTool('todoist_get_projects'); console.log('[HELPER] Todoistプロジェクト取得完了'); return result; } /** * ツールタブに切り替える */ async switchToToolsTab(): Promise<void> { console.log('[HELPER] ツールタブに切り替え'); const toolsTab = this.page.locator('[role="tab"]').filter({ hasText: 'ツール' }); await expect(toolsTab).toBeVisible(); await toolsTab.click(); // ツールセレクタが表示されるまで待機 const toolSelect = this.page.locator('[data-testid="tool-select"]'); await expect(toolSelect).toBeVisible({ timeout: 10000 }); console.log('[HELPER] ツールタブ切り替え完了'); } /** * 実行結果が成功レスポンスかエラーレスポンスかを判定する * @param resultText 実行結果のテキスト * @returns { isSuccess: boolean, isAuthError: boolean, message: string } */ analyzeToolResult(resultText: string): { isSuccess: boolean; isAuthError: boolean; message: string } { console.log('[HELPER] ツール実行結果の解析開始'); console.log(`[DEBUG] 結果テキスト: "${resultText}"`); if (!resultText) { return { isSuccess: false, isAuthError: false, message: 'レスポンスが空です' }; } // 認証エラーの検出(より広範囲なパターン) if (resultText.includes('認証に失敗') || resultText.includes('Authentication failed') || resultText.includes('token') || resultText.includes('Token') || resultText.includes('Unauthorized') || resultText.includes('401')) { console.log('[DEBUG] 実行結果解析: 認証エラー'); return { isSuccess: false, isAuthError: true, message: '認証エラーが発生しました' }; } // エラーレスポンスの検出(より詳細なパターン) if (resultText.includes('Tool execution failed') || resultText.includes('Unknown error') || resultText.includes('Error:') || resultText.includes('エラー:') || resultText.includes('Failed to') || resultText.includes('失敗')) { console.log('[DEBUG] 実行結果解析: ツール実行エラー'); return { isSuccess: false, isAuthError: false, message: 'ツール実行エラーが発生しました' }; } // まず純粋なJSONとしてパースを試行 let parsedData = this.extractJsonFromResponse(resultText); if (parsedData) { return this.analyzeJsonResponse(parsedData); } // JSONでない場合、成功メッセージをチェック if (resultText.includes('成功') || resultText.includes('success') || resultText.includes('successfully') || resultText.includes('completed') || resultText.includes('created') || resultText.includes('updated') || resultText.includes('closed') || resultText.includes('deleted') || resultText.includes('marked as completed')) { console.log('[DEBUG] 実行結果解析: テキストベース成功判定'); return { isSuccess: true, isAuthError: false, message: '操作が正常に完了しました' }; } console.log('[DEBUG] 実行結果解析: その他のレスポンス'); return { isSuccess: false, isAuthError: false, message: '期待されるレスポンス形式ではありません' }; } /** * レスポンステキストからJSONデータを抽出する * @param resultText レスポンステキスト * @returns パースされたJSONオブジェクト(失敗した場合はnull) */ private extractJsonFromResponse(resultText: string): any | null { try { // まず純粋なJSONとしてパースを試行 return JSON.parse(resultText); } catch (directParseError) { console.log('[DEBUG] 直接JSON解析エラー、ネストされたJSONを抽出試行'); try { // "text": "..." の中身を抽出 const textMatch = resultText.match(/"text":\s*"([^"]+(?:\\.[^"]*)*?)"/); if (textMatch && textMatch[1]) { // エスケープされた文字列をアンエスケープ const unescapedText = textMatch[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\'); console.log('[DEBUG] 抽出されたテキスト:', unescapedText.substring(0, 200) + '...'); return JSON.parse(unescapedText); } } catch (nestedParseError) { console.log('[DEBUG] ネストされたJSON解析もエラー'); } try { // content配列から抽出を試行 const outerMatch = resultText.match(/\{[\s\S]*"content"[\s\S]*\}/); if (outerMatch) { const outerData = JSON.parse(outerMatch[0]); if (outerData.content && Array.isArray(outerData.content) && outerData.content[0] && outerData.content[0].text) { const innerText = outerData.content[0].text; console.log('[DEBUG] content配列から抽出されたテキスト:', innerText.substring(0, 200) + '...'); return JSON.parse(innerText); } } } catch (contentParseError) { console.log('[DEBUG] content配列からの抽出もエラー'); } return null; } } /** * JSONレスポンスの内容を解析する * @param parsedData パースされたJSONオブジェクト * @returns 解析結果 */ private analyzeJsonResponse(parsedData: any): { isSuccess: boolean; isAuthError: boolean; message: string } { console.log('[DEBUG] JSON解析成功、データタイプを判定中'); // 配列の場合 if (Array.isArray(parsedData)) { if (parsedData.length === 0) { console.log('[DEBUG] 実行結果解析: 空の配列(正常)'); return { isSuccess: true, isAuthError: false, message: '空のリストを取得しました' }; } // タスクリストの場合 const hasValidTasks = parsedData.every(item => item.id && item.content && typeof item.project_id === 'string' && typeof item.is_completed === 'boolean' && item.url && item.url.includes('todoist.com') ); if (hasValidTasks) { console.log(`[DEBUG] 実行結果解析: 実行成功 (${parsedData.length}個のタスク取得)`); return { isSuccess: true, isAuthError: false, message: `タスク一覧の取得成功: ${parsedData.length}個のタスク` }; } // プロジェクトリストの場合 const hasValidProjects = parsedData.every(item => item.id && item.name && typeof item.is_shared === 'boolean' && item.url && item.url.includes('todoist.com') ); if (hasValidProjects) { console.log(`[DEBUG] 実行結果解析: 実行成功 (${parsedData.length}個のプロジェクト取得)`); return { isSuccess: true, isAuthError: false, message: `プロジェクト一覧の取得成功: ${parsedData.length}個のプロジェクト` }; } } // 単一オブジェクト(タスク作成/更新)の場合 if (parsedData.id && parsedData.content) { console.log('[DEBUG] 実行結果解析: 実行成功 (単一タスク操作)'); return { isSuccess: true, isAuthError: false, message: 'タスク操作成功' }; } // その他の成功レスポンス(success: trueなど) if (parsedData.success === true) { console.log('[DEBUG] 実行結果解析: 実行成功 (成功フラグ)'); return { isSuccess: true, isAuthError: false, message: '操作成功' }; } console.log('[DEBUG] 実行結果解析: 認識できないデータ形式'); return { isSuccess: false, isAuthError: false, message: '認識できないレスポンス形式です' }; } /** * タスクの詳細情報を取得する * @param taskId タスクID * @returns タスクの詳細情報(取得に失敗した場合はnull) */ async getTaskDetails(taskId: string): Promise<any | null> { console.log(`[HELPER] タスク詳細取得開始: ${taskId}`); try { // 全タスクを取得してIDでフィルタ const allTasksResult = await this.getTodoistTasks(); const analysis = this.analyzeToolResult(allTasksResult); if (!analysis.isSuccess) { console.log('[HELPER] タスク一覧取得に失敗'); return null; } const extractedJson = this.extractJsonFromResponse(allTasksResult); if (!extractedJson || !Array.isArray(extractedJson)) { console.log('[HELPER] タスク一覧の解析に失敗'); return null; } const task = extractedJson.find(t => t.id === taskId || t.id === parseInt(taskId)); if (task) { console.log(`[HELPER] タスク詳細取得成功: ${task.content} (プロジェクト: ${task.project_id})`); return task; } else { console.log(`[HELPER] タスクが見つかりません: ${taskId}`); return null; } } catch (error) { console.log(`[HELPER] タスク詳細取得エラー: ${error}`); return null; } } /** * タスクが指定されたプロジェクトに存在するかを検証する * @param taskId タスクID * @param expectedProjectId 期待されるプロジェクトID * @returns 検証結果 */ async verifyTaskLocation(taskId: string, expectedProjectId: string): Promise<{ success: boolean; message: string; actualProjectId?: string }> { console.log(`[HELPER] タスク位置検証開始: ${taskId} → プロジェクト ${expectedProjectId}`); const taskDetails = await this.getTaskDetails(taskId); if (!taskDetails) { return { success: false, message: 'タスクの詳細取得に失敗しました' }; } const actualProjectId = taskDetails.project_id?.toString(); const expected = expectedProjectId === 'inbox' ? null : expectedProjectId; const actual = actualProjectId === null ? 'inbox' : actualProjectId; const expectedDisplay = expected === null ? 'inbox' : expected; if (actual === expectedDisplay) { console.log(`[HELPER] タスク位置検証成功: ${taskId} は ${expectedDisplay} に存在`); return { success: true, message: `タスクは期待されるプロジェクト(${expectedDisplay})に存在します`, actualProjectId: actual }; } else { console.log(`[HELPER] タスク位置検証失敗: ${taskId} は ${actual} に存在(期待: ${expectedDisplay})`); return { success: false, message: `タスクは期待されるプロジェクト(${expectedDisplay})ではなく、${actual}に存在します`, actualProjectId: actual }; } } /** * タスクの内容が期待される値と一致するかを検証する * @param taskId タスクID * @param expectedContent 期待される内容 * @returns 検証結果 */ async verifyTaskContent(taskId: string, expectedContent: string): Promise<{ success: boolean; message: string; actualContent?: string }> { console.log(`[HELPER] タスク内容検証開始: ${taskId} → "${expectedContent}"`); const taskDetails = await this.getTaskDetails(taskId); if (!taskDetails) { return { success: false, message: 'タスクの詳細取得に失敗しました' }; } const actualContent = taskDetails.content; if (actualContent === expectedContent) { console.log(`[HELPER] タスク内容検証成功: "${actualContent}"`); return { success: true, message: 'タスクの内容が期待される値と一致します', actualContent }; } else { console.log(`[HELPER] タスク内容検証失敗: "${actualContent}" (期待: "${expectedContent}")`); return { success: false, message: `タスクの内容が期待される値と異なります(実際: "${actualContent}", 期待: "${expectedContent}")`, actualContent }; } } /** * 改善されたツール実行結果解析(実際の状態検証を含む) * @param resultText APIレスポンステキスト * @param expectedChanges 期待される変更内容(オプション) * @returns 解析結果 */ async analyzeToolResultWithVerification( resultText: string, expectedChanges?: { taskId?: string; expectedProjectId?: string; expectedContent?: string; operationType?: 'create' | 'update' | 'move' | 'close' | 'get'; } ): Promise<{ isSuccess: boolean; isAuthError: boolean; message: string; verificationDetails?: any }> { console.log('[HELPER] 実行結果の詳細解析を開始'); // 基本的な実行結果解析 const basicAnalysis = this.analyzeToolResult(resultText); if (basicAnalysis.isAuthError) { return { ...basicAnalysis, message: '認証エラー: APIトークンが設定されていません' }; } if (!basicAnalysis.isSuccess) { return basicAnalysis; } let verificationDetails: any = {}; // 期待される変更が指定されている場合は実際の状態を検証 if (expectedChanges?.taskId && (expectedChanges.operationType === 'move' || expectedChanges.operationType === 'update')) { if (expectedChanges.expectedProjectId) { console.log('[HELPER] タスクの位置検証を実行中...'); const locationVerification = await this.verifyTaskLocation(expectedChanges.taskId, expectedChanges.expectedProjectId); verificationDetails.location = locationVerification; if (!locationVerification.success && expectedChanges.operationType === 'move') { return { ...basicAnalysis, isSuccess: false, message: `移動操作後の検証で問題を検出: ${locationVerification.message}`, verificationDetails }; } } if (expectedChanges.expectedContent) { console.log('[HELPER] タスクの内容検証を実行中...'); const contentVerification = await this.verifyTaskContent(expectedChanges.taskId, expectedChanges.expectedContent); verificationDetails.content = contentVerification; if (!contentVerification.success) { return { ...basicAnalysis, isSuccess: false, message: `内容更新の検証で問題を検出: ${contentVerification.message}`, verificationDetails }; } } } return { ...basicAnalysis, verificationDetails: Object.keys(verificationDetails).length > 0 ? verificationDetails : undefined }; } /** * タスク作成結果からタスクIDを抽出する * @param createResult タスク作成APIの実行結果 * @returns タスクID(抽出できない場合はnull) */ extractTaskIdFromCreateResult(createResult: string): string | null { console.log('[HELPER] タスクID抽出を開始'); console.log(`[DEBUG] 抽出対象テキスト: "${createResult}"`); try { // JSONレスポンスからIDを抽出 const parsedResult = JSON.parse(createResult); if (parsedResult.id) { const taskId = parsedResult.id.toString(); console.log(`[DEBUG] タスクID抽出成功: ${taskId}`); return taskId; } console.log('[DEBUG] JSONレスポンスにIDフィールドが見つかりません'); return null; } catch (parseError) { console.log('[DEBUG] JSON解析エラー、テキストからの抽出を試行'); // テキストレスポンスからIDを抽出(複数パターンの正規表現) const patterns = [ /"id":\s*["']?(\d+)["']?/, // "id": "123456789" または "id": 123456789 /\(ID:\s*(\d+)\)/, // (ID: 123456789) /ID:\s*(\d+)/, // ID: 123456789 /task\s+ID\s*:\s*(\d+)/i, // task ID: 123456789 /created\s+successfully.*?(\d{8,})/i // created successfully ... 123456789 ]; for (const pattern of patterns) { const match = createResult.match(pattern); if (match && match[1]) { const taskId = match[1]; console.log(`[DEBUG] テキストからタスクID抽出成功: ${taskId} (パターン: ${pattern.source})`); return taskId; } } console.log('[DEBUG] テキストからのタスクID抽出に失敗'); return null; } } /** * タスクを作成してIDを返す(テスト専用) * @param content タスク内容 * @param projectId プロジェクトID(オプション) * @param description 説明(オプション) * @returns タスクID(作成に失敗した場合はnull) */ async createTodoistTaskAndGetId(content: string, projectId?: string, description?: string): Promise<string | null> { console.log('[HELPER] タスク作成とID取得を開始'); const createResult = await this.createTodoistTask(content, projectId, description); const analysis = this.analyzeToolResult(createResult); if (analysis.isSuccess) { const taskId = this.extractTaskIdFromCreateResult(createResult); console.log(`[HELPER] タスク作成とID取得完了: ${taskId}`); return taskId; } else if (analysis.isAuthError) { console.log('[HELPER] APIトークン未設定のためタスク作成をスキップ'); return null; } else { console.log('[HELPER] タスク作成に失敗'); return null; } } /** * Web UIでエラー表示をチェックする * @returns エラーが検出された場合はエラーメッセージ、なければnull */ async checkForUIErrors(): Promise<string | null> { console.log('[HELPER] Web UIエラー表示チェック開始'); try { // 様々なエラー表示要素をチェック const errorSelectors = [ '[data-testid="error-message"]', '[data-testid="tool-error"]', '.ant-notification-notice-error', '.ant-message-error', '.ant-alert-error', '[role="alert"][class*="error"]', '.error-message', '.error-display', '[class*="error"][class*="notification"]', '[class*="error"][class*="toast"]' ]; for (const selector of errorSelectors) { try { const errorElement = this.page.locator(selector); const count = await errorElement.count(); if (count > 0) { // 表示されているエラー要素があるかチェック for (let i = 0; i < count; i++) { const element = errorElement.nth(i); const isVisible = await element.isVisible(); if (isVisible) { const errorText = await element.textContent(); if (errorText && errorText.trim()) { console.log(`[HELPER] UIエラー検出: "${errorText}" (セレクター: ${selector})`); return errorText.trim(); } } } } } catch (selectorError) { // このセレクターでは見つからなかった、次を試す continue; } } // コンソールエラーもチェック const consoleErrors = await this.page.evaluate(() => { const errors: string[] = []; const originalError = console.error; // 一時的にconsole.errorをフック console.error = (...args: any[]) => { errors.push(args.map(arg => String(arg)).join(' ')); originalError.apply(console, args); }; // 復元 setTimeout(() => { console.error = originalError; }, 100); return errors; }); if (consoleErrors.length > 0) { const errorMessage = `コンソールエラー: ${consoleErrors.join('; ')}`; console.log(`[HELPER] ${errorMessage}`); return errorMessage; } console.log('[HELPER] Web UIエラー表示チェック: エラーなし'); return null; } catch (checkError) { console.log(`[HELPER] エラーチェック自体でエラー: ${checkError}`); return null; } } /** * ツール実行後にUIエラーをチェックし、エラーがあれば例外を投げる * @param toolName 実行したツール名 */ async verifyNoUIErrors(toolName: string): Promise<void> { const errorMessage = await this.checkForUIErrors(); if (errorMessage) { throw new Error(`${toolName} 実行後にUIエラーが検出されました: ${errorMessage}`); } } /** * ツール実行とエラーチェックを組み合わせた安全な実行メソッド * @param toolName ツール名 * @param params パラメーター * @returns 実行結果 */ async executeMCPToolSafely(toolName: string, params?: string): Promise<string> { console.log(`[HELPER] 安全なMCPツール実行開始: ${toolName}`); // 実行前のエラーチェック await this.verifyNoUIErrors(`${toolName}実行前`); // ツール実行 const result = await this.executeMCPTool(toolName, params); // 少し待機してUIが更新されるのを待つ await this.page.waitForTimeout(1000); // 実行後のエラーチェック await this.verifyNoUIErrors(`${toolName}実行後`); console.log(`[HELPER] 安全なMCPツール実行完了: ${toolName}`); return result; } /** * Todoistプロジェクトを作成する * @param name プロジェクト名 * @param color プロジェクトの色(オプション) * @param parentId 親プロジェクトID(オプション) * @param isFavorite お気に入りに設定するか(オプション) * @returns 実行結果のテキスト */ async createTodoistProject(name: string, color?: string, parentId?: string, isFavorite?: boolean): Promise<string> { console.log('[HELPER] Todoistプロジェクト作成を開始'); const params: any = { name }; if (color) params.color = color; if (parentId) params.parent_id = parentId; if (isFavorite !== undefined) params.is_favorite = isFavorite; const result = await this.executeMCPTool('todoist_create_project', JSON.stringify(params)); console.log('[HELPER] Todoistプロジェクト作成完了'); return result; } /** * Todoistプロジェクトを更新する * @param projectId プロジェクトID * @param name 新しいプロジェクト名(オプション) * @param color 新しいプロジェクトの色(オプション) * @param isFavorite お気に入りに設定するか(オプション) * @returns 実行結果のテキスト */ async updateTodoistProject(projectId: string, name?: string, color?: string, isFavorite?: boolean): Promise<string> { console.log('[HELPER] Todoistプロジェクト更新を開始'); const params: any = { project_id: projectId }; if (name) params.name = name; if (color) params.color = color; if (isFavorite !== undefined) params.is_favorite = isFavorite; const result = await this.executeMCPTool('todoist_update_project', JSON.stringify(params)); console.log('[HELPER] Todoistプロジェクト更新完了'); return result; } /** * Todoistプロジェクトを削除する * @param projectId プロジェクトID * @returns 実行結果のテキスト */ async deleteTodoistProject(projectId: string): Promise<string> { console.log('[HELPER] Todoistプロジェクト削除を開始'); const params = { project_id: projectId }; const result = await this.executeMCPTool('todoist_delete_project', JSON.stringify(params)); console.log('[HELPER] Todoistプロジェクト削除完了'); return result; } /** * プロジェクト作成結果からプロジェクトIDを抽出する * @param createResult プロジェクト作成APIの実行結果 * @returns プロジェクトID(抽出できない場合はnull) */ extractProjectIdFromCreateResult(createResult: string): string | null { console.log('[HELPER] プロジェクトID抽出を開始'); console.log(`[DEBUG] 抽出対象テキスト: "${createResult}"`); try { // JSONレスポンスからIDを抽出 const parsedResult = JSON.parse(createResult); if (parsedResult.id) { const projectId = parsedResult.id.toString(); console.log(`[DEBUG] プロジェクトID抽出成功: ${projectId}`); return projectId; } console.log('[DEBUG] JSONレスポンスにIDフィールドが見つかりません'); return null; } catch (parseError) { console.log('[DEBUG] JSON解析エラー、テキストからの抽出を試行'); // テキストレスポンスからIDを抽出(複数パターンの正規表現) const patterns = [ /"id":\s*["']?(\d+)["']?/, // "id": "123456789" または "id": 123456789 /\(ID:\s*(\d+)\)/, // (ID: 123456789) /ID:\s*(\d+)/, // ID: 123456789 /project\s+ID\s*:\s*(\d+)/i, // project ID: 123456789 /created\s+successfully.*?(\d{8,})/i // created successfully ... 123456789 ]; for (const pattern of patterns) { const match = createResult.match(pattern); if (match && match[1]) { const projectId = match[1]; console.log(`[DEBUG] テキストからプロジェクトID抽出成功: ${projectId} (パターン: ${pattern.source})`); return projectId; } } console.log('[DEBUG] テキストからのプロジェクトID抽出に失敗'); return null; } } /** * プロジェクトを作成してIDを返す(テスト専用) * @param name プロジェクト名 * @param color プロジェクトの色(オプション) * @param parentId 親プロジェクトID(オプション) * @param isFavorite お気に入りに設定するか(オプション) * @returns プロジェクトID(作成に失敗した場合はnull) */ async createTodoistProjectAndGetId(name: string, color?: string, parentId?: string, isFavorite?: boolean): Promise<string | null> { console.log('[HELPER] プロジェクト作成とID取得を開始'); const createResult = await this.createTodoistProject(name, color, parentId, isFavorite); const analysis = this.analyzeToolResult(createResult); if (analysis.isSuccess) { const projectId = this.extractProjectIdFromCreateResult(createResult); console.log(`[HELPER] プロジェクト作成とID取得完了: ${projectId}`); return projectId; } else if (analysis.isAuthError) { console.log('[HELPER] APIトークン未設定のためプロジェクト作成をスキップ'); return null; } else { console.log('[HELPER] プロジェクト作成に失敗'); return null; } } /** * プロジェクト名に特定の文字列を含むプロジェクトを検索する * @param searchTerm 検索する文字列 * @returns マッチしたプロジェクトの配列(見つからない場合は空配列) */ async findProjectsByName(searchTerm: string): Promise<any[]> { console.log(`[HELPER] プロジェクト名検索を開始: "${searchTerm}"`); const projectsResult = await this.getTodoistProjects(); const analysis = this.analyzeToolResult(projectsResult); if (!analysis.isSuccess) { console.log('[HELPER] プロジェクト一覧の取得に失敗'); return []; } try { // JSONレスポンスから projects を抽出 const extractedJson = this.extractJsonFromResponse(projectsResult); if (!extractedJson || !Array.isArray(extractedJson)) { console.log('[HELPER] プロジェクト一覧の解析に失敗'); return []; } const matchingProjects = extractedJson.filter(project => project.name && project.name.includes(searchTerm) ); console.log(`[HELPER] プロジェクト名検索完了: ${matchingProjects.length}件見つかりました`); matchingProjects.forEach(project => { console.log(`[DEBUG] 見つかったプロジェクト: ${project.name} (ID: ${project.id})`); }); return matchingProjects; } catch (parseError) { console.log(`[HELPER] プロジェクト検索中にエラー: ${parseError}`); return []; } } /** * タスクを別のプロジェクトに移動する * @param taskId 移動するタスクのID * @param targetProjectId 移動先のプロジェクトID * @returns 移動結果のレスポンス文字列 */ async moveTodoistTask(taskId: string, targetProjectId: string): Promise<string> { console.log(`[HELPER] タスクを移動: ${taskId} → プロジェクト ${targetProjectId}`); const params = JSON.stringify({ task_id: taskId, project_id: targetProjectId }); const result = await this.executeMCPTool('todoist_move_task', params); console.log('[HELPER] タスク移動完了'); return result; } /** * 移動操作の結果から新しいタスクIDを抽出する * @param moveResult 移動操作の実行結果 * @returns 新しいタスクID(抽出できない場合はnull) */ extractNewTaskIdFromMoveResult(moveResult: string): string | null { try { // "New task ID: XXXXXXXX" のパターンを検索 const taskIdMatch = moveResult.match(/New task ID:\s*(\d+)/); if (taskIdMatch && taskIdMatch[1]) { return taskIdMatch[1]; } console.log('[HELPER] 新しいタスクIDの抽出に失敗'); return null; } catch (error) { console.log(`[HELPER] タスクID抽出でエラー: ${error}`); return null; } } /** * 移動操作の改善された解析機能(検証付き) * @param moveResult 移動操作の実行結果 * @param originalTaskId 元のタスクID * @param targetProjectId 移動先プロジェクトID * @returns 移動操作の解析結果 */ async analyzeMoveResultWithVerification( moveResult: string, originalTaskId: string, targetProjectId: string ): Promise<{ isSuccess: boolean; isAuthError: boolean; message: string; newTaskId?: string; verificationDetails?: { location?: { success: boolean; message: string; actualProjectId?: string; }; }; }> { console.log('[HELPER] 改善されたツール実行結果解析開始'); // 基本的な結果解析 const basicAnalysis = await this.analyzeToolResult(moveResult); if (basicAnalysis.isAuthError) { return { ...basicAnalysis, message: '認証エラー: APIトークンが設定されていません' }; } if (!basicAnalysis.isSuccess) { return { ...basicAnalysis, message: `移動操作が失敗しました: ${basicAnalysis.message}` }; } // 新しいタスクIDを抽出 const newTaskId = this.extractNewTaskIdFromMoveResult(moveResult); if (!newTaskId) { return { ...basicAnalysis, isSuccess: false, message: '移動操作は成功したようですが、新しいタスクIDを取得できませんでした' }; } // 新しいタスクの位置を検証 console.log(`[HELPER] 新しいタスク ${newTaskId} の位置検証を実行中...`); const locationVerification = await this.verifyTaskLocation(newTaskId, targetProjectId); if (locationVerification.success) { return { ...basicAnalysis, newTaskId, message: `タスク移動が成功しました。新しいタスクID: ${newTaskId}`, verificationDetails: { location: locationVerification } }; } else { return { ...basicAnalysis, newTaskId, isSuccess: false, message: `移動操作は完了しましたが、期待されたプロジェクトに新しいタスクが見つかりません: ${locationVerification.message}`, verificationDetails: { location: locationVerification } }; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kentaroh7777/mcp-todoist'

If you have feedback or need assistance with the MCP directory API, please join our Discord server