MCP Server
Moat ships an MCP server that exposes selected Moat APIs as tools, so an LLM client (Claude Code, Claude Desktop, or any other MCP client) can query Moat directly.
The server is moat/src/mcp_serve.py. It does not reimplement anything: it reads the
Flask app's OpenAPI document at runtime and turns each matching operation into an MCP
tool via FastMCP.from_openapi.
Which endpoints become tools
Only operations tagged mcp are exposed. Everything else is explicitly excluded, so
adding an API to Moat does not silently publish it to LLM clients.
To publish an endpoint, tag it in the flask-smorest blueprint:
@bp.route("", methods=["GET"])
@bp.doc(tags=["mcp"]) # <- this is what makes it an MCP tool
@bp.arguments(UserEntitlementQuerySchema, location="query")
@bp.response(200, PaginatedEntitlementSchema)
def list_user_entitlements(query_args):
...
The endpoint must be registered through the flask_smorest.Api object in app.py
(api.register_blueprint(...)), not flask_app.register_blueprint(...) - only the
former contributes to /openapi.json.
Currently exposed:
| Tool | Endpoint |
|---|---|
Paginated_list_of_user_entitlements |
GET /api/entitlements/v1/users |
Configuration
| Variable | Default | Purpose |
|---|---|---|
MOAT_BASE_URL |
http://127.0.0.1:8001 |
Moat instance the tools call |
MOAT_OPENAPI_URL |
$MOAT_BASE_URL/openapi.json |
Where the spec is read from |
MOAT_MCP_TAG |
mcp |
OpenAPI tag that marks an operation as a tool |
Starting the server
The Moat app must already be running - the MCP server fetches its OpenAPI document at startup and will fail immediately if Moat is unreachable.
# 1. start Moat (defaults assume port 8001)
export PYTHONPATH=moat/src
flask --app moat.src.app run --port 8001
# 2. start the MCP server over stdio, in a second shell
export PYTHONPATH=moat/src
export MOAT_BASE_URL=http://127.0.0.1:8001
python moat/src/mcp_serve.py
Running it by hand like this is mostly a smoke test - stdio transport expects a client
on the other end of the pipe. Normally the client launches the process itself.
Registering with Claude Code
claude mcp add moat \
--env PYTHONPATH=moat/src \
--env MOAT_BASE_URL=http://127.0.0.1:8001 \
-- .venv/bin/python moat/src/mcp_serve.py
Registering with Claude Desktop / any mcpServers config
{
"mcpServers": {
"moat": {
"command": ".venv/bin/python",
"args": ["moat/src/mcp_serve.py"],
"env": {
"PYTHONPATH": "moat/src",
"MOAT_BASE_URL": "http://127.0.0.1:8001"
}
}
}
}
HTTP transport
For a shared/remote deployment, run it over HTTP instead of stdio:
fastmcp run moat/src/mcp_serve.py:mcp --transport http --port 8002
Verifying it works
With Moat running, check the endpoint that backs the tool returns 200:
curl -s http://127.0.0.1:8001/api/entitlements/v1/users?page_size=2
# confirm the operation is tagged and present in the spec
curl -s http://127.0.0.1:8001/openapi.json | jq '.paths["/api/entitlements/v1/users"].get.tags'
Then list and call the tools through an MCP client:
PYTHONPATH=moat/src MOAT_BASE_URL=http://127.0.0.1:8001 python - <<'PY'
import asyncio
from fastmcp import Client
from mcp_serve import build_mcp
async def main():
async with Client(build_mcp()) as client:
tools = await client.list_tools()
print("tools:", [t.name for t in tools])
result = await client.call_tool(tools[0].name, {"page_size": 1})
print(result.content[0].text)
asyncio.run(main())
PY
Troubleshooting
httpx.ConnectError on startup - Moat is not running, or MOAT_BASE_URL points at
the wrong port. The MCP server reads the spec eagerly at import time.
No tools listed - nothing in the spec carries the mcp tag. Check the blueprint is
registered via api.register_blueprint and that @bp.doc(tags=["mcp"]) is present.
Tool returns a 500 - the tool is a thin proxy, so the fault is in the underlying
Moat endpoint. Call it with curl and read the Flask log.