Skip to content

Commit ff0b0aa

Browse files
cursoragentbenjibc
andcommitted
Refactor tool handling and deprecate mcp_agent main for better compatibility
Co-authored-by: bchen <bchen@fireworks.ai>
1 parent ed1dc59 commit ff0b0aa

File tree

5 files changed

+52
-291
lines changed

5 files changed

+52
-291
lines changed

eval_protocol/agent/orchestrator.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,29 @@
1616
# Determine OpenAI availability without importing symbols for typing
1717
OPENAI_AVAILABLE = _importlib_util.find_spec("openai") is not None
1818

19+
# Expose AsyncOpenAI/OpenAI at module level for tests/patching, even if we import lazily elsewhere
20+
if OPENAI_AVAILABLE:
21+
try:
22+
from openai import AsyncOpenAI as AsyncOpenAI, OpenAI as OpenAI # type: ignore[import-not-found]
23+
except Exception:
24+
25+
class AsyncOpenAI: # type: ignore[no-redef]
26+
def __init__(self, **_: Any) -> None:
27+
pass
28+
29+
class OpenAI: # type: ignore[no-redef]
30+
def __init__(self, **_: Any) -> None:
31+
pass
32+
else:
33+
34+
class AsyncOpenAI: # type: ignore[no-redef]
35+
def __init__(self, **_: Any) -> None:
36+
pass
37+
38+
class OpenAI: # type: ignore[no-redef]
39+
def __init__(self, **_: Any) -> None:
40+
pass
41+
1942

2043
# Max steps for the inner loop within a single user turn
2144
MAX_STEPS_PER_USER_TURN = 10

eval_protocol/mcp/mcp_multi_client.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from contextlib import AsyncExitStack
44
from dataclasses import dataclass
55
from typing import Any, Dict, List, Optional, Union
6+
from types import SimpleNamespace
67

78
from dotenv import load_dotenv
89
from mcp import ClientSession, StdioServerParameters
@@ -134,11 +135,11 @@ async def get_available_tools(self) -> List[Dict[str, Any]]:
134135
all_tools.append(
135136
{
136137
"type": "function",
137-
"function": {
138-
"name": tool.name,
139-
"description": tool.description,
140-
"parameters": tool.inputSchema,
141-
},
138+
"function": SimpleNamespace(
139+
name=tool.name,
140+
description=tool.description,
141+
parameters=tool.inputSchema,
142+
),
142143
}
143144
)
144145
except Exception as e:

eval_protocol/mcp_agent/main.py

