HERE Discover API で周辺情報を検索してくれる MCP サーバーのメモ

HERE Discover API で周辺情報を検索してくれる MCP サーバーのメモ

この記事は HERE Advent Calendar 2025 の 20 日目の記事です。

HERE Discover API で周辺情報を検索してくれる MCP サーバーのメモです。

背景

HUG-JP Meetup vol.1 で登壇した ときの MCP サーバーの出来が結構よかったので、この機会にまとめておきます。

HERE API キーの取得

HERE API を使うにはAPIキーが必要です。HERE ポータル からアカウント登録して取得できます。

GPS デバイスと連携するものです

LT でライブデモしていましたが自作の GPS デバイスと連携するものです。

このようなデバイスで、同一のローカルネットワーク上から、以下のような JSON データが取得できる想定です。

URL 例

http://192.168.XXX.XXX/gps

データ例

{
  "lat": 35.681236,
  "lng": 139.767125,
  "alt": 40.5,
  "satellites": 8,
  "valid": true
}

package.json の様子

2025 年 9 月時点 tsx でより手軽に TypeScript な MCP サーバーを動かすメモ を前提で、ライブラリや Claude Desktop から実行するスクリプト実行は以下のようになっています。

{
  "name": "here-mcp-sample",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "here-server": "tsx --no-warnings here-server.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.18.0",
    "dotenv": "^17.2.2",
    "zod": "^3.25.76"
  },
  "devDependencies": {
    "@types/node": "^24.5.0",
    "tsx": "^4.20.5"
  }
}

here-server.ts の内容

実際こういったコードになっております。

  • HERE API や GPS デバイスのレスポンス用の型定義
  • 「カフェ」→「cafe」など日本語対応を想定したカテゴリマッピング
  • GPS デバイスから位置情報を取得する関数
  • HERE Discover API で周辺検索する関数
  • MCP サーバーとして 2 つのツールを定義
    • get_gps_location の GPS 位置取得
    • search_nearby_places の周辺スポット検索

といったことをしています。

import 'dotenv/config';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool,
} from '@modelcontextprotocol/sdk/types.js';

// HERE API用の型定義
interface HereDiscoverResponse {
  items: Array<{
    title: string;
    id: string;
    ontologyId?: string;
    resultType: string;
    address: {
      label: string;
      countryCode: string;
      countryName: string;
      stateCode?: string;
      state?: string;
      county?: string;
      city: string;
      district?: string;
      street?: string;
      postalCode?: string;
      houseNumber?: string;
    };
    position: {
      lat: number;
      lng: number;
    };
    access?: Array<{
      lat: number;
      lng: number;
    }>;
    distance?: number;
    categories: Array<{
      id: string;
      name: string;
      primary?: boolean;
    }>;
    contacts?: Array<{
      phone?: Array<{
        value: string;
        categories?: Array<{ id: string }>;
      }>;
      www?: Array<{
        value: string;
        categories?: Array<{ id: string }>;
      }>;
      email?: Array<{
        value: string;
        categories?: Array<{ id: string }>;
      }>;
    }>;
    openingHours?: Array<{
      categories?: Array<{ id: string }>;
      text: string[];
      isOpen: boolean;
      structured?: Array<{
        start: string;
        duration: string;
        recurrence: string;
      }>;
    }>;
  }>;
}

// GPS位置情報の型定義
interface GPSLocation {
  latitude: number;
  longitude: number;
  accuracy?: number;
  timestamp: string;
}

// GPSデバイスAPIのレスポンス型定義
interface GPSDeviceResponse {
  lat: number;
  lng: number;
  alt: number;
  satellites: number;
  valid: boolean;
}

// HERE APIキー(環境変数から取得)
const HERE_API_KEY = process.env.HERE_API_KEY;

// GPSデバイスの URL の例
const GPS_DEVICE_URL = 'http://192.168.XXX.XXX/gps';

// カテゴリマッピング(日本語→HERE APIクエリ)
const CATEGORY_MAP: Record<string, string> = {
  'カフェ': 'cafe',
  'コーヒー': 'cafe',
  'coffee': 'cafe',
  'レストラン': 'restaurant',
  '食事': 'restaurant',
  'restaurant': 'restaurant',
  'コンビニ': 'convenience store',
  '買い物': 'shopping',
  'shopping': 'shopping',
  '銀行': 'bank',
  'bank': 'bank',
  '病院': 'hospital',
  'hospital': 'hospital',
  'ガソリンスタンド': 'gas station',
  'gas station': 'gas station'
};

