Ver Fonte

Update docs for FastMCP transport

Lukas Goldschmidt há 1 mês atrás
pai
commit
5db5b8786e
6 ficheiros alterados com 87 adições e 70 exclusões
  1. 7 6
      PROJECT.md
  2. 12 37
      README.md
  3. 5 5
      mcp_tools.py
  4. 1 1
      run.sh
  5. 56 0
      server_fastmcp.py
  6. 6 21
      tests.py

+ 7 - 6
PROJECT.md

@@ -7,9 +7,11 @@ The current goal is a clean HTTP JSON-RPC 2.0 endpoint that works well with MCP
 
 ## Current interface
 
-- `GET /` → tool discovery JSON
-- `GET /health` → health and cache stats
-- `POST /mcp` → JSON-RPC 2.0 MCP transport
+- `GET /` → health + tool list
+- `GET /health` → health + cache stats
+- FastMCP SSE transport mounted at `GET /mcp/sse`
+
+Tool calls are performed via FastMCP’s message transport under `/mcp/messages/`.
 
 ## Tool set
 
@@ -21,9 +23,8 @@ The current goal is a clean HTTP JSON-RPC 2.0 endpoint that works well with MCP
 
 ## Notes
 
-- No SSE transport.
-- No event stream.
-- No URL-fetching helper tool.
+- Uses SSE transport (FastMCP) for MCP compatibility.
+- No extra custom event endpoints beyond FastMCP.
 - Keep the transport small and predictable.
 
 ## Verification

+ 12 - 37
README.md

@@ -1,14 +1,13 @@
 # Crypto MCP Server
 
-A small MCP-first server for crypto market data and technical indicators.
+FastMCP-based crypto tools server.
 
-## Transport
+## Transport (compliance-first)
 
-- **HTTP JSON-RPC 2.0** at `POST /mcp`
-- **Discovery** at `GET /`
-- **Health** at `GET /health`
-
-No SSE, no event stream, no extra REST API surface.
+- MCP SSE transport mounted at `/mcp`
+- SSE stream endpoint: `/mcp/sse`
+- message endpoint handled by FastMCP under `/mcp/messages/`
+- no legacy `/rpc` compatibility path
 
 ## Runtime
 
@@ -17,17 +16,7 @@ pip install -r requirements.txt
 ./run.sh
 ```
 
-Default URL:
-
-```bash
-http://127.0.0.1:8505/mcp
-```
-
-## MCP methods
-
-- `initialize`
-- `tools/list`
-- `tools/call`
+Default URL base: `http://127.0.0.1:8505`
 
 ## Tools
 
@@ -37,27 +26,13 @@ http://127.0.0.1:8505/mcp
 - `get_market_snapshot`
 - `get_top_movers`
 
+## Health
+
+- `GET /`
+- `GET /health`
+
 ## Tests
 
 ```bash
 ./tests.sh
 ```
-
-## Project layout
-
-```text
-crypto-mcp/
-├── main.py
-├── mcp_tools.py
-├── cache/
-├── indicators/
-├── providers/
-├── services/
-├── config.py
-├── errors.py
-├── run.sh
-├── killserver.sh
-├── restart.sh
-├── tests.py
-└── tests.sh
-```

+ 5 - 5
mcp_tools.py

@@ -1,9 +1,9 @@
 """MCP tool definitions."""
 
 MCP_TOOLS = [
-    {"name": "get_price", "description": "Get the current USD price of a cryptocurrency.", "parameters": {"type": "object", "properties": {"symbol": {"type": "string"}}, "required": ["symbol"]}},
-    {"name": "get_ohlcv", "description": "Get OHLCV candlestick data for a crypto asset.", "parameters": {"type": "object", "properties": {"symbol": {"type": "string"}, "timeframe": {"type": "string", "default": "1h"}, "limit": {"type": "integer", "default": 100}} ,"required": ["symbol"]}},
-    {"name": "get_indicator", "description": "Compute a technical indicator for a crypto asset.", "parameters": {"type": "object", "properties": {"symbol": {"type": "string"}, "indicator": {"type": "string"}, "timeframe": {"type": "string", "default": "1h"}, "params": {"type": "object", "default": {}}}, "required": ["symbol", "indicator"]}},
-    {"name": "get_market_snapshot", "description": "Get a compact market snapshot for a crypto asset.", "parameters": {"type": "object", "properties": {"symbol": {"type": "string"}}, "required": ["symbol"]}},
-    {"name": "get_top_movers", "description": "Get top gaining and losing crypto assets by 24h % change.", "parameters": {"type": "object", "properties": {"limit": {"type": "integer", "default": 10}}, "required": []}},
+    {"name": "get_price", "description": "Get the current USD price of a cryptocurrency.", "inputSchema": {"type": "object", "properties": {"symbol": {"type": "string"}}, "required": ["symbol"]}},
+    {"name": "get_ohlcv", "description": "Get OHLCV candlestick data for a crypto asset.", "inputSchema": {"type": "object", "properties": {"symbol": {"type": "string"}, "timeframe": {"type": "string", "default": "1h"}, "limit": {"type": "integer", "default": 100}}, "required": ["symbol"]}},
+    {"name": "get_indicator", "description": "Compute a technical indicator for a crypto asset.", "inputSchema": {"type": "object", "properties": {"symbol": {"type": "string"}, "indicator": {"type": "string"}, "timeframe": {"type": "string", "default": "1h"}, "params": {"type": "object", "default": {}}}, "required": ["symbol", "indicator"]}},
+    {"name": "get_market_snapshot", "description": "Get a compact market snapshot for a crypto asset.", "inputSchema": {"type": "object", "properties": {"symbol": {"type": "string"}}, "required": ["symbol"]}},
+    {"name": "get_top_movers", "description": "Get top gaining and losing crypto assets by 24h % change.", "inputSchema": {"type": "object", "properties": {"limit": {"type": "integer", "default": 10}}, "required": []}},
 ]

