Overview
Tools List
Content Details
Alternatives
What is the AutoNavi Map MCP Service?
This is a tool that encapsulates the capabilities of the AutoNavi Map API into an intelligent dialogue service, allowing you to obtain location information through natural language queries. For example, asking 'Nearby restaurants' or 'Where am I', the service will automatically locate and return the results.How to use the AutoNavi Map MCP Service?
Simply configure the service address in the MCP - supported client, and then make location query requests like chatting. The system will automatically handle the location and search requests.Applicable Scenarios
Suitable for scenarios where you need to quickly obtain location information, such as travel navigation, nearby merchant queries, location sharing, etc. It is especially suitable for integration into chatbots or intelligent assistants.Main Features
How to Use
Usage Examples
Frequently Asked Questions
Related Resources
Installation
🚀 A Comprehensive Tutorial on MCP Development: From Scratch to Deployment
The popularity of MCP (Model Context Protocol) needs no further elaboration. As an emerging technology, there is an abundance of information on how to develop MCP services on Chinese-language Internet platforms. However, most of these materials are either vague or only scratch the surface, with many examples being mere rehashes of the official documentation.
As a developer, I understand the importance of engineering. Developing an MCP service is not just about writing a couple of service interfaces; it involves considering various aspects such as code structure, configuration management, logging, and exception handling.
Through exploration and practice, I have compiled a detailed guide on the development process of an MCP service, aiming to help more developers quickly get started and build high-quality MCP services.
The code for this project has been open-sourced on GitHub. Feel free to star and fork it!
🚀 Quick Start
What kind of MCP service are we building?
To have a clear practical goal, we will build a simple MCP service based on the Gaode Map API with the following core functions:
Get the user's geographical location based on their IP
- Parameters:
IP address (optional, if not provided, the current host's IP will be used) - Return: The user's longitude and latitude information
Get nearby POI information based on the user's geographical location
- Parameters:
Longitude, latitude, POI type - Return: A list of nearby POI information
Once these functions are implemented, we can obtain real POI information through large model conversations. For example:
- User: I want to know what restaurants are near me.
- MCP Service: Based on your location, the following restaurants are nearby: 1. Restaurant A 2. Restaurant B 3. Restaurant C
- User: What is the address of Restaurant A?
- MCP Service: The address of Restaurant A is:
- Address: XXX
- Phone: XXX
- Business Hours: XXX
Screenshot

✨ Features
Core MCP Concepts
If you have no prior knowledge of what MCP is, it is recommended to read the MCP official documentation to understand the basic concepts.
An MCP server can provide three main types of functions:
- Resources: Data similar to files that clients can read (e.g., API responses or file contents).
- Tools: Functions that LLMs can call (with user approval).
- Prompts: Pre-written templates to assist users in completing specific tasks.
Prerequisite Knowledge
Before you start, it is recommended that you have the following knowledge:
- Basic Python programming
- Understanding of LLMs (Large Language Models)
- Familiarity with UV (Python package management tool)
System Requirements
- Python 3.10 or higher
- Python MCP SDK 1.2.0
📦 Installation
Configuring the Development Environment
⚠️ Important Note
Please adjust the commands according to your operating system, as the command syntax for PowerShell and Bash differs.
The author used Windows with a Git terminal. The first half of this tutorial is similar to the official documentation. You can refer to the server development example in the official documentation.
Installing UV
# Linux or macOS
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Creating a Virtual Environment and Initializing the Project
# Create and enter the project directory using UV
uv init build-mcp
cd build-mcp
# Create a virtual environment
uv venv
source .venv/Scripts/activate
# Install relevant dependencies
uv add mcp[cli] httpx pytest
Planning the Project Directory Structure (Recommended src/ Layout)
build-mcp/
├── src/ # Core source code directory (Python package)
│ └── build_mcp/ # Main package namespace
│ ├── __init__.py # Package initialization file
│ ├── __main__.py # Command-line entry point
│ ├── common/ # Common functionality module
│ │ ├── config.py # Configuration management
│ │ └── logger.py # Logging system
│ └── services/ # Business service module
│ ├── gd_sdk.py # Gaode service integration
│ └── server.py # Main service implementation
├── tests/ # Test suite directory
│ ├── common/ # Common module tests
│ └── services/ # Service module tests
├── docs/ # Project documentation
│ └── build‑mcp 项目开发指南.md # Core documentation
├── pyproject.toml # Project build configuration
├── Makefile # Automated command management
└── README.md # Project overview document
Structure Design Analysis
Core Design: src/ Layout (Key Advantages)
build-mcp/
└── src/
└── mirakl_mcp/
├── ...
Why choose this structure?
- ✅ Isolate the installation environment (Core value): During testing, packages are installed using
pip install, avoiding direct reference to the source code path, ensuring that the testing environment is the same as the user's runtime environment. - ✅ Prevent implicit path dependencies: Eliminates incorrect imports caused by the development directory being at the top of
sys.path(common in traditional layouts withoutsrc/). - ✅ Packaging security: Forces verification that package contents are correctly included in the distribution files (missing files will be immediately exposed during testing).
- ✅ Consistency across multiple environments: The same package structure is used in development, testing, and production environments, eliminating the "it works on my machine" problem.
📊 Data support: A PyPA official survey shows that projects using the
src/layout have a 63% lower packaging error rate (Source).
💻 Usage Examples
Writing Utility Code
After planning the directory structure, we can start coding. A formal and standardized project may involve reading a large number of project configurations. First, we will encapsulate the configuration file reading function.
Using the pyyaml Package to Manage Configurations
uv add pyyaml
Creating a Configuration Management Module
mkdir -p src/build_mcp/common
touch src/build_mcp/__init__.py
touch src/build_mcp/common/__init__.py
touch src/build_mcp/common/config.py
Creating a Configuration File
touch src/build_mcp/config.yaml
Add the following content to the src/build_mcp/config.yaml file:
# Gaode Map API configuration
api_key: test
# Base URL of the Gaode Map API
base_url: https://restapi.amap.com
# Proxy settings
proxy: http://127.0.0.1:10809
# Log level
log_level: INFO
# Maximum number of API retries
max_retries: 5
# Retry interval for API requests (seconds)
retry_delay: 1
# Exponential backoff factor
backoff_factor: 2
# Log file path
log_dir: /var/log/build_mcp
⚠️ Important Note
The
config.yamlfile should be placed in thesrc/build_mcp/directory so that the configuration can be loaded correctly.
This configuration file is just an example for engineering purposes. In a production environment, sensitive information such as API keys should not be written directly into the configuration file. It is recommended to use environment variables or secure storage services.
Writing Configuration Management Code
# src/build_mcp/common/config.py
import os
import yaml
def load_config(config_file="config.yaml") -> dict:
"""
Load the configuration file.
Args:
config_file (str): The name of the configuration file, default is "config.yaml".
Returns:
dict: The content of the configuration file.
Example:
config = load_config("config.yaml")
print(config)
"""
# Find the root directory (config.yaml is placed in the root directory)
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_path = os.path.join(base_dir, config_file)
with open(config_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
return config
Installing the Code
⚠️ Important Note
When installing the code for the first time, use the
pip install -e .command. This will install the current directory as an editable package in the virtual environment, so any changes made to the code during development will take effect immediately without the need for reinstallation.
uv pip install -e .
Writing Test Code
It is a good practice to write as much test code as possible in a project. In project engineering, we should write test code for some core functions to ensure the correctness and stability of the code.
mkdir -p tests/common
touch tests/common/test_config.py
# tests/common/test_config.py
from build_mcp.common.config import load_config
def test_load_config():
"""Test the configuration file loading function"""
config = load_config("config.yaml")
assert config["api_key"] == "test"
assert config["log_level"] == "INFO"
Running Tests
uv run pytest tests
Writing the Logging Module
As a programmer, a good logging system is crucial for quickly locating problems. An excellent programmer not only knows how to write code but also how to write logs. We will simply encapsulate a logging module for future use.
touch src/build_mcp/common/logger.py
Here, we implement a logging system that outputs to both the console and a file, supporting log rotation and backup.
# src/build_mcp/common/logger.py
import logging
import os
from logging.handlers import RotatingFileHandler
from build_mcp.common.config import load_config
config = load_config("config.yaml")
def get_logger(name: str = "default", max_bytes=5 * 1024 * 1024, backup_count=3) -> logging.Logger:
"""
Get a logger with both file and console output.
Args:
name (str): Logger name, default is "default".
max_bytes (int): Maximum size of a single log file, default is 5MB.
backup_count (int): Number of log files to retain, default is 3.
Returns:
logging.Logger: A configured logger instance.
Example:
logger = get_logger("my_logger")
logger.info("This is an info message.")
"""
log_level = config.get("log_level", "INFO")
log_dir = config.get("log_dir", "./logs")
if isinstance(log_level, str):
log_level = getattr(logging, log_level.upper(), logging.INFO)
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"{name}.log")
logger = logging.getLogger(name)
logger.setLevel(log_level)
logger.propagate = False
if not logger.hasHandlers():
console_handler = logging.StreamHandler()
console_formatter = logging.Formatter('[%(asctime)s] %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
file_handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8')
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
logger.info(f"Logger initialized, writing to file: {log_file}")
return logger
So far, the basic modules for building a system have been completed. Next, we will implement the core service functions.
Writing the Gaode Map Request SDK
According to the Gaode Map API documentation, we need to implement two main functions:
- Get the geographical location based on the user's IP.
- Get nearby POI information based on the geographical location.
Creating the Gaode Map Service Module
mkdir -p src/build_mcp/services
touch src/build_mcp/services/__init__.py
touch src/build_mcp/services/gd_sdk.py
Writing the Gaode Map Service Code
# src/build_mcp/services/gd_sdk.py
import asyncio
import logging
from typing import Any
import httpx
class GdSDK:
"""
GdSDK API asynchronous SDK wrapper.
Supports automatic retries and exponential backoff strategies.
Args:
config (dict): Configuration dictionary, example:
{
"base_url": "https://restapi.amap.com",
"api_key": "your_api_key",
"proxies": {"http": "...", "https": "..."}, # Optional
"max_retries": 5,
"retry_delay": 1,
"backoff_factor": 2,
}
logger (logging.Logger, optional): Logger, default is the module logger.
"""
def __init__(self, config: dict, logger=None):
self.api_key = config.get("api_key", "")
self.base_url = config.get("base_url", "").rstrip('/')
self.proxy = config.get("proxy", None)
self.logger = logger or logging.getLogger(__name__)
self.max_retries = config.get("max_retries", 5)
self.retry_delay = config.get("retry_delay", 1)
self.backoff_factor = config.get("backoff_factor", 2)
# Create an asynchronous HTTP client with automatic request headers and proxy configuration
self._client = httpx.AsyncClient(proxy=self.proxy, timeout=10)
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
await self._client.aclose()
def _should_retry(self, response: httpx.Response = None, exception: Exception = None) -> bool:
"""
Determine whether to retry after a request fails.
Args:
response (httpx.Response, optional): HTTP response object.
exception (Exception, optional): Request exception.
Returns:
bool: Whether to retry.
"""
if exception is not None:
# Network exceptions, etc., recommend retrying
return True
if response is not None and response.status_code in (429, 500, 502, 503, 504):
# Server errors or too many requests, recommend retrying
return True
# Do not retry in other cases
return False
async def _request_with_retry(self, method: str, url: str, params=None, json=None):
"""
Send an HTTP request with automatic retries and exponential backoff.
Args:
method (str): HTTP method, e.g., 'GET', 'POST'.
url (str): Request URL.
params (dict, optional): URL query parameters.
json (dict, optional): Request body JSON.
Returns:
dict or None: JSON parsing result on success, None on failure.
"""
for attempt in range(self.max_retries + 1):
try:
self.logger.info(f"Sending request: {method} {url}, params: {params}, JSON: {json}, attempt: {attempt + 1}/{self.max_retries + 1}")
response = await self._client.request(
method=method,
url=url,
params=params,
json=json,
)
self.logger.info(f"Received response: {response.status_code} {response.text}")
if response.status_code in [200, 201]:
# Successfully return JSON data
return response.json()
if not self._should_retry(response=response):
self.logger.error(f"Request failed and cannot be retried, status code: {response.status_code}, URL: {url}")
return None
self.logger.warning(
f"Request failed (status code: {response.status_code}), "
f"Retry {attempt + 1}/{self.max_retries}, URL: {url}"
)
except httpx.RequestError as e:
self.logger.warning(
f"Request exception: {str(e)}, "
f"Retry {attempt + 1}/{self.max_retries}, URL: {url}"
)
# If not the last retry, wait according to exponential backoff
if attempt < self.max_retries:
delay = self.retry_delay * (self.backoff_factor ** attempt)
await asyncio.sleep(delay)
self.logger.error(f"All retries failed, URL: {url}")
return None
async def close(self):
"""
Close the asynchronous HTTP client and release resources.
"""
await self._client.aclose()
async def locate_ip(self, ip: str = None) -> Any | None:
"""
IP location interface
https://lbs.amap.com/api/webservice/guide/api/ipconfig
Args:
ip (str, optional): IP to query, if empty, use the public IP of the requester.
Returns:
dict: Location result, None if failed.
"""
url = f"{self.base_url}/v3/ip"
params = {
"key": self.api_key,
}
if ip:
params["ip"] = ip
result = await self._request_with_retry(
method="GET",
url=url,
params=params
)
if result and result.get("status") == "1":
return result
else:
self.logger.error(f"IP location failed: {result}")
return None
async def search_nearby(self, location: str, keywords: str = "", types: str = "", radius: int = 1000, page_num: int = 1, page_size: int = 20) -> dict | None:
"""
Nearby search (new POI)
https://lbs.amap.com/api/webservice/guide/api-advanced/newpoisearch#t4
Args:
location (str): Center longitude and latitude, format: "lng,lat"
keywords (str, optional): Search keywords
types (str, optional): POI categories
radius (int, optional): Search radius (meters), maximum 50000, default 1000
page_num (int, optional): Page number, default 1
page_size (int, optional): Number of items per page, default 20, maximum 25
Returns:
dict | None: Search result, None on failure
"""
url = f"{self.base_url}/v5/place/around"
params = {
"key": self.api_key,
"location": location,
"keywords": keywords,
"types": types,
"radius": radius,
"page_num": page_num,
"page_size": page_size,
}
result = await self._request_with_retry(
method="GET",
url=url,
params=params,
)
if result and result.get("status") == "1":
return result
else:
self.logger.error(f"Nearby search failed: {result}")
return None
The code implements the following:
- Asynchronous HTTP requests with automatic retries and exponential backoff.
- The
locate_ipmethod to get the geographical location based on the IP. - The
search_nearbymethod for nearby POI search based on longitude and latitude.
Writing Gaode Map Service Test Code
mkdir -p tests/services
touch tests/services/test_gd_sdk.py
# tests/test_gd_sdk_real.py
import logging
import os
import pytest
import pytest_asyncio
from build_mcp.services.gd_sdk import GdSDK
API_KEY = os.getenv("API_KEY", "your_api_key_here") # Get the API Key from the environment variable, or use the default value
@pytest_asyncio.fixture
async def sdk():
config = {
"base_url": "https://restapi.amap.com",
"api_key": API_KEY,
"max_retries": 2,
}
async with GdSDK(config, logger=logging.getLogger("GdSDK")) as client:
yield client
@pytest.mark.asyncio
async def test_locate_ip(sdk):
result = await sdk.locate_ip()
assert result is not None, "locate_ip returned None"
assert result.get("status") == "1", f"locate_ip call failed: {result}"
assert "province" in result, "locate_ip response does not contain province"
@pytest.mark.asyncio
async def test_search_nearby(sdk):
result = await sdk.search_nearby(
location="116.481488,39.990464",
keywords="gas station",
radius=3000,
page_num=1,
page_size=5
)
assert result is not None, "search_nearby returned None"
assert result.get("status") == "1", f"search_nearby call failed: {result}"
assert "pois" in result, "search_nearby response does not contain pois"
Running Tests
uv run pytest tests/services/test_gd_sdk.py
# If you have a Gaode API Key, you can directly run the following command for testing
API_KEY=your_API_KEY uv run pytest -s tests/services/test_gd_sdk.py
Introduction to the Three Transport Protocols of MCP Services
1. stdio
- Communication method: Bidirectional transmission of JSON-RPC messages between local processes via standard input/output (stdin/stdout).
- Use cases: Local tool calls or subprocesses, such as lightweight integration in desktop applications.
- Advantages: Low latency, simple implementation, no network required.
2. SSE (Server-Sent Events)
- Communication method: Based on HTTP: The client sends messages using
POST, and the server establishes atext/event-streamfor one-way push viaGET. - Current status: Deprecated as of MCP v2024-11-05, replaced by "streamable-http", but compatibility is still supported.
- Advantages: Suitable for simple implementations in early remote scenarios where only server push is required.
- Disadvantages: One-way communication from server to client only, connection does not support breakpoint recovery.
3. streamable-http
- Communication method: Bidirectional transmission based on HTTP: The client sends JSON-RPC requests via
POST, and the server can return a one-time response (JSON) or stream SSE messages. Additionally, the server can establish a push viaGET. - Supported features:
- A single
/mcpendpoint handles all communication. - Session management (via
Mcp-Session-Id). - Stream breakpoint resumption and message replay (HTTP disconnection recovery supports
Last-Event-ID). - Backward compatibility with SSE.
- A single
- Current status: Recommended as the default starting from MCP v2025-03-26, suitable for cloud and remote deployments, and the preferred choice for remote scenarios. ([modelcontextprotocol.io][1])
Protocol Comparison Overview
| Protocol | Communication Direction | Use Cases | Key Features | Recommendation |
|---|---|---|---|---|
| stdio | Bidirectional (local) | Local subprocess calls | Simple, low latency, no network dependency | ⭐ Preferred for local use |
| SSE | Unidirectional (server → client) | Early remote implementations | Simple implementation, but does not support recovery | ⚠️ Deprecated |
| streamable-http | Bidirectional/Optional SSE push | Cloud/remote interactions | Single endpoint, multi-functional, breakpoint resumption, strong compatibility | ✅ Recommended |
Writing the MCP Service Main Program
Next, we will write the main program for the MCP service to handle client requests and call the Gaode Map SDK.
touch src/build_mcp/services/server.py
import os
from typing import Annotated
from typing import Any, Dict, Generic, Optional, TypeVar
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel
from pydantic import Field
from build_mcp.common.config import load_config
from build_mcp.common.logger import get_logger
from build_mcp.services.gd_sdk import GdSDK
# Read the API_KEY from the environment variable first, if not available, read it from the configuration file
env_api_key = os.getenv("API_KEY")
config = load_config("config.yaml")
if env_api_key:
config["api_key"] = env_api_key
# Initialize the FastMCP service
mcp = FastMCP("amap-maps", description="Gaode Map MCP Service", version="1.0.0")
sdk = GdSDK(config=config, logger=get_logger(name="gd_sdk"))
logger = get_logger(name="amap-maps")
# Define a generic API response model
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
success: bool
data: Optional[T] = None
error: Optional[str] = None
meta: Optional[Dict[str, Any]] = None
@classmethod
def ok(cls, data: T, meta: Dict[str, Any] = None) -> "ApiResponse[T]":
return cls(success=True, data=data, meta=meta)
@classmethod
def fail(cls, error: str, meta: Dict[str, Any] = None) -> "ApiResponse[None]":
return cls(success=False, error=error, meta=meta)
# Define a Prompt
@mcp.prompt(name="assistant", description="Gaode Map Intelligent Navigation Assistant, supports IP location, nearby POI query, etc.")
def amap_assistant(query: str) -> str:
return (
"You are a Gaode Map intelligent navigation assistant, proficient in IP location and nearby POI query. Please retrieve relevant information based on the user's needs by calling tools.\n"
"## Steps to call tools:\n"
"1. Call the `locate_ip` tool to get the user's longitude and latitude.\n"
"2. If the longitude and latitude are successfully obtained, use them to call the `search_nearby` tool to search for nearby information based on the search keywords.\n"
"## Notes:\n"
"- Do not ask the user for longitude and latitude information directly; use the `locate_ip` tool instead.\n"
"- If the user's request contains longitude and latitude information, you can directly use it for nearby search.\n"
f"The user's request is:\n\n {query}.\n"
)
@mcp.tool(name="locate_ip", description="Get the user's IP address location information, returning province, city, district, longitude, and latitude.")
async def locate_ip(ip: Annotated[Optional[str], Field(description="User's IP address")] = None) -> ApiResponse:
"""
Locate the position based on the IP address.
Args:
ip (str): The IP address to locate.
Returns:
dict: A dictionary containing the location result.
"""
logger.info(f"Locating IP: {ip}")
try:
result = await sdk.locate_ip(ip)
if not result:
return ApiResponse.fail("Location result is empty. Please check the logs. If there is a system exception, check the relevant logs. The default log path is /var/log/build_mcp.")
logger.info(f"Location result for IP: {result}")
return ApiResponse.ok(data=result, meta={"ip": ip})
except Exception as e:
logger.error(f"Error locating IP {ip}: {e}")
return ApiResponse.fail(str(e))
@mcp.tool(name="search_nearby", description="Perform a nearby search based on longitude, latitude, and keywords, returning a list of POIs within the specified radius.")
async def search_nearby(
location: Annotated[str, Field(description="Center longitude and latitude, format: 'lng,lat', e.g., '116.397128,39.916527'")],
keywords: Annotated[str, Field(description="Search keywords, e.g., 'restaurant'.", min_length=0)] = "",
types: Annotated[str, Field(description="POI category codes, multiple categories separated by commas")] = "",
radius: Annotated[int, Field(description="Search radius (meters), maximum 50000", ge=0, le=50000)] = 1000,
page_num: Annotated[int, Field(description="Page number, starting from 1", ge=1)] = 1,
page_size: Annotated[int, Field(description="Number of items per page, maximum 25", ge=1, le=25)] = 20,
) -> ApiResponse:
"""
Nearby search.
Args:
location (str): Center longitude and latitude, format: "lng,lat".
keywords (str, optional): Search keywords, default is empty.
types (str, optional): POI categories, default is empty.
radius (int, optional): Search radius (meters), maximum 50000, default is 1000.
page_num (int, optional): Page number, default is 1.
page_size (int, optional): Number of items per page, maximum 25, default is 10.
Returns:
dict: A dictionary containing the search result.
"""
logger.info(f"Searching nearby: location={location}, keywords={keywords}, types={types}, radius={radius}, page_num={page_num}, page_size={page_size}")
try:
result = await sdk.search_nearby(location=location, keywords=keywords, types=types, radius=radius, page_num=page_num, page_size=page_size)
if not result:
return ApiResponse.fail("Search result is empty. Please check the logs. If there is a system exception, check the relevant logs. The default log path is /var/log/build_mcp.")
logger.info(f"Nearby search result: {result}")
return ApiResponse.ok(data=result, meta={
"location": location,
"keywords": keywords,
"types": types,
"radius": radius,
"page_num": page_num,
"page_size": page_size
})
except Exception as e:
logger.error(f"Error searching nearby: {e}")
return ApiResponse.fail(str(e))
In the code, we encapsulated a unified response class and provided two utility functions:
locate_ip: Get the geographical location based on the IP address.search_nearby: Perform a nearby search based on longitude, latitude, and keywords.
⚠️ Important Note
The
Annotatedtype in the code is essential. It allows LLMs to call tools more accurately based on metadata. Currently, most developers of MCP services do not have this awareness and simply define tools, which actually yields poor results.
💡 Usage Tip
We also wrote a prompt, which is provided in the conversation context. This is a very important point that many developers have not realized. In the AI era, we not only need to write good code but also learn how to polish prompts.
In fact, the core of this article lies in the above code. Please take the time to understand this information carefully.
At this point, we have completed the core functionality implementation of the MCP service. Next, we need to write the service entry point to start the MCP service.
Writing the MCP Service Entry Point
touch src/build_mcp/__init__.py
# src/build_mcp/__init__.py
import argparse
import asyncio
from build_mcp.common.logger import get_logger
from build_mcp.services.server import mcp
def main():
"""Main function to run the MCP server."""
logger = get_logger('app')
parser = argparse.ArgumentParser(description="Amap MCP Server")
parser.add_argument(
'transport',
nargs='?',
default='stdio',
choices=['stdio', 'sse', 'streamable-http'],
help='Transport type (stdio, sse, or streamable-http)'
)
args = parser.parse_args()
logger.info(f"🚀 Starting MCP server with transport type: %s", args.transport)
try:
mcp.run(transport=args.transport)
except (KeyboardInterrupt, asyncio.CancelledError):
logger.info("🛑 MCP Server received shutdown signal. Cleaning up...")
except Exception as e:
logger.exception("❌ MCP Server crashed with unhandled exception: %s", e)
else:
logger.info("✅ MCP Server shut down cleanly.")
if __name__ == "__main__":
main()
touch src/build_mcp/__main__.py
# src/build_mcp/__main__.py
from build_mcp import main
if __name__ == "__main__":
main()
Modifying the pyproject.toml File
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "build-mcp"
version = "0.1.0"
description = "Build an MCP server"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.28.1",
"mcp[cli]>=1.9.4",
"pytest>=8.4.1",
"pytest-asyncio>=1.0.0",
"pyyaml>=6.0.2",
]
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
[project.scripts]
build_mcp = "build_mcp.__main__:main"
[tool.hatch.build.targets.wheel]
packages = ["src/build_mcp"]
The key part is:
[project.scripts]
build_mcp = "build_mcp.__main__:main"
This means:
When the
build_mcpcommand is executed, it is equivalent to running:
from build_mcp import main
main()
We can start the MCP service using the following commands:
- Start the MCP service using the
stdioprotocol:
uv run build_mcp
- Start the MCP service using the
streamable-httpprotocol:
uv run build_mcp streamable-http
Debugging MCP Services
How to debug an MCP service depends on how we start the service.
1. Writing Client Code to Debug the MCP Service Using the stdio Protocol
mkdir -p tests/services
touch tests/services/test_mcp_client.py
import pytest
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
@pytest.mark.asyncio
async def test_mcp_server():
async with stdio_client(
StdioServerParameters(command="uv", args=["run", "build_mcp"])
) as (read, write):
print("Starting the server...")
async with ClientSession(read, write) as session:
await session.initialize()
print("Initialization completed")
tools = await session.list_tools()
print("Available tools:", tools)
assert hasattr(tools, "tools")
assert isinstance(tools.tools, list)
assert any(tool.name == "locate_ip" for tool in tools.tools)
Running Tests
API_KEY=your_API_KEY uv run pytest -s tests/services/test_mcp_client.py
This is just a test code example. You can write more test code according to your needs to verify the functionality of the MCP service.
2. Using the Inspector for Testing
The Inspector is an official debugging tool for MCP services. It can start a local web interface where you can directly call the tools of the MCP service. It is more intuitive and user-friendly, so it is recommended. For more details, please refer to the official documentation.
# Use the Inspector to debug the MCP service using the `stdio` protocol
API_KEY=your_KEY mcp dev src/build_mcp/__init__.py
Writing a Makefile
To facilitate development and testing, we can write a Makefile to manage common commands.
touch Makefile
# Makefile for MCP Service
# Default target - display help information
.DEFAULT_GOAL := help
# Project environment variables
API_KEY ?= your_api_key_here # Default API_KEY for testing
# Install project dependencies
install:
@echo "Installing project dependencies..."
uv pip install -e .
# Run tests (API_KEY needs to be set)
test:
@echo "Running tests with API_KEY=$(API_KEY)..."
API_KEY=$(API_KEY) uv run pytest -s tests
# Start the MCP service using the `stdio` protocol
stdio:
@echo "Starting MCP service with stdio protocol..."
uv run build_mcp
# Start the MCP service using the `streamable-http` protocol
http:
@echo "Starting MCP service with streamable-http protocol..."
uv run build_mcp streamable-http
# dev
dev:
@echo "Starting MCP service with stdio protocol in development mode..."
API_KEY=$(API_KEY) mcp dev src/build_mcp/__init__.py
# Alias target
streamable-http: http
# Help information
help:
@echo "MCP Service Management"
@echo ""
@echo "Usage:"
@echo " make install Install project dependencies"
@echo " make test Run tests (set API_KEY in Makefile or override)"
@echo " make stdio Start MCP service with stdio protocol"
@echo " make http Start MCP service with streamable-http protocol"
@echo ""
@echo "Advanced:"
@echo " Override API_KEY: make test API_KEY=custom_key"
@echo " Clean: make clean"
@echo " Full setup: make setup"
# Clean the project
clean:
@echo "Cleaning project..."
rm -rf build dist *.egg-info
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '__pycache__' -exec rm -rf {} +
# Full setup: clean + install + test
setup: clean install test
@echo "Project setup completed!"
# Declare phony targets
.PHONY: install test stdio http streamable-http help clean setup
So far, we have fully developed an MCP service from scratch. Congratulations on learning a new skill!
How to Use This MCP Service
First, you need to have an MCP client. There are various types of MCP clients on the market, and you can choose one according to your preference.
Here is a very detailed guide on using MCP clients, which is a great project on GitHub: MCP Client Usage Guide
Choose a client, download and install it, and then configure our developed service.
Configuring the MCP Service Using the stdio Protocol
{
"mcpServers": {
"build_mcp": {
"command": "uv",
"args": [
"run",
"-m"
"build_mcp"
],
"env": {
"API_KEY": "your_Gaode_API_Key"
}
}
}
}
⚠️ Important Note
Pay attention to the local UV environment. Installing multiple UV versions may cause environment confusion, which can be a headache during development. Make sure to manage it properly.
Configuring the MCP Service Using the streamable-http Protocol
Starting the Project
make streamable-http
$ make streamable-http
Starting MCP service with streamable-http protocol...
uv run build_mcp streamable-http
[2025-06-26 15:01:33,775] INFO - Logger initialized, writing to file: /var/log/build_mcp\gd_sdk.log
[2025-06-26 15:01:33,839] INFO - Logger initialized, writing to file: /var/log/build_mcp\amap-maps.log
[2025-06-26 15:01:33,847] INFO - Logger initialized, writing to file: /var/log/build_mcp\app.log
[2025-06-26 15:01:33,848] INFO - 🚀 Starting MCP server with transport type: streamable-http
INFO: Started server process [6064]
INFO: Waiting for application startup.
[06/26/25 15:01:33] INFO StreamableHTTP session manager started streamable_http_manager.py:109
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
After successful startup, an HTTP service will be started on port 8000.
Client Configuration
{
"mcpServers": {
"build_mcp_http": {
"url": "http://localhost:8000/mcp"
}
}
}
Summary
This article introduced how to build a Gaode Map MCP service from scratch, covering the following aspects:
- Basic concepts and configuration of MCP services.
- How to use the Gaode Map API for IP location and nearby search.
- How to write the core functionality of an MCP service, including configuration management, logging systems, and the Gaode Map SDK.
- How to write the main program and entry point of an MCP service.
- How to debug an MCP service, including using the Inspector and writing test code.
- How to use a Makefile to manage project commands.
- How to configure an MCP client to connect to our service.
That concludes this article. I hope you enjoy learning! If you have any questions or suggestions, please submit an issue or pull request to the GitHub repository.
References
Alternatives










