|
|
""" |
|
|
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": result.get('api_name', 'Generated API'), |
|
|
"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 |
|
|
) |
|
|
|