Searching PLATEAU Data with AI! We've Released a Remote MCP Server
2025-12-19
We Released a PLATEAU MCP Server!
Hello. I’m Hiroki Inoue, the CTO.
This is the article for Day 19 of the 3D City Model Project PLATEAU Advent Calendar 2025.
PLATEAU data is extremely valuable, but when you actually try to use it, you often hit unexpected friction even just finding the data.
PLATEAU is an initiative led by Japan’s Ministry of Land, Infrastructure, Transport and Tourism (MLIT) to develop and publish open data for 3D city models nationwide.
For example, you often need to figure out things like:
- Which city, which year, which LOD, and which format (CityGML / 3D Tiles / MVT) you need
- Whether data matching those conditions exists, and if it does, where you can download it
- Where in the specification it is written, and what related terms and assumptions you need to understand
In practice, you end up going back and forth between catalogs and documents to narrow things down. If you’re already familiar with PLATEAU, it’s manageable. But if it’s your first time, a lot of time goes into “research work,” and it becomes hard to move on to the actual validation and implementation.
That’s why there has been growing interest in MCP (Model Context Protocol), a mechanism that lets AI connect to external tools and “go look things up.” With MCP, AI does not merely generate text. It can also call tools to search and fetch information, then use the results to decide the next step.
In fact, more MCP servers are appearing in the ecosystem, and MLIT has also released an experimental example, the MLIT DATA PLATFORM MCP Server.
With this trend in mind, we have developed and publicly released an MCP server for the PLATEAU data catalog! (Confirmed by MLIT representatives)
With this MCP server, AI can do the following.
- Data catalog
- Get metadata: Retrieve statistics such as available years, PLATEAU specification versions, number of regions, number of datasets, and more
- Search areas: Search prefectures and municipalities (filterable by parent region code, dataset type, free-text search, etc.)
- Search datasets: Search datasets (CityGML, 3D Tiles, MVT, etc.) by region code, dataset type, year, PLATEAU spec version, and more
- List dataset categories: Retrieve available feature type codes (bldg, tran, luse, dem, fld, etc.) and their descriptions
- CityGML retrieval
- Search CityGML files: Retrieve a list of CityGML file URLs by mesh code, spatial ID, or bounding box (lat/lon)
- Get feature IDs: Retrieve a list of feature IDs (e.g., buildings) intersecting a spatial ID
- Get attributes: Retrieve attribute information for a specified building ID from a CityGML file (including code list resolution)
- PLATEAU specifications (you can ask questions about the documents)
- Get table of contents: Retrieve the TOC (chapter/section hierarchy) for the “Standard Product Specification for 3D City Models” and the “Standard Work Procedures for 3D City Models”
- Read content: Retrieve the full content of a specified section in Markdown format
In addition, this MCP server is a remote MCP server, which means it works over the internet. There is no complicated setup required, and you can try it immediately from your local AI client. Here is the MCP server URL (no authentication):
http://api.plateauview.mlit.go.jp/mcp
For details such as how to set up the MCP server, please refer to the repository below.
https://github.com/Project-PLATEAU/plateau-streaming-tutorial/blob/main/mcp/plateau-mcp.md
⚠️ This MCP server is provided experimentally. The provider assumes no responsibility for any damages arising from the use of this service.
In the rest of this article (as an engineering blog post), I will explain practical know-how for implementing an MCP server in Go.
Implementing an MCP Server in Go
From here, I will explain how to implement an MCP server using mcp-go (github.com/mark3labs/mcp-go).
The MCP server discussed in this article is a process that provides tools (Functions) and resources (Resources), called by clients such as Claude Desktop or ChatGPT. We implement it in Go, but what matters most is how to design an interface that AI can call without hesitation.
If you proceed with the mindset of typical API server development, you may run into pitfalls related to tool granularity, response size, and how to provide schemas. I’ll cover those points in the second half.
In this chapter, we will first cover server startup modes (stdio / HTTP). Then we will look at tool definitions (name, description, input schema) and handler implementations (extracting inputs, formatting results, returning errors). The recommended path is:
- Start with the smallest working implementation
- Embed it over HTTP
- Review key design principles
What is mcp-go?
mcp-go (github.com/mark3labs/mcp-go) is a library for implementing MCP servers in Go.
MCP itself is a “common language for LLMs to call external tools,” but when you get down to implementation, the flow is surprisingly straightforward:
- Build the “container” of the server
- Define tools and resources that clients can call
- Write handlers for what happens when tools are called
mcp-go helps you assemble this entire flow naturally as Go code.
Setup is simple. Install it with:
go get github.com/mark3labs/mcp-go
Basic MCP Server Implementation
Here we prioritize simply “getting it working” and build the smallest possible server.
There are a few ways to start an MCP server, but the two most common are:
- stdio (standard input/output) for local clients
- HTTP, which is easier to embed in remote services or existing web apps
Because the surrounding implementation changes slightly depending on which you choose, let’s walk through both.
Server Initialization (stdio)
This is the minimal configuration for running via stdio. Local clients such as Claude Desktop will start the process and exchange MCP messages over standard input/output.
The key point is that server.NewMCPServer(...) declares “what this server is” and “what it can do.” Clients use this information to determine which tools and resources are available.
package main
import (
"log"
"github.com/mark3labs/mcp-go/server"
)
func main() {
// Create MCP server
s := server.NewMCPServer(
"my-mcp-server", // Server name
"1.0.0", // Version
server.WithToolCapabilities(true), // Enable tool capabilities
server.WithResourceCapabilities(true, false), // Enable resource capabilities
server.WithLogging(), // Enable logging
)
// Start server via stdio (for Claude Desktop, etc.)
if err := server.ServeStdio(s); err != nil {
log.Fatalf("Server error: %v", err)
}
}
Implementing an HTTP MCP Server
Next is the HTTP pattern. This is natural when you want to embed it in your own web app or publicly provide it as a remote MCP server in the cloud.
With mcp-go, you use StreamableHTTPServer. Conceptually it is a three-step setup:
- Create the core server (tool set) with
NewMCPServer - Wrap it in an HTTP handler
- Publish it with
ListenAndServeor similar
In this example, we also set server.WithStateLess(true). Stateless operation removes the need for session management and makes scaling and restarts much easier. Unless you have a specific reason not to, starting with stateless mode is usually a good default.
package main
import (
"log"
"net/http"
"github.com/mark3labs/mcp-go/server"
)
func main() {
// Create MCP server
mcpServer := server.NewMCPServer(
"my-http-mcp-server",
"1.0.0",
server.WithToolCapabilities(true),
)
// Register tools (described later)
registerTools(mcpServer)
// Create HTTP server with StreamableHTTPServer
httpServer := server.NewStreamableHTTPServer(
mcpServer,
server.WithStateLess(true), // Stateless mode
server.WithEndpointPath("/mcp"), // Endpoint path
)
// Start HTTP server
log.Println("MCP server listening on :8080")
if err := http.ListenAndServe(":8080", httpServer); err != nil {
log.Fatalf("Server error: %v", err)
}
}
Integrating with the Echo Framework
If you want to embed this into an existing Echo application, you simply connect Echo’s routing layer with the MCP HTTP handler.
It is very simple: you call StreamableHTTPServer.ServeHTTP from an Echo handler. Using g.Any to accept all HTTP methods avoids unnecessary constraints for clients and future extensions.
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),
)
// Register tools
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 *<a href="http://echo.Group">echo.Group</a>) {
g.Any("", h.ServeHTTP)
g.Any("/*", h.ServeHTTP)
}
Implementing Tool Definitions
Ultimately, the value of an MCP server comes down to how well you design the “toolbox AI can use.” In tool definitions, you should design for AI usage:
- Tool names should make the intended action easy to imagine
- Descriptions should be short but unambiguous
- Input schemas should be easy for clients to handle
With mcp-go, you define tool metadata and input schemas with mcp.NewTool, then attach handlers with s.AddTool. Let’s start with the simplest example.
A Simple Tool
A tool with no parameters is useful even for connectivity checks. For example, if you create a tool that “just returns metadata,” you can quickly verify whether clients are connected and responses are correct with a short round trip.
func registerTools(s *server.MCPServer) {
// Tool definition
tool := mcp.NewTool(
"get_metadata",
mcp.WithDescription("Get metadata"),
mcp.WithReadOnlyHintAnnotation(true), // Explicitly mark as read-only
)
// Register handler
s.AddTool(tool, handleGetMetadata)
}
func handleGetMetadata(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Run processing
result := fetchMetadata()
// Return result as JSON
jsonBytes, err := json.MarshalIndent(result, "", " ")
if err != nil {
return mcp.NewToolResultError("failed to marshal response"), nil
}
return mcp.NewToolResultText(string(jsonBytes)), nil
}
Tools with Parameters
In practice, most useful tools take some input.
The key point here is that Go types, MCP schema definitions, and client expectations (especially ChatGPT) may not align perfectly.
This example uses a variety of basic types: string, number, boolean, and array. You can declare required parameters with mcp.Required(). In the handler, using APIs like request.RequireString (which treat the parameter as required) helps keep error paths clean.
func createSearchTool() mcp.Tool {
return mcp.NewTool(
"search_areas",
mcp.WithDescription("Search areas"),
mcp.WithReadOnlyHintAnnotation(true),
// String parameter
mcp.WithString("search_text",
mcp.Description("Search text"),
),
// Required string parameter
mcp.WithString("code",
mcp.Required(),
mcp.Description("Area code (e.g., \"13101\")"),
),
// Number parameter
mcp.WithNumber("year",
mcp.Description("Target year"),
),
// Boolean parameter
mcp.WithBoolean("include_empty",
mcp.Description("Whether to include empty data"),
),
// Array parameter (specifying item type is important!)
mcp.WithArray("categories",
mcp.Description("List of categories"),
mcp.WithStringItems(), // Specify type of array items
),
)
}
func handleSearch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Get required parameter
code, err := request.RequireString("code")
if err != nil {
return mcp.NewToolResultError("code parameter is required"), nil
}
// Get optional parameters
searchText := request.GetString("search_text", "")
year := request.GetInt("year", 0)
includeEmpty := request.GetBool("include_empty", false)
categories := request.GetStringSlice("categories", nil)
// Run processing
result, err := search(ctx, code, searchText, year, includeEmpty, categories)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return convertToToolResult(result)
}
Enum Parameters
Where possible, it is better to restrict values. For example, making an “operation type” an enum makes branching and validation simpler, and it helps prevent the AI from generating unexpected inputs.
mcp.WithString("operation",
mcp.Required(),
mcp.Description("Operation to perform"),
mcp.Enum("add", "subtract", "multiply", "divide"),
)
Numeric Range Constraints
You can also declare valid ranges for numeric values. This is particularly useful for parameters that easily become noisy (such as “limit,” “distance,” “zoom level,” etc.).
mcp.WithNumber("age",
mcp.Description("Age"),
mcp.Min(0),
mcp.Max(150),
)
Implementation Tips
Here are common pitfalls when implementing with mcp-go, including why they happen.
1. Specify the item type for arrays
Array parameters may look fine with just mcp.WithArray, but if the item type is not specified, clients may fail to interpret the schema. This tends to affect ChatGPT’s MCP integration especially, causing errors during tool registration or tool calls. It is safest to always specify the item type (for example, mcp.WithStringItems()).
// NG: item type not specified
mcp.WithArray("categories",
mcp.Description("List of categories"),
)
// OK: item type specified
mcp.WithArray("categories",
mcp.Description("List of categories"),
mcp.WithStringItems(), // Required
)
2. Add read-only annotations to read-only tools
MCP allows you to provide hints that help clients judge whether a tool is safe. Data retrieval tools are generally read-only, so adding mcp.WithReadOnlyHintAnnotation(true) makes it easier for clients to confidently select those tools.
mcp.NewTool(
"get_data",
mcp.WithDescription("Get data"),
mcp.WithReadOnlyHintAnnotation(true), // Explicitly read-only
)
3. Use stateless mode
An HTTP MCP server can be implemented with session state, but starting with stateless mode makes the system easier to scale, restart, and run behind a load balancer. Unless you have a specific reason, using server.WithStateLess(true) is a good default.
httpServer := server.NewStreamableHTTPServer(
mcpServer,
server.WithStateLess(true), // Stateless mode
)
Error Handling
In MCP error handling, you typically do not return errors via Go’s error as the primary mechanism. Instead, you return an error message as the tool result.
From the client’s perspective, it is often easier to handle “the tool call succeeded, but the result was an error.” In mcp-go, mcp.NewToolResultError() corresponds to that.
func handleTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Parameter validation error
id, err := request.RequireString("id")
if err != nil {
return mcp.NewToolResultError("id parameter is required"), nil
}
// Business logic error
data, err := fetchData(ctx, id)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// Not found
if data == nil {
return mcp.NewToolResultError("data not found"), nil
}
// Success
return mcp.NewToolResultText(formatResult(data)), nil
}
How to Return Results
There are two main ways to return tool results.
- Text, when logs or human-readable messages are sufficient
- JSON, when you want structured results that the AI can use for subsequent tool calls
Text results
return mcp.NewToolResultText("Hello, World!"), nil
JSON results
In real-world work, returning JSON often makes it easier for AI to compose follow-up processing. Returning pretty-printed JSON also helps with debugging.
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
}
Defining Resources
Tools are the interface for “doing something,” but MCP also has a resource mechanism for “reading documents or static data.”
For example, if you publish a README or specification notes as a URI like docs://..., AI can consult it when needed, acting like a “dictionary.” This is useful for filling gaps in AI knowledge.
Implementation-wise, you define URI and metadata with mcp.NewResource, then register what to return on reads with AddResource. Here is an example that returns a README.
func registerResources(s *server.MCPServer) {
// Static resource
resource := mcp.NewResource(
"docs://readme",
"Project README",
mcp.WithResourceDescription("Project README"),
mcp.WithMIMEType("text/markdown"),
)
s.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
content, err := os.ReadFile("
You Can’t Just Turn an API into an MCP Server As-Is
So far, we have covered the basics of implementing an MCP server with mcp-go: the server container, tool definitions, handlers, and resources.
Here is a common misconception:
“If we already have a web API, can’t we just put it behind MCP and we’re done?”
In conclusion, it is not necessarily a good idea to simply convert an API into an MCP server as-is. The real value of MCP comes when you design an experience where AI can reach the needed information without hesitation and without backtracking.
Below, I will summarize the differences between API design and MCP tool design, and highlight optimizations that matter in real work.
Why “as-is” causes problems
Most web APIs are designed assuming:
- Humans operate them through a UI
- Engineers who understand the specs write SDKs or client implementations
In contrast, MCP tools exist in a world where the LLM autonomously decides which tool to call and in what order.
So even if an API “returns data,” what you need changes:
- Not only correctness, but also making it clear what the next step should be
- Do not assume a clever client. Assume inputs will be messy
- Communication efficiency matters, but token efficiency matters even more
Bridging that gap is the job of MCP server design.
Pitfall 1: Returning too much data
If you follow the usual API mindset and “return everything that matches,” MCP will break quickly.
- It consumes the LLM context (tokens)
- You get huge JSON blobs that nobody reads
- Conversations become long and users take detours before achieving their goal
For domains like PLATEAU, where candidates can explode due to combinations of city × year × LOD × format × area, returning too much easily causes the AI to get lost.
Countermeasure 1: Two-step flow (summary → details)
A recommended approach is to separate exploration and retrieval.
- Exploration: Return a small set of candidates, or aggregated lists (available years, available LODs, etc.)
- Retrieval: After narrowing down, return URLs, attributes, and other details
Countermeasure 2: Return “too many” as a result
Pagination is common for APIs, but asking the LLM to page tends to lengthen the conversation. Instead:
- Limit the number of returned items
- If it is too many, say “it is too many”
- Explicitly state which keys to narrow next (city, year, LOD, etc.)
This kind of guidance stabilizes the experience.
Pitfall 2: Inaccurate inputs occur
MCP clients are not humans filling out forms. The LLM generates parameters, so it is normal to see:
- Unexpected values
- Type mismatches (string vs number, array vs single value)
- Typos in enum values
- Incorrect interpretations of units or ranges
Countermeasure 1: Reduce generation freedom in the schema
mcp-go allows you to prevent many issues upfront.
- Use enums to restrict values
- Add Min/Max constraints for numbers
- Clearly declare required parameters
- For arrays, specify the item type (important for ChatGPT integration)
Instead of relying on “the LLM will figure it out,” it is safer to make the server accept only the correct shape of input.
Countermeasure 2: Correct inputs on the server side
AI sometimes produces inaccurate inputs. For example, an MCP server that accepts SQL often receives syntactically invalid SQL. If the server only returns errors, the conversation becomes long. If mistakes can be fixed mechanically, correcting them server-side can be effective.
Pitfall 3: Tool names, granularity, and descriptions are unclear to AI
If you copy an API endpoint structure into tool definitions as-is, AI often ends up in a state like:
- It does not know which tool to call
- It does not know the right order
- Tools are too general, so they are less likely to be selected correctly, or they get used in strange ways
Countermeasure 1: Use clear verb-first names
Instead of using endpoint-style names (e.g., /api/v1/datasets/search), choose names that AI can intuitively understand, such as search_datasets, get_dataset_details, or list_available_cities.
Also, for the description, adding short notes like:
- When to use it
- Input prerequisites (e.g., code is required)
- Candidate next tools
helps reduce misuse.
Countermeasure 2: Create an “explore → fetch” pathway
Applied to PLATEAU exploration, a structure like this works well.
search_datasets: Find candidate datasets by city, year, LOD, formatget_datasets: Fetch dataset information by ID
Pitfall 4: API-style error handling
In APIs, HTTP status codes (4xx/5xx) and exceptions are often enough. But in MCP, the readability of errors changes. For the LLM, what matters is:
- What is missing
- How to fix it
and that needs to be conveyed clearly in text.
Countermeasure 1: Return errors as tool results
With mcp-go, use mcp.NewToolResultError(...) and provide messages that clearly indicate what to do next, separating cases such as:
- Missing input
- Out of range
- Not found
- Temporary failure
This reduces cascading failures.
Pitfall 5: Tools are not used if you don’t fill knowledge gaps
In PLATEAU exploration, knowledge gaps about terms and prerequisites (LOD meaning, assumptions behind spatial IDs, spec terminology, etc.) can prevent the AI from deciding the next action.
Resources help here.
MCP also defines the concept of a “Resource.” People often assume MCP is only about tools, but you can store and let AI read documents as needed as well.
https://modelcontextprotocol.io/specification/2025-06-18/server/resources
Countermeasure 1: Provide resources as a “dictionary”
Publish a README or glossary as a resource, and divide responsibilities between tools and resources.
This encourages AI to first confirm definitions and then run exploration, and because it reads only when needed, it also saves context. On the other hand, if you make tool descriptions too long, the AI may waste context just by connecting to the MCP server, so be careful.
Summary
To make the PLATEAU data catalog easier to use from AI, we developed and publicly released a remote MCP server.
On the implementation side, using mcp-go, we organized the basic patterns for:
- MCP server startup modes (stdio / HTTP)
- Tool definitions (name, description, input schema)
- Handler implementations (extracting inputs and formatting results)
- Resource definitions (providing a “dictionary” via
docs://...)
Finally, the most important point is that simply converting an existing API into MCP does not guarantee that AI will use it well. When designing for AI, you need optimizations such as result-size design to reduce token usage, schema restrictions to constrain inputs, an explore → fetch flow, error messages that guide the next step, and resources that fill knowledge gaps.
To use AI effectively, the quality of the context you provide is critical. Even if you build an MCP server, if it does not behave as expected, often the server is not returning enough information. Ultimately, human review and verification remains indispensable.
For MCP server setup and usage, please refer to this repository.
https://github.com/Project-PLATEAU/plateau-streaming-tutorial/blob/main/mcp/plateau-mcp.md
Eukaryaでは様々な職種で採用を行っています!OSSにコントリビュートしていただける皆様からの応募をお待ちしております!
Eukarya is hiring for various positions! We are looking forward to your application from everyone who can contribute to OSS!
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.