PLATEAUデータをAIで検索!リモートMCPサーバーをリリースしました

2025-12-19

PLATEAUデータをAIで検索!リモートMCPサーバーをリリースしました

PLATEAU MCPサーバーをリリースしました!

こんにちは。CTOの井上です。

3D都市モデル Project PLATEAU Advent Calendar 2025 19日目の記事です。

PLATEAUのデータはとても価値がある一方で、実際に触ろうとすると「データを探す」だけでも意外と壁があったりします。

※PLATEAU = 国土交通省が推進している、全国の3D都市モデルのオープンデータ整備プロジェクト

たとえば、

  • 欲しいのは「どの都市」で「どの年度」で「どのLOD」で「どの形式(CityGML/3D Tiles/MVT)」なのか
  • その条件に合うデータがあるのかないのか、あるならどこから取れるのか
  • 仕様書のどこに書いてあるのか、関連する用語や前提は何か

…みたいなことを、カタログやドキュメントを行ったり来たりしながら詰めていく必要があります。慣れている人ならまだしも、初見だと「調べる作業」に時間を取られてしまって、肝心の検証や実装に進みにくいという問題もあります。

そこで最近注目されているのが、AIが外部ツールに接続して“調べに行く”ための仕組みである MCP(Model Context Protocol) です。MCPを使うと、AIが単に文章を生成するだけではなく、ツールを呼び出して検索や取得を行い、その結果を使って次の判断まで進められるようになります。実際に世の中でもMCPサーバーが増えてきていて、国交省側でも「MLIT DATA PLATFORM MCP Server」が試験的にリリースされるなどの事例も登場しています。

こういう流れも踏まえて、弊社でも PLATEAUデータカタログ用のMCPサーバーを開発・公開しました!(国交省担当者確認済)

このMCPサーバーを使うと、AIは以下のことができるようになります。

  • データカタログ
    • メタデータ取得: 利用可能な年度、PLATEAU仕様バージョン、地域数、データセット数などの統計情報を取得
    • 地域検索: 都道府県・市区町村を検索(親地域コード、データセット種類、テキスト検索などで絞り込み可能)
    • データセット検索: 地域コード・データセット種類・年度・PLATEAU仕様バージョンなどを指定してデータセット(CityGML・3D Tiles・MVTなど)を検索
    • データセット種類一覧: 利用可能な地物型コード(bldg, tran, luse, dem, fld 等)とその説明を取得
  • CityGML取得
    • CityGMLファイル検索: メッシュコード、空間ID、矩形範囲(緯度経度)を指定してCityGMLファイルのURL一覧を取得
    • 地物ID取得: 空間IDに交差する地物(建築物など)のIDリストを取得
    • 属性取得: CityGMLファイルから指定した建物IDの属性情報を取得(コードリスト解決も対応)
  • PLATEAU仕様書: 仕様書について質問できます
    • 目次取得: 「3D都市モデル標準製品仕様書」「3D都市モデル標準作業手順書」の目次(章・節の階層構造)を取得
    • 内容読み取り: 仕様書の指定セクションの全内容をMarkdown形式で取得

さらにこのMCPサーバーは リモートMCPサーバー になっているので、インターネット経由で動作します。複雑なセットアップ不要で、手元のAIからすぐに試せます。以下がMPCサーバーのURLです(認証なし)。

http://api.plateauview.mlit.go.jp/mcp

MCPサーバーのセットアップ方法などの詳細は、以下のURLからご確認ください。

https://github.com/Project-PLATEAU/plateau-streaming-tutorial/blob/main/mcp/plateau-mcp.md

このMCPサーバーは試験的な提供です。本サービスの利用により生じたいかなる損害についても、提供者は一切の責任を負いません。

この記事では、エンジニアブログとして、MCPサーバーをGoで実装する際のノウハウを解説していきます。

GoでMCPサーバーを実装する

