|
|
"""
|
|
|
MCP Generator - Gradio Frontend
|
|
|
Turn any API into an MCP server in seconds!
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
import gradio as gr
|
|
|
from pathlib import Path
|
|
|
import zipfile
|
|
|
import io
|
|
|
import os
|
|
|
|
|
|
from src.agents.factory import AgentFactory
|
|
|
from src.mcp_host import mcp_host
|
|
|
from src.mcp_http_host import mcp_http_host
|
|
|
from src.mcp_registry import mcp_registry
|
|
|
from src.hf_deployer import deploy_to_huggingface
|
|
|
from src.config import LLM_PROVIDER, ANTHROPIC_API_KEY, OPENAI_API_KEY, HOSTED_MCPS_DIR
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
agent_factory = AgentFactory()
|
|
|
print(f"β
Using {LLM_PROVIDER.upper()} for code generation")
|
|
|
except ValueError as e:
|
|
|
print(f"β Error: {e}")
|
|
|
print(f"Please set {'ANTHROPIC_API_KEY' if LLM_PROVIDER == 'anthropic' else 'OPENAI_API_KEY'} in .env file")
|
|
|
agent_factory = None
|
|
|
|
|
|
|
|
|
async def generate_and_host_mcp(api_url: str, api_key: str = None, force_regenerate: bool = False, progress=gr.Progress()):
|
|
|
"""Generate and host an MCP server
|
|
|
|
|
|
Args:
|
|
|
api_url: The API URL to analyze
|
|
|
api_key: Optional API key for the target API
|
|
|
force_regenerate: If True, regenerate even if exists
|
|
|
progress: Gradio progress tracker
|
|
|
|
|
|
Returns:
|
|
|
Tuple of (status_text, code, download_file, readme, connection_config)
|
|
|
"""
|
|
|
if not agent_factory:
|
|
|
api_key_name = "ANTHROPIC_API_KEY" if LLM_PROVIDER == "anthropic" else "OPENAI_API_KEY"
|
|
|
return (
|
|
|
f"β Error: {api_key_name} not configured. Please set it in your .env file.",
|
|
|
"",
|
|
|
None,
|
|
|
"",
|
|
|
""
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
existing_mcp = mcp_registry.find_by_url(api_url)
|
|
|
|
|
|
if existing_mcp and not force_regenerate:
|
|
|
progress(0.5, desc="Found existing MCP, reusing...")
|
|
|
|
|
|
|
|
|
mcp_id = existing_mcp['mcp_id']
|
|
|
mcp_path = HOSTED_MCPS_DIR / mcp_id
|
|
|
|
|
|
|
|
|
mcp_registry.update_last_used(api_url)
|
|
|
|
|
|
|
|
|
server_code = (mcp_path / "server.py").read_text()
|
|
|
readme = (mcp_path / "README.md").read_text()
|
|
|
|
|
|
|
|
|
base_url = os.getenv("SPACE_HOST", "http://localhost:7860")
|
|
|
hosted_base_url = f"{base_url}/{mcp_id}"
|
|
|
hosted_info_url = f"{base_url}/{mcp_id}/info"
|
|
|
|
|
|
status_text = f"""β»οΈ **Reusing Existing MCP!**
|
|
|
|
|
|
**MCP ID:** `{mcp_id}`
|
|
|
**Originally Created:** {existing_mcp['created_at']}
|
|
|
**Last Used:** {existing_mcp['last_used']}
|
|
|
|
|
|
This MCP was already generated for this API URL. Using existing version to save time and API calls!
|
|
|
|
|
|
**π HTTP Transport (MCP 2025-03-26):**
|
|
|
- **Base URL:** `{hosted_base_url}`
|
|
|
- **Info:** `{hosted_info_url}`
|
|
|
- **Protocol:** JSON-RPC 2.0 over HTTP Streamable
|
|
|
- **Note:** Claude Desktop automatically appends `/mcp` to the base URL
|
|
|
|
|
|
π‘ **Tip:** To regenerate from scratch, check "Force Regenerate" below.
|
|
|
"""
|
|
|
|
|
|
|
|
|
zip_buffer = io.BytesIO()
|
|
|
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
|
for file_path in mcp_path.rglob('*'):
|
|
|
if file_path.is_file():
|
|
|
arcname = file_path.relative_to(mcp_path.parent)
|
|
|
zipf.write(file_path, arcname)
|
|
|
|
|
|
zip_buffer.seek(0)
|
|
|
temp_zip = f"/tmp/{mcp_id}.zip"
|
|
|
with open(temp_zip, 'wb') as f:
|
|
|
f.write(zip_buffer.read())
|
|
|
|
|
|
connection_config = f"""# Local stdio transport:
|
|
|
{{
|
|
|
"mcpServers": {{
|
|
|
"{mcp_id}": {{
|
|
|
"command": "python",
|
|
|
"args": ["server.py"]
|
|
|
}}
|
|
|
}}
|
|
|
}}
|
|
|
|
|
|
# HTTP transport (hosted):
|
|
|
# Claude Desktop will automatically append /mcp to this URL
|
|
|
{{
|
|
|
"mcpServers": {{
|
|
|
"{mcp_id}": {{
|
|
|
"url": "{hosted_base_url}"
|
|
|
}}
|
|
|
}}
|
|
|
}}"""
|
|
|
|
|
|
|
|
|
mcp_info = {
|
|
|
"api_name": existing_mcp['api_name'],
|
|
|
"mcp_id": mcp_id
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
status_text,
|
|
|
server_code,
|
|
|
temp_zip,
|
|
|
readme,
|
|
|
connection_config,
|
|
|
mcp_info
|
|
|
)
|
|
|
|
|
|
|
|
|
if existing_mcp:
|
|
|
progress(0.1, desc="Regenerating MCP (forced)...")
|
|
|
else:
|
|
|
progress(0.1, desc="Analyzing API...")
|
|
|
|
|
|
|
|
|
result = await agent_factory.generate_mcp(api_url, api_key)
|
|
|
|
|
|
if result["status"] == "error":
|
|
|
return (
|
|
|
f"β Error: {result['error']}",
|
|
|
"",
|
|
|
None,
|
|
|
"",
|
|
|
"",
|
|
|
None
|
|
|
)
|
|
|
|
|
|
progress(0.6, desc="Generating code...")
|
|
|
|
|
|
|
|
|
mcp_id = result["mcp_id"]
|
|
|
server_code = result["server_code"]
|
|
|
readme = result["readme_content"]
|
|
|
|
|
|
progress(0.8, desc="Starting MCP server...")
|
|
|
|
|
|
|
|
|
start_result = await mcp_host.start_mcp(mcp_id)
|
|
|
|
|
|
|
|
|
base_url = os.getenv("SPACE_HOST", "http://localhost:7860")
|
|
|
hosted_base_url = f"{base_url}/{mcp_id}"
|
|
|
hosted_info_url = f"{base_url}/{mcp_id}/info"
|
|
|
|
|
|
if not start_result["success"]:
|
|
|
status_text = f"β οΈ MCP generated but failed to start locally: {start_result.get('error')}\n\n**HTTP Base URL:** {hosted_base_url}"
|
|
|
else:
|
|
|
status_text = f"""β
**MCP Server Generated & Hosted!**
|
|
|
|
|
|
**MCP ID:** `{mcp_id}`
|
|
|
**Status:** {start_result['status']}
|
|
|
|
|
|
**π HTTP Transport (MCP 2025-03-26):**
|
|
|
- **Base URL:** `{hosted_base_url}`
|
|
|
- **Info:** `{hosted_info_url}`
|
|
|
- **Protocol:** JSON-RPC 2.0 over HTTP Streamable
|
|
|
- **Note:** Claude Desktop automatically appends `/mcp` to the base URL
|
|
|
|
|
|
**π¦ Local Use (stdio):**
|
|
|
- Download ZIP below and extract
|
|
|
- Configure in Claude Desktop (see Connection Config tab)
|
|
|
|
|
|
Your MCP server is live and accessible via HTTP! π
|
|
|
"""
|
|
|
|
|
|
progress(0.9, desc="Creating download package...")
|
|
|
|
|
|
|
|
|
zip_buffer = io.BytesIO()
|
|
|
mcp_path = Path(result["download_path"])
|
|
|
|
|
|
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
|
for file_path in mcp_path.rglob('*'):
|
|
|
if file_path.is_file():
|
|
|
arcname = file_path.relative_to(mcp_path.parent)
|
|
|
zipf.write(file_path, arcname)
|
|
|
|
|
|
zip_buffer.seek(0)
|
|
|
|
|
|
|
|
|
temp_zip = f"/tmp/{mcp_id}.zip"
|
|
|
with open(temp_zip, 'wb') as f:
|
|
|
f.write(zip_buffer.read())
|
|
|
|
|
|
|
|
|
connection_config = f"""# Local stdio transport (extract ZIP first):
|
|
|
{{
|
|
|
"mcpServers": {{
|
|
|
"{mcp_id}": {{
|
|
|
"command": "python",
|
|
|
"args": ["server.py"]
|
|
|
}}
|
|
|
}}
|
|
|
}}
|
|
|
|
|
|
# HTTP transport (use hosted endpoint):
|
|
|
# Claude Desktop will automatically append /mcp to this URL
|
|
|
{{
|
|
|
"mcpServers": {{
|
|
|
"{mcp_id}": {{
|
|
|
"url": "{hosted_base_url}"
|
|
|
}}
|
|
|
}}
|
|
|
}}"""
|
|
|
|
|
|
progress(1.0, desc="Done!")
|
|
|
|
|
|
|
|
|
mcp_info = {
|
|
|
"api_name": state['api_name'],
|
|
|
"mcp_id": mcp_id
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
status_text,
|
|
|
server_code,
|
|
|
temp_zip,
|
|
|
readme,
|
|
|
connection_config,
|
|
|
mcp_info
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
return (
|
|
|
f"β Unexpected error: {str(e)}",
|
|
|
"",
|
|
|
None,
|
|
|
"",
|
|
|
"",
|
|
|
None
|
|
|
)
|
|
|
|
|
|
|
|
|
def deploy_to_hf_space(hf_token: str, mcp_info: dict) -> str:
|
|
|
"""Deploy generated MCP to HuggingFace Space
|
|
|
|
|
|
Args:
|
|
|
hf_token: HuggingFace API token
|
|
|
mcp_info: Dict with api_name and mcp_id
|
|
|
|
|
|
Returns:
|
|
|
Deployment status message
|
|
|
"""
|
|
|
if not hf_token:
|
|
|
return "β Please enter your HuggingFace token above"
|
|
|
|
|
|
if not mcp_info or not mcp_info.get("mcp_id"):
|
|
|
return "β Please generate an MCP first before deploying"
|
|
|
|
|
|
try:
|
|
|
api_name = mcp_info["api_name"]
|
|
|
mcp_id = mcp_info["mcp_id"]
|
|
|
mcp_dir = HOSTED_MCPS_DIR / mcp_id
|
|
|
|
|
|
if not mcp_dir.exists():
|
|
|
return f"β MCP directory not found: {mcp_id}"
|
|
|
|
|
|
|
|
|
result = deploy_to_huggingface(
|
|
|
mcp_dir=mcp_dir,
|
|
|
api_name=api_name,
|
|
|
mcp_id=mcp_id,
|
|
|
hf_token=hf_token
|
|
|
)
|
|
|
|
|
|
if result["success"]:
|
|
|
return f"""β
**Deployed Successfully!**
|
|
|
|
|
|
**Space URL:** [{result['repo_id']}]({result['space_url']})
|
|
|
|
|
|
**MCP Endpoint:** `{result['mcp_endpoint']}`
|
|
|
|
|
|
**Next Steps:**
|
|
|
1. Visit your Space (may take 1-2 min to build)
|
|
|
2. Copy the MCP endpoint URL
|
|
|
3. Add to Claude Desktop config:
|
|
|
```json
|
|
|
{{
|
|
|
"mcpServers": {{
|
|
|
"{mcp_id}": {{
|
|
|
"url": "{result['mcp_endpoint'].replace('/mcp', '')}"
|
|
|
}}
|
|
|
}}
|
|
|
}}
|
|
|
```
|
|
|
|
|
|
π Your MCP is now live and accessible!
|
|
|
"""
|
|
|
else:
|
|
|
return f"β **Deployment Failed**\n\nError: {result['error']}"
|
|
|
|
|
|
except Exception as e:
|
|
|
return f"β **Deployment Error**\n\n{str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(
|
|
|
title="MCP Generator"
|
|
|
) as app:
|
|
|
|
|
|
gr.Markdown("""
|
|
|
# π€ MCP Generator
|
|
|
## Turn Any API into an MCP Server in Seconds!
|
|
|
|
|
|
Simply enter an API URL and we'll generate a complete, working MCP server with:
|
|
|
- β
Automatically analyzed endpoints
|
|
|
- β
Generated MCP tools
|
|
|
- β
Complete documentation
|
|
|
- β
Ready to use immediately!
|
|
|
|
|
|
**Built for the MCP 1st Birthday Hackathon** π
|
|
|
""")
|
|
|
|
|
|
with gr.Accordion("βΉοΈ API URL Guidelines & Tips", open=False):
|
|
|
gr.Markdown("""
|
|
|
### β
What Works Best:
|
|
|
- **Base API URLs** (e.g., `https://api.example.com`)
|
|
|
- **REST APIs** with clear endpoints
|
|
|
- **OpenAPI/Swagger** documented APIs
|
|
|
- **Public APIs** (no complex auth)
|
|
|
|
|
|
### π― Try These Free APIs:
|
|
|
|
|
|
**No Auth Required (Perfect for Testing!):**
|
|
|
- `https://jsonplaceholder.typicode.com` - Fake REST API (best for testing!)
|
|
|
- `https://api.github.com` - GitHub public API
|
|
|
- `https://dog.ceo/api` - Random dog images
|
|
|
- `https://catfact.ninja` - Random cat facts
|
|
|
- `https://api.coindesk.com/v1/bpi` - Bitcoin prices
|
|
|
- `https://api.ipify.org` - Get IP address
|
|
|
|
|
|
**With API Key (Free Tier):**
|
|
|
- `https://api.openweathermap.org/data/2.5` - Weather data
|
|
|
- `https://newsapi.org/v2` - News articles
|
|
|
- `https://api.stripe.com` - Payment processing (test mode)
|
|
|
|
|
|
### π‘ Tips:
|
|
|
- **Start with jsonplaceholder.typicode.com** - always works!
|
|
|
- Paste the **base URL** (not a specific endpoint)
|
|
|
- If API needs a key, add it in the "API Key" field below
|
|
|
- Cached URLs generate instantly (try the same URL twice!)
|
|
|
|
|
|
### β οΈ May Not Work Well:
|
|
|
- GraphQL APIs (REST only for now)
|
|
|
- APIs requiring OAuth flows
|
|
|
- WebSocket-only APIs
|
|
|
- APIs with very complex authentication
|
|
|
""")
|
|
|
|
|
|
with gr.Row():
|
|
|
with gr.Column(scale=2):
|
|
|
gr.Markdown("### π Input")
|
|
|
|
|
|
api_url = gr.Textbox(
|
|
|
label="API URL or Documentation URL",
|
|
|
placeholder="https://api.example.com",
|
|
|
info="Enter the base URL or documentation URL of the API"
|
|
|
)
|
|
|
|
|
|
api_key = gr.Textbox(
|
|
|
label="API Key (Optional)",
|
|
|
placeholder="sk-...",
|
|
|
type="password",
|
|
|
info="If the API requires authentication"
|
|
|
)
|
|
|
|
|
|
force_regenerate = gr.Checkbox(
|
|
|
label="Force Regenerate",
|
|
|
value=False,
|
|
|
info="Regenerate even if MCP already exists for this URL (saves API calls when unchecked)"
|
|
|
)
|
|
|
|
|
|
generate_btn = gr.Button(
|
|
|
"π Generate & Host MCP Server",
|
|
|
variant="primary",
|
|
|
size="lg"
|
|
|
)
|
|
|
|
|
|
with gr.Accordion("π Quick Start Examples", open=True):
|
|
|
gr.Markdown("""
|
|
|
**Click to copy and paste:**
|
|
|
|
|
|
```
|
|
|
https://jsonplaceholder.typicode.com
|
|
|
```
|
|
|
β **Recommended first try!** Always works, no API key needed.
|
|
|
|
|
|
---
|
|
|
|
|
|
**More examples:**
|
|
|
- `https://api.github.com` - GitHub API (no auth)
|
|
|
- `https://dog.ceo/api` - Dog images (fun!)
|
|
|
- `https://catfact.ninja` - Cat facts (simple)
|
|
|
|
|
|
π‘ **Tip:** MCPs are cached - try the same URL twice to see instant results!
|
|
|
""")
|
|
|
|
|
|
gr.Markdown("---")
|
|
|
|
|
|
with gr.Row():
|
|
|
with gr.Column():
|
|
|
gr.Markdown("### π Results")
|
|
|
|
|
|
status_output = gr.Markdown(label="Status")
|
|
|
|
|
|
with gr.Tab("Generated Code"):
|
|
|
code_output = gr.Code(
|
|
|
label="server.py",
|
|
|
language="python",
|
|
|
lines=20
|
|
|
)
|
|
|
|
|
|
with gr.Tab("README"):
|
|
|
readme_output = gr.Markdown()
|
|
|
|
|
|
with gr.Tab("Connection Config"):
|
|
|
connection_output = gr.Code(
|
|
|
label="Claude Desktop Config",
|
|
|
language="json"
|
|
|
)
|
|
|
|
|
|
download_output = gr.File(
|
|
|
label="π¦ Download Complete Package (ZIP)"
|
|
|
)
|
|
|
|
|
|
|
|
|
gr.Markdown("---")
|
|
|
gr.Markdown("### π One-Click Deploy to HuggingFace")
|
|
|
|
|
|
gr.Markdown("""
|
|
|
β οΈ **Security Best Practices:**
|
|
|
- Use a **temporary, disposable token** that you'll delete immediately after deployment
|
|
|
- **Required permissions:** `write` access (to create Spaces)
|
|
|
- **Recommended flow:** Create token β Deploy β Delete token
|
|
|
- Get tokens from: [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
|
|
|
|
|
|
π‘ **How to create a safe token:**
|
|
|
1. Go to HuggingFace β Settings β Access Tokens
|
|
|
2. Click "New token"
|
|
|
3. Name: `mcp-temp-deploy` (or similar)
|
|
|
4. Type: **Write** (required to create Spaces)
|
|
|
5. Click "Generate" β Copy the token
|
|
|
6. Paste below and deploy
|
|
|
7. **β οΈ DELETE THE TOKEN** immediately after deployment succeeds!
|
|
|
""")
|
|
|
|
|
|
with gr.Row():
|
|
|
hf_token_input = gr.Textbox(
|
|
|
label="HuggingFace Token (hidden with β’β’β’β’β’β’)",
|
|
|
placeholder="hf_β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’",
|
|
|
type="password",
|
|
|
info="β οΈ Use a TEMPORARY token with 'write' permission. Delete it after deploying!"
|
|
|
)
|
|
|
|
|
|
with gr.Row():
|
|
|
deploy_hf_btn = gr.Button(
|
|
|
"π Deploy to HuggingFace Space",
|
|
|
variant="secondary",
|
|
|
size="lg"
|
|
|
)
|
|
|
|
|
|
hf_deploy_status = gr.Markdown(label="Deployment Status")
|
|
|
|
|
|
|
|
|
last_mcp_state = gr.State(value=None)
|
|
|
|
|
|
|
|
|
generate_btn.click(
|
|
|
fn=generate_and_host_mcp,
|
|
|
inputs=[api_url, api_key, force_regenerate],
|
|
|
outputs=[
|
|
|
status_output,
|
|
|
code_output,
|
|
|
download_output,
|
|
|
readme_output,
|
|
|
connection_output,
|
|
|
last_mcp_state
|
|
|
]
|
|
|
)
|
|
|
|
|
|
|
|
|
deploy_hf_btn.click(
|
|
|
fn=deploy_to_hf_space,
|
|
|
inputs=[hf_token_input, last_mcp_state],
|
|
|
outputs=[hf_deploy_status]
|
|
|
)
|
|
|
|
|
|
with gr.Accordion("π Previously Generated MCPs", open=False):
|
|
|
def get_existing_mcps():
|
|
|
"""Get list of existing MCPs for display"""
|
|
|
mcps = mcp_registry.list_all()
|
|
|
if not mcps:
|
|
|
return "No MCPs generated yet. Generate your first one above! π"
|
|
|
|
|
|
output = "| API Name | URL | Created | Last Used |\n"
|
|
|
output += "|----------|-----|---------|----------|\n"
|
|
|
for mcp in mcps[:10]:
|
|
|
api_name = mcp['api_name']
|
|
|
api_url = mcp['api_url'][:40] + "..." if len(mcp['api_url']) > 40 else mcp['api_url']
|
|
|
created = mcp['created_at'].split('T')[0]
|
|
|
last_used = mcp['last_used'].split('T')[0]
|
|
|
output += f"| {api_name} | {api_url} | {created} | {last_used} |\n"
|
|
|
|
|
|
return output
|
|
|
|
|
|
existing_mcps_display = gr.Markdown(get_existing_mcps())
|
|
|
refresh_btn = gr.Button("π Refresh List", size="sm")
|
|
|
refresh_btn.click(fn=get_existing_mcps, outputs=existing_mcps_display)
|
|
|
|
|
|
gr.Markdown("""
|
|
|
---
|
|
|
### π― How to Use Your Generated MCP
|
|
|
|
|
|
1. **Download** the ZIP file above
|
|
|
2. **Extract** it to a folder
|
|
|
3. **Add** the connection config to your Claude Desktop settings
|
|
|
4. **Restart** Claude Desktop
|
|
|
|
|
|
Your MCP server is ready to use! π
|
|
|
|
|
|
### π About This Project
|
|
|
|
|
|
This is a meta-MCP: an MCP server that generates other MCP servers!
|
|
|
|
|
|
- Built with [Gradio](https://gradio.app)
|
|
|
- Powered by [LangGraph](https://github.com/langchain-ai/langgraph) agents
|
|
|
- Uses [Anthropic's Claude](https://anthropic.com) for code generation
|
|
|
- Integrates with [MCP Fetch Server](https://github.com/modelcontextprotocol/servers)
|
|
|
|
|
|
**For MCP 1st Birthday Hackathon - Track 2: MCP in Action** π
|
|
|
""")
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
if not ANTHROPIC_API_KEY and not OPENAI_API_KEY:
|
|
|
print("β οΈ WARNING: No API key set!")
|
|
|
print("Please set either OPENAI_API_KEY or ANTHROPIC_API_KEY in your .env file")
|
|
|
print(f"Example: echo 'OPENAI_API_KEY=your_key_here' > .env")
|
|
|
|
|
|
|
|
|
app.launch(
|
|
|
server_name="0.0.0.0",
|
|
|
server_port=7860,
|
|
|
share=False
|
|
|
)
|
|
|
|