👻

MastraのWorkflowで作る、URLから自動レポート生成ツール

に公開

はじめに

TRUSTARTでWebエンジニアをしています、gumpenです。趣味はGoogle Mapsを探検することです。

今回は、MastraのWorkflow機能を使って、URLを渡すと自動的に関連情報を収集しレポートを生成するツールを試作したお話です。

動機・解決したい課題

  • Deep Researchっぽいものを作ってみたい!
  • 日頃Xなどで興味深い内容や記事を見つけても、専門用語の理解や背景知識が足りず内容を把握しきれないことがある
  • また、自分で調べることができたとしても時間や手間がかかっている

Mastraとは

MastraはTypeScript向けのオープンソースAIエージェントフレームワークです。
エージェント、ワークフロー、RAG、統合、評価などの機能を提供し、AIアプリケーションを迅速に構築できます。

今回はエージェントではなくワークフローで実装します
https://mastra.ai/ja/docs/workflows/overview

使用する技術

  • フレームワーク
    • Mastra
  • LLM API
    • Gemini API
  • 検索 API
    • Tavily API
  • 開発
    • Claude Code

ワークフロー全体の流れ

  1. 入力されたURLのコンテンツ取得
  2. 要約
  3. 関連コンテンツの検索クエリ生成
  4. Web検索
  5. 関連コンテンツ取得
  6. レポート作成

コード

export const deepResearchWorkflow = createWorkflow({
  id: "deep-research-workflow",
  description: "URLの内容を深く調査し、包括的なレポートを生成するワークフロー",
  inputSchema: z.object({
    url: z.string().url().describe("調査対象のURL"),
  }),
  outputSchema: z.object({
    report: z.string().describe("最終的な調査レポート"),
    summary: z.string().describe("レポートの要約"),
  }),
})
  .then(fetchUrlStep)
  .then(summarizeContentStep)
  .then(generateQueriesStep)
  .then(searchTavilyStep)
  .then(fetchMultipleUrlsStep)
  .then(generateReportStep)
  .commit();

各ステップ

全てのコードでは長いので、ステップの定義コードのみ記載します

入力されたURLのコンテンツ取得

const fetchUrlStep = createStep({
  id: "fetchUrlStep",
  description: "指定されたURLからコンテンツを取得",
  inputSchema: z.object({
    url: z.string().url().describe("調査対象のURL"),
  }),
  outputSchema: z.object({
    content: z.string(),
    title: z.string().optional(),
    url: z.string(),
  }),
  execute: async ({ inputData, runtimeContext }) => {
    console.log("🔍 [WORKFLOW STEP 1] URLコンテンツ取得を開始");
    const result = await fetchUrl.execute({ context: { url: inputData.url }, runtimeContext });
    return result;
  },
});

コンテンツの要約

const summarizeContentStep = createStep({
  id: "summarizeContentStep", 
  description: "コンテンツを要約してトークン使用量を最適化",
  inputSchema: z.object({
    content: z.string(),
    title: z.string().optional(),
    url: z.string(),
  }),
  outputSchema: z.object({
    summary: z.string(),
    originalLength: z.number(),
    summaryLength: z.number(),
    title: z.string().optional(),
    url: z.string(),
  }),
  execute: async ({ inputData, runtimeContext }) => {
    console.log("🔍 [WORKFLOW STEP 2] コンテンツ要約を開始");
    const result = await summarizeContent.execute({ 
      context: { 
        content: inputData.content,
        maxLength: 800,
        focus: "研究に重要な情報とキーワード"
      },
      runtimeContext
    });
    return {
      ...result,
      title: inputData.title,
      url: inputData.url,
    };
  },
});

関連コンテンツの検索クエリ生成

元コンテンツから、関連コンテンツを検索するための検索用文字列をいくつかLLMに生成してもらいます

const generateQueriesStep = createStep({
  id: "generateQueriesStep",
  description: "要約されたコンテンツから検索クエリを生成", 
  inputSchema: z.object({
    summary: z.string(),
    originalLength: z.number(),
    summaryLength: z.number(),
    title: z.string().optional(),
    url: z.string(),
  }),
  outputSchema: z.object({
    queries: z.array(z.string()),
    summary: z.string(),
    title: z.string().optional(),
    url: z.string(),
  }),
  execute: async ({ inputData, runtimeContext }) => {
    console.log("🔍 [WORKFLOW STEP 3] 検索クエリ生成を開始");
    const result = await generateSearchQueries.execute({
      context: {
        content: inputData.summary,
        context: `元のタイトル: ${inputData.title || 'なし'}`
      },
      runtimeContext
    });
    return {
      queries: result.queries,
      summary: inputData.summary,
      title: inputData.title,
      url: inputData.url,
    };
  },
});