ここからは、mcp-go(github.com/mark3labs/mcp-go)を使ってMCPサーバーを実装する方法を説明します。

この記事で扱うMCPサーバーは、Claude DesktopやChatGPTなどのクライアントから呼び出される、ツール(Functions)とリソース(Resources)を提供するプロセスのことです。実装はGoで行いますが、重要なのは「AIが迷わず呼び出せるインターフェースをどう作るか」です。APIサーバー開発の感覚のまま進めると、ツールの粒度や戻り値のサイズ、スキーマの与え方などでハマりどころが出てくるので、そのあたりも後半で触れます。

本章では、まずサーバーの起動方式(stdio / HTTP)を押さえ、次にツール定義(名前・説明・入力スキーマ)と、ハンドラー実装(入力の取り出し・結果の整形・エラーの返し方)を順に見ていきます。読み進め方としては、「まず動く最小構成」から入り、次にHTTPでの組み込み、最後に設計上の勘所という流れです。

mcp-goとは

mcp-goは、GoでMCPサーバーを実装するためのライブラリです。MCPというプロトコル自体は「LLMが外部ツールを呼び出す」ための共通言語ですが、実装に落とすとやることは意外と素直で、だいたい次の流れになります。

まず、サーバーとしての“器”を作ります。次に、クライアントが呼び出せるツール(Tool)やリソース(Resource)を定義します。最後に、ツールが呼ばれたときの処理(ハンドラー)を書きます。mcp-goは、この一連をGoのコードとして無理なく組み立てられるようにしてくれる、という位置づけです。

導入はシンプルで、以下を入れるだけで始められます。

go get github.com/mark3labs/mcp-go

基本的なMCPサーバーの実装

ここではまず「動く」ことを優先して、最小構成のサーバーを作ります。MCPサーバーにはいくつか起動方式がありますが、代表的なのが

  • ローカルクライアント向けの stdio(標準入出力)
  • リモートや既存Webアプリに組み込みやすい HTTP

の2つです。どちらを選ぶかで周辺の実装が少し変わるので、順番に見ていきます。

サーバーの初期化(stdio)

まずはstdioで起動する最小構成です。Claude Desktopなどのローカルクライアントがプロセスを起動して、標準入出力でMCPメッセージをやり取りする形になります。

ここで押さえておきたいのは、server.NewMCPServer(...)で「このサーバーは何者で、何ができるのか」を宣言している点です。クライアントはこの情報を見て、ツール呼び出しの可否や、利用可能な機能(ツール・リソース)を判断します。

package main

import (
	"log"

	"github.com/mark3labs/mcp-go/server"
)

func main() {
	// MCPサーバーを作成
	s := server.NewMCPServer(
		"my-mcp-server", // サーバー名
		"1.0.0",         // バージョン
		server.WithToolCapabilities(true),            // ツール機能を有効化
		server.WithResourceCapabilities(true, false), // リソース機能を有効化
		server.WithLogging(),                         // ロギングを有効化
	)

	// stdio経由でサーバーを起動(Claude Desktop等向け)
	if err := server.ServeStdio(s); err != nil {
		log.Fatalf("Server error: %v", err)
	}
}

HTTP MCPサーバーの実装

次はHTTPで提供するパターンです。自前のWebアプリに組み込んだり、クラウド上でリモートMCPサーバーとして公開したい場合はこちらが自然です。

mcp-goではStreamableHTTPServerを使います。実装の観点では、

  1. NewMCPServerで中身(ツール群)を作る
  2. それをHTTPハンドラーに包む
  3. ListenAndServeなどで公開する

という3段構えになります。

また、ここではserver.WithStateLess(true)を指定しています。ステートレスにしておくとセッション管理が不要になり、スケールや再起動にも強い構成になります。まず動かす段階では、特別な理由がない限りステートレスをおすすめします。

package main

import (
	"log"
	"net/http"

	"github.com/mark3labs/mcp-go/server"
)

