mirror of
https://github.com/tiennm99/serena.git
synced 2026-08-14 14:27:01 +00:00
Add basic MCP server which can read files, updating README
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
The Serena Model Context Protocol (MCP) Server
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
import yaml
|
||||
from mcp.server.fastmcp.server import Context, FastMCP, Settings
|
||||
from sensai.util import logging
|
||||
|
||||
from multilspy import SyncLanguageServer
|
||||
from multilspy.multilspy_config import Language, MultilspyConfig
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SerenaMCPRequestContext:
|
||||
language_server: SyncLanguageServer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[SerenaMCPRequestContext]:
|
||||
"""Manage server startup and shutdown lifecycle."""
|
||||
argv = sys.argv[1:]
|
||||
if len(argv) != 1:
|
||||
print("\nUsage: mcp_server <.yml project file>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
project_file = argv[0]
|
||||
if not os.path.exists(project_file):
|
||||
print(f"Project file not found: {project_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# read project configuration
|
||||
with open(project_file, encoding="utf-8") as f:
|
||||
project = yaml.safe_load(f)
|
||||
language = Language(project["language"])
|
||||
project_root = project["project_root"]
|
||||
|
||||
# create and start the language server instance
|
||||
config = MultilspyConfig(code_language=language)
|
||||
logger = MultilspyLogger()
|
||||
language_server = SyncLanguageServer.create(config, logger, project_root)
|
||||
language_server.start()
|
||||
try:
|
||||
yield SerenaMCPRequestContext(language_server=language_server)
|
||||
finally:
|
||||
language_server.stop()
|
||||
|
||||
|
||||
mcp_settings = Settings(lifespan=server_lifespan)
|
||||
mcp = FastMCP(**mcp_settings.model_dump())
|
||||
|
||||
|
||||
class Tool:
|
||||
def __init__(self, ctx: Context):
|
||||
lifespan_context = cast(SerenaMCPRequestContext, ctx.request_context.lifespan_context)
|
||||
self.langsrv = lifespan_context.language_server
|
||||
|
||||
def execute(self) -> str:
|
||||
try:
|
||||
return self._execute()
|
||||
except Exception as e:
|
||||
log.error(f"Error executing tool: {e}")
|
||||
return f"Error executing tool: {e}"
|
||||
|
||||
@abstractmethod
|
||||
def _execute(self):
|
||||
pass
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def read_file(ctx: Context, relative_path: str) -> str:
|
||||
log.info(f"read_file: {relative_path=}")
|
||||
|
||||
class ReadFileTool(Tool):
|
||||
def _execute(self):
|
||||
with self.langsrv.open_file(relative_path):
|
||||
return self.langsrv.get_open_file_text(relative_path)
|
||||
|
||||
return ReadFileTool(ctx).execute()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, stream=sys.stderr, format="%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
||||
)
|
||||
mcp.run()
|
||||
Reference in New Issue
Block a user