-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: Add MCP Registry Plan #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
68bdc0d
0d8fb55
a76b24a
b93b098
7f91a2d
fce884d
c12231f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| # 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/ | ||
| *.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 | ||
|
|
||
| # celery beat schedule file | ||
| celerybeat-schedule | ||
|
|
||
| # SageMath parsed files | ||
| *.sage.py | ||
|
|
||
| # Environments | ||
| .env | ||
| .venv | ||
| env/ | ||
| venv/ | ||
| ENV/ | ||
| env.bak/ | ||
| venv.bak/ | ||
|
|
||
| # Spyder project settings | ||
| .spyderproject | ||
| .spyderworkspace | ||
|
|
||
| # Rope project settings | ||
| .ropeproject | ||
|
|
||
| # mkdocs documentation | ||
| /site | ||
|
|
||
| # mypy | ||
| .mypy_cache/ | ||
| .dmypy.json | ||
| dmypy.json | ||
|
|
||
| # Pyre type checker | ||
| .pyre/ | ||
|
|
||
| # Terraform | ||
| .terraform/ | ||
| .terraform.lock.hcl | ||
| terraform.tfstate | ||
| terraform.tfstate.backup | ||
| tfplan | ||
| terraform.tfvars | ||
| *.plan | ||
| *.tfstate | ||
| *.tfstate.backup | ||
|
|
||
| # VSCode | ||
| .vscode/ | ||
|
|
||
| # PyCharm | ||
| .idea/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Use an official Python runtime as a parent image | ||
| FROM python:3.11-slim | ||
|
|
||
| # Set the working directory in the container | ||
| WORKDIR /app | ||
|
|
||
| # Copy the pyproject.toml file and install dependencies | ||
| COPY pyproject.toml . | ||
| COPY ./mcp_registry ./mcp_registry | ||
| RUN pip install --no-cache-dir . | ||
|
|
||
| # Expose the port the app runs on | ||
| EXPOSE 80 | ||
|
|
||
| # Run the application | ||
| CMD ["uvicorn", "mcp_registry.main:app", "--host", "0.0.0.0", "--port", "80"] |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| from sqlalchemy.orm import Session | ||
| from . import models, schemas | ||
|
|
||
| def get_agent(db: Session, agent_id: int): | ||
| return db.query(models.Agent).filter(models.Agent.id == agent_id, models.Agent.is_deleted == False).first() | ||
|
|
||
| def get_agents(db: Session, skip: int = 0, limit: int = 100): | ||
| return db.query(models.Agent).filter(models.Agent.is_deleted == False).offset(skip).limit(limit).all() | ||
|
|
||
| def create_agent(db: Session, agent: schemas.AgentCreate): | ||
| db_agent = models.Agent( | ||
| name=agent.name, | ||
| version=agent.version, | ||
| display_name=agent.display_name, | ||
| description=agent.description, | ||
| owner=agent.owner, | ||
| endpoint=agent.endpoint, | ||
| openapi_spec=agent.openapi_spec, | ||
| environment=agent.environment, | ||
| tags=agent.tags, | ||
| openapi_spec_s3_uri=agent.openapi_spec_s3_uri, | ||
| openapi_spec_checksum=agent.openapi_spec_checksum | ||
| ) | ||
| for capability_data in agent.capabilities: | ||
| capability = get_capability_by_name(db, name=capability_data.name) | ||
| if not capability: | ||
| capability = create_capability(db, capability_data) | ||
| db_agent.capabilities.append(capability) | ||
|
|
||
| db.add(db_agent) | ||
| db.commit() | ||
| db.refresh(db_agent) | ||
| return db_agent | ||
|
|
||
| def get_capability(db: Session, capability_id: int): | ||
| return db.query(models.Capability).filter(models.Capability.id == capability_id).first() | ||
|
|
||
| def get_capability_by_name(db: Session, name: str): | ||
| return db.query(models.Capability).filter(models.Capability.name == name).first() | ||
|
|
||
| def get_capabilities(db: Session, skip: int = 0, limit: int = 100): | ||
| return db.query(models.Capability).offset(skip).limit(limit).all() | ||
|
|
||
| def create_capability(db: Session, capability: schemas.CapabilityCreate): | ||
| db_capability = models.Capability(**capability.dict()) | ||
| db.add(db_capability) | ||
| return db_capability | ||
|
Comment on lines
+44
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing commit when
🐛 Proposed fix def create_capability(db: Session, capability: schemas.CapabilityCreate):
db_capability = models.Capability(**capability.dict())
db.add(db_capability)
+ db.commit()
+ db.refresh(db_capability)
return db_capabilityAlternatively, if you want to keep # In main.py
@app.post("/capabilities/", response_model=schemas.Capability)
def create_capability(
capability: schemas.CapabilityCreate, db: Session = Depends(get_db)
):
db_capability = crud.create_capability(db=db, capability=capability)
db.commit()
db.refresh(db_capability)
return db_capability🤖 Prompt for AI Agents |
||
|
|
||
| def get_agents_by_capability(db: Session, capability_name: str): | ||
| return db.query(models.Agent).join(models.Agent.capabilities).filter(models.Capability.name == capability_name, models.Agent.is_deleted == False).all() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import os | ||
| from dotenv import load_dotenv | ||
|
|
||
| from sqlalchemy import create_engine, Column, String, Text, Boolean, DateTime, ARRAY, Integer, UniqueConstraint, Table, ForeignKey | ||
| from sqlalchemy.dialects.postgresql import JSONB | ||
| from sqlalchemy.ext.declarative import declarative_base | ||
| from sqlalchemy.orm import sessionmaker, relationship | ||
| from datetime import datetime | ||
|
|
||
| load_dotenv() # Load environment variables from .env file | ||
|
|
||
| # Database connection string from environment variable | ||
| DATABASE_URL = os.getenv("DATABASE_URL") | ||
| if not DATABASE_URL: | ||
| raise ValueError("DATABASE_URL environment variable not set") | ||
|
|
||
| engine = create_engine(DATABASE_URL) | ||
| SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | ||
| Base = declarative_base() | ||
|
|
||
| agent_capability_association = Table( | ||
| 'agent_capability_association', | ||
| Base.metadata, | ||
| Column('agent_id', Integer, ForeignKey('agents.id'), primary_key=True), | ||
| Column('capability_id', Integer, ForeignKey('capabilities.id'), primary_key=True) | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| class Agent(Base): | ||
| __tablename__ = "agents" | ||
|
|
||
| id = Column(Integer, primary_key=True, index=True) # Auto-incrementing primary key | ||
| name = Column(String, index=True, nullable=False) | ||
| version = Column(String, index=True, nullable=False) | ||
| display_name = Column(String, nullable=True) | ||
| description = Column(Text, nullable=False) | ||
| owner = Column(String, nullable=False) | ||
| endpoint = Column(String, nullable=False) | ||
| openapi_spec = Column(JSONB, nullable=False) # Storing full OpenAPI spec as JSONB for now | ||
| environment = Column(String, index=True, nullable=False) | ||
| tags = Column(ARRAY(String), nullable=True) | ||
| openapi_spec_s3_uri = Column(String, nullable=True) | ||
| openapi_spec_checksum = Column(String, nullable=True) | ||
| created_at = Column(DateTime, default=datetime.utcnow) | ||
| is_deleted = Column(Boolean, default=False) | ||
|
|
||
| capabilities = relationship( | ||
| "Capability", | ||
| secondary=agent_capability_association, | ||
| back_populates="agents" | ||
| ) | ||
|
|
||
| # Unique constraint on name and version | ||
| __table_args__ = ( | ||
| UniqueConstraint('name', 'version', name='_name_version_uc'), | ||
| ) | ||
|
|
||
| def __repr__(self): | ||
| return f"<Agent(name='{self.name}', version='{self.version}', environment='{self.environment}')>" | ||
|
|
||
| class Capability(Base): | ||
| __tablename__ = 'capabilities' | ||
|
|
||
| id = Column(Integer, primary_key=True, index=True) | ||
| name = Column(String, unique=True, index=True) | ||
| description = Column(Text) | ||
|
|
||
| agents = relationship( | ||
| "Agent", | ||
| secondary=agent_capability_association, | ||
| back_populates="capabilities" | ||
| ) | ||
|
|
||
| def get_db(): | ||
| db = SessionLocal() | ||
| try: | ||
| yield db | ||
| finally: | ||
| db.close() | ||
|
|
||
| def create_db_and_tables(): | ||
| Base.metadata.create_all(bind=engine) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # main.py | ||
|
|
||
| from typing import List | ||
| from fastapi import FastAPI, Depends | ||
| from sqlalchemy.orm import Session | ||
|
|
||
| from . import crud, models, schemas | ||
| from .database import engine, create_db_and_tables, get_db | ||
|
|
||
|
|
||
|
|
||
| app = FastAPI() | ||
|
|
||
|
|
||
|
|
||
| @app.on_event("startup") | ||
| def on_startup(): | ||
| create_db_and_tables() | ||
|
|
||
|
|
||
| @app.post("/agents/", response_model=schemas.Agent) | ||
| def create_agent(agent: schemas.AgentCreate, db: Session = Depends(get_db)): | ||
| return crud.create_agent(db=db, agent=agent) | ||
|
|
||
|
|
||
| @app.get("/agents/", response_model=List[schemas.Agent]) | ||
| def read_agents(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): | ||
| agents = crud.get_agents(db, skip=skip, limit=limit) | ||
| return agents | ||
|
|
||
|
|
||
| @app.get("/search/", response_model=List[schemas.Agent]) | ||
| def search_agents_by_capability(capability_name: str, db: Session = Depends(get_db)): | ||
| return crud.get_agents_by_capability(db=db, capability_name=capability_name) | ||
|
|
||
|
|
||
| @app.post("/capabilities/", response_model=schemas.Capability) | ||
| def create_capability( | ||
| capability: schemas.CapabilityCreate, db: Session = Depends(get_db) | ||
| ): | ||
| return crud.create_capability(db=db, capability=capability) | ||
|
|
||
|
|
||
| @app.get("/") | ||
| async def root(): | ||
| return {"message": "Hello MCP Registry!"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| from typing import List, Optional | ||
| from pydantic import BaseModel, Field | ||
|
|
||
| class CapabilityBase(BaseModel): | ||
| name: str | ||
| description: Optional[str] = None | ||
|
|
||
| class CapabilityCreate(CapabilityBase): | ||
| pass | ||
|
|
||
| class CapabilitySimple(CapabilityBase): | ||
| id: int | ||
|
|
||
| class Config: | ||
| orm_mode = True | ||
|
|
||
| class AgentBase(BaseModel): | ||
| name: str | ||
| version: str | ||
| display_name: Optional[str] = None | ||
| description: str | ||
| owner: str | ||
| endpoint: str | ||
| openapi_spec: dict | ||
| environment: str | ||
| tags: Optional[List[str]] = None | ||
| openapi_spec_s3_uri: Optional[str] = None | ||
| openapi_spec_checksum: Optional[str] = None | ||
|
|
||
| class AgentSimple(AgentBase): | ||
| id: int | ||
|
|
||
| class Config: | ||
| orm_mode = True | ||
|
|
||
| class AgentCreate(AgentBase): | ||
| capabilities: List[CapabilityCreate] = [] | ||
|
|
||
| class Agent(AgentBase): | ||
| id: int | ||
| capabilities: List[CapabilitySimple] = Field(default_factory=list) | ||
|
|
||
| class Config: | ||
| orm_mode = True | ||
|
|
||
| class Capability(CapabilityBase): | ||
| id: int | ||
| agents: List[AgentSimple] = Field(default_factory=list) | ||
|
|
||
| class Config: | ||
| orm_mode = True | ||
|
|
||
| Capability.update_forward_refs() | ||
| Agent.update_forward_refs() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Transaction integrity issue: nested commits can leave orphan capabilities.
When
create_agentcallscreate_capability(line 27), the capability is committed immediately (line 47). If the subsequent agent creation fails, capabilities remain in the database as orphans. Consider deferring commits until the entire operation succeeds.♻️ Proposed fix: defer commits
🤖 Prompt for AI Agents