func main() {
	// MCPサーバーを作成
	mcpServer := server.NewMCPServer(
		"my-http-mcp-server",
		"1.0.0",
		server.WithToolCapabilities(true),
	)

	// ツールを登録(後述)
	registerTools(mcpServer)

	// StreamableHTTPServerでHTTPサーバーを作成
	httpServer := server.NewStreamableHTTPServer(
		mcpServer,
		server.WithStateLess(true),      // ステートレスモード
		server.WithEndpointPath("/mcp"), // エンドポイントパス
	)

	// HTTPサーバーを起動
	log.Println("MCP server listening on :8080")
	if err := http.ListenAndServe(":8080", httpServer); err != nil {
		log.Fatalf("Server error: %v", err)
	}
}

Echoフレームワークとの統合

既存のEchoアプリケーションに組み込みたい場合は、「Echoのルーティングの世界」と「MCPのHTTPハンドラー」を繋ぐだけです。

やっていることは単純で、EchoのハンドラーからStreamableHTTPServer.ServeHTTPを呼び出しています。g.AnyでどのHTTPメソッドも受けるようにしているのは、クライアントや将来の拡張に対して余計な制約を作らないためです。

package mcp

import (
	"github.com/labstack/echo/v4"
	"github.com/mark3labs/mcp-go/server"
)

type HTTPHandler struct {
	streamableServer *server.StreamableHTTPServer
}

func NewHTTPHandler() *HTTPHandler {
	mcpServer := server.NewMCPServer(
		"plateau-mcp",
		"1.0.0",
		server.WithToolCapabilities(true),
	)

	// ツールを登録
	registerTools(mcpServer)

	streamableServer := server.NewStreamableHTTPServer(
		mcpServer,
		server.WithStateLess(true),
	)

	return &HTTPHandler{
		streamableServer: streamableServer,
	}
}

func (h *HTTPHandler) ServeHTTP(c echo.Context) error {
	h.streamableServer.ServeHTTP(c.Response(), c.Request())
	return nil
}

func (h *HTTPHandler) RegisterRoutes(g *
echo.Group
) {
	g.Any("", h.ServeHTTP)
	g.Any("/*", h.ServeHTTP)
}

ツール定義の実装

MCPサーバーの価値は結局「AIが呼び出せる道具箱をどう設計するか」にあります。ツール定義で大事なのは、AIが呼ぶ前提で

  • ツール名が行動を想像しやすいこと
  • 説明文が短くても誤解がないこと
  • 入力スキーマがクライアント側で扱いやすいこと

を満たすことです。

mcp-goでは、mcp.NewToolでツールのメタ情報と入力スキーマを定義し、s.AddToolでハンドラーを紐付けます。まずは一番簡単な例から始めます。

シンプルなツール

パラメータがないツールは疎通確認にも便利です。たとえば「メタデータを返すだけ」のツールを1つ作っておくと、クライアントが接続できているか、レスポンスが正しく返っているかを短い往復で確認できます。

func registerTools(s *server.MCPServer) {
	// ツール定義
	tool := mcp.NewTool(
		"get_metadata",
		mcp.WithDescription("メタデータを取得します"),
		mcp.WithReadOnlyHintAnnotation(true), // 読み取り専用であることを明示
	)

	// ハンドラー登録
	s.AddTool(tool, handleGetMetadata)
}

func handleGetMetadata(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	// 処理を実行
	result := fetchMetadata()

	// JSON形式で結果を返す
	jsonBytes, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		return mcp.NewToolResultError("failed to marshal response"), nil
	}
	return mcp.NewToolResultText(string(jsonBytes)), nil
}

パラメータ付きツール

実用上は、何かしらの入力を受け取るツールがほとんどです。ここでのポイントは、Goの型とMCPのスキーマ定義と、クライアント側の期待(特にChatGPT)が微妙にずれることがある点です。