const server = new Server(
  {
    name: 'here-mcp-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// GPS位置取得ツール
async function getGPSLocation(): Promise<GPSLocation> {
  try {
    const response = await fetch(GPS_DEVICE_URL);
    
    if (!response.ok) {
      throw new Error(`GPSデバイスAPI Error: ${response.status} ${response.statusText}`);
    }
    
    const gpsData = await response.json() as GPSDeviceResponse;
    
    // GPSデータが有効かチェック
    if (!gpsData.valid) {
      throw new Error('GPSデータが無効です(GPS信号を受信できていません)');
    }
    
    // 衛星数が少ない場合の警告
    if (gpsData.satellites < 4) {
      console.warn(`GPS衛星数が少なめです: ${gpsData.satellites}`);
    }
    
    // 精度を衛星数から推定(衛星数が多いほど精度が高い)
    const estimatedAccuracy = Math.max(10 - gpsData.satellites, 1);
    
    return {
      latitude: gpsData.lat,
      longitude: gpsData.lng,
      accuracy: estimatedAccuracy,
      timestamp: new Date().toISOString()
    };
  } catch (error) {
    console.error('GPSデバイスからの位置取得エラー:', error);
    throw new Error(`GPSデバイスから位置情報を取得できませんでした: ${error instanceof Error ? error.message : String(error)}`);
  }
}

// HERE Discover APIで周辺検索
async function searchNearbyPlaces(
  latitude: number,
  longitude: number,
  category: string,
  limit: number = 10
): Promise<HereDiscoverResponse> {
  if (!HERE_API_KEY) {
    throw new Error('HERE_API_KEY環境変数が設定されていません');
  }

  // カテゴリマッピングを適用
  const searchQuery = CATEGORY_MAP[category.toLowerCase()] || category;

  const url = new URL('https://discover.search.hereapi.com/v1/discover');
  url.searchParams.set('at', `${latitude},${longitude}`);
  url.searchParams.set('limit', limit.toString());
  url.searchParams.set('q', searchQuery);
  url.searchParams.set('in', 'countryCode:JPN'); // 日本に限定
  url.searchParams.set('apiKey', HERE_API_KEY);

  try {
    const response = await fetch(url.toString());
    if (!response.ok) {
      throw new Error(`HERE API Error: ${response.status} ${response.statusText}`);
    }
    
    const data = await response.json() as HereDiscoverResponse;
    return data;
  } catch (error) {
    console.error('HERE API呼び出しエラー:', error);
    throw error;
  }
}

// ツールリストの定義
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'get_gps_location',
        description: 'デバイスから位置情報を取得するツールです',
        inputSchema: {
          type: 'object',
          properties: {},
          required: [],
        },
      } satisfies Tool,
      {
        name: 'search_nearby_places',
        description: '指定された位置の周辺で特定のカテゴリの場所を検索するツールです(例:カフェ、レストラン、コンビニなど)',
        inputSchema: {
          type: 'object',
          properties: {
            latitude: {
              type: 'number',
              description: '検索の中心となる緯度',
            },
            longitude: {
              type: 'number',
              description: '検索の中心となる経度',
            },
            category: {
              type: 'string',
              description: '検索するカテゴリ(カフェ、レストラン、コンビニ、買い物、銀行、病院など)',
            },
            limit: {
              type: 'number',
              description: '検索結果の最大件数(デフォルト: 10)',
              default: 10,
            },
          },
          required: ['latitude', 'longitude', 'category'],
        },
      } satisfies Tool,
    ],
  };
});

// ツール呼び出しハンドラー
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case 'get_gps_location': {
        const location = await getGPSLocation();
        return {
          content: [
            {
              type: 'text',
              text: `リアルタイムGPS位置を取得しました:
緯度: ${location.latitude}
経度: ${location.longitude}
推定精度: ${location.accuracy}m
取得時刻: ${location.timestamp}

GPSデバイス(192.168.XXX.XXX)からの実際の位置情報です。`,
            },
          ],
        };
      }

      case 'search_nearby_places': {
        const { latitude, longitude, category, limit = 10 } = args as {
          latitude: number;
          longitude: number;
          category: string;
          limit?: number;
        };

        const results = await searchNearbyPlaces(latitude, longitude, category, limit);
        
        if (results.items.length === 0) {
          return {
            content: [
              {
                type: 'text',
                text: `指定された位置(緯度: ${latitude}, 経度: ${longitude})周辺で「${category}」に該当する場所は見つかりませんでした。`,
              },
            ],
          };
        }

        const resultText = results.items.map((item, index) => {
          const distance = item.distance ? `(約${item.distance}m)` : '';
          const phone = item.contacts?.[0]?.phone?.[0]?.value || '不明';
          const website = item.contacts?.[0]?.www?.[0]?.value || 'なし';
          const categories = item.categories.map(cat => cat.name).join(', ');
          const openingHours = item.openingHours?.[0]?.text?.join(', ') || '営業時間不明';
          
          return `${index + 1}. ${item.title}${distance}
   住所: ${item.address.label}
   カテゴリ: ${categories}
   電話: ${phone}
   ウェブサイト: ${website}
   営業時間: ${openingHours}
   座標: ${item.position.lat}, ${item.position.lng}`;
        }).join('\n\n');

        return {
          content: [
            {
              type: 'text',
              text: `${category}」の検索結果(${results.items.length}件見つかりました):

${resultText}`,
            },
          ],
        };
      }

      default:
        throw new Error(`不明なツール: ${name}`);
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    return {
      content: [
        {
          type: 'text',
          text: `エラーが発生しました: ${errorMessage}`,
        },
      ],
      isError: true,
    };
  }
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('HERE MCP Server が開始されました');
}

main();

設定を実際の環境に合わせる

.env ファイルを作成して HERE API キーを設定します。

HERE_API_KEY=your-api-key-here

また、ソースコード内の GPS_DEVICE_URL は自分の GPS デバイスの IP アドレスに合わせて変更してください。

const GPS_DEVICE_URL = 'http://192.168.XXX.XXX/gps';

Claude Desktop の設定

Claude Desktop の設定ファイル claude_desktop_config.json に以下を追加します。

"mcpServers": {
    "here-server": {
      "command": "npm",
      "args": [
        "--prefix",
        "C:\\workspace\\here-mcp-sample",
        "run",
        "here-server",
        "--silent"
      ]
    }
  }

--prefix のパスは自分の環境に合わせて変更してください。

実際の動作

「デバイスから位置情報取得して」と聞くと get_gps_location ツールが動いて、GPS デバイスから現在地を取得してくれます。

まずこれで GPS 情報が Claude が記憶します。準備完了。

取得した位置情報をもとに「近くのコンビニ教えて」と聞くと、search_nearby_places ツールで HERE API を使った周辺検索が動きます。

ちゃんと距離順で近くのコンビニが返ってきます!