Skip to main content
Glama

FastAPI MCP Template

by joonheeu
setup_template.pyβ€’22.4 kB
#!/usr/bin/env python3 """ FastAPI + MCP ν…œν”Œλ¦Ώ μ„€μ • 슀크립트 - κΈ°μ‘΄ ν…œν”Œλ¦Ώ μ»€μŠ€ν„°λ§ˆμ΄μ§• - λ°±μ—… 및 λ‘€λ°± κΈ°λŠ₯ """ import argparse import json import os import shutil import subprocess import sys from datetime import datetime from pathlib import Path from typing import Dict, Any class TemplateSetup: def __init__(self, project_path: Path): self.project_path = project_path self.backup_dir = project_path / ".template_backup" self.template_files = [ "pyproject.toml", "README.md", ".cursor/mcp.json", "src/core/config.py", "src/core/models.py", "src/api/app.py", "src/mcp/server.py", "src/mcp/tools.py", "src/mcp/resources.py", "run_api_server.py", "run_mcp_server.py", "run_server.py", "run_docker.py", "Dockerfile", "docker-compose.yml", ".dockerignore", "examples/api_usage.py", "tests/test_api.py", "docs/TEMPLATE_GUIDE.md" ] def get_user_input(self, prompt: str, default: str = "") -> str: """μ‚¬μš©μž μž…λ ₯을 λ°›λŠ” ν•¨μˆ˜""" if default: user_input = input(f"{prompt} [{default}]: ").strip() return user_input if user_input else default return input(f"{prompt}: ").strip() def get_yes_no(self, prompt: str, default: bool = True) -> bool: """예/μ•„λ‹ˆμ˜€ μž…λ ₯을 λ°›λŠ” ν•¨μˆ˜""" default_str = "Y/n" if default else "y/N" while True: response = input(f"{prompt} [{default_str}]: ").strip().lower() if not response: return default if response in ['y', 'yes', '예']: return True if response in ['n', 'no', 'μ•„λ‹ˆμ˜€']: return False print("'y' λ˜λŠ” 'n'을 μž…λ ₯ν•΄μ£Όμ„Έμš”.") def create_backup(self) -> bool: """ν˜„μž¬ ν…œν”Œλ¦Ώ μƒνƒœλ₯Ό λ°±μ—…""" try: if self.backup_dir.exists(): shutil.rmtree(self.backup_dir) self.backup_dir.mkdir(exist_ok=True) print("πŸ’Ύ ν˜„μž¬ ν…œν”Œλ¦Ώ μƒνƒœλ₯Ό λ°±μ—…ν•©λ‹ˆλ‹€...") for file_path in self.template_files: source_file = self.project_path / file_path if source_file.exists(): backup_file = self.backup_dir / file_path backup_file.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source_file, backup_file) # 메타데이터 μ €μž₯ metadata = { "backup_time": datetime.now().isoformat(), "original_files": [str(f) for f in self.template_files if (self.project_path / f).exists()] } with open(self.backup_dir / "metadata.json", 'w', encoding='utf-8') as f: json.dump(metadata, f, indent=2, ensure_ascii=False) print(f"βœ… λ°±μ—… μ™„λ£Œ: {self.backup_dir}") return True except Exception as e: print(f"❌ λ°±μ—… μ‹€νŒ¨: {e}") return False def restore_backup(self) -> bool: """λ°±μ—…μ—μ„œ 볡원""" try: if not self.backup_dir.exists(): print("❌ λ°±μ—… 파일이 μ—†μŠ΅λ‹ˆλ‹€.") return False metadata_file = self.backup_dir / "metadata.json" if not metadata_file.exists(): print("❌ λ°±μ—… 메타데이터가 μ—†μŠ΅λ‹ˆλ‹€.") return False with open(metadata_file, 'r', encoding='utf-8') as f: metadata = json.load(f) print("πŸ”„ λ°±μ—…μ—μ„œ λ³΅μ›ν•©λ‹ˆλ‹€...") for file_path in metadata["original_files"]: backup_file = self.backup_dir / file_path target_file = self.project_path / file_path if backup_file.exists(): target_file.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(backup_file, target_file) print("βœ… 볡원 μ™„λ£Œ") return True except Exception as e: print(f"❌ 볡원 μ‹€νŒ¨: {e}") return False def replace_in_file(self, file_path: Path, replacements: Dict[str, str]) -> bool: """파일 λ‚΄μš©μ—μ„œ λ¬Έμžμ—΄ λŒ€μΉ˜""" try: if not file_path.exists(): return False content = file_path.read_text(encoding='utf-8') original_content = content for old_text, new_text in replacements.items(): content = content.replace(old_text, new_text) if content != original_content: file_path.write_text(content, encoding='utf-8') return True return False except Exception as e: print(f"❌ 파일 μˆ˜μ • μ‹€νŒ¨ {file_path}: {e}") return False def update_all_files(self, project_info: Dict[str, Any]) -> bool: """λͺ¨λ“  νŒŒμΌμ—μ„œ ν…œν”Œλ¦Ώ 정보λ₯Ό ν”„λ‘œμ νŠΈ μ •λ³΄λ‘œ λŒ€μΉ˜""" # κΈ°λ³Έ λŒ€μΉ˜ λ§΅ν•‘ replacements = { # ν”„λ‘œμ νŠΈ 이름 κ΄€λ ¨ "fastapi-mcp-template": project_info["name"], "FastAPI + MCP Template": project_info["title"], "FastAPI MCP Template": project_info["title"], # μ„€λͺ… κ΄€λ ¨ "FastAPI + MCP Template - ν˜„λŒ€μ μΈ API와 LLM 톡합을 μœ„ν•œ 개발 ν…œν”Œλ¦Ώ": project_info["description"], "**FastAPI**와 **MCP(Model Context Protocol)**λ₯Ό κ²°ν•©ν•œ 개발 ν…œν”Œλ¦Ώμž…λ‹ˆλ‹€.": project_info["description"], "ν˜„λŒ€μ μΈ API μ„œλ²„μ™€ LLM 톡합을 μœ„ν•œ MCP μ„œλ²„λ₯Ό λ™μ‹œμ— μ œκ³΅ν•˜λŠ” μ™„μ „ν•œ 개발 ν™˜κ²½μ„ μ œκ³΅ν•©λ‹ˆλ‹€.": f"{project_info['description']} ν”„λ‘œμ νŠΈμž…λ‹ˆλ‹€.", # μž‘μ„±μž κ΄€λ ¨ 'authors = ["Your Name <your.email@example.com>"]': f'authors = ["{project_info.get("author", "Your Name")} <{project_info.get("email", "your.email@example.com")}>"]', "Your Name": project_info.get("author", "Your Name"), "your.email@example.com": project_info.get("email", "your.email@example.com"), # 클래슀/λ³€μˆ˜λͺ… κ΄€λ ¨ (Python μ‹λ³„μžλ‘œ μ‚¬μš© κ°€λŠ₯ν•œ ν˜•νƒœ) "FastApiMcpTemplate": self.to_pascal_case(project_info["name"]), "fastapi_mcp_template": self.to_snake_case(project_info["name"]), "FASTAPI_MCP_TEMPLATE": self.to_upper_snake_case(project_info["name"]), # Docker κ΄€λ ¨ (μ»¨ν…Œμ΄λ„ˆ 이름) "fastapi-mcp-app": f"{self.to_snake_case(project_info['name'])}-app", "fastapi-mcp-dev": f"{self.to_snake_case(project_info['name'])}-dev", # 디렉토리/νŒ¨ν‚€μ§€λͺ… κ΄€λ ¨ "fastapi-mcp-template/": f"{project_info['name']}/", # λ¬Έμ„œ κ΄€λ ¨ "FastAPI + MCP ν…œν”Œλ¦Ώ": project_info["title"], "ν…œν”Œλ¦Ώ": "ν”„λ‘œμ νŠΈ", # URL/도메인 κ΄€λ ¨ (μ˜ˆμ‹œ) "fastapi-mcp-template.com": f"{project_info['name']}.com", "fastapi-mcp-template.example.com": f"{project_info['name']}.example.com", } print("πŸ”„ ν”„λ‘œμ νŠΈ νŒŒμΌλ“€μ„ μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€...") updated_files = [] for file_path in self.template_files: full_path = self.project_path / file_path if self.replace_in_file(full_path, replacements): updated_files.append(file_path) # μΆ”κ°€ νŒŒμΌλ“€λ„ 검사 additional_files = [ "setup_template.py", "scripts/setup_template.py", "scripts/init_blank_template.py" ] for file_path in additional_files: full_path = self.project_path / file_path if self.replace_in_file(full_path, replacements): updated_files.append(file_path) if updated_files: print("βœ… λ‹€μŒ νŒŒμΌλ“€μ΄ μ—…λ°μ΄νŠΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€:") for file_path in updated_files: print(f" - {file_path}") else: print("ℹ️ μ—…λ°μ΄νŠΈν•  파일이 μ—†μŠ΅λ‹ˆλ‹€.") return True def to_pascal_case(self, text: str) -> str: """kebab-caseλ₯Ό PascalCase둜 λ³€ν™˜""" return ''.join(word.capitalize() for word in text.replace('-', '_').split('_')) def to_snake_case(self, text: str) -> str: """kebab-caseλ₯Ό snake_case둜 λ³€ν™˜""" return text.replace('-', '_') def to_upper_snake_case(self, text: str) -> str: """kebab-caseλ₯Ό UPPER_SNAKE_CASE둜 λ³€ν™˜""" return text.replace('-', '_').upper() def create_gitignore(self) -> bool: """κΈ°λ³Έ .gitignore 파일 생성""" try: gitignore_path = self.project_path / ".gitignore" # 이미 .gitignoreκ°€ 있으면 κ±΄λ“œλ¦¬μ§€ μ•ŠμŒ if gitignore_path.exists(): print("ℹ️ .gitignore 파일이 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€.") return True gitignore_content = """# 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/ pip-wheel-metadata/ 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/ # 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 target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .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 # PEP 582; used by e.g. github.com/David-OConnor/pyflow __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/ # IDE .vscode/ .idea/ *.swp *.swo *~ # OS .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db # Project specific logs/ *.log .template_backup/ .template_archive/ # uv .python-version """ gitignore_path.write_text(gitignore_content, encoding='utf-8') print("βœ… .gitignore 파일이 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.") return True except Exception as e: print(f"❌ .gitignore 파일 생성 μ‹€νŒ¨: {e}") return False def run_git_command(self, command: list[str]) -> bool: """Git λͺ…λ Ήμ–΄ μ‹€ν–‰""" try: result = subprocess.run( command, cwd=self.project_path, capture_output=True, text=True, check=True ) return True except subprocess.CalledProcessError as e: print(f"❌ Git λͺ…λ Ήμ–΄ μ‹€ν–‰ μ‹€νŒ¨: {' '.join(command)}") print(f" 였λ₯˜: {e.stderr.strip()}") return False except FileNotFoundError: print("❌ Git이 μ„€μΉ˜λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.") return False def is_git_repository(self) -> bool: """ν˜„μž¬ 디렉토리가 Git μ €μž₯μ†ŒμΈμ§€ 확인""" return (self.project_path / ".git").exists() def init_git_repository(self) -> bool: """Git μ €μž₯μ†Œ μ΄ˆκΈ°ν™”""" try: print("πŸ”§ Git μ €μž₯μ†Œλ₯Ό μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€...") # 이미 Git μ €μž₯μ†ŒμΈμ§€ 확인 if self.is_git_repository(): print("ℹ️ 이미 Git μ €μž₯μ†Œλ‘œ μ΄ˆκΈ°ν™”λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.") return True # Git μ΄ˆκΈ°ν™” if not self.run_git_command(["git", "init"]): return False # κΈ°λ³Έ 브랜치λ₯Ό main으둜 μ„€μ • if not self.run_git_command(["git", "branch", "-M", "main"]): print("⚠️ κΈ°λ³Έ 브랜치 μ„€μ • μ‹€νŒ¨ (계속 μ§„ν–‰)") # .gitignore 생성 self.create_gitignore() # λͺ¨λ“  파일 μΆ”κ°€ if not self.run_git_command(["git", "add", "."]): return False # 초기 컀밋 생성 commit_message = "Initial commit: FastAPI + MCP project setup" if not self.run_git_command(["git", "commit", "-m", commit_message]): return False print("βœ… Git μ €μž₯μ†Œ μ΄ˆκΈ°ν™” μ™„λ£Œ!") print(" - 초기 컀밋 생성됨") print(" - κΈ°λ³Έ 브랜치: main") print(" - .gitignore 파일 생성됨") return True except Exception as e: print(f"❌ Git μ €μž₯μ†Œ μ΄ˆκΈ°ν™” μ‹€νŒ¨: {e}") return False def move_template_files(self) -> bool: """ν…œν”Œλ¦Ώ κ΄€λ ¨ νŒŒμΌλ“€μ„ 별도 ν΄λ”λ‘œ 이동""" try: template_archive = self.project_path / ".template_archive" template_archive.mkdir(exist_ok=True) files_to_move = [ "setup_template.py", "scripts/setup_template.py", "scripts/init_blank_template.py" ] moved_files = [] for file_path in files_to_move: source_file = self.project_path / file_path if source_file.exists(): target_file = template_archive / file_path target_file.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(source_file), str(target_file)) moved_files.append(file_path) if moved_files: print(f"πŸ“ ν…œν”Œλ¦Ώ νŒŒμΌλ“€μ„ {template_archive}둜 μ΄λ™ν–ˆμŠ΅λ‹ˆλ‹€:") for file_path in moved_files: print(f" - {file_path}") # 볡원 슀크립트 생성 restore_script = template_archive / "restore_template.py" restore_code = f'''#!/usr/bin/env python3 """ ν…œν”Œλ¦Ώ 파일 볡원 슀크립트 """ import shutil from pathlib import Path def main(): current_dir = Path(__file__).parent.parent archive_dir = Path(__file__).parent files_to_restore = {moved_files} print("πŸ”„ ν…œν”Œλ¦Ώ νŒŒμΌλ“€μ„ λ³΅μ›ν•©λ‹ˆλ‹€...") for file_path in files_to_restore: source_file = archive_dir / file_path target_file = current_dir / file_path if source_file.exists(): target_file.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(str(source_file), str(target_file)) print(f"βœ… 볡원됨: {{file_path}}") print("βœ… 볡원 μ™„λ£Œ!") if __name__ == "__main__": main() ''' restore_script.write_text(restore_code, encoding='utf-8') restore_script.chmod(0o755) print(f"πŸ“ 볡원 슀크립트 생성: {restore_script}") return True except Exception as e: print(f"❌ 파일 이동 μ‹€νŒ¨: {e}") return False def customize_project(self, skip_git: bool = False) -> bool: """ν”„λ‘œμ νŠΈ μ»€μŠ€ν„°λ§ˆμ΄μ§•""" print("πŸš€ FastAPI + MCP ν”„λ‘œμ νŠΈ μ»€μŠ€ν„°λ§ˆμ΄μ§•") print("=" * 50) # λ°±μ—… 생성 if not self.create_backup(): return False # ν”„λ‘œμ νŠΈ 정보 μˆ˜μ§‘ project_info = {} print("\nπŸ“ ν”„λ‘œμ νŠΈ 정보λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”:") project_info["name"] = self.get_user_input( "ν”„λ‘œμ νŠΈ 이름 (νŒ¨ν‚€μ§€λͺ…)", "my-awesome-api" ) project_info["title"] = self.get_user_input( "ν”„λ‘œμ νŠΈ 제λͺ©", f"{project_info['name'].replace('-', ' ').title()}" ) project_info["description"] = self.get_user_input( "ν”„λ‘œμ νŠΈ μ„€λͺ…", f"{project_info['title']} - FastAPI와 MCPλ₯Ό ν™œμš©ν•œ API μ„œλ²„" ) project_info["author"] = self.get_user_input("μž‘μ„±μž 이름", "") if project_info["author"]: project_info["email"] = self.get_user_input("μž‘μ„±μž 이메일", "") # 확인 print(f"\nπŸ“‹ μ„€μ • μš”μ•½:") print(f" - ν”„λ‘œμ νŠΈλͺ…: {project_info['name']}") print(f" - 제λͺ©: {project_info['title']}") print(f" - μ„€λͺ…: {project_info['description']}") if project_info.get("author"): print(f" - μž‘μ„±μž: {project_info['author']}") if project_info.get("email"): print(f" - 이메일: {project_info['email']}") if not self.get_yes_no("\nβœ… μœ„ μ„€μ •μœΌλ‘œ ν”„λ‘œμ νŠΈλ₯Ό μ»€μŠ€ν„°λ§ˆμ΄μ§•ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?", True): print("❌ μ»€μŠ€ν„°λ§ˆμ΄μ§•μ΄ μ·¨μ†Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.") return False # 파일 μ—…λ°μ΄νŠΈ if not self.update_all_files(project_info): return False # Git μ €μž₯μ†Œ μ΄ˆκΈ°ν™” if not skip_git and self.get_yes_no("\nπŸ”§ Git μ €μž₯μ†Œλ₯Ό μ΄ˆκΈ°ν™”ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?", True): git_success = self.init_git_repository() if not git_success: print("⚠️ Git μ΄ˆκΈ°ν™”μ— μ‹€νŒ¨ν–ˆμ§€λ§Œ ν”„λ‘œμ νŠΈ 섀정은 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.") elif skip_git: print("\nℹ️ Git μ΄ˆκΈ°ν™”λ₯Ό κ±΄λ„ˆλœλ‹ˆλ‹€.") # ν…œν”Œλ¦Ώ 파일 정리 if self.get_yes_no("\n🧹 ν…œν”Œλ¦Ώ κ΄€λ ¨ νŒŒμΌλ“€μ„ μ •λ¦¬ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?", True): self.move_template_files() print("\nπŸŽ‰ ν”„λ‘œμ νŠΈ μ»€μŠ€ν„°λ§ˆμ΄μ§•μ΄ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€!") print("\nπŸ“š λ‹€μŒ 단계:") print("1. uv sync - μ˜μ‘΄μ„± μ„€μΉ˜") print("2. python run_server.py - μ„œλ²„ μ‹€ν–‰") print("3. μ½”λ“œ μˆ˜μ • 및 개발 μ‹œμž‘") if self.is_git_repository(): print("\nπŸ“ Git μ‚¬μš©λ²•:") print("- git status - 변경사항 확인") print("- git add . && git commit -m 'message' - 변경사항 컀밋") print("- git remote add origin <repository-url> - 원격 μ €μž₯μ†Œ μ—°κ²°") print("- git push -u origin main - 원격 μ €μž₯μ†Œμ— ν‘Έμ‹œ") print("\nπŸ”„ 볡원 방법:") print("변경사항을 되돌리렀면 λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μ‚¬μš©ν•˜μ„Έμš”:") print("python setup_template.py --restore") return True def main(): """메인 ν•¨μˆ˜""" parser = argparse.ArgumentParser( description="FastAPI + MCP ν…œν”Œλ¦Ώ μ„€μ • 도ꡬ", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" μ‚¬μš© μ˜ˆμ‹œ: python setup_template.py # ν…œν”Œλ¦Ώ μ»€μŠ€ν„°λ§ˆμ΄μ§• (κΈ°λ³Έ) python setup_template.py --customize # λͺ…μ‹œμ μœΌλ‘œ μ»€μŠ€ν„°λ§ˆμ΄μ§• python setup_template.py --no-git # Git μ΄ˆκΈ°ν™” 없이 μ»€μŠ€ν„°λ§ˆμ΄μ§• python setup_template.py --restore # λ°±μ—…μ—μ„œ 볡원 """ ) parser.add_argument( "--customize", action="store_true", help="ν˜„μž¬ ν…œν”Œλ¦Ώμ„ μ»€μŠ€ν„°λ§ˆμ΄μ§• (κΈ°λ³Έ λ™μž‘)" ) parser.add_argument( "--restore", action="store_true", help="λ°±μ—…μ—μ„œ 볡원" ) parser.add_argument( "--no-git", action="store_true", help="Git μ €μž₯μ†Œ μ΄ˆκΈ°ν™” κ±΄λ„ˆλ›°κΈ°" ) args = parser.parse_args() current_path = Path.cwd() setup = TemplateSetup(current_path) try: if args.restore: # λ°±μ—…μ—μ„œ 볡원 print("πŸ”„ λ°±μ—…μ—μ„œ 볡원") print("=" * 50) success = setup.restore_backup() else: # κΈ°λ³Έ λ™μž‘: ν˜„μž¬ ν…œν”Œλ¦Ώ μ»€μŠ€ν„°λ§ˆμ΄μ§• if not (current_path / "pyproject.toml").exists(): print("❌ 였λ₯˜: ν…œν”Œλ¦Ώ 루트 λ””λ ‰ν† λ¦¬μ—μ„œ μ‹€ν–‰ν•΄μ£Όμ„Έμš”.") sys.exit(1) success = setup.customize_project(skip_git=args.no_git) if success: print("\nβœ… μž‘μ—…μ΄ μ„±κ³΅μ μœΌλ‘œ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€!") else: print("\n❌ μž‘μ—…μ΄ μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.") sys.exit(1) except KeyboardInterrupt: print("\n❌ μ‚¬μš©μžμ— μ˜ν•΄ μ·¨μ†Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.") sys.exit(1) except Exception as e: print(f"\n❌ μ˜ˆμƒμΉ˜ λͺ»ν•œ 였λ₯˜ λ°œμƒ: {e}") sys.exit(1) if __name__ == "__main__": main()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joonheeu/fastapi-mcp-template'

If you have feedback or need assistance with the MCP directory API, please join our Discord server