この例では文字列・数値・真偽値・配列といった基本的な型をひと通り使っています。必須入力はmcp.Required()で宣言し、ハンドラー側はrequest.RequireStringのように「必須として取り出す」APIを使うと、エラーパスが整理しやすくなります。

func createSearchTool() mcp.Tool {
	return mcp.NewTool(
		"search_areas",
		mcp.WithDescription("地域を検索します"),
		mcp.WithReadOnlyHintAnnotation(true),

		// 文字列パラメータ
		mcp.WithString("search_text",
			mcp.Description("検索文字列"),
		),

		// 必須の文字列パラメータ
		mcp.WithString("code",
			mcp.Required(),
			mcp.Description("地域コード(例: \"13101\")"),
		),

		// 数値パラメータ
		mcp.WithNumber("year",
			mcp.Description("対象年度"),
		),

		// 真偽値パラメータ
		mcp.WithBoolean("include_empty",
			mcp.Description("空のデータも含めるか"),
		),

		// 配列パラメータ(itemsの型指定が重要!)
		mcp.WithArray("categories",
			mcp.Description("カテゴリのリスト"),
			mcp.WithStringItems(), // 配列要素の型を指定
		),
	)
}

func handleSearch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	// 必須パラメータの取得
	code, err := request.RequireString("code")
	if err != nil {
		return mcp.NewToolResultError("code parameter is required"), nil
	}

	// オプションパラメータの取得
	searchText := request.GetString("search_text", "")
	year := request.GetInt("year", 0)
	includeEmpty := request.GetBool("include_empty", false)
	categories := request.GetStringSlice("categories", nil)

	// 処理を実行
	result, err := search(ctx, code, searchText, year, includeEmpty, categories)
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	return convertToToolResult(result)
}

列挙型パラメータ

値を限定できるところは限定した方が、AIが想定外の入力を作りにくくなります。たとえば「実行する操作」を列挙にしておくと、ツール側の分岐やバリデーションがシンプルになります。

mcp.WithString("operation",
	mcp.Required(),
	mcp.Description("実行する演算"),
	mcp.Enum("add", "subtract", "multiply", "divide"),
)

数値の範囲制約

同様に、数値も取りうる範囲を先に宣言できます。入力が荒れやすいツール(たとえば「件数上限」「距離」「ズームレベル」など)では特に有効です。

mcp.WithNumber("age",
	mcp.Description("年齢"),
	mcp.Min(0),
	mcp.Max(150),
)

実装のポイント

ここからは、mcp-goで実装していてつまずきやすい点を、できるだけ「なぜハマるのか」も含めて整理します。

1. 配列のitemsの型を指定する

配列パラメータは一見するとmcp.WithArrayだけで良さそうに見えますが、配列要素(items)の型が未指定だと、クライアント側がスキーマを解釈できずに失敗するケースがあります。

ChatGPT側のMCP連携ではこの影響が出やすく、ツール登録時や呼び出し時にエラーになることがあるため、mcp.WithStringItems()のように必ず要素型まで指定しておくのが安全です。

// NG: itemsの型指定がない
mcp.WithArray("categories",
	mcp.Description("カテゴリのリスト"),
)

// OK: itemsの型を指定
mcp.WithArray("categories",
	mcp.Description("カテゴリのリスト"),
	mcp.WithStringItems(), // これが必要!
)

2. 読み取り専用ツールにはアノテーションを付ける

MCPでは「このツールは安全か」をクライアントが判断する材料として、読み取り専用のヒントを付けられます。データ取得系のツールは基本的に読み取り専用なので、mcp.WithReadOnlyHintAnnotation(true)を付けておくと、クライアント側が安心してツールを選びやすくなります。

mcp.NewTool(
	"get_data",
	mcp.WithDescription("データを取得します"),
	mcp.WithReadOnlyHintAnnotation(true), // 読み取り専用を明示
)

3. ステートレスモードを使用する

