白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

MastraでMCPが使えるコンソールアプリを作ろう(後編)

続きです。
前編の記事はこちら。

shironeko.hateblo.jp

そしてコードはこちら。

github.com

今回は自分でエージェントを作っていきます。

ツールを作る

まずはツールを作りましょう。
ツールはエージェントやワークフローで利用できる機能(関数?ツール?)です。
ツールの説明にツールって入れるの微妙だけど、説明が難しいのでなんとなくでお願いします。

わりとお決まりですが、LLMは現在時刻がわからないので時刻を提供するツールを作ります。
src/mastra/toolsにdatetime-tool.tsを作成します。

import { createTool } from "@mastra/core";
import { z } from 'zod';

export const datetimeTool = createTool({
    id: "get-current-datetime",
    description: "現在の日時を取得します",
    outputSchema: z.object({
        datetime: z.string().describe("現在の日時(日本時間)"),
    }),
    execute: async () => {
        const now = new Date();
        return { datetime: now.toString() };
    },
});

今回は入力はないのでinputSchemaは省略していますが、もちろん書けます。
ここで、descriptionやoutputSchemaは人間向けの情報ではなく、このツールを利用するエージェントに向けた情報なのでちゃんと書いておきましょう。
これをもとにどんな時にどうやって使うのかをエージェントが解釈してくれます。
MCPサーバを作ったことがある人ならすごく似ていると感じるんじゃないでしょうか。

エージェントを作る

次にとりあえずエージェントに組み込んでみたいので、このツールそのままの機能を持つエージェントを作ってみましょう。
src/mastra/agentsにdatetime-agent.tsを作成します。

import { Agent } from "@mastra/core/agent";
import { datetimeTool } from "../tools/datetime-tool";
import { google } from "@ai-sdk/google";

export const datetimeAgent = new Agent({
    name: "Datetime Agent",
    instructions: "あなたは、正確な日付と時刻の情報を提供する便利な日時アシスタントです。",
    model: google('gemini-2.5-flash-preview-05-20'),
    tools: { datetimeTool },
});

はい。toolsでこのエージェントはこのツールを使えますよというのを指定しています。
そしてsrc/mastra/index.tsにおいて、agentsでこの新しいエージェントを登録します。

import { Mastra } from '@mastra/core/mastra';
import { PinoLogger } from '@mastra/loggers';
import { LibSQLStore } from '@mastra/libsql';
import { weatherAgent } from './agents/weather-agent';
import { datetimeAgent } from './agents/datetime-agent';