Web検索

Tavily APIを使って、検索クエリを渡して結果を取得します

const searchTavilyStep = createStep({
  id: "searchTavilyStep",
  description: "生成された検索クエリでWeb検索を実行",
  inputSchema: z.object({
    queries: z.array(z.string()),
    summary: z.string(),
    title: z.string().optional(),
    url: z.string(),
  }),
  outputSchema: z.object({
    searchResults: z.array(z.object({
      query: z.string(),
      results: z.array(z.object({
        title: z.string(),
        url: z.string(),
        content: z.string(),
        score: z.string(),
      })),
    })),
    summary: z.string(),
    title: z.string().optional(),
    url: z.string(),
  }),
  execute: async ({ inputData, runtimeContext }) => {
    console.log("🔍 [WORKFLOW STEP 4] Web検索を開始");
    const searchResults = [];
    
    for (const query of inputData.queries) {
      const result = await searchTavily.execute({
        context: { query, maxResults: 3 },
        runtimeContext
      });
      searchResults.push({
        query,
        results: result.results,
      });
    }
    
    return {
      searchResults,
      summary: inputData.summary,
      title: inputData.title,
      url: inputData.url,
    };
  },
});

関連コンテンツ取得

const fetchMultipleUrlsStep = createStep({
  id: "fetchMultipleUrlsStep",
  description: "検索結果のURLからコンテンツを取得",
  inputSchema: z.object({
    searchResults: z.array(z.object({
      query: z.string(),
      results: z.array(z.object({
        title: z.string(),
        url: z.string(),
        content: z.string(),
        score: z.string(),
      })),
    })),
    summary: z.string(),
    title: z.string().optional(),
    url: z.string(),
  }),
  outputSchema: z.object({
    fetchedResults: z.array(z.object({
      url: z.string(),
      content: z.string().optional(),
      title: z.string().optional(),
      error: z.string().optional(),
    })),
    summary: z.string(),
    title: z.string().optional(),
    url: z.string(),
  }),
  execute: async ({ inputData, runtimeContext }) => {
    console.log("🔍 [WORKFLOW STEP 5] 関連URLコンテンツ取得を開始");
    
    // Extract all URLs from search results
    const urls: string[] = [];
    for (const searchResult of inputData.searchResults) {
      for (const result of searchResult.results) {
        urls.push(result.url);
      }
    }
    
    // Remove duplicates
    const uniqueUrls = [...new Set(urls)];
    
    const result = await fetchMultipleUrls.execute({
      context: { urls: uniqueUrls, maxConcurrency: 3 },
      runtimeContext
    });
    
    return {
      fetchedResults: result.results,
      summary: inputData.summary,
      title: inputData.title,
      url: inputData.url,
    };
  },
});

レポート作成

今まで取得したデータをLLMに渡してレポート作成をしてもらいます

const generateReportStep = createStep({
  id: "generateReportStep",
  description: "すべての調査結果を統合して最終レポートを生成",
  inputSchema: z.object({
    fetchedResults: z.array(z.object({
      url: z.string(),
      content: z.string().optional(),
      title: z.string().optional(),
      error: z.string().optional(),
    })),
    summary: z.string(),
    title: z.string().optional(),
    url: z.string(),
  }),
  outputSchema: z.object({
    report: z.string(),
    summary: z.string(),
  }),
  execute: async ({ inputData, runtimeContext }) => {
    console.log("🔍 [WORKFLOW STEP 6] 最終レポート生成を開始");
    
    // Prepare research results for report generation
    const researchResults = inputData.fetchedResults
      .filter((result: any) => result.content && !result.error)
      .map((result: any) => ({
        source: result.title || result.url,
        content: result.content!,
      }));
    
    // Generate search queries array (we'll reconstruct from context)
    const searchQueries = ["関連情報検索"];
    
    const result = await generateFinalReport.execute({
      context: {
        originalUrl: inputData.url,
        originalContent: inputData.summary,
        searchQueries,
        researchResults,
      },
      runtimeContext
    });
    
    return result;
  },
});

実際に動かす

入力

先週テックブログに投稿された、片ノ坂さんの記事を入れてみます

https://zenn.dev/trustart_dev/articles/b654479104af0a

出力

出力内容
承知いたしました。ご提供いただいた情報を基に、包括的で洞察に富んだ研究レポートを作成します。

***

# 研究レポート:textlintとModel Context Protocol (MCP) の統合が拓く、次世代の文章校正とドキュメンテーションの未来

## エグゼクティブサマリー

本レポートは、自然言語の静的解析ツールである **`textlint`** が、AIと外部システムの連携を標準化する **`Model Context Protocol (MCP)`** に実験的に対応したという最新動向を分析するものです。

