Allow starting mcp as sse server, using click for args parsing

This commit is contained in:
Michael Panchenko
2025-04-17 11:48:46 +02:00
parent c6d0fb9f37
commit 30643228d0
2 changed files with 83 additions and 27 deletions
+2 -2
View File
@@ -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
View File
@@ -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)