自然言語で宇宙探索！衛星データ検索基盤のSTAC API用のローカルMCPサーバー作ってみた！

みなさんこんにちは！

「**STAC API**」という衛星データ（など）検索のためのAPI規格があります。

詳しくはこちら！

https://qiita.com/nokonoko_1203/items/a9bfe50150c397da5e3e

https://qiita.com/nokonoko_1203/items/7860b92847f5d6bcc868

実は衛星データって無料で使えるものも結構多くて、広域で地球観測するハードルは意外と低かったりします。

当然、全球分のデータがあるので、自宅にいながら地球のどこでも分析することができるわけです。

冒頭で述べたSTAC APIを利用すると矩形で座標指定できたり、雲量が少ないデータだけ抽出できたり（衛星写真の中に雲が多いと分析しづらい）、特定の日付のデータのみ抽出できたり…

WebAPIのため、取得できたデータは

```json
{
  "id": "sample-collection",
  "type": "Collection",
  "stac_version": "1.0.0",
  "description": "サンプルSTACコレクション",
  "title": "サンプルコレクション",
  "license": "CC-BY-4.0",
  "providers": [
    {
      "name": "サンプルプロバイダー",
      "description": "サンプルデータのプロバイダー",
      "roles": ["producer", "processor"],
      "url": "https://example.com"
    }
  ],
  "extent": {
    "spatial": {
      "bbox": [[135.0, 35.0, 136.0, 36.0]]
    },
    "temporal": {
      "interval": [["2020-01-01T00:00:00Z", "2023-01-01T00:00:00Z"]]
    }
  },
  "links": [
    {
      "rel": "self",
      "href": "./collection.json",
      "type": "application/json"
    }
  ]
}
```

こんな感じのJSONになってしまいます（実際のデータはこのJSONの中に衛星画像のURLが記載されています）がしっかり取得できていますね！

ただ、STAC APIは便利ですが、適切なクエリパラメータを組み立てるのは結構面倒です。「東京の2023年の衛星画像が欲しい」と思っても、bboxやdatetimeパラメータを正確に指定する必要があり、APIの仕様を理解していないといけません。

一方、[STAC Browser](https://radiantearth.github.io/stac-browser/#/?.language=ja)を利用すると、地図などを含む見やすい画面で閲覧することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/5a6440dc-a5e2-4e34-8f69-8da90869e1cc.png)

で、当然これでもいいんですが、最近はVS CodeのようなエディターからMCPが触れたりするし、そもそも毎回ブラウザでポチポチやるのもめんどくさいし位置とか日付とかも細かく指定するのめんどくさいよなーと思って、**ローカルMCPサーバー**を作ってみることにしました！

以前、QGISとClaudeを繋ぐMCPの記事を書きましたが、今度はMCPサーバーを作ってみる方向からもアプローチしてみました。これで地理空間データの検索が効率化されるはずです！

https://qiita.com/nokonoko_1203/items/1e841cbecf6895273f5d

結果的にこんなものができました！

https://github.com/nokonoko1203/stac-mcp

## STAC APIの課題について

STAC（SpatioTemporal Asset Catalog）は、衛星画像などの地理空間データのメタデータを標準化された形で管理・検索するための仕様です。

従来の課題として「いつ、どこで撮影された衛星画像なのか」を統一された形式で記録・検索する仕組みがなく、各プロバイダーが独自の形式でデータを提供していたため、開発者は毎回異なるAPIと格闘する必要がありました。

STAC自体は衛星画像でなくても、というか画像ですらなくて良いのですが一般的にはこんなデータを取り扱うこと多いです。

- 航空機やドローンの画像
- ハイパースペクトラル光学データ
- 合成開口レーダー（SAR）
- 点群データ（ライダー）
- デジタル標高モデル（DEM）

STAC APIでは、本来以下のようなクエリパラメータを正確に指定する必要があります。

```bash
# 特定のエリアの2023年のSentinel-2データを検索する場合
curl "https://earth-search.aws.element84.com/v1/search" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "collections": ["sentinel-2-l2a"],
    "datetime": "2023-01-01T00:00:00Z/2023-12-31T23:59:59Z",
    "bbox": [139.6, 35.6, 139.8, 35.8],
    "limit": 10
  }'
```

めちゃくちゃめんどいですよね。
座標系やdatetimeの形式を理解する必要があり、技術的ハードルは高いです。
（最近だとAIがよしなにクエリを生成してくれたりしますが）

## MCPサーバーの実装

このめんどくささを解決するためにMCPサーバーを実装しました。

https://github.com/nokonoko1203/stac-mcp

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/91b981d5-e511-46ea-a421-0eb0dbe7c652.png)

この中のMCPクライアントとMCPサーバーを実装しています。

### 実装内容

だいたいこのような構成になっています。

```sh
src/
 ├── index.ts              # エントリーポイント
 ├── server/               # MCPサーバーコア
 ├── tools/                # MCPツール定義・実装
 ├── types/                # 型定義
 └── utils/                # ユーティリティ
```

`index.ts`はサーバーのコードを呼び出しており、実行するとMCPサーバーが立ち上がります。

```ts
  const server = new StacMcpServer();
  server.run().catch((error) => {
    logger.error("Failed to start server", { error: error.message });
    console.error("Failed to start server:", error);
    process.exit(1);
  });
```