この統合は、従来のルールベースの厳密な文章校正と、大規模言語モデル(LLM)が持つ高度な文脈理解能力を融合させる画期的な試みです。

主要な発見事項は以下の通りです。

-   **ハイブリッドアプローチの到来:** `textlint`とMCPの連携は、ルールベースの正確性とAIの柔軟性を両立させる「ハイブリッド型文章校正」という新たなトレンドを象徴しています。これにより、単なる誤字脱字の指摘に留まらず、文脈に応じたより高度な文章の提案や修正が可能になります。

-   **AI連携の標準化と開発効率の向上:** MCPは、これまで個別実装が必要だったAIと外部ツール間の連携を標準化します。これにより、開発者はAI連携機能をより迅速かつ容易に自身のアプリケーションやワークフローに組み込めるようになり、エコシステムの発展が加速します。

-   **「生きたドキュメント」の実現可能性:** この技術的進歩は、ソフトウェアの仕様書や企業のナレッジベースといったドキュメントが、コードの変更や新たな情報に追従して半自動的に更新される「生きたドキュメント」の実現を大きく前進させます。

本レポートでは、これらの動向がソフトウェア開発、コンテンツ制作、ナレッジマネジメントといった分野に与える具体的な影響と、今後の展望について深く考察します。


## 元のコンテンツの概要

本分析の起点となるのは、TRUSTART株式会社のシニアマネージャーである片ノ坂卓磨氏が、同社のテックブログに寄稿した記事「textlintがMCP(Model Context Protocol)に実験的に対応したので早速試してみた」です。

この記事は、自然言語のリンティングツールとして広く利用されている`textlint`が、AI連携のための新しいプロトコルであるMCPに実験的に対応したことを受け、その機能をいち早く検証した実践的な内容となっています。

この動きは、技術コミュニティがAI関連の新しい標準技術にいかに迅速に反応し、その可能性を積極的に探求しているかを示す好例と言えます。
開発者が自ら最新技術を試し、その知見を共有する文化が、イノベーションを加速させる原動力となっていることが伺えます。


## 関連調査から得られた主要な洞察

関連調査を通じて、今回の動向を多角的に理解するための3つの重要な洞察が得られました。

1.  **Model Context Protocol (MCP) の戦略的重要性**
    MCPは、ChatGPTやClaudeのような生成AI(LLM)が、外部のデータやツールと効率的に連携するための**標準化された通信規約**です(ソース1, 2, 3)。従来、AIと外部システムを連携させるには、システムごとにAPIを個別開発する必要があり、開発効率の低下や保守性の問題を抱えていました。MCPは、このプロセスを標準化することで、**AIが正確なコンテキスト(文脈)を理解し、より質の高い応答を生成する**ことを可能にします。これは、AIの能力を最大限に引き出すための基盤技術として、極めて戦略的な重要性を持っています。

2.  **textlint のエコシステムと役割**
    `textlint`は、単なる文章校正ツールではなく、**ルールを自由に組み合わせ、独自ルールも作成できる拡張性の高いプラットフォーム**です(ソース5, 7, 9)。多くの企業や開発者が、技術ドキュメントの品質維持や表記揺れの統一のために導入しており(ソース6, 8)、VS Codeなどのエディタ拡張機能を通じて執筆フローにシームレスに統合されています(ソース11)。この堅牢なエコシステムとカスタマイズ性が、`textlint`がAI連携のフロントランナーとなり得た土台となっています。

3.  **自然言語処理(NLP)におけるAI校正の進化**
    自然言語処理(NLP)技術は、BERT(ソース14)などの登場により飛躍的に進化し、AIを活用した文章校正ツールが多数登場しています(ソース21, 22, 23)。しかし、多くは汎用的な校正に留まります。一方で、NTTコミュニケーションズの事例(ソース18)のように、特定の文脈(自社ブログ)に合わせてLLMのプロンプトを調整し、校正CIを構築する試みも見られます。`textlint`とMCPの統合は、この**「文脈に合わせた高度なAI校正」を、より標準的かつ容易に実現する**道を開くものです。


## トレンドと発展

今回の`textlint`とMCPの統合は、より大きな技術トレンドの一部として捉えることができます。

-   **トレンド1: ルールベースとAIのハイブリッド化**
    厳密で予測可能な結果を出すルールベースの手法と、文脈やニュアンスを理解するAIの手法を組み合わせる動きが加速しています。`textlint`が持つ「表記揺れは許さない」といった厳格なルールと、MCP経由で連携するLLMの「この文脈なら、より自然な表現はこちらです」といった柔軟な提案が共存することで、**文章の品質管理は新たな次元へと進化します。**

