Skip to content

Instantly share code, notes, and snippets.

@shimizu
Created July 8, 2025 01:19
Show Gist options
  • Save shimizu/7d8b60d0f78f2cd8788663dcf4594f65 to your computer and use it in GitHub Desktop.
Save shimizu/7d8b60d0f78f2cd8788663dcf4594f65 to your computer and use it in GitHub Desktop.
OSM GeoJSON MCP Server alpha

OSM GeoJSON MCP Server alpha

OSM-GeoJSON-MCP-Serverのアルファ版

Overpass APIからのレスポンスをトークン化してClaudeに送信するためトークン消費が激しい。

上記の問題があるが、MCP Server作成の参考になるので残す。

セットアップ

# プロジェクトディレクトリに移動
cd /home/shimizu/_mcp/osm-geojson-mcp-server/

# ファイルを作成(まだない場合)
nano osm-download-mcp.js
# 上記のコードを貼り付けて保存

# 実行権限を付与
chmod +x osm-download-mcp.js

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

Calude Codeへの登録

# MCPサーバーをClaude Codeに追加
claude mcp add osm-geojson -- node /home/shimizu/_mcp/osm-geojson-mcp-server/osm-geojson-mcp-server.js

# 登録されたか確認
claude mcp list

MCP Inspectorでのデバッグ

# MCP Inspectorで対話的にテスト
npx @modelcontextprotocol/inspector node osm-geojson-mcp-server.js

コンソールに表示されたトークン付きのURLにブラウザでアクセスしてデバッグ

