data fixes, partial custom indicator support
This commit is contained in:
384
sandbox/main.py
384
sandbox/main.py
@@ -10,6 +10,7 @@ Brings together:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
@@ -20,8 +21,8 @@ from typing import Optional
|
||||
import uvicorn
|
||||
import yaml
|
||||
from mcp.server import Server
|
||||
from mcp.server.sse import SseServerTransport
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
||||
from mcp.types import Tool, TextContent, ImageContent
|
||||
from starlette.applications import Starlette
|
||||
from starlette.requests import Request
|
||||
@@ -34,8 +35,11 @@ from dexorder.conda_manager import sync_packages, install_packages
|
||||
from dexorder.events import EventType, UserEvent, DeliverySpec
|
||||
from dexorder.impl.charting_api_impl import ChartingAPIImpl
|
||||
from dexorder.impl.data_api_impl import DataAPIImpl
|
||||
from dexorder.tools.category_tools import get_category_manager
|
||||
from dexorder.tools.python_tools import get_category_manager
|
||||
from dexorder.tools.workspace_tools import get_workspace_store
|
||||
from dexorder.tools.evaluate_indicator import evaluate_indicator
|
||||
from dexorder.tools.backtest_strategy import backtest_strategy
|
||||
from dexorder.tools.activate_strategy import activate_strategy, deactivate_strategy, list_active_strategies
|
||||
|
||||
# =============================================================================
|
||||
# Global Data Directory
|
||||
@@ -249,7 +253,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="category_write",
|
||||
name="python_write",
|
||||
description="Write a new strategy, indicator, or research script with validation",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -273,15 +277,27 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Optional category-specific metadata (e.g., default_length for indicators, data_feeds for strategies)"
|
||||
"description": (
|
||||
"Optional category-specific metadata. "
|
||||
"For strategy: include 'data_feeds' (list of {symbol, period_seconds, description}) "
|
||||
"and 'parameters' (object mapping param_name → {default, description}). "
|
||||
"Example: {\"data_feeds\": [{\"symbol\": \"BTC/USDT.BINANCE\", \"period_seconds\": 3600, \"description\": \"Primary BTC/USDT hourly feed\"}], "
|
||||
"\"parameters\": {\"rsi_length\": {\"default\": 14, \"description\": \"RSI lookback period\"}, \"threshold\": {\"default\": 70, \"description\": \"Overbought level\"}}}. "
|
||||
"For indicator: include 'default_length' (int). "
|
||||
"For any category: 'conda_packages' (list of package names) if extra dependencies are needed."
|
||||
)
|
||||
}
|
||||
},
|
||||
"required": ["category", "name", "description", "code"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="category_edit",
|
||||
description="Edit an existing category script (updates code, description, or metadata)",
|
||||
name="python_edit",
|
||||
description=(
|
||||
"Edit an existing category script. "
|
||||
"Use 'patches' for targeted string replacements (preferred for small changes), "
|
||||
"or 'code' to replace the full implementation. Do not supply both."
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -296,7 +312,24 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Updated Python code (optional, omit to keep existing)"
|
||||
"description": "Full replacement Python code. Use only when rewriting the entire implementation; prefer 'patches' for targeted edits."
|
||||
},
|
||||
"patches": {
|
||||
"type": "array",
|
||||
"description": (
|
||||
"Targeted code edits as old/new string pairs. Preferred over 'code' for small changes. "
|
||||
"Each patch: {\"old_string\": \"exact text to find\", \"new_string\": \"replacement text\"}. "
|
||||
"old_string must be unique in the file (add surrounding context if needed). "
|
||||
"Patches are applied in order."
|
||||
),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"old_string": {"type": "string"},
|
||||
"new_string": {"type": "string"}
|
||||
},
|
||||
"required": ["old_string", "new_string"]
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
@@ -304,14 +337,20 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Updated metadata fields (optional)"
|
||||
"description": (
|
||||
"Updated metadata fields (optional). "
|
||||
"For strategy: 'data_feeds' (list of {symbol, period_seconds, description}) "
|
||||
"and/or 'parameters' (object mapping param_name → {default, description}). "
|
||||
"For indicator: 'default_length' (int). "
|
||||
"For any category: 'conda_packages' (list of package names)."
|
||||
)
|
||||
}
|
||||
},
|
||||
"required": ["category", "name"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="category_read",
|
||||
name="python_read",
|
||||
description="Read a category script and its metadata",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -330,7 +369,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="category_list",
|
||||
name="python_list",
|
||||
description="List all items in a category with names and descriptions",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -344,6 +383,53 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"required": ["category"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_log",
|
||||
description="Show git commit history for category items. Filter by category and/or name to see history for a specific item.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": ["strategy", "indicator", "research"],
|
||||
"description": "Filter to this category (optional)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Filter to this item (optional, requires category)"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max commits to return (default 20)",
|
||||
"default": 20
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_revert",
|
||||
description="Restore a category item to a previous git revision. Creates a new commit — non-destructive.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "string",
|
||||
"description": "Git commit hash (full or short) to restore to"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": ["strategy", "indicator", "research"],
|
||||
"description": "Category of the item"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the item to restore"
|
||||
}
|
||||
},
|
||||
"required": ["revision", "category", "name"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="conda_sync",
|
||||
description="Sync conda packages: scan all metadata, remove unused packages (excluding base environment)",
|
||||
@@ -381,13 +467,179 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
)
|
||||
),
|
||||
Tool(
|
||||
name="evaluate_indicator",
|
||||
description=(
|
||||
"Evaluate a pandas-ta indicator against real OHLC data and return a structured "
|
||||
"array of timestamped values. Use this to validate that an indicator computes "
|
||||
"correctly before adding it to the workspace, or to inspect its output values."
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"symbol": {
|
||||
"type": "string",
|
||||
"description": "Market symbol in 'MARKET.EXCHANGE' format, e.g. 'BTC/USDT.BINANCE'"
|
||||
},
|
||||
"from_time": {
|
||||
"description": "Start of time range. Unix timestamp (int) or date string e.g. '30 days ago', '2024-01-01'"
|
||||
},
|
||||
"to_time": {
|
||||
"description": "End of time range. Unix timestamp (int) or date string e.g. 'now', '2024-03-01'"
|
||||
},
|
||||
"period_seconds": {
|
||||
"type": "integer",
|
||||
"description": "Candle period in seconds (e.g. 3600 for 1h, 900 for 15m, 86400 for 1d)",
|
||||
"default": 3600
|
||||
},
|
||||
"pandas_ta_name": {
|
||||
"type": "string",
|
||||
"description": "Lowercase pandas-ta function name, e.g. 'rsi', 'macd', 'bbands'"
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"description": "pandas-ta keyword arguments, e.g. {\"length\": 14} or {\"fast\": 12, \"slow\": 26, \"signal\": 9}",
|
||||
"default": {}
|
||||
}
|
||||
},
|
||||
"required": ["symbol", "from_time", "to_time", "pandas_ta_name"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="backtest_strategy",
|
||||
description=(
|
||||
"Run a saved trading strategy against historical OHLC data using Nautilus Trader "
|
||||
"BacktestEngine. Returns performance metrics (total return, Sharpe ratio, "
|
||||
"max drawdown, win rate, trade count) and a full equity curve. "
|
||||
"Supports multiple data feeds and includes order-flow fields (buy_vol, sell_vol, "
|
||||
"open_interest) in the strategy's DataFrame."
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"strategy_name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the strategy as saved via python_write"
|
||||
},
|
||||
"feeds": {
|
||||
"type": "array",
|
||||
"description": "Data feeds to backtest against",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"symbol": {
|
||||
"type": "string",
|
||||
"description": "Market symbol in 'MARKET.EXCHANGE' format, e.g. 'BTC/USDT.BINANCE'"
|
||||
},
|
||||
"period_seconds": {
|
||||
"type": "integer",
|
||||
"description": "Candle period in seconds (e.g. 3600 for 1h)",
|
||||
"default": 3600
|
||||
}
|
||||
},
|
||||
"required": ["symbol"]
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"from_time": {
|
||||
"description": "Backtest start. Unix timestamp or date string e.g. '2024-01-01', '90 days ago'"
|
||||
},
|
||||
"to_time": {
|
||||
"description": "Backtest end. Unix timestamp or date string e.g. '2025-01-01', 'now'"
|
||||
},
|
||||
"initial_capital": {
|
||||
"type": "number",
|
||||
"description": "Starting capital in quote currency (e.g. 10000.0 USDT)",
|
||||
"default": 10000.0
|
||||
},
|
||||
"paper": {
|
||||
"type": "boolean",
|
||||
"description": "Always true for historical backtest (reserved for forward testing)",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": ["strategy_name", "feeds", "from_time", "to_time"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="activate_strategy",
|
||||
description=(
|
||||
"Activate a strategy for paper or live forward trading with a capital allocation. "
|
||||
"paper=true (default): simulated fills on live data — no API keys required. "
|
||||
"paper=false: real execution via user secrets vault (not yet implemented). "
|
||||
"Note: live data streaming is TBD; this registers the strategy for when it becomes available."
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"strategy_name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the strategy as saved via python_write"
|
||||
},
|
||||
"feeds": {
|
||||
"type": "array",
|
||||
"description": "Data feeds for the strategy",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"symbol": {"type": "string"},
|
||||
"period_seconds": {"type": "integer", "default": 3600}
|
||||
},
|
||||
"required": ["symbol"]
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"allocation": {
|
||||
"type": "number",
|
||||
"description": "Capital allocated in quote currency (e.g. 5000.0 USDT)"
|
||||
},
|
||||
"paper": {
|
||||
"type": "boolean",
|
||||
"description": "True = paper/simulated (default); False = live execution",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": ["strategy_name", "feeds", "allocation"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="deactivate_strategy",
|
||||
description="Stop an active strategy and return its final P&L summary.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"strategy_name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the active strategy to stop"
|
||||
}
|
||||
},
|
||||
"required": ["strategy_name"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="list_active_strategies",
|
||||
description="List all currently active (live or paper) strategies and their status.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_tool_call(name: str, arguments: dict):
|
||||
"""Handle tool calls including workspace and category tools"""
|
||||
get_lifecycle_manager().record_activity()
|
||||
try:
|
||||
return await _handle_tool_call_inner(name, arguments)
|
||||
except Exception:
|
||||
logging.exception("Unhandled exception in tool '%s'", name)
|
||||
raise
|
||||
|
||||
async def _handle_tool_call_inner(name: str, arguments: dict):
|
||||
if name == "workspace_read":
|
||||
return workspace_store.read(arguments.get("store_name", ""))
|
||||
elif name == "workspace_write":
|
||||
@@ -400,7 +652,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
arguments.get("store_name", ""),
|
||||
arguments.get("patch", [])
|
||||
)
|
||||
elif name == "category_write":
|
||||
elif name == "python_write":
|
||||
result = category_manager.write(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", ""),
|
||||
@@ -410,6 +662,8 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
)
|
||||
content = []
|
||||
meta_parts = [f"success: {result['success']}", f"path: {result['path']}"]
|
||||
if result.get("revision"):
|
||||
meta_parts.append(f"revision: {result['revision']}")
|
||||
if result.get("validation") and not result["validation"].get("success"):
|
||||
meta_parts.append(f"validation errors: {result['validation'].get('errors', [])}")
|
||||
content.append(TextContent(type="text", text="\n".join(meta_parts)))
|
||||
@@ -417,20 +671,23 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
exec_content = result["execution"].get("content", [])
|
||||
content.extend(exec_content)
|
||||
image_count = sum(1 for item in exec_content if item.type == "image")
|
||||
logging.info(f"category_write '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
logging.info(f"python_write '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
else:
|
||||
logging.info(f"category_write '{arguments.get('name')}': no execution result (category={arguments.get('category')})")
|
||||
logging.info(f"python_write '{arguments.get('name')}': no execution result (category={arguments.get('category')})")
|
||||
return content
|
||||
elif name == "category_edit":
|
||||
elif name == "python_edit":
|
||||
result = category_manager.edit(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", ""),
|
||||
code=arguments.get("code"),
|
||||
patches=arguments.get("patches"),
|
||||
description=arguments.get("description"),
|
||||
metadata=arguments.get("metadata")
|
||||
)
|
||||
content = []
|
||||
meta_parts = [f"success: {result['success']}", f"path: {result['path']}"]
|
||||
if result.get("revision"):
|
||||
meta_parts.append(f"revision: {result['revision']}")
|
||||
if result.get("validation") and not result["validation"].get("success"):
|
||||
meta_parts.append(f"validation errors: {result['validation'].get('errors', [])}")
|
||||
content.append(TextContent(type="text", text="\n".join(meta_parts)))
|
||||
@@ -438,19 +695,43 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
exec_content = result["execution"].get("content", [])
|
||||
content.extend(exec_content)
|
||||
image_count = sum(1 for item in exec_content if item.type == "image")
|
||||
logging.info(f"category_edit '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
logging.info(f"python_edit '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
else:
|
||||
logging.info(f"category_edit '{arguments.get('name')}': no execution result")
|
||||
logging.info(f"python_edit '{arguments.get('name')}': no execution result")
|
||||
return content
|
||||
elif name == "category_read":
|
||||
elif name == "python_read":
|
||||
return category_manager.read(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", "")
|
||||
)
|
||||
elif name == "category_list":
|
||||
elif name == "python_list":
|
||||
return category_manager.list_items(
|
||||
category=arguments.get("category", "")
|
||||
)
|
||||
elif name == "python_log":
|
||||
result = category_manager.git_log(
|
||||
category=arguments.get("category"),
|
||||
name=arguments.get("name"),
|
||||
limit=int(arguments.get("limit", 20))
|
||||
)
|
||||
lines = [f"success: {result['success']}"]
|
||||
for c in result.get("commits", []):
|
||||
lines.append(f"{c['short_hash']} {c['date'][:10]} {c['message']}")
|
||||
return [TextContent(type="text", text="\n".join(lines))]
|
||||
elif name == "python_revert":
|
||||
result = category_manager.git_revert(
|
||||
revision=arguments.get("revision", ""),
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", "")
|
||||
)
|
||||
meta_parts = [f"success: {result['success']}"]
|
||||
if result.get("revision"):
|
||||
meta_parts.append(f"revision: {result['revision']}")
|
||||
if result.get("error"):
|
||||
meta_parts.append(f"error: {result['error']}")
|
||||
if result.get("validation") and not result["validation"].get("success"):
|
||||
meta_parts.append(f"validation errors: {result['validation'].get('errors', [])}")
|
||||
return [TextContent(type="text", text="\n".join(meta_parts))]
|
||||
elif name == "conda_sync":
|
||||
# Get environment.yml path relative to main.py
|
||||
env_yml = Path(__file__).parent / "environment.yml"
|
||||
@@ -469,6 +750,37 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
image_count = sum(1 for item in content if item.type == "image")
|
||||
logging.info(f"execute_research '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
return content
|
||||
elif name == "evaluate_indicator":
|
||||
return await evaluate_indicator(
|
||||
symbol=arguments.get("symbol", ""),
|
||||
from_time=arguments.get("from_time"),
|
||||
to_time=arguments.get("to_time"),
|
||||
period_seconds=int(arguments.get("period_seconds", 3600)),
|
||||
pandas_ta_name=arguments.get("pandas_ta_name", ""),
|
||||
parameters=arguments.get("parameters") or {},
|
||||
)
|
||||
elif name == "backtest_strategy":
|
||||
return await backtest_strategy(
|
||||
strategy_name=arguments.get("strategy_name", ""),
|
||||
feeds=arguments.get("feeds", []),
|
||||
from_time=arguments.get("from_time"),
|
||||
to_time=arguments.get("to_time"),
|
||||
initial_capital=float(arguments.get("initial_capital", 10_000.0)),
|
||||
paper=bool(arguments.get("paper", True)),
|
||||
)
|
||||
elif name == "activate_strategy":
|
||||
return await activate_strategy(
|
||||
strategy_name=arguments.get("strategy_name", ""),
|
||||
feeds=arguments.get("feeds", []),
|
||||
allocation=float(arguments.get("allocation", 0.0)),
|
||||
paper=bool(arguments.get("paper", True)),
|
||||
)
|
||||
elif name == "deactivate_strategy":
|
||||
return await deactivate_strategy(
|
||||
strategy_name=arguments.get("strategy_name", ""),
|
||||
)
|
||||
elif name == "list_active_strategies":
|
||||
return await list_active_strategies()
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
@@ -477,26 +789,18 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SSE Transport Setup
|
||||
# Streamable HTTP Transport Setup
|
||||
# =============================================================================
|
||||
|
||||
def create_sse_app(mcp_server: Server) -> Starlette:
|
||||
"""Create Starlette app with SSE endpoint for MCP"""
|
||||
def create_streamable_http_app(mcp_server: Server) -> Starlette:
|
||||
"""Create Starlette app with Streamable HTTP endpoint for MCP"""
|
||||
|
||||
# Create SSE transport instance
|
||||
sse = SseServerTransport("/messages/")
|
||||
session_manager = StreamableHTTPSessionManager(app=mcp_server)
|
||||
|
||||
async def handle_sse(request: Request) -> Response:
|
||||
"""Handle SSE connections for MCP"""
|
||||
async with sse.connect_sse(
|
||||
request.scope, request.receive, request._send
|
||||
) as streams:
|
||||
await mcp_server.run(
|
||||
streams[0],
|
||||
streams[1],
|
||||
mcp_server.create_initialization_options()
|
||||
)
|
||||
return Response()
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(app: Starlette):
|
||||
async with session_manager.run():
|
||||
yield
|
||||
|
||||
async def handle_health(request: Request) -> Response:
|
||||
"""Health check endpoint for k8s probes and gateway readiness checks"""
|
||||
@@ -506,9 +810,9 @@ def create_sse_app(mcp_server: Server) -> Starlette:
|
||||
)
|
||||
|
||||
app = Starlette(
|
||||
lifespan=lifespan,
|
||||
routes=[
|
||||
Route("/sse", handle_sse),
|
||||
Mount("/messages/", app=sse.handle_post_message),
|
||||
Mount("/mcp", app=session_manager.handle_request),
|
||||
Route("/health", handle_health),
|
||||
]
|
||||
)
|
||||
@@ -648,9 +952,9 @@ class UserContainer:
|
||||
self.mcp_server.create_initialization_options()
|
||||
)
|
||||
elif self.config.mcp_transport == "sse":
|
||||
# Run MCP server via HTTP/SSE (for production)
|
||||
logging.info(f"Starting MCP server with SSE transport on {self.config.mcp_http_host}:{self.config.mcp_http_port}")
|
||||
app = create_sse_app(self.mcp_server)
|
||||
# Run MCP server via Streamable HTTP (for production)
|
||||
logging.info(f"Starting MCP server with Streamable HTTP transport on {self.config.mcp_http_host}:{self.config.mcp_http_port}")
|
||||
app = create_streamable_http_app(self.mcp_server)
|
||||
config = uvicorn.Config(
|
||||
app,
|
||||
host=self.config.mcp_http_host,
|
||||
|
||||
Reference in New Issue
Block a user