Full code for the demo here
./target/debug/praxis --config /tmp/mcp-gateway-two-backends.yaml &
cd <directory with the script at the bottom>
source .venv/bin/activate
MCP_SERVER_NAME=weather uvicorn dev_mcp:app --host 127.0.0.1 --port 8001 &
MCP_SERVER_NAME=calendar uvicorn dev_mcp:app --host 127.0.0.1 --port 8002 &
# Start Praxis gateway (mcp-gateway-two-backends.yaml config pasted below)
./target/debug/praxis --config /tmp/mcp-gateway-two-backends.yaml &
# =================================================================
# Initialize
curl -s http://127.0.0.1:18090/mcp \
-H 'content-type: application/json' -H 'accept: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'
# Initialize Output
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {
"listChanged": false
}
},
"serverInfo": {
"name": "praxis-mcp-gateway",
"version": "0.1.0"
}
}
}
# =================================================================
# List tools (aggregated, prefixed)
curl -s http://127.0.0.1:18090/mcp \
-H 'content-type: application/json' -H 'accept: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# List tools output
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"description": "Search",
"name": "weather_search"
},
{
"description": "Get weather",
"name": "weather_get_weather"
},
{
"description": "Search",
"name": "cal_search"
},
{
"description": "Create event",
"name": "cal_create_event"
}
]
}
}
# =================================================================
# Call weather tool (routes to port 8001, prefix stripped)
curl -s http://127.0.0.1:18090/mcp \
-H 'content-type: application/json' -H 'accept: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"weather_get_weather","arguments":{"city":"London"}}}'
# Call weather tool output
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"server\": \"weather\",\n \"tool\": \"get_weather\",\n \"city\": \"London\",\n \"temp\": 72\n}"
}
],
"structuredContent": {
"server": "weather",
"tool": "get_weather",
"city": "London",
"temp": 72
},
"isError": false
}
}
# =================================================================
# Call calendar tool (routes to port 8002, prefix stripped)
curl -s http://127.0.0.1:18090/mcp \
-H 'content-type: application/json' -H 'accept: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"cal_create_event","arguments":{"title":"standup"}}}'
# Call calendar tool output
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"server\": \"calendar\",\n \"tool\": \"create_event\",\n \"title\": \"standup\",\n \"when\": \"now\"\n}"
}
],
"structuredContent": {
"server": "calendar",
"tool": "create_event",
"title": "standup",
"when": "now"
},
"isError": false
}
}
# =================================================================
# Ping
curl -s http://127.0.0.1:18090/mcp \
-H 'content-type: application/json' -H 'accept: application/json' \
-d '{"jsonrpc":"2.0","id":5,"method":"ping"}'
# Ping output
{
"jsonrpc": "2.0",
"id": 5,
"result": {}
}
listeners:
- name: mcp-gateway
address: "0.0.0.0:18090"
filter_chains: [main]
filter_chains:
- name: main
filters:
- filter: mcp_gateway
path: /mcp
max_body_bytes: 65536
servers:
- name: weather
cluster: weather-mcp
path: /mcp
tool_prefix: "weather_"
tools:
- name: search
description: "Search"
- name: get_weather
description: "Get weather"
- name: calendar
cluster: calendar-mcp
path: /mcp
tool_prefix: "cal_"
tools:
- name: search
description: "Search"
- name: create_event
description: "Create event"
- filter: load_balancer
clusters:
- name: weather-mcp
endpoints:
- "127.0.0.1:8001"
- name: calendar-mcp
endpoints:
- "127.0.0.1:8002""""Quick Streamable HTTP MCP dev server for Praxis validation.
Location:
~/praxxis/agentic/dev_mcp.py
Install:
cd ~/praxxis/agentic
python -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]" uvicorn starlette
Start one stateless JSON-response backend:
MCP_SERVER_NAME=weather uvicorn dev_mcp:app --host 127.0.0.1 --port 8001
Start two stateless backends for mcp_gateway multi-backend tests:
MCP_SERVER_NAME=weather uvicorn dev_mcp:app --host 127.0.0.1 --port 8001
MCP_SERVER_NAME=calendar uvicorn dev_mcp:app --host 127.0.0.1 --port 8002
Start a stateful backend for PR 5 lazy-backend-session testing:
MCP_STATEFUL=1 MCP_SERVER_NAME=weather uvicorn dev_mcp:app --host 127.0.0.1 --port 8001
Endpoint:
http://127.0.0.1:<port>/mcp
Direct smoke test:
curl -s http://127.0.0.1:8001/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Praxis gateway test shape:
- Configure weather backend as 127.0.0.1:8001, path /mcp, prefix weather_.
- Configure calendar backend as 127.0.0.1:8002, path /mcp, prefix cal_.
- tools/list should expose weather_search, weather_get_weather, cal_search,
and cal_create_event.
- tools/call weather_search should reach the weather process and the backend
should see unprefixed params.name == "search".
- tools/call cal_create_event should reach the calendar process and the
backend should see unprefixed params.name == "create_event".
"""
from __future__ import annotations
import contextlib
import os
from typing import Any
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount
SERVER_NAME = os.getenv("MCP_SERVER_NAME", "weather")
STATEFUL = os.getenv("MCP_STATEFUL", "0") == "1"
mcp = FastMCP(
f"Praxis Dev MCP - {SERVER_NAME}",
stateless_http=not STATEFUL,
json_response=True,
)
@mcp.tool()
def search(query: str) -> dict[str, Any]:
"""Search test tool."""
return {
"server": SERVER_NAME,
"tool": "search",
"query": query,
}
@mcp.tool()
def get_weather(city: str = "Raleigh") -> dict[str, Any]:
"""Weather test tool."""
return {
"server": SERVER_NAME,
"tool": "get_weather",
"city": city,
"temp": 72,
}
@mcp.tool()
def create_event(title: str, when: str = "now") -> dict[str, Any]:
"""Calendar test tool."""
return {
"server": SERVER_NAME,
"tool": "create_event",
"title": title,
"when": when,
}
@contextlib.asynccontextmanager
async def lifespan(app: Starlette):
async with mcp.session_manager.run():
yield
app = Starlette(
routes=[Mount("/", app=mcp.streamable_http_app())],
lifespan=lifespan,
)