Lines changed: 4 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -1,213 +1,17 @@
1-
import asyncio
2-
import logging
3-
import signal
4-
from contextlib import asynccontextmanager
5-
from typing import Optional
6-
71
import click
8-
import uvicorn
9-
import yaml
10-
from mcp.server.streamable_http_manager import ( # MCP SDK component
11-
StreamableHTTPSessionManager,
12-
)
13-
from starlette.applications import Starlette
14-
from starlette.routing import Mount, Route # Import Mount
15-
16-
from eval_protocol.mcp_agent.config import AppConfig
17-
from eval_protocol.mcp_agent.intermediary_server import RewardKitIntermediaryServer
18-
19-
logger = logging.getLogger(__name__)
20-
21-
# Global server instance to be managed by signal handlers
22-
# This will now be the Uvicorn server instance.
23-
_uvicorn_server_instance_ref: Optional[uvicorn.Server] = None # Keep a global ref if needed for signals
24-
# Keep a reference to our MCP server for lifespan management
25-
_mcp_server_instance_ref: Optional[RewardKitIntermediaryServer] = None
26-
# _session_manager_ref is not needed globally if lifespan_wrapper handles it.
27-
28-
29-
# Custom app_lifespan is no longer needed if StreamableHTTPSessionManager.lifespan_wrapper is used.
30-
31-
32-
async def main_async(config_path: str, host: str, port: int):
33-
"""
34-
Asynchronous main function to load config, set up the ASGI application,
35-
and run it with Uvicorn.
36-
"""
37-
global _uvicorn_server_instance_ref, _mcp_server_instance_ref # _session_manager_ref removed from globals
38-
try:
39-
with open(config_path, "r") as f:
40-
raw_config = yaml.safe_load(f)
41-
app_config = AppConfig(**raw_config)
42-
except FileNotFoundError:
43-
logger.error(f"Configuration file not found: {config_path}")
44-
return
45-
except yaml.YAMLError as e:
46-
logger.error(f"Error parsing YAML configuration file {config_path}: {e}")
47-
return
48-
except Exception as e:
49-
logger.error(f"Error loading or validating AppConfig from {config_path}: {e}")
50-
return
51-
52-
# Configure logging early
53-
server_root_log_level_str = app_config.log_level.upper()
54-
server_root_log_level = getattr(logging, server_root_log_level_str, logging.INFO)
55-
56-
logging.basicConfig(
57-
level=server_root_log_level, # Root logger for the server process
58-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
59-
datefmt="%Y-%m-%d %H:%M:%S", # Added datefmt for consistency
60-
)
61-
logger.info(f"Configuration loaded from {config_path}. Server root log level set to {server_root_log_level_str}.")
62-
63-
# Ensure eval_protocol.mcp_agent namespace respects this level
64-
rk_mcp_agent_logger = logging.getLogger("eval_protocol.mcp_agent")
65-
rk_mcp_agent_logger.setLevel(server_root_log_level)
66-
67-
# Be very explicit for the intermediary_server logger as well
68-
intermediary_server_logger = logging.getLogger("eval_protocol.mcp_agent.intermediary_server")
69-
intermediary_server_logger.setLevel(server_root_log_level)
70-
# Also ensure its handlers respect this level
71-
for handler in intermediary_server_logger.handlers:
72-
handler.setLevel(server_root_log_level)
73-
# If it's propagating to the 'eval_protocol.mcp_agent' parent, ensure that parent's handlers are also correct.
74-
# The parent rk_mcp_agent_logger already had its level set.
75-
76-
# Quiet down other noisy libraries for the server unless server itself is in DEBUG mode
77-
if server_root_log_level > logging.DEBUG: # e.g. if INFO or WARNING
78-
libraries_to_quiet = [
79-
"httpx",
80-
"mcp",
81-
"uvicorn",
82-
"starlette",
83-
"asyncio",
84-
"hpack",
85-
"httpcore",
86-
]
87-
for lib_name in libraries_to_quiet:
88-
logging.getLogger(lib_name).setLevel(logging.WARNING)
89-
90-
logger.info(
91-
f"Log level for 'eval_protocol.mcp_agent' namespace set to {logging.getLevelName(logging.getLogger('eval_protocol.mcp_agent').getEffectiveLevel())}"
92-
)
93-
94-
# 1. Instantiate RewardKitIntermediaryServer
95-
_mcp_server_instance_ref = RewardKitIntermediaryServer(
96-
app_config=app_config
97-
) # Store globally for lifespan_wrapper
98-
99-
# 2. Instantiate StreamableHTTPSessionManager
100-
# Pass the internal _mcp_server (the MCPServer instance) from our FastMCP subclass
101-
if _mcp_server_instance_ref is None:
102-
logger.error("Failed to initialize RewardKitIntermediaryServer")
103-
return
104-
105-
session_manager = StreamableHTTPSessionManager(
106-
app=_mcp_server_instance_ref._mcp_server, # type: ignore[attr-defined]
107-
event_store=None,
108-
json_response=True, # Changed to True
109-
)
110-
111-
# 3. Create Starlette app, using session_manager.lifespan_wrapper
112-
# This wrapper should handle the startup/shutdown of both the session_manager's task group
113-
# and the underlying _mcp_server_instance_ref.
114-
routes = [
115-
Mount("/mcp", app=session_manager.handle_request),
116-
]
117-
118-
# The lifespan_wrapper approach was incorrect as the method doesn't exist.
119-
# We will now use a custom lifespan for the MCPServer and run Uvicorn
120-
# within the context of session_manager.run() if it's an async context manager.
121-
122-
@asynccontextmanager
123-
async def mcp_server_lifespan_only(app_for_lifespan: Starlette):
124-
# This lifespan only manages the _mcp_server_instance_ref
125-
if _mcp_server_instance_ref:
126-
logger.info("MCP Server Lifespan: Starting up RewardKitIntermediaryServer...")
127-
await _mcp_server_instance_ref.startup()
128-
logger.info("MCP Server Lifespan: RewardKitIntermediaryServer startup complete.")
129-
yield
130-
if _mcp_server_instance_ref:
131-
logger.info("MCP Server Lifespan: Shutting down RewardKitIntermediaryServer...")
132-
await _mcp_server_instance_ref.shutdown()
133-
logger.info("MCP Server Lifespan: RewardKitIntermediaryServer shutdown complete.")
134-
135-
routes = [
136-
Mount("/mcp", app=session_manager.handle_request),
137-
]
138-
starlette_app = Starlette(routes=routes, lifespan=mcp_server_lifespan_only)
139-
140-
# 4. Configure Uvicorn
141-
config = uvicorn.Config(
142-
app=starlette_app, # Starlette app with its own lifespan for MCPServer
143-
host=host,
144-
port=port,
145-
log_level=app_config.log_level.lower(),
146-
log_config=None, # Prevent Uvicorn from overriding our basicConfig for app loggers
147-
)
148-
uvicorn_server = uvicorn.Server(config)
149-
_uvicorn_server_instance_ref = uvicorn_server
150-
151-
logger.info(f"Starting RewardKit Intermediary MCP Server on {host}:{port}/mcp.")
152-
153-
try:
154-
if hasattr(session_manager, "run"):
155-
# Call run() to get the potential context manager
156-
sm_context_manager = session_manager.run()
157-
if hasattr(sm_context_manager, "__aenter__") and hasattr(sm_context_manager, "__aexit__"):
158-
logger.info(
159-
"Attempting to run Uvicorn server within context returned by StreamableHTTPSessionManager.run()..."
160-
)
161-
async with sm_context_manager: # type: ignore
162-
logger.info("Context from StreamableHTTPSessionManager.run() entered. Serving Uvicorn...")
163-
await uvicorn_server.serve()
164-
else:
165-
logger.error(
166-
"Object returned by StreamableHTTPSessionManager.run() is not an async context manager. Falling back to direct Uvicorn serve."
167-
)
168-
await uvicorn_server.serve()
169-
else:
170-
logger.error(
171-
"StreamableHTTPSessionManager does not have a 'run' method. Falling back to direct Uvicorn serve."
172-
)
173-
await uvicorn_server.serve()
174-
175-
except asyncio.CancelledError:
176-
logger.info("Server operation cancelled (main_async level).")
177-
except Exception as e:
178-
logger.error(
179-
f"An error occurred during server operation (main_async level): {e}",
180-
exc_info=True,
181-
)
182-
finally:
183-
logger.info("Uvicorn server has shut down (main_async finally).")
184-
185-
186-
# Signal handling is now primarily managed by Uvicorn.
187-
# If we needed custom logic *before* Uvicorn handles signals, it would be more complex.
188-
# For now, relying on Uvicorn's graceful shutdown which triggers the ASGI lifespan.
1892

