mirror of
https://github.com/tiennm99/serena.git
synced 2026-09-05 02:20:17 +00:00
Allow starting mcp as sse server, using click for args parsing
This commit is contained in:
@@ -176,13 +176,13 @@ want to use Serena.
|
||||
"mcpServers": {
|
||||
"serena": {
|
||||
"command": "/abs/path/to/uv",
|
||||
"args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server", "/abs/path/to/myproject.yml"]
|
||||
"args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server", "--project-file", "/abs/path/to/myproject.yml"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:info: The path to the project file is optional if you have set `enable_project_activation` in your configuration,
|
||||
:info: passing the project file is optional if you have set `enable_project_activation` in your configuration,
|
||||
as this setting will allow you to simply instruct Claude to activate the project you want to work on.
|
||||
|
||||
If you are using paths containing backslashes for paths on Windows
|
||||
|
||||
+81
-25
@@ -7,7 +7,9 @@ from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from logging import Formatter, Logger, StreamHandler
|
||||
from typing import Literal
|
||||
|
||||
import click # Add click import
|
||||
from mcp.server.fastmcp import server
|
||||
from mcp.server.fastmcp.server import FastMCP, Settings
|
||||
from mcp.server.fastmcp.tools.base import Tool as MCPTool
|
||||
@@ -68,30 +70,16 @@ def make_tool(
|
||||
)
|
||||
|
||||
|
||||
def create_mcp_server() -> FastMCP:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
if (len(argv) == 1 and argv[0] == "--help") or len(argv) > 1:
|
||||
print("\nUsage: mcp_server [.yml project file]", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
def create_mcp_server(project_file_path: str | None, host: str = "0.0.0.0", port: int = 8000) -> FastMCP:
|
||||
"""
|
||||
Create an MCP server.
|
||||
|
||||
:param project_file_path: The path to the project file, or None.
|
||||
:param host: The host to bind to
|
||||
:param port: The port to bind to
|
||||
"""
|
||||
mcp: FastMCP | None = None
|
||||
|
||||
def update_tools() -> None:
|
||||
"""Update the tools in the MCP server."""
|
||||
# Tools may change as a result of project activation.
|
||||
# NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not,
|
||||
# unfortunately, query for changed tools. It only queries for changed resources and prompts regularly,
|
||||
# so we need to register all tools at startup, unfortunately.
|
||||
nonlocal mcp
|
||||
tools = agent.get_exposed_tools()
|
||||
if mcp is not None:
|
||||
mcp._tool_manager._tools = {}
|
||||
for tool in tools:
|
||||
# noinspection PyProtectedMember
|
||||
mcp._tool_manager._tools[tool.get_name()] = make_tool(tool)
|
||||
|
||||
project_file_path = argv[0] if len(argv) == 1 else None
|
||||
try:
|
||||
agent = SerenaAgent(
|
||||
project_file_path,
|
||||
@@ -102,14 +90,27 @@ def create_mcp_server() -> FastMCP:
|
||||
show_fatal_exception_safe(e)
|
||||
raise
|
||||
|
||||
def update_tools() -> None:
|
||||
"""Update the tools in the MCP server."""
|
||||
# Tools may change as a result of project activation.
|
||||
# NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not,
|
||||
# unfortunately, query for changed tools. It only queries for changed resources and prompts regularly,
|
||||
# so we need to register all tools at startup, unfortunately.
|
||||
nonlocal mcp, agent
|
||||
tools = agent.get_exposed_tools()
|
||||
if mcp is not None:
|
||||
mcp._tool_manager._tools = {}
|
||||
for tool in tools:
|
||||
# noinspection PyProtectedMember
|
||||
mcp._tool_manager._tools[tool.get_name()] = make_tool(tool)
|
||||
|
||||
@asynccontextmanager
|
||||
async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]:
|
||||
"""Manage server startup and shutdown lifecycle."""
|
||||
nonlocal agent
|
||||
mark_used(mcp_server)
|
||||
yield
|
||||
|
||||
mcp_settings = Settings(lifespan=server_lifespan)
|
||||
mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port)
|
||||
mcp = FastMCP(**mcp_settings.model_dump())
|
||||
|
||||
update_tools()
|
||||
@@ -117,5 +118,60 @@ def create_mcp_server() -> FastMCP:
|
||||
return mcp
|
||||
|
||||
|
||||
def start_mcp_server() -> None:
|
||||
create_mcp_server().run()
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--project-file",
|
||||
"project_file_opt", # Rename to avoid conflict with argument
|
||||
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
||||
default=None,
|
||||
help="Optional path to the .yml project file via option."
|
||||
"Does not need to be provided at startup since you can activate a project later by simply asking the agent to do so "
|
||||
"(there is a dedicated tool for this purpose).",
|
||||
)
|
||||
@click.argument(
|
||||
"project_file_arg",
|
||||
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
||||
required=False,
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"--transport",
|
||||
type=click.Choice(["stdio", "sse"]),
|
||||
default="stdio",
|
||||
show_default=True,
|
||||
help="Transport protocol.",
|
||||
)
|
||||
@click.option(
|
||||
"--host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
show_default=True,
|
||||
help="Host to bind to (for SSE transport).",
|
||||
)
|
||||
@click.option(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
show_default=True,
|
||||
help="Port to bind to (for SSE transport).",
|
||||
)
|
||||
def start_mcp_server(
|
||||
project_file_opt: str | None, project_file_arg: str | None, transport: Literal["stdio", "sse"], host: str, port: int
|
||||
) -> None:
|
||||
"""Starts the Serena MCP server.
|
||||
|
||||
Accepts the project file path either via the --project-file option or as a positional argument.
|
||||
"""
|
||||
# Prioritize the positional argument if provided
|
||||
# This is for backward compatibility with the old CLI, should be removed in the future!
|
||||
project_file = project_file_arg if project_file_arg is not None else project_file_opt
|
||||
mcp_server = create_mcp_server(project_file_path=project_file, host=host, port=port)
|
||||
|
||||
# log after server creation such that the log appears in the GUI
|
||||
if project_file_arg is not None:
|
||||
log.warning(
|
||||
"The positional argument for the project file path is deprecated and will be removed in the future!"
|
||||
"Please pass the project file path via the `--project-file` option instead.\n"
|
||||
f"Used path: {project_file}"
|
||||
)
|
||||
mcp_server.run(transport=transport)
|
||||
|
||||
Reference in New Issue
Block a user