# Directory Structure ``` ├── .gitignore ├── LICENSE ├── README.md └── src └── spring_boot_mcp_converter.py ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. #uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Spring MCP Bridge    **Spring MCP Bridge** is a tool that automatically converts REST endpoints from Spring Boot applications into an MCP (Message Conversation Protocol) server, allowing AI assistants like Claude, Cursor, and other MCP-compatible tools to directly interact with your APIs. *It does not currently include authentication. If your Spring Boot API requires authentication, you will need to modify the handler code to include the appropriate headers or tokens. ## 📖 Overview Integrating existing APIs with AI assistants typically requires manual coding or complex configuration. Spring MCP Bridge eliminates this complexity by automatically scanning your Spring Boot project and generating a ready-to-use MCP server. ### ✨ Features - **Automatic Scanning**: Discovers all REST endpoints (@RestController, @GetMapping, etc.) - **Zero Configuration**: No modifications needed to existing Spring Boot code - **Model Preservation**: Maintains request and response models as MCP tools - **Javadoc Extraction**: Uses existing documentation to enhance MCP tool descriptions - **Complete Documentation**: Generates README and clear instructions for use ## 🚀 Installation ```bash # Clone the repository git clone https://github.com/brunosantos/spring-mcp-bridge.git # Enter the directory cd spring-mcp-bridge ``` ## 🛠️ Usage 1. **Scan your Spring Boot project**: ```bash python spring_boot_mcp_converter.py --project /path/to/spring-project --output ./mcp_server --name MyAPI ``` 2. **Run the generated MCP server**: ```bash cd mcp_server pip install -r requirements.txt python main.py ``` 3. **Connect via MCP client**: - Configure your MCP client (Claude, Cursor, etc.) to use `http://localhost:8000` - The MCP schema will be available at `http://localhost:8000/.well-known/mcp-schema.json` ## 📋 Arguments | Argument | Description | Default | |-------------|----------------------------------|--------------| | `--project` | Path to Spring Boot project | (required) | | `--output` | Output directory | ./mcp_server | | `--name` | Application name | SpringAPI | | `--debug` | Enable debug logging | False | ## 💻 Example ```bash python spring_mcp_bridge.py --project ~/projects/my-spring-api --output ./mcp_server --name MyAPI ``` The tool will: 1. Scan the Spring Boot project for REST controllers 2. Identify model types, parameters, and return types 3. Generate a fully functional MCP server in Python/FastAPI 4. Create a compatible MCP schema that describes all endpoints ## 🔄 How It Works 1. **Scanning**: Analyzes Java source code to extract metadata from REST endpoints 2. **Conversion**: Converts Java types to JSON Schema types for MCP compatibility 3. **Generation**: Creates a FastAPI application that maps MCP calls to Spring REST calls 4. **Forwarding**: Routes requests from MCP client to the Spring Boot application ## 🔍 Spring Boot Features Supported - REST Controllers (`@RestController`, `@Controller`) - HTTP Methods (`@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`) - Request Parameters (`@RequestParam`) - Path Variables (`@PathVariable`) - Request Bodies (`@RequestBody`) - Java Models and DTOs ## ⚙️ Configuration The generated MCP server can be configured by editing the `main.py` file: ```python # The Spring Boot base URL - modify this to match your target API SPRING_BASE_URL = "http://localhost:8080" ``` ## 🧪 Testing To test the MCP server: 1. Ensure your Spring Boot application is running 2. Start the MCP server 3. Visit `http://localhost:8000/docs` to see the FastAPI documentation 4. Check the MCP schema at `http://localhost:8000/.well-known/mcp-schema.json` 5. Connect with an MCP client like Claude ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License This project is licensed under the MIT License - see the LICENSE file for details. ## 👨💻 Author **Bruno Santos** ## 🙏 Acknowledgments - Inspired by FastAPI-MCP and the growing ecosystem of MCP-compatible tools - Thanks to the Spring Boot and FastAPI communities ``` -------------------------------------------------------------------------------- /src/spring_boot_mcp_converter.py: -------------------------------------------------------------------------------- ```python #!/usr/bin/env python3 """ Spring Boot to MCP Converter Author: Bruno Santos Version: 1.0.0 Date: April 15, 2025 License: MIT This tool scans a local Spring Boot application and automatically converts its REST endpoints into an MCP (Message Conversation Protocol) server. This allows Spring Boot APIs to be used with AI assistants and other MCP clients like Claude, Cursor, etc. Usage: python spring_boot_mcp_converter.py --project /path/to/spring-project --output ./mcp_server --name MyAPI For more information about MCP: https://messageconversationprotocol.org """ import os import re import json import logging import argparse from fastapi import FastAPI from pydantic import BaseModel from pathlib import Path from typing import Dict, List, Optional app = FastAPI() # Global variable that will be filled with the MCP schema MCP_SCHEMA = {} class JavaTypeConverter: """Helper class to convert Java types to JSON schema types.""" @staticmethod def to_json_schema_type(java_type: str) -> str: """Convert Java type to JSON schema type.""" if java_type in ["int", "Integer", "long", "Long", "short", "Short", "byte", "Byte"]: return "integer" elif java_type in ["double", "Double", "float", "Float", "BigDecimal"]: return "number" elif java_type in ["boolean", "Boolean"]: return "boolean" elif java_type in ["List", "ArrayList", "Set", "HashSet", "Collection"] or "<" in java_type: return "array" elif java_type in ["Map", "HashMap", "TreeMap"] or java_type.startswith("Map<"): return "object" else: return "string" class SpringEndpointScanner: """Scanner for Spring Boot endpoints and models.""" def __init__(self, project_path: str): self.project_path = Path(project_path) self.endpoints = [] self.models = {} self.base_package = self._detect_base_package() self.logger = self._setup_logger() def _setup_logger(self) -> logging.Logger: """Set up logger for the scanner.""" logger = logging.getLogger("spring_mcp_scanner") logger.setLevel(logging.INFO) # Create console handler handler = logging.StreamHandler() handler.setLevel(logging.INFO) # Create formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) # Add handler to logger logger.addHandler(handler) return logger def _detect_base_package(self) -> str: """Detect the base package of the Spring Boot application.""" main_class_pattern = re.compile(r'@SpringBootApplication') for java_file in Path(self.project_path).glob('**/src/main/java/**/*.java'): try: with open(java_file, 'r', encoding='utf-8') as f: content = f.read() if main_class_pattern.search(content): # Extract package from the file package_match = re.search(r'package\s+([\w.]+);', content) if package_match: package = package_match.group(1) # Remove the last part if it's the filename's package parts = package.split('.') if len(parts) > 2: return '.'.join(parts[:-1]) return package except UnicodeDecodeError: # Skip files that can't be decoded as UTF-8 continue except FileNotFoundError: self.logger.error(f"File not found: {java_file}") continue except PermissionError: self.logger.error(f"Permission denied: {java_file}") continue return "" # Default if not found def _extract_request_mapping(self, content: str, class_mapping: str = "") -> List[Dict]: """Extract request mappings from a controller class.""" mappings = [] # Extract class-level mapping class_mapping_pattern = re.compile(r'@RequestMapping\s*\(\s*(?:value\s*=)?\s*"([^"]+)"\s*\)') class_match = class_mapping_pattern.search(content) if class_match: class_mapping = class_match.group(1) if not class_mapping.startswith('/'): class_mapping = '/' + class_mapping # Extract method mappings method_mapping_patterns = [ (r'@GetMapping\s*\(\s*(?:value\s*=)?\s*"([^"]+)"\s*\)', 'GET'), (r'@PostMapping\s*\(\s*(?:value\s*=)?\s*"([^"]+)"\s*\)', 'POST'), (r'@PutMapping\s*\(\s*(?:value\s*=)?\s*"([^"]+)"\s*\)', 'PUT'), (r'@DeleteMapping\s*\(\s*(?:value\s*=)?\s*"([^"]+)"\s*\)', 'DELETE'), (r'@PatchMapping\s*\(\s*(?:value\s*=)?\s*"([^"]+)"\s*\)', 'PATCH') ] # Extract method descriptions from javadoc comments javadoc_pattern = re.compile(r'/\*\*([\s\S]*?)\*/') javadoc_descriptions = {} for javadoc_match in javadoc_pattern.finditer(content): javadoc = javadoc_match.group(1) description = "" # Extract the main description from the javadoc for line in javadoc.split('\n'): line = line.strip().lstrip('*').strip() if line and not line.startswith('@'): description += line + " " # Find the method name that follows this javadoc method_pos = javadoc_match.end() method_search = content[method_pos:method_pos+200] # Look at next 200 chars method_name_match = re.search(r'(?:public|private|protected)?\s+\w+\s+(\w+)\s*\(', method_search) if method_name_match and description: javadoc_descriptions[method_name_match.group(1)] = description.strip() for pattern, method in method_mapping_patterns: for match in re.finditer(pattern, content): path = match.group(1) if not path.startswith('/'): path = '/' + path # Find method name and details try: method_content = content[match.end():].split('}')[0] method_name_match = re.search(r'(?:public|private|protected)?\s+\w+\s+(\w+)\s*\(', method_content) if method_name_match: method_name = method_name_match.group(1) # Extract method parameters param_section = re.search(r'\(\s*(.*?)\s*\)', method_content) parameters = [] response_type = "Object" if param_section: # Extract return type return_match = re.search(r'(?:public|private|protected)?\s+(\w+(?:<.*?>)?)\s+\w+\s*\(', method_content) if return_match: response_type = return_match.group(1) # Extract parameters param_text = param_section.group(1) if param_text.strip(): param_list = param_text.split(',') for param in param_list: param = param.strip() if param: param_parts = param.split() if len(param_parts) >= 2: param_type = param_parts[-2] param_name = param_parts[-1] # Check for request body is_body = '@RequestBody' in param # Check for path variable path_var_match = re.search(r'@PathVariable\s*(?:\(\s*(?:name|value)\s*=\s*"([^"]+)"\s*\))?', param) path_var = path_var_match.group(1) if path_var_match else None # Check for request param req_param_match = re.search(r'@RequestParam\s*(?:\(\s*(?:name|value)\s*=\s*"([^"]+)"\s*(?:,\s*required\s*=\s*(true|false))?\))?', param) req_param = req_param_match.group(1) if req_param_match else None req_param_required = True if req_param_match and req_param_match.group(2) == "false": req_param_required = False parameter = { "name": param_name.replace(";", ""), "type": param_type, "isBody": is_body, } if path_var: parameter["pathVariable"] = path_var elif req_param: parameter["requestParam"] = req_param parameter["required"] = req_param_required parameters.append(parameter) full_path = class_mapping + path if class_mapping else path # Get description from javadoc if available description = javadoc_descriptions.get(method_name, f"{method} endpoint for {full_path}") endpoint = { "path": full_path, "method": method, "methodName": method_name, "parameters": parameters, "responseType": response_type, "description": description } mappings.append(endpoint) except Exception as e: # Log the error and continue with the next match logging.error(f"Error processing mapping: {str(e)}") continue return mappings def _extract_models(self, java_file: Path) -> Dict: """Extract model classes from Java files.""" try: with open(java_file, 'r', encoding='utf-8') as f: content = f.read() # Check if it's a model class (typically has @Entity or @Data annotation) model_pattern = re.compile(r'@(\w+)\s*\(') models = {} for match in model_pattern.finditer(content): annotation = match.group(1) if annotation in ['Entity', 'Data', 'Serializable']: # Extract class name class_name_match = re.search(r'class\s+(\w+)', content) if class_name_match: class_name = class_name_match.group(1) fields = [] # Extract fields field_pattern = re.compile(r'private\s+(\w+\s+\w+)\s*;') for field_match in field_pattern.finditer(content): field = field_match.group(1).strip() field_parts = field.split() if len(field_parts) == 2: field_type, field_name = field_parts fields.append({ 'name': field_name, 'type': JavaTypeConverter.to_json_schema_type(field_type) }) models[class_name] = fields return models except Exception as e: self.logger.error(f"Error extracting models from {java_file}: {str(e)}") return {} def scan_project(self): """Scan the project directory and extract endpoints and models.""" if not self.project_path.exists(): raise FileNotFoundError(f"Project path '{self.project_path}' does not exist.") # First find all Java files java_files = list(self.project_path.glob('**/src/main/java/**/*.java')) if not java_files: self.logger.warning("No Java files found in the project path. Check if the path is correct.") return for java_file in java_files: self.logger.info(f"Scanning file: {java_file}") try: # Extract models from this file models = self._extract_models(java_file) self.models.update(models) with open(java_file, 'r', encoding='utf-8') as f: content = f.read() # Extract endpoints only if contains controller annotations if re.search(r'@(RestController|Controller)', content): endpoints = self._extract_request_mapping(content) self.endpoints.extend(endpoints) except Exception as e: self.logger.error(f"Error processing file {java_file}: {str(e)}") continue self.logger.info(f"Found {len(self.endpoints)} endpoints.") self.logger.info(f"Found {len(self.models)} models.") class MCPServer: """Generate MCP server from extracted endpoints and models.""" def __init__(self, name: str, endpoints: List[Dict], models: Dict): self.name = name self.endpoints = endpoints self.models = models def generate_schema(self) -> Dict: """Generate MCP schema.""" schema = { "name": self.name, "endpoints": self.endpoints, "models": self.models } return schema def generate_server(self, output_dir: Path): """Generate the MCP server code.""" # Create the output directory if it doesn't exist output_dir.mkdir(parents=True, exist_ok=True) # Create the schema file schema = self.generate_schema() schema_path = output_dir / "mcp_schema.json" with open(schema_path, 'w', encoding='utf-8') as f: json.dump(schema, f, indent=2) # Create main.py with FastAPI server main_py_content = """#!/usr/bin/env python3 ''' MCP Server for {name} Generated by Spring Boot to MCP Converter ''' import json from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware import httpx import os from pydantic import BaseModel from typing import Dict, List, Any, Optional app = FastAPI(title="{name} MCP Server") # CORS configuration app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Load the MCP schema with open("mcp_schema.json", "r") as f: MCP_SCHEMA = json.load(f) # Define the base URL for the Spring Boot application # You should set this to the actual URL of your Spring Boot app SPRING_BOOT_BASE_URL = os.getenv("SPRING_BOOT_URL", "http://localhost:8080") @app.get("/.well-known/mcp-schema.json") async def get_mcp_schema(): ""Return the MCP schema for this server."" return MCP_SCHEMA # Generate dynamic endpoints based on the schema for endpoint in MCP_SCHEMA["endpoints"]: path = endpoint["path"] method = endpoint["method"].lower() method_name = endpoint["methodName"] # Create a function to handle the request async def create_handler(endpoint_info): async def handler(request: Request): # Extract path params, query params, and request body path_params = {{}} query_params = {{}} for param in endpoint_info["parameters"]: if "pathVariable" in param: # Extract path variable from request path path_var = param["pathVariable"] path_params[path_var] = request.path_params.get(path_var) elif "requestParam" in param: # Extract query parameters req_param = param["requestParam"] query_params[req_param] = request.query_params.get(req_param) # Prepare the URL for the Spring Boot application target_url = f"{{SPRING_BOOT_BASE_URL}}{{endpoint_info['path']}}" # Forward the request to the Spring Boot application async with httpx.AsyncClient() as client: if method == "get": response = await client.get(target_url, params=query_params) elif method == "post": body = await request.json() if request.headers.get("content-type") == "application/json" else None response = await client.post(target_url, params=query_params, json=body) elif method == "put": body = await request.json() if request.headers.get("content-type") == "application/json" else None response = await client.put(target_url, params=query_params, json=body) elif method == "delete": response = await client.delete(target_url, params=query_params) elif method == "patch": body = await request.json() if request.headers.get("content-type") == "application/json" else None response = await client.patch(target_url, params=query_params, json=body) else: return {{"error": f"Unsupported method: {{method}}"}} # Return the response from the Spring Boot application return Response( content=response.content, status_code=response.status_code, headers=dict(response.headers), ) return handler # Register the endpoint with FastAPI handler = create_handler(endpoint) # Register the endpoint with FastAPI using the appropriate HTTP method if method == "get": app.get(path)(handler) elif method == "post": app.post(path)(handler) elif method == "put": app.put(path)(handler) elif method == "delete": app.delete(path)(handler) elif method == "patch": app.patch(path)(handler) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) """ main_py = output_dir / "main.py" with open(main_py, 'w', encoding='utf-8') as f: f.write(main_py_content.format(name=self.name)) # Create requirements.txt requirements_txt = output_dir / "requirements.txt" with open(requirements_txt, 'w', encoding='utf-8') as f: f.write("""fastapi>=0.95.0 uvicorn>=0.21.1 httpx>=0.24.0 pydantic>=1.10.7 """) # Create README.md readme_content = """# {name} MCP Server This is an automatically generated MCP (Message Conversation Protocol) server for the Spring Boot application. ## Getting Started 1. Install the required dependencies: ``` pip install -r requirements.txt ``` 2. Set the environment variable to your Spring Boot application URL: ``` export SPRING_BOOT_URL="http://localhost:8080" ``` 3. Start the MCP server: ``` python main.py ``` 4. The server will be available at http://localhost:8000 5. The MCP schema is available at http://localhost:8000/.well-known/mcp-schema.json ## Endpoints Total endpoints: {endpoint_count} """ readme_md = output_dir / "README.md" with open(readme_md, 'w', encoding='utf-8') as f: f.write(readme_content.format(name=self.name, endpoint_count=len(self.endpoints))) # Add endpoint details to README for endpoint in self.endpoints: f.write(f"- **{endpoint['method']}** `{endpoint['path']}`: {endpoint['description']}\n") @app.get("/.well-known/mcp-schema.json") async def get_mcp_schema(): """Return the MCP schema for this server.""" return MCP_SCHEMA # Main function def main(): parser = argparse.ArgumentParser(description="Spring Boot to MCP Converter") parser.add_argument('--project', required=True, help="Path to the Spring Boot project") parser.add_argument('--output', required=True, help="Output directory for MCP server") parser.add_argument('--name', default="MyAPI", help="Name of the generated MCP server") args = parser.parse_args() print(f"Starting Spring Boot to MCP conversion...") print(f"Project path: {args.project}") print(f"Output directory: {args.output}") print(f"API name: {args.name}") try: # Scan the Spring Boot project for endpoints and models scanner = SpringEndpointScanner(args.project) scanner.scan_project() # Create MCP server schema mcp_server = MCPServer(args.name, scanner.endpoints, scanner.models) # Generate the MCP server output_path = Path(args.output) mcp_server.generate_server(output_path) print(f"MCP server generated successfully at: {output_path}") print(f"To start the server:") print(f" 1. cd {args.output}") print(f" 2. pip install -r requirements.txt") print(f" 3. python main.py") print(f"The server will be available at http://localhost:8000") print(f"The MCP schema will be available at http://localhost:8000/.well-known/mcp-schema.json") # Update the global MCP_SCHEMA global MCP_SCHEMA MCP_SCHEMA = mcp_server.generate_schema() except Exception as e: print(f"Error: {str(e)}") import traceback traceback.print_exc() return 1 return 0 if __name__ == "__main__": exit(main()) ```