+ 1 - 1
run.sh

@@ -2,7 +2,7 @@
 set -euo pipefail
 
 PORT=${PORT:-8505}
-APP_MODULE=${APP_MODULE:-main:app}
+APP_MODULE=${APP_MODULE:-server_fastmcp:app}
 LOGFILE=${LOGFILE:-uvicorn.log}
 PIDFILE=${PIDFILE:-server.pid}
 

+ 56 - 0
server_fastmcp.py

@@ -0,0 +1,56 @@
+"""FastMCP transport wrapper for crypto tools (compliance-first)."""
+
+from fastapi import FastAPI
+from mcp.server.fastmcp import FastMCP
+from mcp.server.transport_security import TransportSecuritySettings
+
+import services
+from mcp_tools import MCP_TOOLS
+from cache import get_cache_stats
+
+
+mcp = FastMCP(
+    "crypto-mcp",
+    transport_security=TransportSecuritySettings(
+        enable_dns_rebinding_protection=False,
+    ),
+)
+
+
+@mcp.tool(description="Get the current USD price of a cryptocurrency.")
+async def get_price(symbol: str):
+    return await services.get_price(symbol)
+
+
+@mcp.tool(description="Get OHLCV candlestick data for a crypto asset.")
+async def get_ohlcv(symbol: str, timeframe: str = "1h", limit: int = 100):
+    return await services.get_ohlcv(symbol, timeframe, limit)
+
+
+@mcp.tool(description="Compute a technical indicator for a crypto asset.")
+async def get_indicator(symbol: str, indicator: str, timeframe: str = "1h", params: dict | None = None):
+    return await services.get_indicator(symbol, indicator, timeframe, params or {})
+
+
+@mcp.tool(description="Get a compact market snapshot for a crypto asset.")
+async def get_market_snapshot(symbol: str):
+    return await services.get_market_snapshot(symbol)
+
+
+@mcp.tool(description="Get top gaining and losing crypto assets by 24h % change.")
+async def get_top_movers(limit: int = 10):
+    return await services.get_top_movers(limit)
+
+
+app = FastAPI(title="Crypto MCP Server")
+app.mount("/mcp", mcp.sse_app())
+
+
+@app.get("/")
+def root():
+    return {"status": "ok", "transport": "fastmcp+sse", "mount": "/mcp", "tools": [t["name"] for t in MCP_TOOLS]}
+
+
+@app.get("/health")
+def health():
+    return {"status": "ok", "cache": get_cache_stats(), "tools": [t["name"] for t in MCP_TOOLS]}

+ 6 - 21
tests.py

@@ -9,7 +9,7 @@ sys.path.insert(0, os.path.dirname(__file__))
 import pytest
 import time
 from fastapi.testclient import TestClient
-from main import app
+from server_fastmcp import app
 from indicators import rsi, ema, macd, sma, compute_indicator
 from cache import TTLCache
 from errors import InsufficientDataError, UnsupportedIndicatorError
@@ -203,8 +203,8 @@ class TestHTTPMCP:
         resp = client.get("/")
         assert resp.status_code == 200
         body = resp.json()
-        assert body["jsonrpc"] == "2.0"
-        assert "tools" in body["result"]
+        assert body["status"] == "ok"
+        assert "tools" in body
 
     def test_health(self):
         resp = client.get("/health")
@@ -213,25 +213,10 @@ class TestHTTPMCP:
         assert body["status"] == "ok"
         assert "cache" in body
 
-    def test_initialize_rpc(self):
-        resp = client.post(
-            "/mcp",
-            json={"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"clientInfo": {"name": "pytest", "version": "1.0"}}},
-        )
-        assert resp.status_code == 200
-        body = resp.json()
-        assert body["jsonrpc"] == "2.0"
-        assert body["id"] == 1
-        assert "sessionId" in body["result"]
-
-    def test_tools_list_rpc(self):
-        resp = client.post(
-            "/mcp",
-            json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
-        )
+    def test_mcp_mount_path(self):
+        resp = client.get("/")
         assert resp.status_code == 200
-        body = resp.json()
-        assert len(body["result"]["tools"]) >= 1
+        assert resp.json().get("mount") == "/mcp"
 
 
 if __name__ == "__main__":