### MCPサーバー

リクエストをハンドリングする機構と、対応したツールを実行する箇所です。

```ts
  // AIがどんなツールが使えるか問い合わせるためのツール一覧
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
    return { tools: getToolDefinitions() };
  });

  // AIが特定のツールを実行する
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    const handler = getToolHandler(name);
    if (!handler) {
      throw new Error(`Unknown tool: ${name}`);
    }

    return await handler(args);
  });
```

### ツール

実際に実行されるツールを定義しています。
ツールは定義（Difinition）と実際の処理（Handler）からなります。

いくつか定義していますが、以下はコレクション検索の例です。

```ts
  // 定義例：コレクション検索
  export const searchCollectionsDefinition: ToolDefinition = {
    name: "search_collections",
    description: "自然言語でSTACコレクションを検索",
    inputSchema: {
      type: "object",
      properties: {
        query: {
          type: "string",
          description: "検索クエリ（例：'東京の衛星画像'）"
        },
        limit: {
          type: "number",
          description: "最大取得数",
          default: 10
        }
      }
    }
  };

  // 実際の処理
  export const searchCollectionsHandler: ToolHandler = async (args) => {
    // 1. APIクライアントでデータ取得
    const stacClient = new StacApiClient();
    const collections = await stacClient.searchCollections({
      limit: args.limit || 10
    });

    // 2. 人間が読みやすい形式に整形
    let response = `${collections.length}件のコレクションが見つかりました：\n\n`;

    for (const collection of collections) {
      response += `${collection.id}\n`;
      response += `${collection.title || collection.id}\n`;
      if (collection.description) {
        response += `  ${collection.description}\n`;
      }
    }

    // 3. MCP標準形式で返却
    return {
      content: [{
        type: "text",
        text: response
      }]
    };
  };
```

### 処理フロー

実際に構築されたMCPクライアント/サーバーが利用されると以下のようなイメージで処理がされます。

1. 実践的なデータフロー
「2024年の関東地方の衛星画像を検索」

2. AIからのリクエスト
```json
{
    "method": "tools/call",
    "params": {
        "name": "search_items",
        "arguments": {
            "query": "関東地方",
            "bbox": [138.0, 34.0, 141.0, 37.0],
            "datetime": "2024-01-01/2024-12-31",
            "limit": 20
        }
    }
}
```
3. サーバー内での処理
3. 入力検証（JSON Schema）
3. 適切なハンドラーへのルーティング
3. STAC APIへのクエリ変換
3. 結果の整形
3. ユーザーへの応答
```text
20件のアイテムが見つかりました：
  ALOS-2_20240115_KANTO
    撮影日: 2024-01-15
    センサー: PALSAR-2

  Sentinel-2_20240203_TOKYO
    撮影日: 2024-02-03
    センサー: MSI
    ...
```

## 実装したSTAC MCP Serverの機能

実装したSTAC MCP Serverは、とりあえず以下の機能を提供してます。

- **search_collections**：データコレクションの検索
- **search_items**：空間・時間フィルターでのアイテム検索
- **get_collection**：特定コレクションの詳細情報取得
- **get_item**：特定アイテムの詳細情報取得

これらの機能により、以下のような自然言語でのリクエストができるようになっているはずです。

- 「利用可能なデータコレクションを全て表示して」
- 「2023年に東京エリアで撮影された衛星データを探して」
- 「〇〇collectionについて詳しく教えて」

## 使用方法

イメージを配布しているので、以下のような設定を行うだけで動作すると思います。

例えばClaude Desktopを利用する場合、設定ファイル（~/.claude_desktop_config.json）に以下を追加します。
（`--platform`は各々OS毎に読み替えてください。Windowsようには配布していないので、[README.md](https://github.com/nokonoko1203/stac-mcp/blob/main/README.md)を見ながら手動でイメージのビルドをしていただく必要があるかもしれません・）

```json
{
  "mcpServers": {
    "stac_mcp": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "--platform",
        "linux/amd64",
        "-e",
        "STAC_API_URL",
        "ghcr.io/nokonoko1203/stac-mcp:latest",
        "node",
        "/app/dist/index.js"
      ],
      "env": {
        "STAC_API_URL": "https://earth-search.aws.element84.com/v1"
      }
    }
  }
}
```

`STAC_API_URL`は利用したいSTAC APIを入れてください！
こちらのURLだと、AWSのSTAC APIに接続され、Sentinel-2やLandsat-8などの衛星データを無料で検索・アクセスできます。

以下はローカルマシンで立ち上げた独自のSTAC APIに接続していますが、無事に疎通するとこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/b5f6e614-4e24-47da-893c-676c64c204ac.png)

## おわりに

今回のMCPサーバーを使用することで、「〇〇エリアの△△年のデータが欲しい」という自然言語で検索が可能になり、専門知識を持たないユーザーでも衛星データにアクセスできるようになりました！

次は冒頭で紹介したQGISのMCPなどと組み合わせ、自然言語でより強力な分析ができるようにしていく必要がありそうだなと思いました！
データの検索から可視化・解析まで、全てを自然言語で実行できる可能性があります。これにより地理空間データ活用の敷居が下がり、より多くのユーザーが活用できるようになることが期待できますね！

皆さんも自然言語で衛星解析やってみてください！