#!/usr/bin/env node
// osm-download-mcp.js
// OpenStreetMapデータをファイルにダウンロードするMCPサーバー
// レスポンスをトークン化せず、直接ファイルに保存します
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import https from 'https';
import fs from 'fs/promises';
import path from 'path';
import { createWriteStream } from 'fs';
class OSMDownloadServer {
constructor() {
// MCPサーバーのインスタンスを初期化
this.server = new Server(
{
name: 'osm-download-server',
version: '1.0.0',
},
{
capabilities: {
tools: {}, // ツール機能を有効化
},
}
);
// Overpass APIサーバーの設定
// IPアドレスを直接使用してDNS解決の問題を回避
this.servers = [
{ url: 'https://162.55.144.139/api/interpreter', host: 'overpass-api.de' },
{ url: 'https://65.109.112.52/api/interpreter', host: 'lz4.overpass-api.de' },
{ url: 'https://193.219.97.30/api/interpreter', host: 'overpass.kumi.systems' }
];
// ツールハンドラーの設定
this.setupToolHandlers();
}
setupToolHandlers() {
// 利用可能なツールのリストを返すハンドラー
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'download_osm_data',
description: 'OpenStreetMapデータをダウンロードしてファイルに保存します。大きなデータセットも扱えます。カスタムのOverpass QLクエリを使用できます。',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Overpass QLクエリ文字列(例: [out:json];node["amenity"="restaurant"](bbox);out;)'
},
output_path: {
type: 'string',
description: '保存先ファイルパス(例: ./data/tokyo_buildings.json)。ディレクトリは自動作成されます。'
},
format: {
type: 'string',
enum: ['json', 'xml'],
default: 'json',
description: '出力データ形式(json または xml)'
}
},
required: ['query', 'output_path']
}
},
{
name: 'download_area_buildings',
description: '指定した矩形エリア内の建物データをダウンロードします。建物の輪郭がポリゴンデータとして保存されます。',
inputSchema: {
type: 'object',
properties: {
minLon: { type: 'number', description: '最小経度(西端)' },
minLat: { type: 'number', description: '最小緯度(南端)' },
maxLon: { type: 'number', description: '最大経度(東端)' },
maxLat: { type: 'number', description: '最大緯度(北端)' },
output_path: {
type: 'string',
description: '保存先ファイルパス(例: ./buildings/shinjuku.json)'
}
},
required: ['minLon', 'minLat', 'maxLon', 'maxLat', 'output_path']
}
},
{
name: 'download_area_all',
description: '指定エリアの全データ(建物、道路、POI、水域など)を一括ダウンロードします。大容量になる可能性があるため、小さなエリアから始めることを推奨します。',
inputSchema: {
type: 'object',
properties: {
minLon: { type: 'number', description: '最小経度(西端)' },
minLat: { type: 'number', description: '最小緯度(南端)' },
maxLon: { type: 'number', description: '最大経度(東端)' },
maxLat: { type: 'number', description: '最大緯度(北端)' },
output_path: {
type: 'string',
description: '保存先ファイルパス'
}
},
required: ['minLon', 'minLat', 'maxLon', 'maxLat', 'output_path']
}
},
{
name: 'convert_to_geojson',
description: 'ダウンロード済みのOSM JSONファイルをGeoJSON形式に変換します。GeoJSONは地図表示ソフトウェアで広くサポートされている形式です。',
inputSchema: {
type: 'object',
properties: {
input_path: {
type: 'string',
description: 'OSMデータファイルのパス(JSONファイル)'
},
output_path: {
type: 'string',
description: 'GeoJSON出力ファイルのパス(.geojson拡張子推奨)'
}
},
required: ['input_path', 'output_path']
}
}
]
}));
// ツール実行のハンドラー
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// ツール名に応じて適切なメソッドを呼び出す
switch (name) {
case 'download_osm_data':
return await this.downloadOSMData(args);
case 'download_area_buildings':
return await this.downloadAreaBuildings(args);
case 'download_area_all':
return await this.downloadAreaAll(args);
case 'convert_to_geojson':
return await this.convertToGeoJSON(args);
default:
throw new Error(`不明なツール: ${name}`);
}
});
}
// ストリーミング方式でファイルにダウンロード
// メモリに全データを読み込まないため、大容量ファイルも扱える
async downloadToFile(url, query, outputPath, headers = {}) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
// HTTPSリクエストのオプション設定
const options = {
hostname: urlObj.hostname,
port: 443,
path: urlObj.pathname,
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'Content-Length': Buffer.byteLength(query),
'User-Agent': 'OSM-Download-MCP/1.0',
...headers // 追加のヘッダー(主にHostヘッダー)
},
rejectUnauthorized: false // IPアドレス直接アクセスのため証明書検証を無効化
};
const req = https.request(options, (res) => {
// HTTPステータスコードの確認
if (res.statusCode !== 200) {
reject(new Error(`HTTPエラー ${res.statusCode}: サーバーからエラーが返されました`));
return;
}
// 出力先ディレクトリを作成(存在しない場合)
const dir = path.dirname(outputPath);
fs.mkdir(dir, { recursive: true }).then(() => {
// ファイル書き込みストリームを作成
const writeStream = createWriteStream(outputPath);
let downloadedBytes = 0;
// データ受信時の処理
res.on('data', (chunk) => {
downloadedBytes += chunk.length;
// 1MBごとに進捗を報告
if (downloadedBytes % (1024 * 1024) === 0) {
console.error(`ダウンロード中: ${(downloadedBytes / 1024 / 1024).toFixed(1)} MB`);
}
});
// レスポンスを直接ファイルにパイプ
res.pipe(writeStream);
// 書き込み完了時の処理
writeStream.on('finish', () => {
const sizeMB = (downloadedBytes / 1024 / 1024).toFixed(2);
resolve({
success: true,
path: outputPath,
size: `${sizeMB} MB`,
bytes: downloadedBytes
});
});
// エラー処理
writeStream.on('error', reject);
}).catch(reject);
});
// リクエストエラーの処理
req.on('error', (error) => {
reject(new Error(`ネットワークエラー: ${error.message}`));
});
// タイムアウト設定(5分)
req.setTimeout(300000, () => {
req.destroy();
reject(new Error('リクエストがタイムアウトしました(5分経過)'));
});
// クエリを送信
req.write(query);
req.end();
});
}
// カスタムクエリでOSMデータをダウンロード
async downloadOSMData(args) {
const { query, output_path, format = 'json' } = args;
// クエリの先頭に出力形式を追加(まだ指定されていない場合)
const fullQuery = format === 'json' ?
(query.startsWith('[out:json]') ? query : `[out:json];${query}`) :
(query.startsWith('[out:xml]') ? query : `[out:xml];${query}`);
console.error(`OSMデータをダウンロード中: ${output_path}`);
// 複数のサーバーを順番に試す
let lastError = null;
for (const server of this.servers) {
try {
console.error(`サーバーに接続中: ${server.host}`);
const result = await this.downloadToFile(
server.url,
fullQuery,
output_path,
{ 'Host': server.host } // 正しいHostヘッダーを設定
);
return {
content: [{
type: 'text',
text: JSON.stringify({
status: '成功',
message: 'データのダウンロードが完了しました',
file: output_path,
size: result.size,
server: server.host
}, null, 2)
}]
};
} catch (error) {
lastError = error;
console.error(`${server.host} での処理に失敗: ${error.message}`);
}
}
throw new Error(`すべてのサーバーで失敗しました: ${lastError?.message}`);
}
// エリア内の建物データをダウンロード
async downloadAreaBuildings(args) {
const { minLon, minLat, maxLon, maxLat, output_path } = args;
// 建物データ取得用のOverpass QLクエリ
const query = `[out:json][timeout:180];
(
way["building"](${minLat},${minLon},${maxLat},${maxLon});
relation["building"](${minLat},${minLon},${maxLat},${maxLon});
);
out body;
>;
out skel qt;`;
console.error(`建物データをダウンロード中: エリア [${minLon},${minLat},${maxLon},${maxLat}]`);
return await this.downloadOSMData({ query, output_path, format: 'json' });
}
// エリア内の全データをダウンロード
async downloadAreaAll(args) {
const { minLon, minLat, maxLon, maxLat, output_path } = args;
// エリアサイズをチェック
const area = (maxLon - minLon) * (maxLat - minLat);
if (area > 0.01) {
console.error(`警告: 大きなエリア(${area.toFixed(4)}平方度)が指定されています。`);
console.error('ダウンロードに時間がかかる可能性があります。');
}
// すべてのデータを取得する包括的なクエリ
const query = `[out:json][timeout:300];
(
node(${minLat},${minLon},${maxLat},${maxLon});
way(${minLat},${minLon},${maxLat},${maxLon});
relation(${minLat},${minLon},${maxLat},${maxLon});
);
out body;
>;
out skel qt;`;
console.error(`全データをダウンロード中: エリア [${minLon},${minLat},${maxLon},${maxLat}]`);
return await this.downloadOSMData({ query, output_path, format: 'json' });
}
// OSM JSONをGeoJSONに変換
async convertToGeoJSON(args) {
const { input_path, output_path } = args;
try {
console.error(`GeoJSONに変換中: ${input_path}${output_path}`);
// ファイルを読み込み
const data = await fs.readFile(input_path, 'utf8');
const osmData = JSON.parse(data);
// GeoJSONに変換
const geojson = this.osmToGeoJSON(osmData);
// 出力ディレクトリを作成
await fs.mkdir(path.dirname(output_path), { recursive: true });
// GeoJSONファイルとして保存
await fs.writeFile(output_path, JSON.stringify(geojson, null, 2));
// ファイルサイズを取得
const stats = await fs.stat(output_path);
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
return {
content: [{
type: 'text',
text: JSON.stringify({
status: '成功',
message: 'GeoJSONへの変換が完了しました',
input_file: input_path,
output_file: output_path,
size: `${sizeMB} MB`,
feature_count: geojson.features.length
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
status: 'エラー',
message: `変換に失敗しました: ${error.message}`,
input_file: input_path
}, null, 2)
}]
};
}
}
// OSMデータをGeoJSONに変換する内部メソッド
osmToGeoJSON(osmData) {
const features = []; // GeoJSONフィーチャーの配列
const nodes = {}; // ノードIDと座標のマッピング
// データが存在しない場合は空のFeatureCollectionを返す
if (!osmData.elements) {
return { type: 'FeatureCollection', features: [] };
}
// ステップ1: すべてのノードを収集
// ウェイはノードIDの参照で構成されているため、先に座標情報を収集
osmData.elements.forEach(element => {
if (element.type === 'node') {
nodes[element.id] = [element.lon, element.lat];
}
});
// ステップ2: 各要素をGeoJSONフィーチャーに変換
osmData.elements.forEach(element => {
let geometry = null;
switch (element.type) {
case 'node':
// ノードは点(Point)として表現
if (element.lon !== undefined && element.lat !== undefined) {
geometry = {
type: 'Point',
coordinates: [element.lon, element.lat]
};
}
break;
case 'way':
// ウェイは線(LineString)または多角形(Polygon)として表現
if (element.nodes && element.nodes.length > 0) {
// ノードIDから座標を取得
const coordinates = element.nodes
.map(nodeId => nodes[nodeId])
.filter(coord => coord !== undefined);
if (coordinates.length > 0) {
// 閉じたウェイかどうか確認(最初と最後のノードが同じ)
const isClosed = element.nodes[0] === element.nodes[element.nodes.length - 1];
// 閉じていて4点以上ある場合はポリゴン(建物など)
if (isClosed && coordinates.length > 3) {
geometry = {
type: 'Polygon',
coordinates: [coordinates] // GeoJSONポリゴンは配列の配列
};
} else {
// それ以外は線(道路など)
geometry = {
type: 'LineString',
coordinates: coordinates
};
}
}
}
break;
// リレーションは現在サポートしていません
// (複雑な多角形や境界線などに使用される)
}
// ジオメトリが作成できた場合、フィーチャーとして追加
if (geometry) {
features.push({
type: 'Feature',
id: `${element.type}/${element.id}`,
properties: element.tags || {}, // OSMタグをプロパティとして保存
geometry: geometry
});
}
});
// GeoJSON FeatureCollectionとして返す
return {
type: 'FeatureCollection',
features: features
};
}
// MCPサーバーの起動
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('OSM Download MCPサーバーが起動しました...');
console.error('大容量のOSMデータをファイルに直接ダウンロードできます。');
}
}
// サーバーインスタンスの作成と起動
const server = new OSMDownloadServer();
server.run().catch((error) => {
console.error('サーバーの起動に失敗しました:', error);
process.exit(1);
});
{
"name": "osm-geojson-mcp-server",
"version": "1.0.0",
"description": "MCP server for fetching OpenStreetMap data as GeoJSON",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.6.0",
"axios": "^1.7.2",
"@turf/turf": "^7.0.0"
},
"keywords": [
"mcp",
"openstreetmap",
"geojson",
"gis",
"geography"
],
"author": "",
"license": "MIT"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment