client-py connected
This commit is contained in:
@@ -18,12 +18,20 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
import uvicorn
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.server.sse import SseServerTransport
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Route, Mount
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from sse_starlette import EventSourceResponse
|
||||
|
||||
from dexorder import EventPublisher, start_lifecycle_manager, get_lifecycle_manager
|
||||
from dexorder.events import EventType, UserEvent, DeliverySpec
|
||||
from dexorder.api.workspace_tools import get_workspace_store
|
||||
from dexorder.api.category_tools import get_category_manager
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -68,6 +76,9 @@ class Config:
|
||||
|
||||
# MCP server settings
|
||||
self.mcp_server_name: str = os.getenv("MCP_SERVER_NAME", "dexorder-user")
|
||||
self.mcp_transport: str = os.getenv("MCP_TRANSPORT", "sse") # "stdio" or "sse"
|
||||
self.mcp_http_port: int = int(os.getenv("MCP_HTTP_PORT", "3000"))
|
||||
self.mcp_http_host: str = os.getenv("MCP_HTTP_HOST", "0.0.0.0")
|
||||
|
||||
# Lifecycle settings
|
||||
self.idle_timeout_minutes: int = int(os.getenv("IDLE_TIMEOUT_MINUTES", "15"))
|
||||
@@ -134,6 +145,10 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
workspace_store = get_workspace_store(config.workspace_dir)
|
||||
logging.info(f"Workspace store initialized at {config.workspace_dir}")
|
||||
|
||||
# Initialize category file manager
|
||||
category_manager = get_category_manager(config.data_dir)
|
||||
logging.info(f"Category manager initialized at {config.data_dir}")
|
||||
|
||||
@server.list_resources()
|
||||
async def list_resources():
|
||||
"""List available resources"""
|
||||
@@ -143,6 +158,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"name": "Hello World",
|
||||
"description": "A simple hello world resource",
|
||||
"mimeType": "text/plain",
|
||||
"annotations": {
|
||||
"agent_accessible": True, # Available to agent for ad-hoc queries
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -170,7 +188,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools():
|
||||
"""List available tools including workspace tools"""
|
||||
"""List available tools including workspace and category tools"""
|
||||
return [
|
||||
{
|
||||
"name": "workspace_read",
|
||||
@@ -184,6 +202,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
},
|
||||
"required": ["store_name"]
|
||||
},
|
||||
"annotations": {
|
||||
"agent_accessible": True, # Agent can read workspace stores
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -201,6 +222,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
},
|
||||
"required": ["store_name", "data"]
|
||||
},
|
||||
"annotations": {
|
||||
"agent_accessible": True, # Agent can write workspace stores
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -228,13 +252,124 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
},
|
||||
"required": ["store_name", "patch"]
|
||||
},
|
||||
"annotations": {
|
||||
"agent_accessible": True, # Agent can patch workspace stores
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "category_write",
|
||||
"description": "Write a new strategy, indicator, or research script with validation",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": ["strategy", "indicator", "research"],
|
||||
"description": "Category of the script"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name (can contain special characters)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "LLM-generated description of what this does (required)"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Python implementation code"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Optional category-specific metadata (e.g., default_length for indicators, data_feeds for strategies)"
|
||||
}
|
||||
},
|
||||
"required": ["category", "name", "description", "code"]
|
||||
},
|
||||
"annotations": {
|
||||
"agent_accessible": True,
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "category_edit",
|
||||
"description": "Edit an existing category script (updates code, description, or metadata)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": ["strategy", "indicator", "research"],
|
||||
"description": "Category of the script"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the existing item"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Updated Python code (optional, omit to keep existing)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Updated description (optional, omit to keep existing)"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Updated metadata fields (optional)"
|
||||
}
|
||||
},
|
||||
"required": ["category", "name"]
|
||||
},
|
||||
"annotations": {
|
||||
"agent_accessible": True,
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "category_read",
|
||||
"description": "Read a category script and its metadata",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": ["strategy", "indicator", "research"],
|
||||
"description": "Category of the script"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the item"
|
||||
}
|
||||
},
|
||||
"required": ["category", "name"]
|
||||
},
|
||||
"annotations": {
|
||||
"agent_accessible": True,
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "category_list",
|
||||
"description": "List all items in a category with names and descriptions",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": ["strategy", "indicator", "research"],
|
||||
"description": "Category to list"
|
||||
}
|
||||
},
|
||||
"required": ["category"]
|
||||
},
|
||||
"annotations": {
|
||||
"agent_accessible": True,
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_tool_call(name: str, arguments: dict):
|
||||
"""Handle tool calls including workspace tools"""
|
||||
"""Handle tool calls including workspace and category tools"""
|
||||
if name == "workspace_read":
|
||||
return workspace_store.read(arguments.get("store_name", ""))
|
||||
elif name == "workspace_write":
|
||||
@@ -247,13 +382,78 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
arguments.get("store_name", ""),
|
||||
arguments.get("patch", [])
|
||||
)
|
||||
elif name == "category_write":
|
||||
return category_manager.write(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", ""),
|
||||
description=arguments.get("description", ""),
|
||||
code=arguments.get("code", ""),
|
||||
metadata=arguments.get("metadata")
|
||||
)
|
||||
elif name == "category_edit":
|
||||
return category_manager.edit(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", ""),
|
||||
code=arguments.get("code"),
|
||||
description=arguments.get("description"),
|
||||
metadata=arguments.get("metadata")
|
||||
)
|
||||
elif name == "category_read":
|
||||
return category_manager.read(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", "")
|
||||
)
|
||||
elif name == "category_list":
|
||||
return category_manager.list_items(
|
||||
category=arguments.get("category", "")
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
logging.info(f"MCP server '{config.mcp_server_name}' created with workspace tools")
|
||||
logging.info(f"MCP server '{config.mcp_server_name}' created with workspace and category tools")
|
||||
return server
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SSE Transport Setup
|
||||
# =============================================================================
|
||||
|
||||
def create_sse_app(mcp_server: Server) -> Starlette:
|
||||
"""Create Starlette app with SSE endpoint for MCP"""
|
||||
|
||||
# Create SSE transport instance
|
||||
sse = SseServerTransport("/messages/")
|
||||
|
||||
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()
|
||||
|
||||
async def handle_health(request: Request) -> Response:
|
||||
"""Health check endpoint for k8s probes and gateway readiness checks"""
|
||||
return Response(
|
||||
content='{"status":"ok"}',
|
||||
media_type="application/json"
|
||||
)
|
||||
|
||||
app = Starlette(
|
||||
routes=[
|
||||
Route("/sse", handle_sse),
|
||||
Mount("/messages/", app=sse.handle_post_message),
|
||||
Route("/health", handle_health),
|
||||
]
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main Application
|
||||
# =============================================================================
|
||||
@@ -347,17 +547,34 @@ class UserContainer:
|
||||
logging.info("User container stopped")
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Run the MCP server via stdio"""
|
||||
"""Run the MCP server with configured transport"""
|
||||
await self.start()
|
||||
|
||||
try:
|
||||
# Run MCP server on stdio
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await self.mcp_server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
self.mcp_server.create_initialization_options()
|
||||
if self.config.mcp_transport == "stdio":
|
||||
# Run MCP server on stdio (for dev/testing)
|
||||
logging.info("Starting MCP server with stdio transport")
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await self.mcp_server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
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)
|
||||
config = uvicorn.Config(
|
||||
app,
|
||||
host=self.config.mcp_http_host,
|
||||
port=self.config.mcp_http_port,
|
||||
log_level=os.getenv("LOG_LEVEL", "info").lower(),
|
||||
access_log=True,
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
await server.serve()
|
||||
else:
|
||||
raise ValueError(f"Unknown MCP transport: {self.config.mcp_transport}")
|
||||
finally:
|
||||
await self.stop()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user