HTTP MCPサーバーは、実装次第でセッションを持たせることもできます。ただ、まずはステートレスで作っておくと、スケールや再起動に強く、ロードバランサ配下でも扱いやすくなります。

特別な理由がない限り、最初はserver.WithStateLess(true)を指定して進めるのが無難です。

httpServer := server.NewStreamableHTTPServer(
	mcpServer,
	server.WithStateLess(true), // ステートレスモード
)

エラーハンドリング

MCPのエラー処理では、「Goのerrorで返す」のではなく、ツール結果としてエラー文字列を返します。

クライアントから見ると「ツール呼び出しは成功したが、結果がエラーだった」という形にした方が扱いやすい、という設計意図があります。mcp-goではmcp.NewToolResultError()がそれに相当します。

func handleTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	// パラメータ検証エラー
	id, err := request.RequireString("id")
	if err != nil {
		return mcp.NewToolResultError("id parameter is required"), nil
	}

	// ビジネスロジックエラー
	data, err := fetchData(ctx, id)
	if err != nil {
		return mcp.NewToolResultError(err.Error()), nil
	}

	// データが見つからない場合
	if data == nil {
		return mcp.NewToolResultError("data not found"), nil
	}

	// 成功
	return mcp.NewToolResultText(formatResult(data)), nil
}

結果の返し方

ツール結果の返し方は大きく2種類です。ログや人間向けのメッセージで十分な場合はテキスト、構造化して次のツール呼び出しに繋げたい場合はJSON、という使い分けになります。

テキスト結果

return mcp.NewToolResultText("Hello, World!"), nil

JSON結果

実務では、JSONで返しておく方がAIが後段処理を組み立てやすいことが多いです。整形して返しておくとデバッグにも役立ちます。

func convertToToolResult(data interface{}) (*mcp.CallToolResult, error) {
	jsonBytes, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return mcp.NewToolResultError("failed to marshal response"), nil
	}
	return mcp.NewToolResultText(string(jsonBytes)), nil
}

リソースの定義

ツールは「何かを実行する」インターフェースですが、MCPにはそれとは別に「ドキュメントや静的データを読む」ためのリソース機構があります。

たとえば、READMEや仕様説明をdocs://...のURIで公開しておくと、AIが迷ったときに参照できる“辞書”になります。後半で触れる「AIに足りない知識を補う」という観点でも便利です。

実装としては、URIと表示名などのメタ情報をmcp.NewResourceで定義し、AddResourceで「そのURIが読まれたときに何を返すか」を登録します。ここではREADMEを返す例です。

func registerResources(s *server.MCPServer) {
	// 静的リソース
	resource := mcp.NewResource(
		"docs://readme",
		"Project README",
		mcp.WithResourceDescription("プロジェクトのREADME"),
		mcp.WithMIMEType("text/markdown"),
	)

	s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
		content, err := os.ReadFile("
README.md
")
		if err != nil {
			return nil, err
		}

		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				URI:      "docs://readme",
				MIMEType: "text/markdown",
				Text:     string(content),
			},
		}, nil
	})
}

APIをそのままMCPサーバー化すればいいわけではない

ここまでで、mcp-goでMCPサーバーを実装するための「器」「ツール定義」「ハンドラー」「リソース」まで一通り見てきました。

ここで一つ、よくある誤解があります。それは 「既存のWeb APIがあるなら、それをMCPに載せ替えれば終わりでしょ?」 という発想です。

結論から言うと、APIをそのままMCPサーバー化すれば良いとは限りません。MCPで本当に価値が出るのは「AIが迷わず、手戻りなく、必要な情報に到達できる体験」を設計できたときです。

以下では、API設計とMCPツール設計の違い、そして実務で効く“最適化”のポイントを整理します。

なぜ「そのまま」だとハマるのか

多くのWeb APIは、

  • 人間が画面で操作する
  • 仕様を理解したエンジニアがSDKやクライアント実装を書く

という前提で設計されます。