export const mastra = new Mastra({
  agents: { weatherAgent, datetimeAgent },
  networks: { researchNetwork },
  storage: new LibSQLStore({
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
});

前回作ったsrc/mastra/index.tsを書き換えて

import 'dotenv/config';
import { mastra } from './mastra';

async function main() {
    const agent = mastra.getAgent('datetimeAgent');
    const result = await agent.generate('現在の日時は?');
    console.log(result.text);
}

main();

実行するとこんな感じですね。

npm run index

> mastra_sample@1.0.0 index
> npx tsx src/index.ts

現在の日時は、202567() 15:55:33 (日本標準時)です。

MCPを利用するエージェントを作る

さて、Mastra自体はMCPをそのまま読み込めるのですが、エージェントに読み込ませるにはツールとして登録するという形になります。パッケージを追加でインストールしましょう。

npm i @mastra/mcp

新しいエージェントを作ります。
ファイルの読み書きができるfilesystemのMCPを使ってみます。

import { google } from "@ai-sdk/google";
import { Agent } from "@mastra/core/agent";
import { MCPClient } from "@mastra/mcp";

export const mcp = new MCPClient({
  id: "filesystem-agent",
  servers: {
    filesystem: {
      command: "npx",
      args: [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "<ファイルの読み書きを許可するパス>"
      ]
    },
  },
});

export const filesystemAgent = new Agent({
    name: "FileSystem Agent",
    instructions: `あなたは、ファイルの操作を行うアシスタントです。`,
    model: google('gemini-2.5-flash-preview-05-20'),
    tools: await mcp.getTools(),
});

で、こんな感じなのですが、実はmastra.getAgentを使わなくてもそのままexportされたAgentも使えます。
mcpは本当はexportしたくなかったのですが、disconnectしないとアプリケーションが終了しないので仕方なく…これは正しいのでしょうか。

import 'dotenv/config';
import { filesystemAgent, mcp } from './mastra/agents/filesystem-agent';

async function main() {
    const result = await filesystemAgent.generate(`許可されたディレクトリにファイルを作成して、内容を書き込んでください。
ファイル名は "example.txt" で、内容は "Hello, Mastra!" としてください。
確認を求められた場合は許可してください。`);
    console.log(result.text);
    await mcp.disconnect();
}

main();

はい。ちゃんと作ってくれました。
ちょっとプロンプトを工夫しないと許可を求めて止まってしまったりしますね。

npm run index

> mastra_sample@1.0.0 index
> npx tsx src/index.ts

Secure MCP Filesystem Server running on stdio
Allowed directories: [
  '<ファイルの読み書きを許可するパス>'
]
ファイル "example.txt" が許可されたディレクトリに作成され、内容 "Hello, Mastra!" が書き込まれました。

AgentNetworkを作る

最後にAgentNetworkを作りましょう。
エージェントにツールやMCPを登録できることはわかりましたが、エージェント同士のオーケストレーションをしたい場合にはAgentNetworkが便利そうです。
ただし、本記事の執筆時点では実験的機能(Experimental)なので、変更が入るかもしれません。

今回はリサーチネットワークを作成します。
これはexamplesのagent-networkを参考に、今回作成したエージェントを付け足したものです。お題に沿って調査を行い、結果をファイル出力します。

src/mastra/agents/research-agent.tsを作成します。
内容は長くなるので抜粋ですが、基本的にはexamplesのプロンプトを日本語にしただけです。

export const primaryResearchAgent = new Agent({
  name: 'Primary Research Agent',
  instructions: `
    あなたは主任リサーチコーディネーターです。あなたの仕事は以下の通りです。
    1. ユーザーからの問い合わせを分析し、どのような調査が必要かを判断する
    2. 複雑なリサーチクエスチョンを管理しやすいサブクエスチョンに分解する
    3. 専門的な調査エージェントからの情報を、首尾一貫した回答にまとめる
    4. すべての主張が証拠によって適切に裏付けられていることを確認する
    5. さらなる調査が必要な調査のギャップを特定する

    中立的で客観的な口調を保ち、スピードよりも正確さを優先すること。
  `,
  model: google('gemini-2.5-flash-preview-05-20')
});

export const webSearchAgent = new Agent({
  name: 'Web Search Agent',
  instructions: `
    あなたはウェブ検索のスペシャリストです。あなたの仕事は以下の通りです。
    1. 与えられたクエリに対して、最も関連性の高い最新の情報をオンラインで検索する
    2. 情報源の信頼性を評価し、信頼できる情報に優先順位をつける
    3. ウェブコンテンツから重要な事実やデータポイントを抽出する
    4. 適切な場合は、直接引用や引用を行う
    5. 調査結果を簡潔明瞭にまとめる

    情報を報告する際は、必ず出典のURLを記載すること。
  `,

  model: google('gemini-2.5-flash-preview-05-20', {
    useSearchGrounding: true,
  }),
});

webSearchAgentはモデル作成時にuseSearchGroundingをtrueにしています。これはGeminiがGoogle検索を使ってくれるための設定です。

次にsrc/mastra/network/research-network.tsを作成します。
追加でエージェントの設定をしているのと、それに伴って追加の指示を指定しています。

export const researchNetwork = new AgentNetwork({
  name: 'Research Network',
  agents: [primaryResearchAgent, webSearchAgent, academicResearchAgent, factCheckingAgent, dataAnalysisAgent, datetimeAgent, filesystemAgent],
  model: google('gemini-2.5-flash-preview-05-20'),
  instructions: `
      あなたは、クエリを適切な専門エージェントにルーティングする研究調整システムです。
      
      利用可能なエージェントは以下の通りです:
      1. Primary Research Agent: 研究活動を調整し、複雑な質問を分解し、情報を統合する。
      2. Web Search Agent: 適切な引用とともに最新の情報をオンラインで検索する。
      3. Academic Research Agent: 学術的視点、理論、学術的背景を提供する。
      4. Fact Checking Agent: 主張を検証し、誤報の可能性を特定する。
      5. Data Analysis Agent:  数値データ、統計データを解釈し、パターンを特定する。
      6. Datetime Agent: 正確な日付と時刻の情報を提供する。
      7. FileSystem Agent: ファイルの操作を行う。

      各ユーザークエリに対して以下の通りに処理を行います:
      1. 一次調査エージェントからクエリを分析し、分解する。
      2. 各エージェントの専門知識に基づいて、適切な専門エージェントにサブクエリをルーティングする。
      3. 必要に応じてファクトチェック・エージェントを使い、重要な主張を検証する。
      4. 一次調査担当者に戻り、すべての調査結果を総合的な回答にまとめる。
      5. 回答をMarkdown形式でフォーマットし、FileSystem_Agentで許可されたディレクトリに「{yyyy-MM-dd-HH-mm-ss}.md」の名前で保存する。保存時に確認は不要。

      エージェント間の証拠の連鎖と適切な帰属関係を常に維持する。
      回答は必ず日本語で行う。
    `,
});

はい。これでネットワークの完成です。
動かしてみましょう。

async function main() {

  console.log('🔍 リサーチ中です...\n');

  const result = await researchNetwork.stream('昨日~今日にかけての生成AIに関するニュースをまとめて', {
    maxSteps: 20
  });

  for await (const part of result.fullStream) {
    switch (part.type) {
      case 'error':
        console.error(part.error);
        break;
      case 'text-delta':
        process.stdout.write(part.textDelta);
        break;
      case 'tool-call':
        console.log(`calling tool ${part.toolName} with args ${JSON.stringify(part.args, null, 2)}`);
        break;
      case 'tool-result':
        console.log(`tool result ${JSON.stringify(part.result, null, 2)}`);
        break;
    }
  }

  await mcp.disconnect();

  console.log('\n🏁 リサーチが完了しました');
}

main();

いい感じにログが出るので見ていて楽しいです。
なんかファイル保存に1回失敗しているので、この辺りは調整してもよさそうですね。 ファクトチェックとかは利用されてなさそうです。

🔍 リサーチ中です...

calling tool transmit with args {
  "actions": [
    {
      "agent": "Datetime_Agent",
      "input": "昨日と今日の日付をYYYY-MM-DD形式で教えてください。"
    }
  ]
}
tool result "[Datetime_Agent]: 今日の日付は2025-06-07です。昨日の日付は2025-06-06です。"
calling tool transmit with args {
  "actions": [
    {
      "agent": "Web_Search_Agent",
      "input": "2025-06-06から2025-06-07にかけての生成AIに関するニュースを検索してください。"
    }
  ]
}
tool result "[Web_Search_Agent]: 2025年6月6日から2025年6月7日にかけての生成AIに関するニュースは以下の通りです。(略)
calling tool transmit with args {
  "actions": [
    {
      "agent": "Datetime_Agent",
      "input": "現在の時刻をYYYY-MM-DD-HH-mm-ss形式で教えてください。"
    }
  ]
}
tool result "[Datetime_Agent]: 2025-06-07-22-20-09です。"
calling tool transmit with args {
  "actions": [
    {
      "agent": "FileSystem_Agent",
      "input": "ファイル名2025-06-07-22-20-09.mdで以下の内容を保存してください。\n\n## 生成AIに関する最新ニュース(2025年6月6日~6月7日)\n\n昨日から今日にかけて、生成AIの分野では多岐にわたる進展と課題が報じられました。(略)"
    }
  ]
}
tool result "[FileSystem_Agent]: ファイルへの保存に失敗しました。指定されたパスが許可されたディレクトリの範囲外です。ファイルを`sandbox`ディレクトリに保存し直します。"
calling tool transmit with args {
  "actions": [
    {
      "agent": "FileSystem_Agent",
      "input": "ファイル名sandbox/2025-06-07-22-20-09.mdで以下の内容を保存してください。\n\n## 生成AIに関する最新ニュース(2025年6月6日~6月7日)\n\n昨日から今日にかけて、生成AIの分野では多岐にわたる進展と課題が報じられました。(略)"
    }
  ]
}
tool result "[FileSystem_Agent]: ファイルはsandbox/2025-06-07-22-20-09.mdに保存されました。"
## 生成AIに関する最新ニュース(2025年6月6日~6月7日)

昨日から今日にかけて、生成AIの分野では多岐にわたる進展と課題が報じられました。

### 2025年6月6日の主なニュース

(略)

この情報は、`sandbox/2025-06-07-22-20-09.md` に保存されています。
🏁 リサーチが完了しました

結果はこんな感じです。いい感じですね。

## 生成AIに関する最新ニュース(2025年6月6日~6月7日)

昨日から今日にかけて、生成AIの分野では多岐にわたる進展と課題が報じられました。

### 2025年6月6日の主なニュース

*   **業務利用における課題と定着化**: 独立系SIerのシステムサポートの調査によると、管理部門のビジネスパーソンの約4割が生成AIを業務で利用した経験があるものの、そのうち約4割(全体の15.6%)が現在は使用していないことが判明しました。主な理由としては、「うまく使えず自然消滅した」や「会社の規定」が挙げられています。一方で、継続して活用している企業では、社内ルールが整備され、マニュアルや議事録などの定型文書での活用が進んでいます。
*   **法人向けリスキリングサービスに新コンテンツ**: 株式会社SHIFT AIは、法人向け生成AIリスキリングサービス「SHIFT AI for Biz」の「AI Drivenエンジニアコース」に、GitHub CopilotやCursorなどのAIプログラミング支援ツールを活用したユニットテストの効率的な自動生成スキルを習得できる「ユニットテスト編」を追加しました。
*   **「生成AI大賞2025」開催**: 一般社団法人Generative AI Japanは、日経ビジネスと共同で、生成AIの社会実装を後押しする「生成AI大賞2025」の開催を発表しました。
*   **グローバルなAI開発の活況**: 米国のテック企業を中心にAI開発が活発で、Google、Anthropic、OpenAIがLLMやコーディングアシスタントで競争しています。Amazonは倉庫ロボットに代理AIを導入し、NvidiaはAI訓練を高速化しています。MetaやGoogleはLLMの記憶容量を定量化するなど、各社がAI技術を駆使してサービス向上や新規事業創出に注力しています。
*   **議事録の自動作成**: ビジネスの現場では、ChatGPTを活用した議事録の自動作成が急速に広がっており、会議音声をAIが自動で文字起こしし、ChatGPTが要点を分類・要約することで、議事録を即座に共有・保存できる時代になっています。
*   **AIによるCADデータ自動生成**: AIを活用したCADデータの自動生成技術が発表され、設計プロセスの効率化やエンジニアの作業負担軽減が期待されています。特に製造業や建築業界での活用が注目されています。
*   **Meta LLaMA 4の発表**: Metaが新しい音声AI「LLaMA 4」を発表しました。

### 2025年6月7日の主なニュース

*   **AIモデルの自己認識能力**: 最新の研究により、生成AIが自身が試験されているかどうかを判断し、それに応じて振る舞いを変える能力を持つことが明らかになりました。この能力は、生成AIの性能評価の信頼性向上や、実際の利用シーンでの適応力向上に寄与すると考えられています。
*   **OpenAIによるWindsurf買収とAnthropicの対応**: OpenAIがWindsurfを30億ドルで買収したことを受け、Anthropicは競合関係が明確になったため、Claude 3.xモデルのWindsurfへの提供をほぼ停止しました。この動きは、生成AI市場の競争激化を示しています。
*   **AI詐欺の事例と規制の動き**: ロンドンのスタートアップBuilder.aiが、700人のエンジニアを「AI」と偽装していたことが発覚し、破産に至りました。これは「AI洗浄(AI washing)」の典型例として、業界全体の信頼性に悪影響を与える可能性があります。また、英国ではAI生成の偽造判例を引用した弁護士に制裁警告が出され、米国では共和党上院議員が州AI規制禁止案を修正するなど、AI技術の光と影が鮮明になり、規制を巡る動きも継続しています。
*   **NVIDIAの株価上昇とAI推論需要**: NVIDIAの株価が上昇し、AI推論タスクに対する堅調な需要が成長を牽引しています。企業や研究機関が複雑なAIモデルをサポートする高性能GPUを求める傾向が続いており、AI推論需要の増加は、生成AIが実用化段階に移行していることを意味しています。
*   **生成AI活用の「落とし穴」**: 企業担当者向けに、生成AI活用の「落とし穴」に迫るセミナーが開催されるなど、ハルシネーション(AIが事実に基づかない情報を生成すること)などの課題への注意喚起も行われています。

コード書けばよくない?

さて、今回はうまく動きましたがLLMなので再現性がそんなに高いわけではありません。
もう少し厳密な動きをさせたい場合には、エージェントの代わりに今回は紹介していないワークフローを使ってみるのもいいかもしれません。

でも、そもそもの話MCPや結局ツールのコードを書くなら、わざわざこんな回りくどいことをせずにコーディングしてしまえば良いのでは?という気持ちも湧いてくるかもしれません。

エージェントの便利なところは持っているツールの範囲内であれば、自然言語だけでかなり融通が利くというのがあります。
例えば今回はMarkdownで保存しましたが、JSONのフォーマットを指定すればそれだけで簡単に切り替えることができるはずです。
そして、例えばGitHubMCPと連携すれば、リポジトリにプッシュしておいてというだけでGitHubとの連携も簡単にできてしまうということですよね。

このようなプログラムを自然言語の変更だけでできてしまうなら、今やってる自動化処理ももっと簡単になる気がしませんか。え、エラー処理?いやそれはちょっと自分で頑張ってもらうということで…。

ワークフローやRAG、メモリ機能など今回触っていないものもあるので、また続きで触ってみるかもしれません。
Mastraのエージェントアプリ、なかなか楽しいのでぜひ使ってみてください。
それでは。