1903

1914
@click.command()
1925
@click.option(
1936
"--config",
1947
"config_path",
1958
default="mcp_agent_config.yaml",
196-
help="Path to the YAML configuration file for the MCP agent server.",
197-
type=click.Path(exists=True, dir_okay=False),
9+
help="(deprecated) path to MCP agent config",
19810
)
199-
@click.option("--host", default="0.0.0.0", help="Host for the server to listen on.")
200-
@click.option("--port", default=8001, type=int, help="Port for the server to listen on.")
11+
@click.option("--host", default="0.0.0.0")
12+
@click.option("--port", default=8001, type=int)
20113
def main_cli(config_path: str, host: str, port: int):
202-
"""
203-
CLI entry point to run the RewardKit Intermediary MCP Server using Uvicorn.
204-
"""
205-
try:
206-
asyncio.run(main_async(config_path, host, port))
207-
except KeyboardInterrupt: # This will be caught by Uvicorn first usually
208-
logger.info("CLI interrupted by KeyboardInterrupt. Uvicorn should handle shutdown.")
209-
finally:
210-
logger.info("MCP Agent Server CLI finished.")
14+
click.echo("eval_protocol.mcp_agent.main is deprecated and disabled.")
21115

21216

21317
if __name__ == "__main__":

eval_protocol/mcp_agent/session.py

Lines changed: 0 additions & 81 deletions
This file was deleted.

0 commit comments

Comments
 (0)