一方でMCPツールは、LLMが自律的に「どのツールを」「どんな順番で」呼ぶかを判断する世界です。

つまり、同じ「データを返すAPI」であっても、求められる性質が変わります。

  • 正しさだけでなく、次の一手が分かること
  • クライアントが賢い前提ではなく、入力が荒れる前提
  • 通信効率よりも、トークン効率が重要

この前提差を埋めるのが、MCPサーバー側の設計です。

落とし穴1: 結果を返しすぎる

APIの感覚で「条件に合うデータを全部返す」をやると、MCPではすぐに破綻します。

  • LLM側のコンテキスト(トークン)を圧迫する
  • “読まれない巨大なJSON”が増える
  • 会話が長くなり、目的達成まで遠回りになる

特にPLATEAUのように「都市×年度×LOD×形式×範囲」などで候補が増える領域は、返しすぎ=迷子の原因になりやすいです。

対策1: 「サマリ → 詳細」の二段階にする

おすすめは、探索系ツールと取得系ツールを分けることです。

  • 探索: 候補を少数返す。あるいは集計して返す(年度一覧、利用可能なLOD一覧など)。
  • 取得: 絞り込めた後に、URL一覧や属性などを返す。

対策2: “多すぎる”ことを結果として返す

APIのページングは定番ですが、LLMにページングさせると会話が伸びがちです。

その代わり、

  • 返却件数を制限する
  • 多すぎる場合は「多すぎる」ことを伝える
  • 次に絞るべきキー(都市・年度・LODなど)を明示する

といった「誘導」を返す方が、体験が安定します。

落とし穴2: 不正確な入力が発生する

MCPクライアント側は、人間がフォームで入力するのではなく、LLMがパラメータを生成します

このため、

  • 想定外の値
  • 型の揺れ(数値のつもりが文字列、配列のつもりが単一値)
  • 列挙の打ち間違い
  • 単位や範囲の解釈違い

が普通に起こります。

対策1: スキーマで“生成の自由度”を先に削る

mcp-goでは、ツール定義側でかなり防げます。

  • 列挙(Enum)を使って値を限定する
  • 数値に Min/Max を付ける
  • 必須パラメータを明確にする
  • 配列は items まで型指定する(特にChatGPT連携で重要)

「LLMが賢いから何とかなる」ではなく、サーバー側が“正しい入力しか通らない形”に寄せるのが安全です。

対策2: 不正確な入力をサーバー側で補正する

AIは不正確な入力を行うことがあります。例えば、SQLを入力するMCPサーバーでは、シンタックスエラーになるSQLが頻繁に入力されます。このとき、サーバーがエラーを返すだけでは、AIとの会話が長引いてしまいます。機械的に修正できる入力ミスであれば、サーバー側で補正してしまうのも有効です。

落とし穴3: ツール名・粒度・説明がAIにとって分かりづらい

APIのエンドポイント構成をそのままツールにすると、AIはこんな状態になりがちです。

  • どのツールを呼べば良いか分からない
  • どの順番で呼べば良いか分からない
  • ツールが万能すぎて、逆に選ばれにくい or 変な使われ方をする

対策1: 動詞→名詞を使ったわかりやすい名前にする

APIのエンドポイント名(例: /api/v1/datasets/search)をそのままツール名にするのではなく、AIが「何をするツールか」を直感的に理解できる名前にします。例えば、search_datasetsget_dataset_detailslist_available_cities のように、動詞から始まる具体的な名前が効果的です。

また、descriptionには「このツールは〇〇を行います。△△の情報が必要です。」のように、

  • いつ使うか
  • 入力の前提(例: codeが必要)
  • 次に呼ぶツール候補

を短く入れておくと、AIの誤用が減ります。

対策2: 「探索 → 取得」の導線を作る

PLATEAUの探索に当てはめると、例えば以下のように分けると良いです。

  • search_datasets: 都市・年度・LOD・形式でデータセット候補を探す
  • get_datasets : データセットの情報をID指定で得る

