├── .dockerignore ├── envs ├── youtube.env └── backend.env ├── .env.sample ├── backend ├── api │ ├── core │ │ ├── agent │ │ │ ├── prompts │ │ │ │ ├── system.md │ │ │ │ └── __init__.py │ │ │ ├── persistence.py │ │ │ └── orchestration.py │ │ ├── logs.py │ │ ├── models.py │ │ ├── mcps.py │ │ ├── config.py │ │ └── dependencies.py │ ├── main.py │ ├── Dockerfile │ ├── .vscode │ │ └── launch.json │ ├── pyproject.toml │ └── routers │ │ ├── checkpoints.py │ │ ├── mcps.py │ │ └── llms.py ├── mcp │ ├── main.py │ ├── pyproject.toml │ ├── Dockerfile │ └── uv.lock └── shared_mcp │ ├── models.py │ └── tools.py ├── inspector └── Dockerfile ├── community └── youtube │ └── build.sh ├── docs ├── grafana-stack.md ├── supabase.md ├── langgraph.md ├── langfuse.md └── mcp.md ├── compose.yaml ├── .github └── workflows │ └── build.yaml ├── nginx └── nginx.conf ├── LICENSE ├── compose-dev.yaml ├── .devcontainer └── devcontainer.json ├── .gitignore └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .venv 2 | -------------------------------------------------------------------------------- /envs/youtube.env: -------------------------------------------------------------------------------- 1 | YOUTUBE_API_KEY= 2 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # change if required 2 | MCP_SERVER_PORT=8050 3 | -------------------------------------------------------------------------------- /backend/api/core/agent/prompts/system.md: -------------------------------------------------------------------------------- 1 | You are a helpful assistant. 2 | -------------------------------------------------------------------------------- /backend/mcp/main.py: -------------------------------------------------------------------------------- 1 | from shared_mcp.tools import mcp 2 | 3 | if __name__ == "__main__": 4 | mcp.run(transport="sse") 5 | -------------------------------------------------------------------------------- /backend/shared_mcp/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class ToolRequest(BaseModel): 5 | tool_name: str 6 | a: int 7 | b: int 8 | -------------------------------------------------------------------------------- /backend/api/core/logs.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | 3 | from rich.pretty import pprint as print 4 | 5 | print # facade 6 | 7 | uvicorn = getLogger("uvicorn") 8 | -------------------------------------------------------------------------------- /inspector/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.15.0-alpine 2 | 3 | WORKDIR /app 4 | RUN apk update && apk add curl && \ 5 | npm install -g @modelcontextprotocol/inspector 6 | ENTRYPOINT ["npx", "@modelcontextprotocol/inspector"] 7 | -------------------------------------------------------------------------------- /backend/mcp/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-server" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "mcp[cli]>=1.6.0", 9 | ] 10 | -------------------------------------------------------------------------------- /backend/api/core/agent/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def read_system_prompt(): 5 | with open(os.path.join(os.path.dirname(__file__), "system.md"), "r") as f: 6 | return f.read() 7 | 8 | 9 | SYSTEM_PROMPT = read_system_prompt() 10 | -------------------------------------------------------------------------------- /backend/api/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from api.routers import checkpoints, llms, mcps 4 | 5 | app = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True}) 6 | app.include_router(llms.router, prefix="/v1") 7 | app.include_router(mcps.router, prefix="/v1") 8 | app.include_router(checkpoints.router, prefix="/v1") 9 | -------------------------------------------------------------------------------- /envs/backend.env: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | # do not specify driver (do not specify `+psycopg`) 3 | POSTGRES_DSN= 4 | 5 | LANGFUSE_PUBLIC_KEY=pk-lf- 6 | LANGFUSE_SECRET_KEY=sk-lf- 7 | LANGFUSE_HOST=https://cloud.langfuse.com 8 | 9 | ENVIRONMENT=production 10 | # if you set this to `MCP_HOSTNAMES_CSV=mcp,youtube`, make sure both services are up 11 | MCP_HOSTNAMES_CSV=mcp 12 | -------------------------------------------------------------------------------- /community/youtube/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run --rm --entrypoint sh \ 4 | --volume /var/run/docker.sock:/var/run/docker.sock \ 5 | --workdir /app \ 6 | docker:dind \ 7 | -c " 8 | git clone https://github.com/Klavis-AI/klavis.git . && \ 9 | touch mcp_servers/youtube/.env && \ 10 | docker build -t youtube-mcp-server -f mcp_servers/youtube/Dockerfile . 11 | " 12 | -------------------------------------------------------------------------------- /backend/mcp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:python3.13-bookworm 2 | 3 | WORKDIR /app 4 | COPY ./backend/mcp/uv.lock ./backend/mcp/pyproject.toml . 5 | RUN uv sync --frozen && rm ./uv.lock ./pyproject.toml 6 | RUN apt-get update && apt-get install -y --no-install-recommends curl 7 | COPY ./backend/mcp ./mcp 8 | COPY ./backend/shared_mcp ./shared_mcp 9 | ENV PYTHONPATH /app:$PYTHONPATH 10 | ENTRYPOINT ["uv", "run", "mcp/main.py"] 11 | -------------------------------------------------------------------------------- /backend/api/core/models.py: -------------------------------------------------------------------------------- 1 | from langchain_core.tools import StructuredTool 2 | from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver 3 | from mcp import ClientSession 4 | from pydantic import BaseModel 5 | 6 | 7 | class Resource(BaseModel): 8 | checkpointer: AsyncPostgresSaver 9 | tools: list[StructuredTool] 10 | sessions: list[ClientSession] 11 | 12 | class Config: 13 | arbitrary_types_allowed = True 14 | -------------------------------------------------------------------------------- /backend/shared_mcp/tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from mcp.server.fastmcp import FastMCP 4 | 5 | mcp = FastMCP( 6 | "MCP Server", 7 | port=os.environ["MCP_SERVER_PORT"], 8 | ) 9 | 10 | 11 | @mcp.tool() 12 | def add(a: int, b: int) -> int: 13 | """Add two numbers""" 14 | return a + b 15 | 16 | 17 | @mcp.resource("greeting://{name}") 18 | def get_greeting(name: str) -> str: 19 | """Get a personalized greeting""" 20 | return f"Hello, {name}!" 21 | -------------------------------------------------------------------------------- /backend/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:python3.13-bookworm 2 | 3 | WORKDIR /app 4 | COPY ./backend/api/uv.lock ./backend/api/pyproject.toml ./ 5 | RUN uv sync --frozen && rm ./uv.lock ./pyproject.toml 6 | RUN apt-get update && apt-get install -y --no-install-recommends curl 7 | COPY ./envs/backend.env /opt/.env 8 | COPY ./backend/api /app/api 9 | COPY ./backend/shared_mcp /app/shared_mcp 10 | ENV PYTHONPATH /app:$PYTHONPATH 11 | ENTRYPOINT ["uv", "run", "fastapi", "run", "api/main.py", "--root-path=/api"] 12 | -------------------------------------------------------------------------------- /docs/grafana-stack.md: -------------------------------------------------------------------------------- 1 | # Grafana Stack 2 | 3 | > By configuring the OpenAI Integration, users can gain valuable insights into token usage rates, response times, and overall costs. This integration empowers users to make data-driven decisions while ensuring optimal utilization of OpenAI APIs. 4 | 5 | Learn more [here](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/integration-reference/integration-openai/) 6 | 7 | [![Grafana](https://img.shields.io/github/stars/prometheus/prometheus?logo=grafana&label=Grafana)](https://github.com/grafana/grafana) 8 | -------------------------------------------------------------------------------- /docs/supabase.md: -------------------------------------------------------------------------------- 1 | # Supabase 2 | 3 | Demo on getting `POSTGRES_DSN` with Supabase’s free database, auth, and APIs. 4 | 5 | [![Supabase](https://img.shields.io/github/stars/supabase/supabase?logo=supabase&label=Supabase)](https://github.com/supabase/supabase) 6 | 7 | ## Features 8 | 9 | Visit [here](https://supabase.com/) for a full list of features and learn more. 10 | 11 | - Postgres Relational Database 12 | - Authentication 13 | - User Sign up and Login 14 | - Row and Column Security 15 | - Data APIs 16 | - Auto Generated from Table Schema 17 | - Self Host for free 18 | - Free Cloud usage for rapid prototyping 19 | -------------------------------------------------------------------------------- /backend/api/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python Debugger: FastAPI", 9 | "type": "debugpy", 10 | "request": "launch", 11 | "module": "fastapi", 12 | "args": [ 13 | "run", 14 | "api/main.py", 15 | "--root-path=/api", 16 | "--reload" 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /backend/api/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "api" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "bs4==0.0.2", 9 | "faker==30.8.1", 10 | "fastapi[standard]==0.115.11", 11 | "langchain==0.3.6", 12 | "langchain-community==0.3.4", 13 | "langchain-mcp-adapters>=0.0.9", 14 | "langchain-openai==0.2.3", 15 | "langfuse==2.60.2", 16 | "langgraph==0.2.39", 17 | "langgraph-checkpoint-postgres>=2.0.21", 18 | "mcp[cli]>=1.6.0", 19 | "prometheus-client==0.21.1", 20 | "psycopg[binary]==3.2.3", 21 | "pydantic-settings==2.6.0", 22 | "pypdf==5.1.0", 23 | "rich==13.9.4", 24 | "sqlmodel>=0.0.24", 25 | "sse-starlette==2.1.3", 26 | ] 27 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | image: api:prod 4 | env_file: 5 | - .env 6 | restart: unless-stopped 7 | healthcheck: 8 | test: curl -f http://localhost:8000/docs 9 | interval: 30s 10 | timeout: 10s 11 | retries: 3 12 | 13 | mcp: 14 | image: mcp:prod 15 | env_file: 16 | - .env 17 | restart: unless-stopped 18 | 19 | nginx: 20 | image: nginx:1.26.3-alpine 21 | ports: 22 | - 80:80 23 | volumes: 24 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf 25 | restart: unless-stopped 26 | depends_on: 27 | api: 28 | condition: service_healthy 29 | healthcheck: 30 | test: curl -f http://localhost/docs 31 | interval: 30s 32 | timeout: 10s 33 | retries: 3 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Compose CI 2 | 3 | on: 4 | pull_request: 5 | branches: ["main"] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | services: 11 | docker: 12 | image: docker:dind 13 | options: --privileged 14 | volumes: 15 | - /var/run/docker.sock:/var/run/docker.sock 16 | env: 17 | MCP_SERVER_PORT: ${{ secrets.MCP_SERVER_PORT }} 18 | YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }} 19 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 20 | POSTGRES_DSN: ${{ secrets.POSTGRES_DSN }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4.2.2 24 | 25 | - name: Build production and development docker images 26 | run: | 27 | ./community/youtube/build.sh 28 | docker compose build 29 | 30 | - name: Run docker compose 31 | uses: hoverkraft-tech/compose-action@v2.0.1 32 | with: 33 | compose-file: "compose-dev.yaml" 34 | -------------------------------------------------------------------------------- /backend/api/core/mcps.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import AsyncGenerator 3 | 4 | from mcp import ClientSession 5 | from mcp.client.sse import sse_client 6 | 7 | from api.core.config import settings 8 | 9 | 10 | @asynccontextmanager 11 | async def mcp_sse_client( 12 | mcp_host: str = "mcp", 13 | ) -> AsyncGenerator[ClientSession]: 14 | """ 15 | Creates and initializes an MCP client session over SSE. 16 | 17 | Establishes an SSE connection to the MCP server and yields an initialized 18 | `ClientSession` for communication. 19 | 20 | Yields: 21 | ClientSession: An initialized MCP client session. 22 | """ 23 | async with sse_client( 24 | f"http://{mcp_host}:{settings.mcp_server_port}/sse" 25 | ) as ( 26 | read_stream, 27 | write_stream, 28 | ): 29 | async with ClientSession(read_stream, write_stream) as session: 30 | await session.initialize() 31 | yield session 32 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream api { 2 | server api:8000; 3 | } 4 | 5 | server { 6 | listen 80; 7 | 8 | location / { 9 | return 301 /api/docs; 10 | } 11 | 12 | location /api/ { 13 | add_header Access-Control-Allow-Origin "*"; 14 | add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, HEAD"; 15 | add_header Access-Control-Allow-Headers "content-type"; 16 | 17 | proxy_set_header Cache-Control 'no-cache'; 18 | proxy_set_header Content-Type $http_content_type; 19 | proxy_set_header X-Accel-Buffering 'no'; 20 | 21 | proxy_http_version 1.1; 22 | proxy_set_header Connection ""; 23 | proxy_set_header Upgrade $http_upgrade; 24 | proxy_set_header Host $host; 25 | 26 | proxy_read_timeout 600; 27 | proxy_send_timeout 600; 28 | 29 | proxy_buffering off; 30 | proxy_redirect off; 31 | proxy_pass http://api/; 32 | } 33 | 34 | location /api/docs { 35 | proxy_redirect off; 36 | proxy_pass http://api/docs; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Nicholas Goh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/api/routers/checkpoints.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from sqlalchemy import text 3 | 4 | from api.core.dependencies import EngineDep 5 | from api.core.logs import uvicorn 6 | 7 | TABLES = [ 8 | "checkpoints", 9 | "checkpoint_migrations", 10 | "checkpoint_blobs", 11 | "checkpoint_writes", 12 | ] 13 | router = APIRouter(tags=["checkpoints"]) 14 | 15 | 16 | @router.delete("/truncate") 17 | async def truncate_checkpoints(engine: EngineDep): 18 | """ 19 | Truncates all checkpoint-related tables from LangGraph AsyncPostgresSaver. 20 | 21 | This operation removes all records from the following tables: 22 | - checkpoints 23 | - checkpoint_migrations 24 | - checkpoint_blobs 25 | - checkpoint_writes 26 | 27 | **Warning**: This action is irreversible and should be used with caution. Ensure proper backups are in place 28 | before performing this operation. 29 | """ 30 | 31 | async with engine.begin() as conn: 32 | for table in TABLES: 33 | await conn.execute(text(f"TRUNCATE TABLE {table};")) 34 | uvicorn.info(f"Truncated table {table}") 35 | return { 36 | "status": "success", 37 | "message": "All checkpoint-related tables truncated successfully.", 38 | } 39 | -------------------------------------------------------------------------------- /backend/api/routers/mcps.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from fastapi import APIRouter 4 | from mcp import types 5 | 6 | from api.core.mcps import mcp_sse_client 7 | from shared_mcp.models import ToolRequest 8 | 9 | router = APIRouter(prefix="/mcps", tags=["mcps"]) 10 | 11 | 12 | @router.get("/list-tools") 13 | async def list_tools() -> Iterable[types.Tool]: 14 | """ 15 | Lists tools available from MCP server 16 | 17 | This endpoint establishes a Server-Sent Events connection with the client 18 | and forwards communication to the Model Context Protocol server. 19 | """ 20 | async with mcp_sse_client() as session: 21 | response = await session.list_tools() 22 | return response.tools 23 | 24 | 25 | @router.post("/call-tool") 26 | async def call_tool(request: ToolRequest) -> str: 27 | """ 28 | Calls tool available from MCP server 29 | 30 | This endpoint establishes a Server-Sent Events connection with the client 31 | and forwards communication to the Model Context Protocol server. 32 | """ 33 | async with mcp_sse_client() as session: 34 | response = await session.call_tool( 35 | request.tool_name, 36 | arguments=request.model_dump(exclude=["tool_name"]), 37 | ) 38 | return response.content[0].text 39 | -------------------------------------------------------------------------------- /compose-dev.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | image: api:prod 4 | build: 5 | dockerfile: ./backend/api/Dockerfile 6 | entrypoint: uv run fastapi run api/main.py --root-path=/api --reload 7 | env_file: 8 | - ./envs/backend.env 9 | ports: 10 | - 8000:8000 11 | volumes: 12 | - ./backend/api:/app/api 13 | - ./backend/api/.vscode/:/app/.vscode 14 | - ./backend/shared_mcp:/app/shared_mcp 15 | 16 | mcp: 17 | image: mcp:prod 18 | build: 19 | dockerfile: ./backend/mcp/Dockerfile 20 | environment: 21 | - MCP_SERVER_PORT=${MCP_SERVER_PORT} 22 | ports: 23 | - ${MCP_SERVER_PORT}:${MCP_SERVER_PORT} 24 | volumes: 25 | - ./backend/mcp:/app/mcp 26 | - ./backend/shared_mcp:/app/shared_mcp 27 | 28 | youtube: 29 | image: youtube-mcp-server 30 | env_file: 31 | - ./envs/youtube.env 32 | environment: 33 | - YOUTUBE_MCP_SERVER_PORT=${MCP_SERVER_PORT} 34 | ports: 35 | - 5000:${MCP_SERVER_PORT} 36 | 37 | dbhub: 38 | image: bytebase/dbhub:0.3.3 39 | ports: 40 | - 8080:${MCP_SERVER_PORT} 41 | command: > 42 | --transport sse 43 | --port ${MCP_SERVER_PORT} 44 | --dsn ${POSTGRES_DSN} 45 | 46 | inspector: 47 | image: inspector:prod 48 | build: 49 | dockerfile: ./inspector/Dockerfile 50 | ports: 51 | - 6274:6274 52 | - 6277:6277 53 | 54 | nginx: 55 | image: nginx:1.26.3-alpine 56 | ports: 57 | - 80:80 58 | volumes: 59 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf 60 | -------------------------------------------------------------------------------- /backend/api/core/agent/persistence.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import AsyncGenerator 3 | 4 | import psycopg 5 | import psycopg.errors 6 | import uvicorn 7 | from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver 8 | from psycopg_pool import AsyncConnectionPool 9 | 10 | from api.core.logs import uvicorn 11 | 12 | 13 | @asynccontextmanager 14 | async def checkpointer_context( 15 | conn_str: str, 16 | ) -> AsyncGenerator[AsyncPostgresSaver]: 17 | """ 18 | Async context manager that sets up and yields a LangGraph checkpointer. 19 | 20 | Uses a psycopg async connection pool to initialize AsyncPostgresSaver. 21 | Skips setup if checkpointer is already configured. 22 | 23 | Args: 24 | conn_str (str): PostgreSQL connection string. 25 | 26 | Yields: 27 | AsyncPostgresSaver: The initialized checkpointer. 28 | """ 29 | # NOTE: LangGraph AsyncPostgresSaver does not support SQLAlchemy ORM Connections. 30 | # A compatible psycopg connection is created via the connection pool to connect to the checkpointer. 31 | async with AsyncConnectionPool( 32 | conninfo=conn_str, 33 | kwargs=dict(prepare_threshold=None, autocommit=True), 34 | ) as pool: 35 | checkpointer = AsyncPostgresSaver(pool) 36 | try: 37 | await checkpointer.setup() 38 | except ( 39 | psycopg.errors.DuplicateColumn, 40 | psycopg.errors.ActiveSqlTransaction, 41 | ): 42 | uvicorn.warning("Skipping checkpointer setup — already configured.") 43 | yield checkpointer 44 | -------------------------------------------------------------------------------- /backend/api/core/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import PostgresDsn, computed_field 2 | from pydantic_settings import BaseSettings, SettingsConfigDict 3 | 4 | 5 | class Settings(BaseSettings): 6 | model_config = SettingsConfigDict( 7 | env_file="/opt/.env", 8 | env_ignore_empty=True, 9 | extra="ignore", 10 | ) 11 | 12 | model: str = "gpt-4o-mini-2024-07-18" 13 | openai_api_key: str = "" 14 | mcp_server_port: int = 8050 15 | 16 | postgres_dsn: PostgresDsn = ( 17 | "postgresql://postgres:password@example.supabase.com:6543/postgres" 18 | ) 19 | 20 | @computed_field 21 | @property 22 | def orm_conn_str(self) -> str: 23 | # NOTE: Explicitly follow LangGraph AsyncPostgresSaver 24 | # and use psycopg driver for ORM 25 | return self.postgres_dsn.encoded_string().replace( 26 | "postgresql://", "postgresql+psycopg://" 27 | ) 28 | 29 | @computed_field 30 | @property 31 | def checkpoint_conn_str(self) -> str: 32 | # NOTE: LangGraph AsyncPostgresSaver has some issues 33 | # with specifying psycopg driver explicitly 34 | return self.postgres_dsn.encoded_string() 35 | 36 | langfuse_public_key: str = "" 37 | langfuse_secret_key: str = "" 38 | langfuse_host: str = "https://cloud.langfuse.com" 39 | 40 | environment: str = "development" 41 | mcp_hostnames_csv: str = "mcp" 42 | 43 | @computed_field 44 | @property 45 | def mcp_hostnames(self) -> list[str]: 46 | return [ 47 | h.strip() for h in self.mcp_hostnames_csv.split(",") if h.strip() 48 | ] 49 | 50 | 51 | settings = Settings() 52 | -------------------------------------------------------------------------------- /docs/langgraph.md: -------------------------------------------------------------------------------- 1 | # LangGraph 2 | 3 | > Gain control with LangGraph to design agents that reliably handle complex tasks. 4 | 5 | Learn more [here](https://www.langchain.com/langgraph) 6 | 7 | [![LangGraph](https://img.shields.io/github/stars/langchain-ai/langgraph?logo=langgraph&label=LangGraph)](https://github.com/langchain-ai/langgraph) 8 | 9 | ## Streaming 10 | 11 | At its core, a compiled LangGraph is a [Runnable](https://github.com/langchain-ai/langchain/blob/langchain%3D%3D0.3.6/libs/core/langchain_core/runnables/base.py#L108). This template utilizes LangChain’s built-in streaming support through [`astream_events`](https://python.langchain.com/docs/how_to/streaming/#using-stream-events), granting programmatic access to every stage of the Agentic Workflow. You can observe and interact with key components—LLM, prompt, and tool—throughout their full execution lifecycle: start, stream, and end. For a comprehensive list of event types and usage examples, refer to the [Event Reference](https://python.langchain.com/docs/how_to/streaming/#event-reference). 12 | 13 | ## Persistence 14 | 15 | LangGraph offers built-in state management and persistence via the [AsyncPostgresSaver](https://github.com/langchain-ai/langgraph/blob/0.2.39/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py#L39), enabling faster iteration on agentic workflows. Since LLMs are inherently stateless, chat history must typically be injected as context for each query—but LangGraph abstracts this away, requiring only a `thread_id`. It seamlessly handles chat history and metadata serialization/deserialization, simplifying development. Learn more about its advanced persistence capabilities [here](https://langchain-ai.github.io/langgraph/concepts/persistence/). 16 | -------------------------------------------------------------------------------- /docs/langfuse.md: -------------------------------------------------------------------------------- 1 | # Langfuse 2 | 3 | > Traces, evals, prompt management and metrics to debug and improve your LLM application. 4 | 5 | Learn more [here](https://langfuse.com/) 6 | 7 | [![LangFuse](https://img.shields.io/github/stars/langfuse/langfuse?logo=langfuse&label=LangFuse)](https://github.com/langfuse/langfuse) 8 | 9 | Below is an outline of the steps involved in a simple Math Agent. Key elements illustrated include: 10 | 11 | - A visual breakdown of each step—e.g., when the agent invokes a tool and when control returns to the agent 12 | - Inputs and outputs at each stage of the process: 13 | - **User to Agent**: The user asks a natural language question — e.g., `What is 1 + 1?` 14 | - **Agent to Tool**: The agent decides to call a calculator tool with structured arguments — e.g., `args: { a: 1, b: 1 }`. 15 | - **Tool to Agent**: The tool executes the operation and returns the result — e.g., `2`. 16 | - **Agent to User**: The agent responds with the final answer in natural language — e.g., `1 + 1 = 2`. 17 | - The full chat history throughout the interaction 18 | - Latency and cost associated with each node 19 | 20 | ### Step 1: Math Agent (User to Agent → Agent to Tool) 21 | 22 | This section shows: 23 | 24 | - **User to Agent**: The user asks a natural language question — e.g., `What is 1 + 1?` 25 | - **Agent to Tool**: The agent decides to call a calculator tool with structured arguments — e.g., `args: { a: 1, b: 1 }`. 26 | - **Full Chat History Throughout the Interaction**: You can inspect earlier user-agent messages. For instance: 27 | 28 | ```text 29 | User: reply only no 30 | Agent: No. 31 | ``` 32 | 33 | In this example, the agent responded directly without calling any tools. 34 | 35 | ### Step 2: Tool Call (Tool to Agent) 36 | 37 | This section shows: 38 | 39 | - **Tool to Agent**: The tool executes the operation and returns the result — e.g., `2`. 40 | 41 | ### Step 3: Math Agent (Agent to User) 42 | 43 | This section shows: 44 | 45 | - **Agent to User**: The agent responds with the final answer in natural language — e.g., `1 + 1 = 2`. 46 | -------------------------------------------------------------------------------- /backend/api/core/dependencies.py: -------------------------------------------------------------------------------- 1 | from contextlib import AsyncExitStack, asynccontextmanager 2 | from typing import Annotated, AsyncGenerator 3 | 4 | from fastapi import Depends 5 | from langchain_mcp_adapters.tools import load_mcp_tools 6 | from langchain_openai import ChatOpenAI 7 | from langfuse.callback import CallbackHandler 8 | from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine 9 | 10 | from api.core.agent.persistence import checkpointer_context 11 | from api.core.config import settings 12 | from api.core.mcps import mcp_sse_client 13 | from api.core.models import Resource 14 | 15 | 16 | def get_llm() -> ChatOpenAI: 17 | return ChatOpenAI( 18 | streaming=True, 19 | model=settings.model, 20 | temperature=0, 21 | api_key=settings.openai_api_key, 22 | stream_usage=True, 23 | ) 24 | 25 | 26 | LLMDep = Annotated[ChatOpenAI, Depends(get_llm)] 27 | 28 | 29 | engine: AsyncEngine = create_async_engine(settings.orm_conn_str) 30 | 31 | 32 | def get_engine() -> AsyncEngine: 33 | return engine 34 | 35 | 36 | EngineDep = Annotated[AsyncEngine, Depends(get_engine)] 37 | 38 | 39 | @asynccontextmanager 40 | async def setup_graph() -> AsyncGenerator[Resource]: 41 | async with checkpointer_context( 42 | settings.checkpoint_conn_str 43 | ) as checkpointer: 44 | tools = [] 45 | sessions = [] 46 | async with AsyncExitStack() as stack: 47 | for hostname in settings.mcp_hostnames: 48 | session = await stack.enter_async_context( 49 | mcp_sse_client(hostname) 50 | ) 51 | tools += await load_mcp_tools(session) 52 | sessions.append(session) 53 | yield Resource( 54 | checkpointer=checkpointer, 55 | tools=tools, 56 | sessions=sessions, 57 | ) 58 | 59 | 60 | def get_langfuse_handler() -> CallbackHandler: 61 | 62 | return CallbackHandler( 63 | public_key=settings.langfuse_public_key, 64 | secret_key=settings.langfuse_secret_key, 65 | host=settings.langfuse_host, 66 | session_id=settings.environment, 67 | environment=settings.environment, 68 | ) 69 | 70 | 71 | LangfuseHandlerDep = Annotated[CallbackHandler, Depends(get_langfuse_handler)] 72 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "API Development Container", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "dockerComposeFile": [ 7 | "../compose-dev.yaml" 8 | ], 9 | "service": "api", 10 | "workspaceFolder": "/app", 11 | "customizations": { 12 | "vscode": { 13 | "extensions": [ 14 | "IronGeek.vscode-env", 15 | "cweijan.vscode-redis-client", 16 | "emeraldwalk.RunOnSave", 17 | "mohamed-nouri.websocket-client", 18 | "ms-azuretools.vscode-docker", 19 | "usernamehw.errorlens", 20 | "Gruntfuggly.todo-tree", 21 | "ms-python.black-formatter", 22 | "ms-python.debugpy", 23 | "ms-python.isort", 24 | "ms-python.python" 25 | ], 26 | "settings": { 27 | "files.exclude": { 28 | "**/.venv": true 29 | }, 30 | "workbench.colorCustomizations": { 31 | "editorUnnecessaryCode.border": "#ff0000" 32 | }, 33 | "explorer.confirmDelete": false, 34 | "editor.cursorBlinking": "expand", 35 | "editor.cursorSmoothCaretAnimation": "on", 36 | "todo-tree.highlights.enabled": false, 37 | "window.density.editorTabHeight": "compact", 38 | "workbench.activityBar.location": "top", 39 | "editor.formatOnSave": true, 40 | "files.insertFinalNewline": true, 41 | "files.trimFinalNewlines": true, 42 | "python.venvPath": "/app/.venv/bin/python", 43 | "[python]": { 44 | "editor.formatOnSave": true, 45 | "editor.codeActionsOnSave": { 46 | "source.organizeImports": "always", 47 | "source.unusedImports": "always" 48 | } 49 | }, 50 | "editor.rulers": [ 51 | { 52 | "column": 80, 53 | "color": "#303030" 54 | }, 55 | { 56 | "column": 72, 57 | "color": "#242424" 58 | } 59 | ], 60 | "black-formatter.args": [ 61 | "--line-length=80" 62 | ], 63 | "emeraldwalk.runonsave": { 64 | "commands": [ 65 | { 66 | "match": "((packages)|(requirements))\\.txt", 67 | "isAsync": true, 68 | "cmd": "sort -o ${file} ${file}" 69 | } 70 | ] 71 | }, 72 | "python.analysis.autoImportCompletions": true 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/api/core/agent/orchestration.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 4 | from langchain_core.runnables.base import RunnableSequence 5 | from langchain_core.tools import StructuredTool 6 | from langchain_openai import ChatOpenAI 7 | from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver 8 | from langgraph.graph import MessagesState, StateGraph 9 | from langgraph.graph.state import CompiledStateGraph 10 | from langgraph.prebuilt import ToolNode, tools_condition 11 | 12 | from api.core.agent.prompts import SYSTEM_PROMPT 13 | from api.core.dependencies import LangfuseHandlerDep 14 | 15 | 16 | class State(MessagesState): 17 | next: str 18 | 19 | 20 | def agent_factory( 21 | llm: ChatOpenAI, tools: list[StructuredTool], system_prompt: str 22 | ) -> RunnableSequence: 23 | prompt = ChatPromptTemplate.from_messages( 24 | [ 25 | ("system", system_prompt), 26 | MessagesPlaceholder(variable_name="messages"), 27 | ] 28 | ) 29 | if tools: 30 | agent = prompt | llm.bind_tools(tools) 31 | else: 32 | agent = prompt | llm 33 | return agent 34 | 35 | 36 | def agent_node_factory( 37 | state: State, 38 | agent: RunnableSequence, 39 | ) -> State: 40 | result = agent.invoke(state) 41 | return dict(messages=[result]) 42 | 43 | 44 | def graph_factory( 45 | agent_node: functools.partial, 46 | tools: list[StructuredTool], 47 | checkpointer: AsyncPostgresSaver | None = None, 48 | name: str = "agent_node", 49 | ) -> CompiledStateGraph: 50 | graph_builder = StateGraph(State) 51 | graph_builder.add_node(name, agent_node) 52 | graph_builder.add_node("tools", ToolNode(tools)) 53 | 54 | graph_builder.add_conditional_edges(name, tools_condition) 55 | graph_builder.add_edge("tools", name) 56 | 57 | graph_builder.set_entry_point(name) 58 | graph = graph_builder.compile(checkpointer=checkpointer) 59 | return graph 60 | 61 | 62 | def get_graph( 63 | llm: ChatOpenAI, 64 | tools: list[StructuredTool] = [], 65 | system_prompt: str = SYSTEM_PROMPT, 66 | name: str = "agent_node", 67 | checkpointer: AsyncPostgresSaver | None = None, 68 | ) -> CompiledStateGraph: 69 | agent = agent_factory(llm, tools, system_prompt) 70 | worker_node = functools.partial(agent_node_factory, agent=agent) 71 | return graph_factory(worker_node, tools, checkpointer, name) 72 | 73 | 74 | def get_config(langfuse_handler: LangfuseHandlerDep): 75 | return dict( 76 | configurable=dict(thread_id="1"), 77 | callbacks=[langfuse_handler], 78 | ) 79 | -------------------------------------------------------------------------------- /backend/api/routers/llms.py: -------------------------------------------------------------------------------- 1 | from typing import AsyncGenerator 2 | 3 | import psycopg.errors 4 | from fastapi import APIRouter 5 | from langchain_core.messages import HumanMessage 6 | from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver 7 | from sse_starlette.sse import EventSourceResponse 8 | from starlette.responses import Response 9 | 10 | from api.core.agent.orchestration import get_config, get_graph 11 | from api.core.dependencies import LangfuseHandlerDep, LLMDep, setup_graph 12 | from api.core.logs import print, uvicorn 13 | 14 | router = APIRouter(tags=["chat"]) 15 | 16 | 17 | @router.get("/chat/completions") 18 | async def completions(query: str, llm: LLMDep) -> Response: 19 | """ 20 | Stream model completions as Server-Sent Events (SSE). 21 | 22 | This endpoint sends the model's responses in real-time as they are generated, 23 | allowing for a continuous stream of data to the client. 24 | """ 25 | return EventSourceResponse(stream_completions(query, llm)) 26 | 27 | 28 | @router.get("/chat/agent") 29 | async def agent( 30 | query: str, 31 | llm: LLMDep, 32 | langfuse_handler: LangfuseHandlerDep, 33 | ) -> Response: 34 | """Stream LangGraph completions as Server-Sent Events (SSE). 35 | 36 | This endpoint streams LangGraph-generated events in real-time, allowing the client 37 | to receive responses as they are processed, useful for agent-based workflows. 38 | """ 39 | return EventSourceResponse(stream_graph(query, llm, langfuse_handler)) 40 | 41 | 42 | async def stream_completions( 43 | query: str, llm: LLMDep 44 | ) -> AsyncGenerator[dict[str, str], None]: 45 | async for chunk in llm.astream_events(query): 46 | yield dict(data=chunk) 47 | 48 | 49 | async def checkpointer_setup(pool): 50 | checkpointer = AsyncPostgresSaver(pool) 51 | try: 52 | await checkpointer.setup() 53 | except ( 54 | psycopg.errors.DuplicateColumn, 55 | psycopg.errors.ActiveSqlTransaction, 56 | ): 57 | uvicorn.warning("Skipping checkpointer setup — already configured.") 58 | return checkpointer 59 | 60 | 61 | async def stream_graph( 62 | query: str, 63 | llm: LLMDep, 64 | langfuse_handler: LangfuseHandlerDep, 65 | ) -> AsyncGenerator[dict[str, str], None]: 66 | async with setup_graph() as resource: 67 | graph = get_graph( 68 | llm, 69 | tools=resource.tools, 70 | checkpointer=resource.checkpointer, 71 | name="math_agent", 72 | ) 73 | config = get_config(langfuse_handler) 74 | events = dict(messages=[HumanMessage(content=query)]) 75 | 76 | async for event in graph.astream_events( 77 | events, 78 | config, 79 | version="v2", 80 | stream_mode="updates", 81 | ): 82 | yield dict(data=event) 83 | print(event) 84 | -------------------------------------------------------------------------------- /docs/mcp.md: -------------------------------------------------------------------------------- 1 | # Model Context Protocol 2 | 3 | > [!INFO] 4 | > MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools. 5 | 6 | Learn more [here](https://modelcontextprotocol.io/introduction). 7 | 8 | [![MCP Client](https://img.shields.io/github/stars/modelcontextprotocol/python-sdk?logo=modelcontextprotocol&label=MCP-Client)](https://github.com/modelcontextprotocol/python-sdk) [![MCP Server](https://img.shields.io/github/stars/modelcontextprotocol/servers?logo=modelcontextprotocol&label=MCP-Servers)](https://github.com/modelcontextprotocol/servers) 9 | 10 | ## Key Features 11 | 12 | > MCP helps you build agents and complex workflows on top of LLMs. LLMs frequently need to integrate with data and tools, and MCP provides: 13 | > - A growing list of pre-built integrations that your LLM can directly plug into 14 | > - The flexibility to switch between LLM providers and vendors 15 | > - Best practices for securing your data within your infrastructure 16 | 17 | ## Inspector 18 | 19 | Explore community and your custom MCP servers via Inspector at [http://localhost:6274](http://localhost:6274) in [Development](./quick-start#development). 20 | 21 | Left Sidebar: 22 | 23 | - Select SSE `Transport Type` 24 | - Input `http://:/sse` in `URL` 25 | - Click `Connect` 26 | 27 | Explore the following tabs in the Top Navbar: 28 | 29 | - `Resources` 30 | - `Prompts` 31 | - `Tools` 32 | 33 | See demo videos to learn more. 34 | 35 | ## Community MCP Servers 36 | 37 | Before building your own custom MCP servers, explore the growing list of hundreds of [community MCP servers](https://github.com/modelcontextprotocol/servers). With integrations spanning databases, cloud services, and web resources, the perfect fit might already exist. 38 | 39 | ### DBHub 40 | 41 | Learn more [here](https://github.com/bytebase/dbhub). Explore more in [Inspector](#inspector). 42 | 43 | Easily plug in this MCP into LLM to allow LLM to: 44 | 45 | - Perform read-only SQL query validation for secure operations 46 | 47 | - Enable deterministic introspection of DB 48 | - List schemas 49 | - List tables in schemas 50 | - Retrieve table structures 51 | 52 | - Enrich user queries deterministically 53 | - Ground DB related queries with DB schemas 54 | - Provide SQL templates for translating natural language to SQL 55 | 56 | ### Youtube 57 | 58 | Learn more [here](https://github.com/Klavis-AI/klavis/tree/main/mcp_servers/youtube). Explore more in [Inspector](#inspector). 59 | 60 | Instead of building logic to: 61 | 62 | - Scrape YouTube content 63 | - Adapt outputs for LLM compatibility 64 | - Validate tool invocation by the LLM 65 | - Chain these steps to fetch transcripts from URLs 66 | 67 | Simply plug in this MCP to enable LLM to: 68 | 69 | - Fetch transcripts from any YouTube URL on demand 70 | 71 | Check out the [demo video](#video-demo) at the top. 72 | 73 | ## Custom MCP 74 | 75 | Should you require a custom MCP server, a template is provided [here](https://github.com/NicholasGoh/fastapi-mcp-langgraph-template/blob/main/backend/shared_mcp/tools.py) for you to reference in development. 76 | 77 | ```python 78 | import os 79 | 80 | from mcp.server.fastmcp import FastMCP 81 | 82 | mcp = FastMCP( 83 | "MCP Server", 84 | port=os.environ["MCP_SERVER_PORT"], 85 | ) 86 | 87 | 88 | @mcp.tool() 89 | def add(a: int, b: int) -> int: 90 | """Add two numbers""" 91 | return a + b 92 | 93 | 94 | @mcp.resource("greeting://{name}") 95 | def get_greeting(name: str) -> str: 96 | """Get a personalized greeting""" 97 | return f"Hello, {name}!" 98 | ``` 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Ruff stuff: 171 | .ruff_cache/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/nicholasgoh-fastapi-mcp-langgraph-template-badge.png)](https://mseep.ai/app/nicholasgoh-fastapi-mcp-langgraph-template) 2 | 3 | # FastAPI MCP LangGraph Template 4 | 5 | A modern template for agentic orchestration — built for rapid iteration and scalable deployment using highly customizable, community-supported tools like MCP, LangGraph, and more. 6 | 7 | Visit the Github: [![FastAPI MCP LangGraph Template](https://img.shields.io/github/stars/nicholasgoh/fastapi-mcp-langgraph-template?label=FastAPI%20MCP%20LangGraph%20Template)](https://github.com/NicholasGoh/fastapi-mcp-langgraph-template) [![FastAPI MCP LangGraph Template](https://img.shields.io/github/v/tag/nicholasgoh/fastapi-mcp-langgraph-template?style=flat)](https://github.com/NicholasGoh/fastapi-mcp-langgraph-template) 8 | 9 | > [!NOTE] 10 | > Read the docs with demo videos [here](https://nicholas-goh.com/docs/intro?ref=fastapi-mcp-langgraph-template). This repo will not contain demo videos. 11 | 12 | 13 | - [FastAPI MCP LangGraph Template](#fastapi-mcp-langgraph-template) 14 | - [Core Features](#core-features) 15 | - [Technology Stack and Features](#technology-stack-and-features) 16 | - [Planned Features](#planned-features) 17 | - [Architecture](#architecture) 18 | - [Inspector](#inspector) 19 | - [Template Setup](#template-setup) 20 | - [Reverse Proxy](#reverse-proxy) 21 | - [Planned Features Diagrams](#planned-features-diagrams) 22 | - [Monitoring and Observability](#monitoring-and-observability) 23 | - [Authentication and Authorization](#authentication-and-authorization) 24 | - [Quick Start](#quick-start) 25 | - [Development](#development) 26 | - [VSCode Devcontainer](#vscode-devcontainer) 27 | - [Without VSCode Devcontainer](#without-vscode-devcontainer) 28 | - [Debugging](#debugging) 29 | - [Refactored Markdown Files](#refactored-markdown-files) 30 | - [MCP](#mcp) 31 | - [LangGraph](#langgraph) 32 | - [Supabase](#supabase) 33 | - [Langfuse](#langfuse) 34 | - [Grafana Stack](#grafana-stack) 35 | 36 | 37 | ## Core Features 38 | 39 | [![MCP Client](https://img.shields.io/github/stars/modelcontextprotocol/python-sdk?logo=modelcontextprotocol&label=MCP-Client)](https://github.com/modelcontextprotocol/python-sdk) is an open protocol that standardizes how apps provide context to LLMs. 40 | - Seamlessly integrates LLM with growing list of community integrations found here [![MCP Server](https://img.shields.io/github/stars/modelcontextprotocol/servers?logo=modelcontextprotocol&label=MCP-Servers)](https://github.com/modelcontextprotocol/servers) 41 | - No LLM provider lock in 42 | 43 | [![LangGraph](https://img.shields.io/github/stars/langchain-ai/langgraph?logo=langgraph&label=LangGraph)](https://github.com/langchain-ai/langgraph) for Customizable Agentic Orchestration 44 | - Native streaming for UX in complex Agentic Workflows 45 | - Native persisted chat history and state management 46 | 47 | ### Technology Stack and Features 48 | 49 | - [![FastAPI](https://img.shields.io/github/stars/fastapi/fastapi?logo=fastapi&label=fastapi)](https://github.com/fastapi/fastapi) for Python backend API 50 | - [![SQLModel](https://img.shields.io/github/stars/fastapi/sqlmodel?logo=sqlmodel&label=SQLModel)](https://github.com/fastapi/sqlmodel) for Python SQL database interactions (ORM + Validation). 51 | - Wrapper of [![SQLAlchemy](https://img.shields.io/github/stars/sqlalchemy/sqlalchemy?logo=sqlalchemy&label=SQLAlchemy)](https://github.com/sqlalchemy/sqlalchemy) 52 | - [![LangFuse](https://img.shields.io/github/stars/langfuse/langfuse?logo=langfuse&label=LangFuse)](https://github.com/langfuse/langfuse) for LLM Observability and LLM Metrics 53 | - [![Pydantic](https://img.shields.io/github/stars/pydantic/pydantic?logo=pydantic&label=Pydantic)](https://github.com/pydantic/pydantic) for Data Validation and Settings Management. 54 | - [![Supabase](https://img.shields.io/github/stars/supabase/supabase?logo=supabase&label=Supabase)](https://github.com/supabase/supabase) for DB RBAC 55 | - [![PostgreSQL](https://img.shields.io/github/stars/postgres/postgres?logo=postgresql&label=Postgres)](https://github.com/postgres/postgres) Relational DB 56 | - [![PGVector](https://img.shields.io/github/stars/pgvector/pgvector?logo=postgresql&label=PGVector)](https://github.com/pgvector/pgvector) Vector Store 57 | - [![Nginx](https://img.shields.io/github/stars/nginx/nginx?logo=nginx&label=Nginx)](https://github.com/nginx/nginx) Reverse Proxy 58 | - [![Compose](https://img.shields.io/github/stars/docker/compose?logo=docker&label=Compose)](https://github.com/docker/compose) for development and production. 59 | 60 | ### Planned Features 61 | 62 | - [![Prometheus](https://img.shields.io/github/stars/prometheus/prometheus?logo=prometheus&label=Prometheus)](https://github.com/prometheus/prometheus) for scraping Metrics 63 | - [![Grafana](https://img.shields.io/github/stars/prometheus/prometheus?logo=grafana&label=Grafana)](https://github.com/grafana/grafana) for visualizing Metrics 64 | - [![Auth0](https://img.shields.io/badge/Auth0-white?logo=auth0)](https://auth0.com/docs) SaaS for Authentication and Authorization with OIDC & JWT via OAuth 2.0 65 | - CI/CD via Github Actions 66 | - :dollar: Deploy live demo to [![Fargate](https://img.shields.io/badge/Fargate-white.svg?logo=awsfargate)](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) 67 | - Provision with [![Terraform](https://img.shields.io/github/stars/hashicorp/terraform?logo=terraform&label=Terraform)](https://github.com/hashicorp/terraform) IaC 68 | - Push built images to ECR and Dockerhub 69 | 70 | ## Architecture 71 | 72 | This section outlines the architecture of the services, their interactions, and planned features. 73 | 74 | ### Inspector 75 | 76 | Inspector communicates via SSE protocol with each MCP Server, while each server adheres to MCP specification. 77 | 78 | ```mermaid 79 | graph LR 80 | 81 | subgraph localhost 82 | A[Inspector] 83 | B[DBHub Server] 84 | C[Youtube Server] 85 | D[Custom Server] 86 | end 87 | 88 | subgraph Supabase Cloud 89 | E[Supabase DB] 90 | end 91 | 92 | subgraph Google Cloud 93 | F[Youtube API] 94 | end 95 | 96 | A<-->|Protocol|B 97 | A<-->|Protocol|C 98 | A<-->|Protocol|D 99 | B<-->E 100 | C<-->F 101 | ``` 102 | 103 | ### Template Setup 104 | 105 | The current template does not connect to all MCP servers. Additionally, the API server communicates with the database using a SQL ORM. 106 | 107 | ```mermaid 108 | graph LR 109 | 110 | subgraph localhost 111 | A[API Server] 112 | B[DBHub Server] 113 | C[Youtube Server] 114 | D[Custom Server] 115 | end 116 | 117 | subgraph Supabase Cloud 118 | E[Supabase DB] 119 | end 120 | 121 | A<-->|Protocol|D 122 | A<-->E 123 | ``` 124 | 125 | ### Reverse Proxy 126 | 127 | Can be extended for other services like Frontend and/or certain backend services self-hosted instead of on cloud (e.g., Langfuse). 128 | 129 | ```mermaid 130 | graph LR 131 | A[Web Browser] 132 | 133 | subgraph localhost 134 | B[Nginx Reverse Proxy] 135 | C[API Server] 136 | end 137 | 138 | A-->B 139 | B-->C 140 | ``` 141 | 142 | ### Planned Features Diagrams 143 | 144 | #### Monitoring and Observability 145 | 146 | ```mermaid 147 | graph LR 148 | 149 | subgraph localhost 150 | A[API Server] 151 | end 152 | 153 | subgraph Grafana Cloud 154 | B[Grafana] 155 | end 156 | 157 | subgraph Langfuse Cloud 158 | C[Langfuse] 159 | end 160 | 161 | A -->|Metrics & Logs| B 162 | A -->|Traces & Events| C 163 | ``` 164 | 165 | #### Authentication and Authorization 166 | 167 | ![Auth0 Diagram](https://images.ctfassets.net/cdy7uua7fh8z/7mWk9No612EefC8uBidCqr/821eb60b0aa953b0d8e4afe897228844/Auth-code-flow-diagram.png) 168 | 169 | [Auth0 Source](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow) 170 | 171 | ## Quick Start 172 | 173 | Setup to run the repository in both production and development environments. 174 | 175 | Build community youtube MCP image with: 176 | 177 | ```bash 178 | ./community/youtube/build.sh 179 | ``` 180 | 181 | :::tip 182 | 183 | Instead of cloning or submoduling the repository locally, then building the image, this script builds the Docker image inside a temporary Docker-in-Docker container. This approach avoids polluting your local environment with throwaway files by cleaning up everything once the container exits. 184 | 185 | ::: 186 | 187 | Then build the other images with: 188 | 189 | ```bash 190 | docker compose -f compose-dev.yaml build 191 | ``` 192 | 193 | Copy environment file: 194 | 195 | ```bash 196 | cp .env.sample .env 197 | ``` 198 | 199 | Add your following API keys and value to the respective file: `./envs/backend.env`, `./envs/youtube.env` and `.env`. 200 | 201 | ```bash 202 | OPENAI_API_KEY=sk-proj-... 203 | POSTGRES_DSN=postgresql://postgres... 204 | 205 | LANGFUSE_PUBLIC_KEY=pk-lf-... 206 | LANGFUSE_SECRET_KEY=sk-lf-... 207 | LANGFUSE_HOST=https://cloud.langfuse.com 208 | 209 | ENVIRONMENT=production 210 | 211 | YOUTUBE_API_KEY=... 212 | ``` 213 | 214 | Set environment variables in shell: (compatible with `bash` and `zsh`) 215 | 216 | ```bash 217 | set -a; for env_file in ./envs/*; do source $env_file; done; set +a 218 | ``` 219 | 220 | Start production containers: 221 | 222 | ```bash 223 | docker compose up -d 224 | ``` 225 | 226 | 227 | 228 | ## Development 229 | 230 | First, set environment variables as per above. 231 | 232 | ### VSCode Devcontainer 233 | 234 | 235 | 236 |
237 | 238 | :::warning 239 | 240 | Only replace the following if you plan to start debugger for FastAPI server in VSCode. 241 | 242 | ::: 243 | 244 | Replace `./compose-dev.yaml` entrypoint to allow debugging FastAPI server: 245 | 246 | ```yaml title="./compose-dev.yaml" 247 | api: 248 | image: api:prod 249 | build: 250 | dockerfile: ./backend/api/Dockerfile 251 | # highlight-next-line 252 | entrypoint: bash -c "sleep infinity" 253 | env_file: 254 | - ./envs/backend.env 255 | ``` 256 | 257 | Then: 258 | 259 | ```bash 260 | code --no-sandbox . 261 | ``` 262 | 263 | Press `F1` and type `Dev Containers: Rebuild and Reopen in Container` to open containerized environment with IntelliSense and Debugger for FastAPI. 264 | 265 | ### Without VSCode Devcontainer 266 | 267 | Run development environment with: 268 | 269 | ```bash 270 | docker compose -f compose-dev.yaml up -d 271 | ``` 272 | 273 | ## Debugging 274 | 275 | Sometimes in development, nginx reverse proxy needs to reload its config to route services properly. 276 | 277 | ```bash 278 | docker compose -f compose-dev.yaml exec nginx sh -c "nginx -s reload" 279 | ``` 280 | 281 | ## Refactored Markdown Files 282 | 283 | The following markdown files provide additional details on other features: 284 | 285 | ### MCP 286 | 287 | [`./docs/mcp.md`](./docs/mcp.md) 288 | 289 | ### LangGraph 290 | 291 | [`./docs/langgraph.md`](./docs/langgraph.md) 292 | 293 | ### Supabase 294 | 295 | [`./docs/supabase.md`](./docs/supabase.md) 296 | 297 | ### Langfuse 298 | 299 | [`./docs/langfuse.md`](./docs/langfuse.md) 300 | 301 | ### Grafana Stack 302 | 303 | [`./docs/grafana-stack.md`](./docs/grafana-stack.md) 304 | 305 | [![Star History Chart](https://api.star-history.com/svg?repos=nicholasgoh/fastapi-mcp-langgraph-template&type=Date)](https://www.star-history.com/#nicholasgoh/fastapi-mcp-langgraph-template&Date) 306 | 307 | > [!NOTE] 308 | > Click above to view live update on star history as per their [article](https://www.star-history.com/blog/a-message-to-github-star-history-users): 309 | > Ongoing Broken Live Chart 310 | > you can still use this website to view and download charts (though you may need to provide your own token). 311 | -------------------------------------------------------------------------------- /backend/mcp/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload_time = "2024-05-20T21:33:25.928Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload_time = "2024-05-20T21:33:24.1Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.9.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "idna" }, 20 | { name = "sniffio" }, 21 | ] 22 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload_time = "2025-03-17T00:02:54.77Z" } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload_time = "2025-03-17T00:02:52.713Z" }, 25 | ] 26 | 27 | [[package]] 28 | name = "mcp-server" 29 | version = "0.1.0" 30 | source = { virtual = "." } 31 | dependencies = [ 32 | { name = "mcp", extra = ["cli"] }, 33 | ] 34 | 35 | [package.metadata] 36 | requires-dist = [{ name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }] 37 | 38 | [[package]] 39 | name = "certifi" 40 | version = "2025.1.31" 41 | source = { registry = "https://pypi.org/simple" } 42 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload_time = "2025-01-31T02:16:47.166Z" } 43 | wheels = [ 44 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload_time = "2025-01-31T02:16:45.015Z" }, 45 | ] 46 | 47 | [[package]] 48 | name = "click" 49 | version = "8.1.8" 50 | source = { registry = "https://pypi.org/simple" } 51 | dependencies = [ 52 | { name = "colorama", marker = "sys_platform == 'win32'" }, 53 | ] 54 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload_time = "2024-12-21T18:38:44.339Z" } 55 | wheels = [ 56 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload_time = "2024-12-21T18:38:41.666Z" }, 57 | ] 58 | 59 | [[package]] 60 | name = "colorama" 61 | version = "0.4.6" 62 | source = { registry = "https://pypi.org/simple" } 63 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" } 64 | wheels = [ 65 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" }, 66 | ] 67 | 68 | [[package]] 69 | name = "h11" 70 | version = "0.14.0" 71 | source = { registry = "https://pypi.org/simple" } 72 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload_time = "2022-09-25T15:40:01.519Z" } 73 | wheels = [ 74 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload_time = "2022-09-25T15:39:59.68Z" }, 75 | ] 76 | 77 | [[package]] 78 | name = "httpcore" 79 | version = "1.0.8" 80 | source = { registry = "https://pypi.org/simple" } 81 | dependencies = [ 82 | { name = "certifi" }, 83 | { name = "h11" }, 84 | ] 85 | sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385, upload_time = "2025-04-11T14:42:46.661Z" } 86 | wheels = [ 87 | { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732, upload_time = "2025-04-11T14:42:44.896Z" }, 88 | ] 89 | 90 | [[package]] 91 | name = "httpx" 92 | version = "0.28.1" 93 | source = { registry = "https://pypi.org/simple" } 94 | dependencies = [ 95 | { name = "anyio" }, 96 | { name = "certifi" }, 97 | { name = "httpcore" }, 98 | { name = "idna" }, 99 | ] 100 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload_time = "2024-12-06T15:37:23.222Z" } 101 | wheels = [ 102 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload_time = "2024-12-06T15:37:21.509Z" }, 103 | ] 104 | 105 | [[package]] 106 | name = "httpx-sse" 107 | version = "0.4.0" 108 | source = { registry = "https://pypi.org/simple" } 109 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload_time = "2023-12-22T08:01:21.083Z" } 110 | wheels = [ 111 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload_time = "2023-12-22T08:01:19.89Z" }, 112 | ] 113 | 114 | [[package]] 115 | name = "idna" 116 | version = "3.10" 117 | source = { registry = "https://pypi.org/simple" } 118 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } 119 | wheels = [ 120 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, 121 | ] 122 | 123 | [[package]] 124 | name = "markdown-it-py" 125 | version = "3.0.0" 126 | source = { registry = "https://pypi.org/simple" } 127 | dependencies = [ 128 | { name = "mdurl" }, 129 | ] 130 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload_time = "2023-06-03T06:41:14.443Z" } 131 | wheels = [ 132 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload_time = "2023-06-03T06:41:11.019Z" }, 133 | ] 134 | 135 | [[package]] 136 | name = "mcp" 137 | version = "1.6.0" 138 | source = { registry = "https://pypi.org/simple" } 139 | dependencies = [ 140 | { name = "anyio" }, 141 | { name = "httpx" }, 142 | { name = "httpx-sse" }, 143 | { name = "pydantic" }, 144 | { name = "pydantic-settings" }, 145 | { name = "sse-starlette" }, 146 | { name = "starlette" }, 147 | { name = "uvicorn" }, 148 | ] 149 | sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031, upload_time = "2025-03-27T16:46:32.336Z" } 150 | wheels = [ 151 | { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077, upload_time = "2025-03-27T16:46:29.919Z" }, 152 | ] 153 | 154 | [package.optional-dependencies] 155 | cli = [ 156 | { name = "python-dotenv" }, 157 | { name = "typer" }, 158 | ] 159 | 160 | [[package]] 161 | name = "mdurl" 162 | version = "0.1.2" 163 | source = { registry = "https://pypi.org/simple" } 164 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload_time = "2022-08-14T12:40:10.846Z" } 165 | wheels = [ 166 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload_time = "2022-08-14T12:40:09.779Z" }, 167 | ] 168 | 169 | [[package]] 170 | name = "pydantic" 171 | version = "2.11.3" 172 | source = { registry = "https://pypi.org/simple" } 173 | dependencies = [ 174 | { name = "annotated-types" }, 175 | { name = "pydantic-core" }, 176 | { name = "typing-extensions" }, 177 | { name = "typing-inspection" }, 178 | ] 179 | sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513, upload_time = "2025-04-08T13:27:06.399Z" } 180 | wheels = [ 181 | { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload_time = "2025-04-08T13:27:03.789Z" }, 182 | ] 183 | 184 | [[package]] 185 | name = "pydantic-core" 186 | version = "2.33.1" 187 | source = { registry = "https://pypi.org/simple" } 188 | dependencies = [ 189 | { name = "typing-extensions" }, 190 | ] 191 | sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload_time = "2025-04-02T09:49:41.8Z" } 192 | wheels = [ 193 | { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551, upload_time = "2025-04-02T09:47:51.648Z" }, 194 | { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785, upload_time = "2025-04-02T09:47:53.149Z" }, 195 | { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758, upload_time = "2025-04-02T09:47:55.006Z" }, 196 | { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109, upload_time = "2025-04-02T09:47:56.532Z" }, 197 | { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159, upload_time = "2025-04-02T09:47:58.088Z" }, 198 | { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222, upload_time = "2025-04-02T09:47:59.591Z" }, 199 | { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980, upload_time = "2025-04-02T09:48:01.397Z" }, 200 | { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840, upload_time = "2025-04-02T09:48:03.056Z" }, 201 | { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518, upload_time = "2025-04-02T09:48:04.662Z" }, 202 | { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025, upload_time = "2025-04-02T09:48:06.226Z" }, 203 | { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991, upload_time = "2025-04-02T09:48:08.114Z" }, 204 | { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262, upload_time = "2025-04-02T09:48:09.708Z" }, 205 | { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626, upload_time = "2025-04-02T09:48:11.288Z" }, 206 | { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590, upload_time = "2025-04-02T09:48:12.861Z" }, 207 | { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963, upload_time = "2025-04-02T09:48:14.553Z" }, 208 | { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896, upload_time = "2025-04-02T09:48:16.222Z" }, 209 | { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810, upload_time = "2025-04-02T09:48:17.97Z" }, 210 | ] 211 | 212 | [[package]] 213 | name = "pydantic-settings" 214 | version = "2.9.1" 215 | source = { registry = "https://pypi.org/simple" } 216 | dependencies = [ 217 | { name = "pydantic" }, 218 | { name = "python-dotenv" }, 219 | { name = "typing-inspection" }, 220 | ] 221 | sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload_time = "2025-04-18T16:44:48.265Z" } 222 | wheels = [ 223 | { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload_time = "2025-04-18T16:44:46.617Z" }, 224 | ] 225 | 226 | [[package]] 227 | name = "pygments" 228 | version = "2.19.1" 229 | source = { registry = "https://pypi.org/simple" } 230 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload_time = "2025-01-06T17:26:30.443Z" } 231 | wheels = [ 232 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload_time = "2025-01-06T17:26:25.553Z" }, 233 | ] 234 | 235 | [[package]] 236 | name = "python-dotenv" 237 | version = "1.1.0" 238 | source = { registry = "https://pypi.org/simple" } 239 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload_time = "2025-03-25T10:14:56.835Z" } 240 | wheels = [ 241 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload_time = "2025-03-25T10:14:55.034Z" }, 242 | ] 243 | 244 | [[package]] 245 | name = "rich" 246 | version = "14.0.0" 247 | source = { registry = "https://pypi.org/simple" } 248 | dependencies = [ 249 | { name = "markdown-it-py" }, 250 | { name = "pygments" }, 251 | ] 252 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload_time = "2025-03-30T14:15:14.23Z" } 253 | wheels = [ 254 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload_time = "2025-03-30T14:15:12.283Z" }, 255 | ] 256 | 257 | [[package]] 258 | name = "shellingham" 259 | version = "1.5.4" 260 | source = { registry = "https://pypi.org/simple" } 261 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload_time = "2023-10-24T04:13:40.426Z" } 262 | wheels = [ 263 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload_time = "2023-10-24T04:13:38.866Z" }, 264 | ] 265 | 266 | [[package]] 267 | name = "sniffio" 268 | version = "1.3.1" 269 | source = { registry = "https://pypi.org/simple" } 270 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload_time = "2024-02-25T23:20:04.057Z" } 271 | wheels = [ 272 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload_time = "2024-02-25T23:20:01.196Z" }, 273 | ] 274 | 275 | [[package]] 276 | name = "sse-starlette" 277 | version = "2.2.1" 278 | source = { registry = "https://pypi.org/simple" } 279 | dependencies = [ 280 | { name = "anyio" }, 281 | { name = "starlette" }, 282 | ] 283 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376, upload_time = "2024-12-25T09:09:30.616Z" } 284 | wheels = [ 285 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120, upload_time = "2024-12-25T09:09:26.761Z" }, 286 | ] 287 | 288 | [[package]] 289 | name = "starlette" 290 | version = "0.46.2" 291 | source = { registry = "https://pypi.org/simple" } 292 | dependencies = [ 293 | { name = "anyio" }, 294 | ] 295 | sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload_time = "2025-04-13T13:56:17.942Z" } 296 | wheels = [ 297 | { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload_time = "2025-04-13T13:56:16.21Z" }, 298 | ] 299 | 300 | [[package]] 301 | name = "typer" 302 | version = "0.15.2" 303 | source = { registry = "https://pypi.org/simple" } 304 | dependencies = [ 305 | { name = "click" }, 306 | { name = "rich" }, 307 | { name = "shellingham" }, 308 | { name = "typing-extensions" }, 309 | ] 310 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload_time = "2025-02-27T19:17:34.807Z" } 311 | wheels = [ 312 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload_time = "2025-02-27T19:17:32.111Z" }, 313 | ] 314 | 315 | [[package]] 316 | name = "typing-extensions" 317 | version = "4.13.2" 318 | source = { registry = "https://pypi.org/simple" } 319 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload_time = "2025-04-10T14:19:05.416Z" } 320 | wheels = [ 321 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload_time = "2025-04-10T14:19:03.967Z" }, 322 | ] 323 | 324 | [[package]] 325 | name = "typing-inspection" 326 | version = "0.4.0" 327 | source = { registry = "https://pypi.org/simple" } 328 | dependencies = [ 329 | { name = "typing-extensions" }, 330 | ] 331 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload_time = "2025-02-25T17:27:59.638Z" } 332 | wheels = [ 333 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload_time = "2025-02-25T17:27:57.754Z" }, 334 | ] 335 | 336 | [[package]] 337 | name = "uvicorn" 338 | version = "0.34.2" 339 | source = { registry = "https://pypi.org/simple" } 340 | dependencies = [ 341 | { name = "click" }, 342 | { name = "h11" }, 343 | ] 344 | sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload_time = "2025-04-19T06:02:50.101Z" } 345 | wheels = [ 346 | { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload_time = "2025-04-19T06:02:48.42Z" }, 347 | ] 348 | --------------------------------------------------------------------------------