-   **トレンド2: AI連携の標準化と民主化**
    MCPや、WebサイトがLLMにどう情報を提供するかを定義する`/llms.txt`(ソース19)のような規格の登場は、AIとの連携を標準化する大きな流れを示しています。これにより、専門家でなくてもAIの力を活用しやすくなり、AI機能の「民主化」が進みます。開発者は複雑なAPI連携の詳細を気にすることなく、本質的な価値創造に集中できるようになります。

-   **トレンド3: 「生きたドキュメント」への挑戦**
    プロダクトの仕様書などが陳腐化し、メンテナンスコストが増大する問題は多くの組織が抱える課題です(ソース20)。AIを活用してドキュメントを常に最新の状態に保つ「生きたドキュメント」は、長年の夢でした。`textlint`とMCPの連携は、コードの変更をトリガーにドキュメントの該当箇所をAIが自動で修正・提案するといった、よりダイナミックなドキュメント管理の実現可能性を大きく高めます。


## 実用的な応用例

`textlint`とMCPの統合は、様々な分野で具体的な価値を生み出す可能性を秘めています。

-   **ソフトウェア開発の現場で**
    -   **技術ドキュメントの品質向上:** プロジェクト固有の用語やアーキテクチャ名を登録した`textlint`ルールとLLMを連携させ、APIドキュメントやREADMEの記述ミス、不整合を自動で検出・修正提案する。
    -   **コードレビューの効率化:** コード内のコメントが、関連する仕様書の内容と一致しているかをAIが検証し、レビュアーの負担を軽減する。

-   **コンテンツ制作とマーケティング**
    -   **ブランドボイスの維持:** 企業のブランドガイドライン(トーン&マナー、使用禁止用語など)を`textlint`ルールとして定義し、AIがそれに沿った文章を生成・校正する。これにより、複数人が執筆しても一貫した品質のコンテンツを維持できる。
    -   **SEOコンテンツの最適化:** SEOのベストプラクティスをルール化し、AIがキーワードの適切な配置や読みやすい構成を提案する。

-   **企業のナレッジマネジメント**
    -   **社内規定・マニュアルの整備:** 社内規定や業務マニュアルの表記揺れを`textlint`で統一し、内容が古くなった箇所をAIが指摘・更新案を提示する。これにより、情報が常に正確で信頼できる状態に保たれる。


## 結論と今後の展望

`textlint`とModel Context Protocol (MCP) の実験的な統合は、単なる一技術のアップデートに留まりません。
これは、**構造化されたルールによる「正確性」と、AIによる高度な文脈理解による「柔軟性」を融合させる、次世代の文章作成・管理パラダイムの幕開け**を告げる重要な一歩です。

今後は、MCPのさらなる普及と共に、この連携をサポートするツールやサービスが増加することが予想されます。
執筆エディタ上で、リアルタイムにAIが文脈を理解し、プロジェクトのルールに準拠した的確な修正案を提示してくれる、そんな執筆体験が当たり前になる日も遠くないでしょう。

この技術革新は、人間を文章作成の定型的な作業から解放し、より創造的で本質的な思考に集中させることを可能にします。
`textlint`とMCPの出会いは、私たちの「書く」という行為そのものを、より高度で効率的なものへと変革していく大きな可能性を秘めているのです。

文章中にソースとありますが、末尾には特に引用元などはありませんね。これはプロンプトで改善できそうです。
調査内容によってどのようなレポート構成にするかなど、工夫の余地はたくさんありますね。

今後の改善

  • レポートの出力フォーマット
    • 現在はマークダウンとしてプレビューするのに一手間必要なため
  • 要約の必要性再検討、検索範囲の調整
    • せっかくGemini2.5使うならば100万トークン入力できるので、データを絞る必要がないのかも
  • 各ステップでどのモデルを使うか最適化
    • Gemini 2.5 Proだと有能だがコストも時間もかかる
  • Xに対応
  • ソースグラウンディング
  • 検証
    • 実際に出てきているレポートの内容が正しいのか、内容として優れているのか検証が必要

おわりに

  • 作ってみて、この仕組みならDifyやn8nでできそうかもと思いました
    • 最初はエージェントでやろうとして、分岐をエージェントに任せる必要がないと気づいてワークフローに方針変更した経緯があります
  • とはいえ、エージェントでの試作、ワークフローでの試作合わせても2~3時間でできるのはMastra + Claude Codeすごい
    • MastraドキュメントのMCPサーバーを繋いでいたのが大きそう
    • Mastraの使い方について間違えることはほとんどありませんでした

TRUSTART株式会社は、一緒に働くメンバーを募集しています!
インターンメンバーも大募集中です!
興味を持っていただいた方は、ぜひ弊社のページをご確認ください!!!

https://www.trustart.co.jp/recruit/

TRUSTARTテックブログ

Discussion