落とし穴4: エラーハンドリングがAPI流

APIだと、HTTPのステータスコード(4xx/5xx)や例外で十分なことが多いですが、MCPでは「エラーの読み方」が変わります。LLMにとって重要なのは、

  • 何が足りないのか
  • どう直せば良いのか

が文章として分かることです。

対策1: ツール結果としてエラーを返す

mcp-goでは mcp.NewToolResultError(...) を使い、

  • 入力不足
  • 範囲外
  • 見つからない
  • 一時的失敗

などを分けて、次にどうするかが分かる文言で返すと、連鎖的に詰まりが減ります。

落とし穴5: “知識の穴”を埋めないとツールが使われない

PLATEAUの探索では、LODの意味・空間IDの前提・仕様書上の用語のような「知識の穴」によって、AIが次の行動を決めきれないケースが出ます。

ここで有効なのがリソースです。

MCPでは “Resource” という概念も定義されています。ToolsだけがMCPかと思われがちですが、文章を格納しAIに必要に応じて読ませることが可能です。Claude Skillsにに似ていますね。

https://modelcontextprotocol.io/specification/2025-06-18/server/resources

対策1: リソースを“辞書”として用意する

READMEや用語集をMCPのリソースとして公開しておき、検索のツールと役割分担をします。

こうしておくと、AIが「まず仕様の確認をしてから探索する」動きに寄りやすくなりますし、必要なタイミングで読むのでコンテキストも節約できます。

逆に、ツールの説明文を必要以上に長くしてしまうと、AIがMCPサーバーに接続しただけでコンテキストを消費してしまうので注意です。

まとめ

今回、PLATEAUデータカタログをAIから扱いやすくするために、リモートMCPサーバーを開発・公開しました。

実装面では、mcp-goを使って

  • MCPサーバーの起動方式(stdio / HTTP)
  • ツール定義(名前・説明・入力スキーマ)
  • ハンドラー実装(入力の取り出しと結果の整形)
  • リソース定義(docs://... で“辞書”を用意)

までの基本パターンを整理しました。

最後に強調したいのは、既存APIをそのままMCP化しても、AIがうまく使えるとは限らないという点です。トークン消費を抑えるための結果サイズ設計、スキーマで入力を縛る工夫、探索→取得の導線設計、エラーの返し方、知識の穴を埋めるリソース設計など、AI前提の最適化が必要です。

AIを効果的に動作させるには、与えるコンテキストの質が重要です。MCPサーバーを作成したものの、期待通りに動かないと感じた場合、よく確認してみると、サーバーが十分な情報を返していなかったというケースがよくあります。最終的には、人の目での確認が欠かせません。

PLATEAU MCPサーバーのセットアップや利用方法は、こちらのリポジトリをご参照ください。

https://github.com/Project-PLATEAU/plateau-streaming-tutorial/blob/main/mcp/plateau-mcp.md

Japanese

Eukaryaでは様々な職種で採用を行っています!OSSにコントリビュートしていただける皆様からの応募をお待ちしております!

Eukarya 採用ページ

Eukarya is hiring for various positions! We are looking forward to your application from everyone who can contribute to OSS!

Eukarya Careers

Eukaryaは、Re:Earthと呼ばれるWebGISのSaaSの開発運営・研究開発を行っています。Web上で3Dを含むGIS(地図アプリの公開、データ管理、データ変換等)に関するあらゆる業務を完結できることを目指しています。ソースコードはほとんどOSSとしてGitHubで公開されています。

Re:Earth / ➔ Eukarya / ➔ note / ➔ GitHub

Eukarya is developing and operating a WebGIS SaaS called Re:Earth. We aim to complete all GIS-related tasks including 3D (such as publishing map applications, data management, and data conversion) on the web. Most of the source code is published on GitHub as OSS.

Re:Earth / ➔ Eukarya / ➔ Medium / ➔ GitHub