# 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())
```