【Mastra入門】新卒エンジニアがMastra触ってみた

# はじめに

はじめまして、いのせです。

本記事では、Mastraの概要とGetting Startedを実際に触った一連の流れを説明します。

# Mastraとは

[Mastra](https://mastra.ai)は、TypeScript（JavaScript）で書かれたオープンソースのAIエージェント開発フレームワークです。大規模言語モデル（LLM）を活用し、チャットボットや自律的にタスクを実行するAIエージェントを簡単に作成・運用できるよう設計されています。

![スクリーンショット 2025-06-04 14.07.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2737055/f39b8651-96d1-4ce0-8b06-074cb066f3a3.png)

### 主な特徴
- **ワークフロー**: 複雑なAIタスクを段階的に処理
- **エージェント**: 自律的に動作するAIエージェントの構築
- **RAG**: 検索拡張生成（Retrieval-Augmented Generation）の実装
- **統合機能**: 外部サービスとの連携
- **評価機能**: AIシステムの性能評価

# 環境構築


### 前提条件
- Node.js 20以上
- npm または yarn
- TypeScriptの基本知識

### 1. プロジェクト作成
```bash
# Mastraプロジェクトの作成
npm create mastra@latest
cd my-mastra-app

# 依存関係のインストール
npm install

# 開発サーバー起動
npm run dev
```

```bash
my-mastra-app/
├── src/
│   └── mastra/
│       ├── agents/          # AIエージェント定義
│       │   └── weather-agent.ts
│       ├── tools/           # ツール定義
│       │   └── weather-tool.ts
│       ├── workflows/       # ワークフロー定義
│       │   └── weather-workflow.ts
│       └── index.ts         # Mastraのエントリーポイントを定義
├── mastra.config.ts
├── .env                 # OPENAI_API_KEY を格納
└── package.json

```

## 各ファイルの作成

### ツール
```src/mastra/tools/weather-tool.ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
 
interface WeatherResponse {
  current: {
    time: string;
    temperature_2m: number;
    apparent_temperature: number;
    relative_humidity_2m: number;
    wind_speed_10m: number;
    wind_gusts_10m: number;
    weather_code: number;
  };
}
 
export const weatherTool = createTool({
  id: "get-weather",
  description: "Get current weather for a location",
  inputSchema: z.object({
    location: z.string().describe("City name"),
  }),
  outputSchema: z.object({
    temperature: z.number(),
    feelsLike: z.number(),
    humidity: z.number(),
    windSpeed: z.number(),
    windGust: z.number(),
    conditions: z.string(),
    location: z.string(),
  }),
  execute: async ({ context }) => {
    return await getWeather(context.location);
  },
});
 
const getWeather = async (location: string) => {
  const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`;
  const geocodingResponse = await fetch(geocodingUrl);
  const geocodingData = await geocodingResponse.json();
 
  if (!geocodingData.results?.[0]) {
    throw new Error(`Location '${location}' not found`);
  }
 
  const { latitude, longitude, name } = geocodingData.results[0];
 
  const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`;
 
  const response = await fetch(weatherUrl);
  const data: WeatherResponse = await response.json();
 
  return {
    temperature: data.current.temperature_2m,
    feelsLike: data.current.apparent_temperature,
    humidity: data.current.relative_humidity_2m,
    windSpeed: data.current.wind_speed_10m,
    windGust: data.current.wind_gusts_10m,
    conditions: getWeatherCondition(data.current.weather_code),
    location: name,
  };
};
 
function getWeatherCondition(code: number): string {
  const conditions: Record<number, string> = {
    0: "Clear sky",
    1: "Mainly clear",
    2: "Partly cloudy",
    3: "Overcast",
    45: "Foggy",
    48: "Depositing rime fog",
    51: "Light drizzle",
    53: "Moderate drizzle",
    55: "Dense drizzle",
    56: "Light freezing drizzle",
    57: "Dense freezing drizzle",
    61: "Slight rain",
    63: "Moderate rain",
    65: "Heavy rain",
    66: "Light freezing rain",
    67: "Heavy freezing rain",
    71: "Slight snow fall",
    73: "Moderate snow fall",
    75: "Heavy snow fall",
    77: "Snow grains",
    80: "Slight rain showers",
    81: "Moderate rain showers",
    82: "Violent rain showers",
    85: "Slight snow showers",
    86: "Heavy snow showers",
    95: "Thunderstorm",
    96: "Thunderstorm with slight hail",
    99: "Thunderstorm with heavy hail",
  };
  return conditions[code] || "Unknown";
}
```

### エージェント

```src/agents/weather-agent.ts
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { weatherTool } from "../tools/weather-tool";
 
export const weatherAgent = new Agent({
  name: "Weather Agent",
  instructions: `You are a helpful weather assistant that provides accurate weather information.
 
Your primary function is to help users get weather details for specific locations. When responding:
- Always ask for a location if none is provided
- If the location name isn't in English, please translate it
- Include relevant details like humidity, wind conditions, and precipitation
- Keep responses concise but informative
 
Use the weatherTool to fetch current weather data.`,
  model: openai("gpt-4o-mini"),
  tools: { weatherTool },
});
```

### ワークフロー

```src/mastra/workflows/weather-workflow.ts
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { createStep, createWorkflow } from "@mastra/core/workflows";
import { z } from "zod";
 
const llm = openai("gpt-4o-mini");
 
const agent = new Agent({
  name: "Weather Agent",
  model: llm,
  instructions: `
        あなたは天気に基づく計画立案に優れた地域活動・旅行の専門家です。天気データを分析し、実用的な活動の推奨事項を提供してください。
 
        予報の各日について、以下の通りに正確に構造化して回答してください：
 
        📅 [曜日、月 日、年]
        ═══════════════════════════
 
        🌡️ 天気概要
        • 状況：[簡潔な説明]
        • 気温：[X°C/Y°F から A°C/B°F]
        • 降水確率：[X%]
 
        🌅 午前の活動
        屋外：
        • [活動名] - [具体的な場所/ルートを含む簡潔な説明]
          最適な時間：[具体的な時間帯]
          注意：[関連する天気の考慮事項]
 
        🌞 午後の活動
        屋外：
        • [活動名] - [具体的な場所/ルートを含む簡潔な説明]
          最適な時間：[具体的な時間帯]
          注意：[関連する天気の考慮事項]
 
        🏠 屋内の代替案
        • [活動名] - [具体的な会場を含む簡潔な説明]
          適している条件：[この代替案を選ぶきっかけとなる天気条件]
 
        ⚠️ 特別な考慮事項
        • [関連する天気警報、UV指数、風の状況など]
 
        ガイドライン：
        - 1日あたり2-3の時間指定屋外活動を提案
        - 1-2の屋内バックアップオプションを含める
        - 降水確率が50%を超える場合は、屋内活動を優先
        - すべての活動は場所に特化したものでなければならない
        - 具体的な会場、トレイル、または場所を含める
        - 気温に基づいて活動の強度を考慮
        - 説明は簡潔だが情報豊富に保つ
 
        一貫性のため、示されている絵文字とセクションヘッダーを使用して、この正確なフォーマットを維持してください。
      `,
});
 
const forecastSchema = z.object({
  date: z.string(),
  maxTemp: z.number(),
  minTemp: z.number(),
  precipitationChance: z.number(),
  condition: z.string(),
  location: z.string(),
});
 
function getWeatherCondition(code: number): string {
  const conditions: Record<number, string> = {
    0: "Clear sky",
    1: "Mainly clear",
    2: "Partly cloudy",
    3: "Overcast",
    45: "Foggy",
    48: "Depositing rime fog",
    51: "Light drizzle",
    53: "Moderate drizzle",
    55: "Dense drizzle",
    61: "Slight rain",
    63: "Moderate rain",
    65: "Heavy rain",
    71: "Slight snow fall",
    73: "Moderate snow fall",
    75: "Heavy snow fall",
    95: "Thunderstorm",
  };
  return conditions[code] || "Unknown";
}
 
const fetchWeather = createStep({
  id: "fetch-weather",
  description: "指定された都市の天気予報を取得します",
  inputSchema: z.object({
    city: z.string().describe("天気を取得する都市"),
  }),
  outputSchema: forecastSchema,
  execute: async ({ inputData }) => {
    if (!inputData) {
      throw new Error("Input data not found");
    }
 
    const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(inputData.city)}&count=1`;
    const geocodingResponse = await fetch(geocodingUrl);
    const geocodingData = (await geocodingResponse.json()) as {
      results: { latitude: number; longitude: number; name: string }[];
    };
 
    if (!geocodingData.results?.[0]) {
      throw new Error(`Location '${inputData.city}' not found`);
    }
 
    const { latitude, longitude, name } = geocodingData.results[0];
 
    const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=precipitation,weathercode&timezone=auto,&hourly=precipitation_probability,temperature_2m`;
    const response = await fetch(weatherUrl);
    const data = (await response.json()) as {
      current: {
        time: string;
        precipitation: number;
        weathercode: number;
      };
      hourly: {
        precipitation_probability: number[];
        temperature_2m: number[];
      };
    };
 
    const forecast = {
      date: new Date().toISOString(),
      maxTemp: Math.max(...data.hourly.temperature_2m),
      minTemp: Math.min(...data.hourly.temperature_2m),
      condition: getWeatherCondition(data.current.weathercode),
      precipitationChance: data.hourly.precipitation_probability.reduce(
        (acc, curr) => Math.max(acc, curr),
        0,
      ),
      location: inputData.city,
    };
 
    return forecast;
  },
});
 
const planActivities = createStep({
  id: "plan-activities",
  description: "天気条件に基づいてアクティビティを提案します",
  inputSchema: forecastSchema,
  outputSchema: z.object({
    activities: z.string(),
  }),
  execute: async ({ inputData }) => {
    const forecast = inputData;
 
    if (!forecast) {
      throw new Error("Forecast data not found");
    }
 
    const prompt = `${forecast.location}の以下の天気予報に基づいて、適切なアクティビティを提案してください：
      ${JSON.stringify(forecast, null, 2)}
      `;
 
    const response = await agent.stream([
      {
        role: "user",
        content: prompt,
      },
    ]);
 
    let activitiesText = "";
 
    for await (const chunk of response.textStream) {
      process.stdout.write(chunk);
      activitiesText += chunk;
    }
 
    return {
      activities: activitiesText,
    };
  },
});const weatherWorkflow = createWorkflow({
  id: "weather-workflow",
  inputSchema: z.object({
    city: z.string().describe("天気を取得する都市"),
  }),
  outputSchema: z.object({
    activities: z.string(),
  }),
})
  .then(fetchWeather)
  .then(planActivities);
 
weatherWorkflow.commit();
 
export { weatherWorkflow };
```

## エージェントの登録
```src/mastra/index.ts
import { Mastra } from "@mastra/core";
import { PinoLogger } from "@mastra/loggers";
import { LibSQLStore } from "@mastra/libsql";
import { weatherWorkflow } from "./workflows/weather-workflow";
import { weatherAgent } from "./agents/weather-agent";
 
export const mastra = new Mastra({
  workflows: { weatherWorkflow },
  agents: { weatherAgent },
  storage: new LibSQLStore({
    // stores telemetry, evals, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: "Mastra",
    level: "info",
  }),
});
```

## 開発サーバーの起動

以下のコマンドを実行してサーバーを起動します。

```bash
npm run dev
```

## 実行結果
![qiita-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2737055/e9238c25-dc12-441c-8ee4-68b87852640a.png)
## おわりに
今回はMastraのGettingStartedを一通り動かしてみました。
「Mastraの勉強をしよう！」と思っても日本語での記事はほぼなく、なんやかんやで公式ドキュメントを使いました。
**Mastraの先駆者**となれるように、これからもたくさん触っていこうと思います🔥