odh-mcp-server
# odh-mcp-server
A [FastMCP](https://github.com/jlowin/fastmcp) server that exposes hospitality data — accommodations, events, and gastronomy — through a unified MCP interface. Part of the **GaiaWM ecosystem**, it is open source, self-hostable, and designed to work both locally (stdio) and remotely (SSE / streamable-http).
The server maintains a **"current world"** in session context. A `set_world` tool switches the active world. All other tools query the current world transparently. The same `find_accommodations` call returns real hotel data from South Tyrol or fictional inn listings from a Spelljammer asteroid city.
---
## Quickstart
```bash
# Install with uv (recommended)
pip install uv
# Run locally via stdio (Claude Desktop, etc.)
uvx odh-mcp-server --stdio
# Or clone and run directly
git clone https://github.com/openfantasymap/odh-mcp-server
cd odh-mcp-server
uv run server.py --stdio
```
---
## Available worlds
| World ID | Name | Data source | Notes |
|-------------|--------------------|------------------------------------|--------------------------------|
| `earth-313` | South Tyrol | Open Data Hub (`opendatahub.com`) | Real hotels, events, restaurants. WGS84 coordinates. |
| `bral` | The Rock of Bral | JSON fixtures | Spelljammer city-asteroid. Mock data for demo/roleplay. |
---
## Tools
| Tool | Description |
|-----------------------|---------------------------------------------------------------------|
| `set_world` | Switch the active world. Returns world description on success. |
| `describe_world` | Describe the current world and how to query it. |
| `find_accommodations` | Search for hotels / inns with optional geo, feature, and date filters. |
| `find_events` | Search for events with optional geo, date, and topic filters. |
| `find_gastronomy` | Search for restaurants and food places with geo and cuisine filters. |
---
## Example session
```
# 1. Start in South Tyrol
set_world("earth-313")
→ World set to: South Tyrol (earth-313)
Real hospitality data from the Open Data Hub...
# 2. Find hotels near Bolzano with a pool
find_accommodations(near="Bolzano centro", features=["pool"], max_results=3)
→ Found 2 accommodations in South Tyrol (earth-313), near Bolzano centro...
# 3. Switch to the Rock of Bral
set_world("bral")
→ World set to: The Rock of Bral
A city-asteroid drifting through Wildspace...
# 4. Find an inn near the Great Market
find_accommodations(near="great market", radius_m=800)
→ Found 1 inn in the Rock of Bral, near great market...
1. The Raised Cup [inn] (budget) — A lively taproom in the lower city...
```
---
## Claude Desktop config
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
```json
{
"mcpServers": {
"odh-world": {
"command": "uv",
"args": ["run", "/path/to/odh-mcp-server/server.py", "--stdio"]
}
}
}
```
Or, once published to PyPI:
```json
{
"mcpServers": {
"odh-world": {
"command": "uvx",
"args": ["odh-mcp-server", "--stdio"]
}
}
}
```
---
## Session context notes
The server uses `contextvars.ContextVar` to track the current world per session.
- **SSE / streamable-http transport**: each client connection gets its own context — multiple users can be in different worlds simultaneously.
- **stdio transport**: single process, single context — fine for local single-user use (Claude Desktop, CLI).
---
## Adding a new world
1. Create `worlds/myworld.py` and implement the `WorldAdapter` abstract base class:
```python
from worlds.base import WorldAdapter, Accommodation, Event, GastronomyPlace, WorldInfo
class MyWorldAdapter(WorldAdapter):
async def find_accommodations(self, ...) -> list[Accommodation]: ...
async def find_events(self, ...) -> list[Event]: ...
async def find_gastronomy(self, ...) -> list[GastronomyPlace]: ...
async def describe(self) -> WorldInfo: ...
```
2. Register it in `worlds/__init__.py`:
```python
from .myworld import MyWorldAdapter
WORLD_REGISTRY = {
"earth-313": Earth313Adapter(),
"bral": BralAdapter(),
"myworld": MyWorldAdapter(), # add this line
}
```
3. That's it. All tools will automatically support your new world once it's in the registry.
---
## Configuration
Copy `.env.example` to `.env` and adjust as needed:
```env
DEFAULT_WORLD=earth-313
ODH_BASE_URL=https://tourism.api.opendatahub.com/v1
ODH_TIMEOUT=10
NOMINATIM_URL=https://nominatim.openstreetmap.org
NOMINATIM_USER_AGENT=odh-mcp-server/0.1 (opensource hospitality MCP)
```
---
## License
MIT
TDQS
Scored across 5 tools
Each tool targets a distinct operation: set_world establishes context, describe_world explains it, and the three find_* tools query for accommodations, events, and gastronomy separately. Even though they share parameters like near and radius_m, the entity types are clearly different, so an agent can easily select the right tool.
All tool names follow a consistent verb_noun pattern in snake_case: set_world, describe_world, find_accommodations, find_events, find_gastronomy. The verbs (set, describe, find) are clear and the nouns are specific, making the pattern predictable.
With 5 tools, the server is well-scoped for its purpose of exploring hospitality and tourism data in a world context. Each tool earns its place: world management (set, describe) and three distinct search types (accommodation, events, gastronomy). No redundancy or bloat.
The server's domain is read-only exploration of world-based hospitality data, and it covers the core workflow: set the world to establish context, understand what data is available via describe_world, and then search for the three primary POI types. There are no obvious dead ends, and all CRUD operations beyond reading are outside the stated purpose.