"""MCP server for code structure analysis."""
import os
from typing import Any
from mcp.server import Server
from mcp.types import Tool, TextContent
from ..config import get_language_from_extension, is_supported_language
from ..extractors.structure import StructureExtractor
from ..formatters.markdown import MarkdownFormatter
from ..models import AnalysisResult
from ..parsers.tree_sitter import get_parser_manager
# Create MCP server
app = Server("tree-sitter-code-structure")
# Initialize components
parser_manager = get_parser_manager()
structure_extractor = StructureExtractor(parser_manager)
markdown_formatter = MarkdownFormatter()
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List available tools."""
return [
Tool(
name="analyze_code_structure",
description=(
"Analyze the structure of one or more source code files and return classes, functions, "
"their line numbers, nesting levels, parameters, and return types. "
"Supports Python, JavaScript, TypeScript, Java, C#, and Go. "
"Accepts either a single file path (string) or an array of file paths."
),
inputSchema={
"type": "object",
"properties": {
"file_path": {
"oneOf": [
{
"type": "string",
"description": "Path to a single source code file to analyze",
},
{
"type": "array",
"items": {
"type": "string",
"description": "Path to a source code file to analyze",
},
"description": "Array of paths to source code files to analyze",
},
],
"description": "Path(s) to the source code file(s) to analyze",
},
"include_docstrings": {
"type": "boolean",
"description": "Whether to include docstrings in the output (default: false)",
"default": False,
},
},
"required": ["file_path"],
},
),
]
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
"""Handle tool calls."""
if name == "analyze_code_structure":
return await analyze_code_structure(**arguments)
else:
raise ValueError(f"Unknown tool: {name}")
async def _analyze_single_file(
file_path: str, include_docstrings: bool = False
) -> AnalysisResult:
"""Analyze code structure of a single file."""
# Resolve file path relative to workspace
if not os.path.isabs(file_path):
# Try to resolve relative to current directory
file_path = os.path.abspath(file_path)
# Check if file exists
if not os.path.exists(file_path):
result = AnalysisResult(
success=False,
language="unknown",
file_path=file_path,
errors=[],
stats={},
)
result.markdown = f"# Error\n\nFile not found: `{file_path}`"
return result
# Detect language from file extension
language = get_language_from_extension(file_path)
if not language or not is_supported_language(language):
result = AnalysisResult(
success=False,
language="unknown",
file_path=file_path,
errors=[],
stats={},
)
result.markdown = f"# Error\n\nUnsupported file type: `{file_path}`\n\nSupported languages: Python (.py), JavaScript (.js), TypeScript (.ts, .tsx), Java (.java), C# (.cs), Go (.go)"
return result
# Read file content
try:
with open(file_path, "rb") as f:
source_code = f.read()
except Exception as e:
result = AnalysisResult(
success=False,
language=language,
file_path=file_path,
errors=[],
stats={},
)
result.markdown = (
f"# Error\n\nFailed to read file: `{file_path}`\n\nError: {str(e)}"
)
return result
# Parse with tree-sitter
tree = parser_manager.parse(source_code, language)
if tree is None:
result = AnalysisResult(
success=False,
language=language,
file_path=file_path,
errors=[],
stats={},
)
result.markdown = (
f"# Error\n\nFailed to initialize parser for language: `{language}`"
)
return result
# Extract structure
structure = structure_extractor.extract_structure(
tree, source_code, language, file_path, include_docstrings
)
# Format as markdown
markdown = markdown_formatter.format_structure(structure)
# Build result
result = AnalysisResult(
success=True,
language=language,
file_path=file_path,
structure=structure,
markdown=markdown,
errors=structure.errors,
stats={
"total_classes": structure.total_classes,
"total_functions": structure.total_functions,
"max_nesting_depth": structure.max_nesting_depth,
"parse_errors": len(structure.errors),
},
)
return result
async def analyze_code_structure(
file_path: str | list[str], include_docstrings: bool = False
) -> list[TextContent]:
"""Analyze code structure of one or more files."""
# Handle single file path (string)
if isinstance(file_path, str):
result = await _analyze_single_file(file_path, include_docstrings)
return [TextContent(type="text", text=result.markdown)]
# Handle multiple file paths (array)
results = []
for path in file_path:
result = await _analyze_single_file(path, include_docstrings)
results.append(result)
# Format multi-file output
markdown = markdown_formatter.format_multi_file_structure(results)
return [TextContent(type="text", text=markdown)]
async def main() -> None:
"""Run the MCP server."""
from mcp.server.stdio import stdio_server
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options(),
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())