HexagonML ModelManager MCP Server
by hexsupport
README.md
# HexagonML ModelManager MCP Server
A modular MCP server for interacting with the HexagonML ModelManager API, with full tool discoverability and clean architecture.
## Architecture
```
server/
├── config.py # Environment configuration and server setup
├── auth.py # Per-caller ModelManager credential resolution
├── clients.py # ModelManager API client factory and context
├── health.py # HTTP /health endpoint
├── main.py # Main entry point (standalone mode)
├── server_simple.py # FastMCP CLI entry point
├── tools/
│ ├── model_tools.py # Model management tools
│ ├── usecase_tools.py # Usecase management tools
│ ├── modelcard_tools.py # Model card management tools
│ ├── forecasting_tools.py # Forecasting tools
│ └── forecasting_governance_tools.py # Governance report tools
├── handlers/
│ ├── response_handlers.py # API response classification and formatting
│ └── prompt_handler.py # MCP prompt templates
└── utils/
├── helpers.py # Response handling and validation helpers
├── validators.py # Payload validation functions
└── base.py # Base classes and common patterns
```
## Available Tools
### Model Management
| Tool | Description |
|---|---|
| `add_model` | Upload a new ML model |
| `update_model` | Update model metadata or configuration |
| `delete_model` | Permanently delete a model |
| `get_latest_metrics` | Retrieve model performance metrics |
### Usecase Management
| Tool | Description |
|---|---|
| `add_usecase` | Create a new usecase (supports Forecasting type) |
| `update_usecase` | Update usecase configuration |
| `delete_usecase` | Permanently delete a usecase |
| `get_usecase_data` | List all usecases |
### Model Cards
| Tool | Description |
|---|---|
| `create_modelcard` | Generate a model card for a model |
| `get_modelcard_data` | Retrieve model card data with optional filtering |
### Forecasting
| Tool | Description |
|---|---|
| `get_forecast` | Retrieve forecasts for a usecase |
### MCP Prompts
| Prompt | Description |
|---|---|
| `forecast_presentation_guide` | Instructs the LLM how to narrate forecast results to users |
## Configuration
Copy `.env.example` to `.env` and fill in your values:
```bash
cp .env.example .env
```
| Variable | Required | Default | Description |
|---|---|---|---|
| `MM_API_BASE_URL` | Yes | — | Base URL of the ModelManager API |
| `OUTPUT_DIR` | Yes | — | Directory for generated output files |
| `MM_API_KEY` | No | — | Fallback ModelManager credential, used only when the calling client sends none. Set it for single-user servers; leave it unset for shared deployments (see [Authentication](#authentication)) |
| `HOST` | No | `0.0.0.0` | Network interface the server listens on |
| `PORT` | No | `9000` | Port the server listens on |
> **Docker note:** `MM_API_BASE_URL=http://127.0.0.1:8000` will not resolve from inside a container. Use `http://host.docker.internal:8000` on Mac/Windows, or `http://172.17.0.1:8000` on Linux. In Kubernetes use the in-cluster Service DNS name, e.g. `http://mm.tai-mm-ws.svc.cluster.local:8000`.
## Authentication
Every ModelManager call this server makes is authenticated with an mmgr3 credential. *Whose* credential depends on how you deploy:
| Deployment | `MM_API_KEY` | Who ModelManager sees |
|---|---|---|
| **Shared** — one HTTP server, many people | **unset** | Each caller, via the key they send on the request |
| **Single-user** — local stdio, CI job | set | The account that owns `MM_API_KEY` |
A caller-supplied credential always wins; `MM_API_KEY` applies only when the caller sent none.
Callers pass their credential in either header:
```
Authorization: Api-Key <your-personal-key> # preferred
X-MM-API-Key: <your-personal-key> # when a proxy already uses Authorization
```
`Authorization: Bearer <jwt>` also works, since mmgr3 accepts JWTs on the same endpoints — but a JWT expires after 60 minutes, so personal API keys are the right choice for MCP clients. Any other scheme is ignored rather than forwarded.
Users generate their own key on the **MCP Endpoints** page (`/mcp`) in ModelManager; it is displayed exactly once. Keys are named, revocable, and optionally expiring, so each client can be revoked independently.
> **Don't set `MM_API_KEY` on a shared deployment.** It becomes an anonymous fallback identity that any caller who can reach the port inherits, which defeats per-user attribution and revocation. The published image excludes `.env`, so a deployed container is passthrough-only unless you explicitly set the variable.
A tool call with no usable credential is rejected before the tool runs, with a message naming the header to send.
## Running the Server
### Option 1: FastMCP CLI (recommended)
```bash
fastmcp run server/server_simple.py --transport http --host 0.0.0.0 --port 9000
```
### Option 2: Standalone Python
```bash
python server/main.py
```
### Option 3: Development / Inspector
```bash
fastmcp dev server/server_simple.py
```
### Option 4: Docker Compose
```bash
# Build and start
docker compose up --build
# Run in background
docker compose up --build -d
# Stop
docker compose down
```
### Option 5: Docker (manual)
```bash
# Build
docker build -t hex-mm-mcp:latest .
# Run
docker run -d --name hex-mm-mcp \
-p 9000:9000 \
--env-file .env \
hex-mm-mcp:latest
```
## Connecting an MCP client
Two things are needed in every case: the server's **URL** (its MCP endpoint is `/mcp`) and **your own ModelManager API key** from the `/mcp` page in ModelManager.
### A deployed server (the normal case)
Point the client at the deployed URL and send your key as a header. This is the shared-deployment shape — everyone uses the same URL with their own key.
**Claude Code** — one command:
```bash
claude mcp add --transport http hex-mm-mcp https://mcp.example.com/mcp \
--header "Authorization: Api-Key ${MM_API_KEY}"
```
**Claude Code / VS Code / Cursor** — or by config file (`.mcp.json` in a project, or the client's own settings):
```json
{
"mcpServers": {
"hex-mm-mcp": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Api-Key YOUR_PERSONAL_KEY"
}
}
}
}
```
**Claude Desktop** and other clients that only launch local commands — bridge to the remote server with `mcp-remote`:
```json
{
"mcpServers": {
"hex-mm-mcp": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://mcp.example.com/mcp",
"--header", "Authorization:Api-Key YOUR_PERSONAL_KEY"
]
}
}
}
```
Replace `https://mcp.example.com/mcp` with your deployment's URL and `YOUR_PERSONAL_KEY` with the key you generated. Prefer your client's secret/environment-variable support over pasting the key into a file that gets committed.
Verify the endpoint is reachable before wiring up a client:
```bash
curl https://mcp.example.com/health
# {"status":"healthy","service":"hexagonml-modelmanager-mcp"}
```
### A local server you run yourself
Single-user, so the key can live in the server's environment (`MM_API_KEY` in `.env`) instead of in each client's config:
```json
{
"mcpServers": {
"hex-mm-mcp": {
"command": "fastmcp",
"args": [
"run",
"/path/to/hex-mm-mcp/server/server_simple.py",
"--transport", "http",
"--host", "127.0.0.1",
"--port", "9000"
]
}
}
}
```
### Local Docker
```json
{
"mcpServers": {
"hex-mm-mcp": {
"command": "docker",
"args": [
"run", "--rm", "-i", "--network=host",
"-e", "MM_API_KEY",
"-e", "MM_API_BASE_URL",
"-e", "OUTPUT_DIR",
"hex-mm-mcp:latest"
],
"env": {
"MM_API_KEY": "your-api-key",
"MM_API_BASE_URL": "http://host.docker.internal:8000",
"OUTPUT_DIR": "/tmp/mm-output"
}
}
}
}
```
## LLM Presentation Guidance
The `get_forecast` tool uses two complementary mechanisms to guide how the LLM presents forecast data to users.
### Embedded `_llm_instructions`
Every `get_forecast` response includes a `_llm_instructions` field. The LLM reads this automatically as part of the tool result and follows the rules when composing its reply — no client configuration needed.
Key rules embedded in every response:
- Lead with the summary (date range, value range min/max/avg)
- Explain `lower_bound`/`upper_bound` as a confidence interval
- Reference `last_actual_update` to indicate data freshness
- Flag `resolved: false` as an unresolvable request
- Avoid echoing raw JSON back to the user
### MCP Prompt (`forecast_presentation_guide`)
A named MCP prompt is also registered on the server. MCP-aware clients (Claude Desktop, MCP Inspector) can fetch it and inject it as a system prompt at the start of a conversation, priming the LLM for the whole session rather than per-call.
To use it in a client that supports MCP prompts:
```
prompts/get → forecast_presentation_guide
```
| Mechanism | When it fires | Client support needed |
|---|---|---|
| `_llm_instructions` | Every `get_forecast` call | None — automatic |
| `forecast_presentation_guide` | On explicit prompt request | MCP prompt-capable client |
## Development
### Tests
```bash
uv run --with pytest python -m pytest tests/ -q
```
`tests/conftest.py` puts `server/` on `sys.path`, so tests import the modules the
same way fastmcp runs them (`from config import mcp`).
CI ([.github/workflows/mcp-ci.yml](.github/workflows/mcp-ci.yml)) gates every push
on those tests plus a check that the server still imports and registers its tools,
then publishes `modelmanagerdev/mcp:<git-sha>` — an immutable tag, so a deployment
always traces back to an exact commit.
### Adding a New Tool
1. Create a new module (e.g., `server/new_tools.py`)
2. Import `mcp` from `config` and use the `@mcp.tool` decorator
3. Import the module in `server/server_simple.py`
```python
# server/new_tools.py
from fastmcp import Context
from config import mcp
from clients import get_mm_client
from utils import safe_response_to_dict, create_error_response
import asyncio
@mcp.tool(
name="new_tool",
description="Description of the tool",
tags={"category", "modelmanager"},
meta={"version": "1.0", "author": "HexagonML"},
)
async def new_tool(ctx: Context, required_param: str, optional_param: str = None) -> dict:
"""Tool docstring shown in MCP playgrounds."""
if not required_param or not required_param.strip():
return create_error_response(
message="required_param is required",
error_type="ValidationError"
)
await ctx.info("Starting operation")
await ctx.report_progress(progress=20, total=100)
try:
client = get_mm_client(ctx, 'client_type')
await ctx.report_progress(progress=40, total=100)
response = await asyncio.to_thread(client.api_method, required_param)
await ctx.report_progress(progress=80, total=100)
result = safe_response_to_dict(response)
await ctx.info("Operation completed")
await ctx.report_progress(progress=100, total=100)
return result
except ValueError as e:
await ctx.error(f"Validation error: {str(e)}")
return create_error_response(
message="A validation error occurred. Please check your input.",
error_type="ValidationError"
)
except Exception as e:
await ctx.error(f"Operation failed: {str(e)}")
return create_error_response(
message="An internal error occurred. Please try again.",
error_type="InternalError"
)
```
## Troubleshooting
**Port already in use**
```bash
kill -9 $(lsof -t -i:9000)
# or use a different port
fastmcp run server/server_simple.py --port 9001
```
**Missing environment variables**
The server exits at startup with a message listing which of `MM_API_BASE_URL` and `OUTPUT_DIR` are missing. `MM_API_KEY` is optional and its absence is normal for a shared deployment.
**"No ModelManager credential for this request"**
The calling client sent no credential and the server has no `MM_API_KEY` fallback. Add `Authorization: Api-Key <key>` to the client's config (see [Connecting an MCP client](#connecting-an-mcp-client)).
**`401` from ModelManager on every tool call**
The credential reached ModelManager but was rejected — the key is revoked, expired, or mistyped. Check its status badge on the `/mcp` page in ModelManager and generate a new one if needed. Note the scheme is `Api-Key`, not `Bearer`.
**Connection refused / API errors**
- Confirm the ModelManager API is reachable at `MM_API_BASE_URL`
- When running in Docker, see the Docker note in the Configuration section above
- Verify the key in use is a valid, non-expired key issued from ModelManager's `/mcp` page and that its owning account can access the use case
## License
MIT License — see the LICENSE file for details.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues