diff --git a/.travis.yml b/.travis.yml index ecbf90eb..e2ffa94d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,7 +68,7 @@ jobs: script: | set -e echo "πŸ—οΈ Building container..." - docker build -f Containerfile -t mcpgateway/mcpgateway:latest . + docker build -f Containerfile.lite -t mcpgateway/mcpgateway:latest . echo "πŸš€ Launching container..." docker run -d --name mcpgateway -p 4444:4444 \ diff --git a/charts/mcp-stack/values.yaml b/charts/mcp-stack/values.yaml index 98955371..ead2cf21 100644 --- a/charts/mcp-stack/values.yaml +++ b/charts/mcp-stack/values.yaml @@ -261,7 +261,7 @@ migration: # Migration command configuration command: waitForDb: "python3 /app/mcpgateway/utils/db_isready.py --max-tries 30 --interval 2 --timeout 5" - migrate: "alembic upgrade head || echo '⚠️ Migration check failed'" + migrate: "alembic -c mcpgateway/alembic.ini upgrade head || echo '⚠️ Migration check failed'" ######################################################################## # POSTGRES DATABASE diff --git a/docker-compose.yml b/docker-compose.yml index b0fbcd48..d054ebc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,12 +60,11 @@ services: condition: service_healthy # β–Ά wait for DB redis: condition: service_started - # migration: - # condition: service_completed_successfully + migration: + condition: service_completed_successfully healthcheck: test: ["CMD", "curl", "-f", "http://localhost:4444/health"] - #test: ["CMD", "curl", "-f", "https://localhost:4444/health"] interval: 30s timeout: 10s retries: 5 @@ -123,19 +122,19 @@ services: # volumes: [mongodata:/data/db] # networks: [mcpnet] - # migration: - # #image: ghcr.io/ibm/mcp-context-forge:0.5.0 # Use the release MCP Context Forge image - # image: mcpgateway/mcpgateway:latest # Use the local latest image. Run `make docker-prod` to build it. - # build: - # context: . - # dockerfile: Containerfile - # environment: - # - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-mysecretpassword}@postgres:5432/mcp - # command: alembic upgrade head - # depends_on: - # postgres: - # condition: service_healthy - # networks: [mcpnet] + migration: + #image: ghcr.io/ibm/mcp-context-forge:0.5.0 # Use the release MCP Context Forge image + image: mcpgateway/mcpgateway:latest # Use the local latest image. Run `make docker-prod` to build it. + build: + context: . + dockerfile: Containerfile.lite + environment: + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-mysecretpassword}@postgres:5432/mcp + command: alembic -c mcpgateway/alembic.ini upgrade head + depends_on: + postgres: + condition: service_healthy + networks: [mcpnet] ############################################################################### # CACHE diff --git a/gunicorn.config.py b/gunicorn.config.py index 3d56821e..ae5716bf 100644 --- a/gunicorn.config.py +++ b/gunicorn.config.py @@ -14,6 +14,13 @@ Reference: https://stackoverflow.com/questions/10855197/frequent-worker-timeout """ +# Standard +from importlib.resources import files + +# Third-Party +from alembic import command +from alembic.config import Config + # First-Party # Import Pydantic Settings singleton from mcpgateway.config import settings @@ -51,8 +58,6 @@ # ca-certs = '/etc/ca_bundle.crt' # server hooks - - def when_ready(server): server.log.info("Server is ready. Spawning workers") diff --git a/mcpgateway/alembic/env.py b/mcpgateway/alembic/env.py index 052798aa..96f4e78a 100644 --- a/mcpgateway/alembic/env.py +++ b/mcpgateway/alembic/env.py @@ -57,10 +57,6 @@ from mcpgateway.config import settings from mcpgateway.db import Base -# from mcpgateway.db import get_metadata -# target_metadata = get_metadata() - - # Create config object - this is the standard way in Alembic config = getattr(context, "config", None) or Config() @@ -118,17 +114,9 @@ def _inside_alembic() -> bool: disable_existing_loggers=False, ) -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel target_metadata = Base.metadata -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - config.set_main_option( "sqlalchemy.url", settings.database_url, diff --git a/mcpgateway/alembic/versions/9c07b4bc5774_auto_migration.py b/mcpgateway/alembic/versions/9c07b4bc5774_auto_migration.py new file mode 100644 index 00000000..2008eab9 --- /dev/null +++ b/mcpgateway/alembic/versions/9c07b4bc5774_auto_migration.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +"""auto migration + +Revision ID: 9c07b4bc5774 +Revises: +Create Date: 2025-07-21 17:05:00.624436 + +""" + +# Standard +from typing import Sequence, Union + +# Third-Party +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "9c07b4bc5774" +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "gateways", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("slug", sa.String(), nullable=False), + sa.Column("url", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("transport", sa.String(), nullable=False), + sa.Column("capabilities", sa.JSON(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("enabled", sa.Boolean(), nullable=False), + sa.Column("reachable", sa.Boolean(), nullable=False), + sa.Column("last_seen", sa.DateTime(), nullable=True), + sa.Column("auth_type", sa.String(), nullable=True), + sa.Column("auth_value", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("slug"), + sa.UniqueConstraint("url"), + ) + op.create_table( + "mcp_sessions", + sa.Column("session_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("last_accessed", sa.DateTime(timezone=True), nullable=False), + sa.Column("data", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("session_id"), + ) + op.create_table( + "servers", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("icon", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), + ) + op.create_table( + "mcp_messages", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("session_id", sa.String(), nullable=False), + sa.Column("message", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("last_accessed", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["session_id"], + ["mcp_sessions.session_id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "prompts", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("template", sa.Text(), nullable=False), + sa.Column("argument_schema", sa.JSON(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("gateway_id", sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint( + ["gateway_id"], + ["gateways.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), + ) + op.create_table( + "resources", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("uri", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("mime_type", sa.String(), nullable=True), + sa.Column("size", sa.Integer(), nullable=True), + sa.Column("template", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("text_content", sa.Text(), nullable=True), + sa.Column("binary_content", sa.LargeBinary(), nullable=True), + sa.Column("gateway_id", sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint( + ["gateway_id"], + ["gateways.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("uri"), + ) + op.create_table( + "server_metrics", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("server_id", sa.String(), nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("response_time", sa.Float(), nullable=False), + sa.Column("is_success", sa.Boolean(), nullable=False), + sa.Column("error_message", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["server_id"], + ["servers.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "tools", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("original_name", sa.String(), nullable=False), + sa.Column("original_name_slug", sa.String(), nullable=False), + sa.Column("url", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("integration_type", sa.String(), nullable=False), + sa.Column("request_type", sa.String(), nullable=False), + sa.Column("headers", sa.JSON(), nullable=True), + sa.Column("input_schema", sa.JSON(), nullable=False), + sa.Column("annotations", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("enabled", sa.Boolean(), nullable=False), + sa.Column("reachable", sa.Boolean(), nullable=False), + sa.Column("jsonpath_filter", sa.String(), nullable=False), + sa.Column("auth_type", sa.String(), nullable=True), + sa.Column("auth_value", sa.String(), nullable=True), + sa.Column("gateway_id", sa.String(length=36), nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["gateway_id"], + ["gateways.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("gateway_id", "original_name", name="uq_gateway_id__original_name"), + sa.UniqueConstraint("name"), + ) + op.create_table( + "prompt_metrics", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("prompt_id", sa.Integer(), nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("response_time", sa.Float(), nullable=False), + sa.Column("is_success", sa.Boolean(), nullable=False), + sa.Column("error_message", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["prompt_id"], + ["prompts.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "resource_metrics", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("resource_id", sa.Integer(), nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("response_time", sa.Float(), nullable=False), + sa.Column("is_success", sa.Boolean(), nullable=False), + sa.Column("error_message", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["resource_id"], + ["resources.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "resource_subscriptions", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("resource_id", sa.Integer(), nullable=False), + sa.Column("subscriber_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("last_notification", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["resource_id"], + ["resources.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "server_prompt_association", + sa.Column("server_id", sa.String(), nullable=False), + sa.Column("prompt_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["prompt_id"], + ["prompts.id"], + ), + sa.ForeignKeyConstraint( + ["server_id"], + ["servers.id"], + ), + sa.PrimaryKeyConstraint("server_id", "prompt_id"), + ) + op.create_table( + "server_resource_association", + sa.Column("server_id", sa.String(), nullable=False), + sa.Column("resource_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["resource_id"], + ["resources.id"], + ), + sa.ForeignKeyConstraint( + ["server_id"], + ["servers.id"], + ), + sa.PrimaryKeyConstraint("server_id", "resource_id"), + ) + op.create_table( + "server_tool_association", + sa.Column("server_id", sa.String(), nullable=False), + sa.Column("tool_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["server_id"], + ["servers.id"], + ), + sa.ForeignKeyConstraint( + ["tool_id"], + ["tools.id"], + ), + sa.PrimaryKeyConstraint("server_id", "tool_id"), + ) + op.create_table( + "tool_metrics", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("tool_id", sa.String(), nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("response_time", sa.Float(), nullable=False), + sa.Column("is_success", sa.Boolean(), nullable=False), + sa.Column("error_message", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["tool_id"], + ["tools.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("tool_metrics") + op.drop_table("server_tool_association") + op.drop_table("server_resource_association") + op.drop_table("server_prompt_association") + op.drop_table("resource_subscriptions") + op.drop_table("resource_metrics") + op.drop_table("prompt_metrics") + op.drop_table("tools") + op.drop_table("server_metrics") + op.drop_table("resources") + op.drop_table("prompts") + op.drop_table("mcp_messages") + op.drop_table("servers") + op.drop_table("mcp_sessions") + op.drop_table("gateways") + # ### end Alembic commands ### diff --git a/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py b/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py deleted file mode 100644 index 0170e4ac..00000000 --- a/mcpgateway/alembic/versions/b77ca9d2de7e_uuid_pk_and_slug_refactor.py +++ /dev/null @@ -1,552 +0,0 @@ -# -*- coding: utf-8 -*- -"""uuid-pk_and_slug_refactor - -Revision ID: b77ca9d2de7e -Revises: -Create Date: 2025-06-26 21:29:59.117140 - -""" - -# Standard -from typing import Sequence, Union -import uuid - -# Third-Party -from alembic import op -import sqlalchemy as sa -from sqlalchemy.orm import Session - -# First-Party -from mcpgateway.config import settings -from mcpgateway.utils.create_slug import slugify - -# revision identifiers, used by Alembic. -revision: str = "b77ca9d2de7e" -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -# ────────────────────────────────────────────────────────────────────────────── -# Helpers -# ────────────────────────────────────────────────────────────────────────────── -def _use_batch() -> bool: - """Determine if batch operations are required for the current database. - - SQLite requires batch mode for certain ALTER TABLE operations like dropping - columns or altering column types. This helper checks the database dialect - to determine if batch operations should be used. - - Returns: - bool: True if the database is SQLite (requires batch mode), False otherwise. - - Examples: - >>> # In a SQLite context - >>> _use_batch() # doctest: +SKIP - True - >>> # In a PostgreSQL context - >>> _use_batch() # doctest: +SKIP - False - """ - return op.get_bind().dialect.name == "sqlite" - - -# ────────────────────────────────────────────────────────────────────────────── -# Upgrade -# ────────────────────────────────────────────────────────────────────────────── -def upgrade() -> None: - """Migrate database schema from integer to UUID primary keys with slugs. - - This migration performs a comprehensive schema transformation in three stages: - - Stage 1 - Add placeholder columns: - - Adds UUID columns (id_new) to gateways, tools, and servers - - Adds slug columns for human-readable identifiers - - Adds columns to preserve original tool names before prefixing - - Stage 2 - Data migration: - - Generates UUIDs for all primary keys - - Creates slugs from names (e.g., "My Gateway" -> "my-gateway") - - Prefixes tool names with gateway slugs (e.g., "my-tool" -> "gateway-slug-my-tool") - - Updates all foreign key references to use new UUIDs - - Stage 3 - Schema finalization: - - Drops old integer columns - - Renames new UUID columns to replace old ones - - Recreates primary keys and foreign key constraints - - Adds unique constraints on slugs and URLs - - The migration is designed to work with both SQLite (using batch operations) - and other databases. It preserves all existing data relationships while - transforming the schema. - - Note: - - Skips migration if database is fresh (no gateways table) - - Uses batch operations for SQLite compatibility - - Commits data changes before schema alterations - - Examples: - >>> # Running the migration - >>> upgrade() # doctest: +SKIP - Fresh database detected. Skipping migration. - >>> # Or for existing database - >>> upgrade() # doctest: +SKIP - Existing installation detected. Starting data and schema migration... - """ - bind = op.get_bind() - sess = Session(bind=bind) - inspector = sa.inspect(bind) - - if not inspector.has_table("gateways"): - print("Fresh database detected. Skipping migration.") - return - - print("Existing installation detected. Starting data and schema migration...") - - # ── STAGE 1: ADD NEW NULLABLE COLUMNS AS PLACEHOLDERS ───────────────── - op.add_column("gateways", sa.Column("slug", sa.String(), nullable=True)) - op.add_column("gateways", sa.Column("id_new", sa.String(36), nullable=True)) - - op.add_column("tools", sa.Column("id_new", sa.String(36), nullable=True)) - op.add_column("tools", sa.Column("original_name", sa.String(), nullable=True)) - op.add_column("tools", sa.Column("original_name_slug", sa.String(), nullable=True)) - op.add_column("tools", sa.Column("name_new", sa.String(), nullable=True)) - op.add_column("tools", sa.Column("gateway_id_new", sa.String(36), nullable=True)) - - op.add_column("resources", sa.Column("gateway_id_new", sa.String(36), nullable=True)) - op.add_column("prompts", sa.Column("gateway_id_new", sa.String(36), nullable=True)) - - op.add_column("servers", sa.Column("id_new", sa.String(36), nullable=True)) - - op.add_column("server_tool_association", sa.Column("server_id_new", sa.String(36), nullable=True)) - op.add_column("server_tool_association", sa.Column("tool_id_new", sa.String(36), nullable=True)) - - op.add_column("tool_metrics", sa.Column("tool_id_new", sa.String(36), nullable=True)) - op.add_column("server_metrics", sa.Column("server_id_new", sa.String(36), nullable=True)) - op.add_column("server_resource_association", sa.Column("server_id_new", sa.String(36), nullable=True)) - op.add_column("server_prompt_association", sa.Column("server_id_new", sa.String(36), nullable=True)) - - # ── STAGE 2: POPULATE THE NEW COLUMNS (DATA MIGRATION) ─────────────── - gateways = sess.execute(sa.select(sa.text("id, name")).select_from(sa.text("gateways"))).all() - for gid, gname in gateways: - g_uuid = uuid.uuid4().hex - sess.execute( - sa.text("UPDATE gateways SET id_new=:u, slug=:s WHERE id=:i"), - {"u": g_uuid, "s": slugify(gname), "i": gid}, - ) - - tools = sess.execute(sa.select(sa.text("id, name, gateway_id")).select_from(sa.text("tools"))).all() - for tid, tname, g_old in tools: - t_uuid = uuid.uuid4().hex - tool_slug = slugify(tname) - sess.execute( - sa.text( - """ - UPDATE tools - SET id_new=:u, - original_name=:on, - original_name_slug=:ons, - name_new = CASE - WHEN :g IS NOT NULL THEN (SELECT slug FROM gateways WHERE id = :g) || :sep || :ons - ELSE :ons - END, - gateway_id_new=(SELECT id_new FROM gateways WHERE id=:g) - WHERE id=:i - """ - ), - { - "u": t_uuid, - "on": tname, - "ons": tool_slug, - "sep": settings.gateway_tool_name_separator, - "g": g_old, - "i": tid, - }, - ) - - servers = sess.execute(sa.select(sa.text("id")).select_from(sa.text("servers"))).all() - for (sid,) in servers: - sess.execute( - sa.text("UPDATE servers SET id_new=:u WHERE id=:i"), - {"u": uuid.uuid4().hex, "i": sid}, - ) - - # Populate all dependent tables - resources = sess.execute(sa.select(sa.text("id, gateway_id")).select_from(sa.text("resources"))).all() - for rid, g_old in resources: - sess.execute(sa.text("UPDATE resources SET gateway_id_new=(SELECT id_new FROM gateways WHERE id=:g) WHERE id=:i"), {"g": g_old, "i": rid}) - prompts = sess.execute(sa.select(sa.text("id, gateway_id")).select_from(sa.text("prompts"))).all() - for pid, g_old in prompts: - sess.execute(sa.text("UPDATE prompts SET gateway_id_new=(SELECT id_new FROM gateways WHERE id=:g) WHERE id=:i"), {"g": g_old, "i": pid}) - sta = sess.execute(sa.select(sa.text("server_id, tool_id")).select_from(sa.text("server_tool_association"))).all() - for s_old, t_old in sta: - sess.execute( - sa.text("UPDATE server_tool_association SET server_id_new=(SELECT id_new FROM servers WHERE id=:s), tool_id_new=(SELECT id_new FROM tools WHERE id=:t) WHERE server_id=:s AND tool_id=:t"), - {"s": s_old, "t": t_old}, - ) - tool_metrics = sess.execute(sa.select(sa.text("id, tool_id")).select_from(sa.text("tool_metrics"))).all() - for tmid, t_old in tool_metrics: - sess.execute(sa.text("UPDATE tool_metrics SET tool_id_new=(SELECT id_new FROM tools WHERE id=:t) WHERE id=:i"), {"t": t_old, "i": tmid}) - server_metrics = sess.execute(sa.select(sa.text("id, server_id")).select_from(sa.text("server_metrics"))).all() - for smid, s_old in server_metrics: - sess.execute(sa.text("UPDATE server_metrics SET server_id_new=(SELECT id_new FROM servers WHERE id=:s) WHERE id=:i"), {"s": s_old, "i": smid}) - server_resource_assoc = sess.execute(sa.select(sa.text("server_id, resource_id")).select_from(sa.text("server_resource_association"))).all() - for s_old, r_id in server_resource_assoc: - sess.execute(sa.text("UPDATE server_resource_association SET server_id_new=(SELECT id_new FROM servers WHERE id=:s) WHERE server_id=:s AND resource_id=:r"), {"s": s_old, "r": r_id}) - server_prompt_assoc = sess.execute(sa.select(sa.text("server_id, prompt_id")).select_from(sa.text("server_prompt_association"))).all() - for s_old, p_id in server_prompt_assoc: - sess.execute(sa.text("UPDATE server_prompt_association SET server_id_new=(SELECT id_new FROM servers WHERE id=:s) WHERE server_id=:s AND prompt_id=:p"), {"s": s_old, "p": p_id}) - - sess.commit() - - # ── STAGE 3: FINALIZE SCHEMA (CORRECTED ORDER) ─────────────────────── - # First, rebuild all tables that depend on `servers` and `gateways`. - # This implicitly drops their old foreign key constraints. - with op.batch_alter_table("server_tool_association") as batch_op: - batch_op.drop_column("server_id") - batch_op.drop_column("tool_id") - batch_op.alter_column("server_id_new", new_column_name="server_id", nullable=False) - batch_op.alter_column("tool_id_new", new_column_name="tool_id", nullable=False) - batch_op.create_primary_key("pk_server_tool_association", ["server_id", "tool_id"]) - - with op.batch_alter_table("server_resource_association") as batch_op: - batch_op.drop_column("server_id") - batch_op.alter_column("server_id_new", new_column_name="server_id", nullable=False) - - with op.batch_alter_table("server_prompt_association") as batch_op: - batch_op.drop_column("server_id") - batch_op.alter_column("server_id_new", new_column_name="server_id", nullable=False) - - with op.batch_alter_table("server_metrics") as batch_op: - batch_op.drop_column("server_id") - batch_op.alter_column("server_id_new", new_column_name="server_id", nullable=False) - - with op.batch_alter_table("tool_metrics") as batch_op: - batch_op.drop_column("tool_id") - batch_op.alter_column("tool_id_new", new_column_name="tool_id", nullable=False) - - with op.batch_alter_table("tools") as batch_op: - batch_op.drop_column("id") - batch_op.alter_column("id_new", new_column_name="id", nullable=False) - batch_op.create_primary_key("pk_tools", ["id"]) - batch_op.drop_column("gateway_id") - batch_op.alter_column("gateway_id_new", new_column_name="gateway_id", nullable=True) - batch_op.drop_column("name") - batch_op.alter_column("name_new", new_column_name="name", nullable=True) - batch_op.alter_column("original_name", nullable=False) - batch_op.alter_column("original_name_slug", nullable=False) - batch_op.create_unique_constraint("uq_tools_name", ["name"]) - batch_op.create_unique_constraint("uq_gateway_id__original_name", ["gateway_id", "original_name"]) - - with op.batch_alter_table("resources") as batch_op: - batch_op.drop_column("gateway_id") - batch_op.alter_column("gateway_id_new", new_column_name="gateway_id", nullable=True) - - with op.batch_alter_table("prompts") as batch_op: - batch_op.drop_column("gateway_id") - batch_op.alter_column("gateway_id_new", new_column_name="gateway_id", nullable=True) - - # Second, now that no tables point to their old IDs, rebuild `gateways` and `servers`. - with op.batch_alter_table("gateways") as batch_op: - batch_op.drop_column("id") - batch_op.alter_column("id_new", new_column_name="id", nullable=False) - batch_op.create_primary_key("pk_gateways", ["id"]) - batch_op.alter_column("slug", nullable=False) - batch_op.create_unique_constraint("uq_gateways_slug", ["slug"]) - batch_op.create_unique_constraint("uq_gateways_url", ["url"]) - - with op.batch_alter_table("servers") as batch_op: - batch_op.drop_column("id") - batch_op.alter_column("id_new", new_column_name="id", nullable=False) - batch_op.create_primary_key("pk_servers", ["id"]) - - # Finally, recreate all the foreign key constraints in batch mode for SQLite compatibility. - # The redundant `source_table` argument has been removed from each call. - with op.batch_alter_table("tools") as batch_op: - batch_op.create_foreign_key("fk_tools_gateway_id", "gateways", ["gateway_id"], ["id"]) - with op.batch_alter_table("resources") as batch_op: - batch_op.create_foreign_key("fk_resources_gateway_id", "gateways", ["gateway_id"], ["id"]) - with op.batch_alter_table("prompts") as batch_op: - batch_op.create_foreign_key("fk_prompts_gateway_id", "gateways", ["gateway_id"], ["id"]) - with op.batch_alter_table("server_tool_association") as batch_op: - batch_op.create_foreign_key("fk_server_tool_association_servers", "servers", ["server_id"], ["id"]) - batch_op.create_foreign_key("fk_server_tool_association_tools", "tools", ["tool_id"], ["id"]) - with op.batch_alter_table("tool_metrics") as batch_op: - batch_op.create_foreign_key("fk_tool_metrics_tool_id", "tools", ["tool_id"], ["id"]) - with op.batch_alter_table("server_metrics") as batch_op: - batch_op.create_foreign_key("fk_server_metrics_server_id", "servers", ["server_id"], ["id"]) - with op.batch_alter_table("server_resource_association") as batch_op: - batch_op.create_foreign_key("fk_server_resource_association_server_id", "servers", ["server_id"], ["id"]) - with op.batch_alter_table("server_prompt_association") as batch_op: - batch_op.create_foreign_key("fk_server_prompt_association_server_id", "servers", ["server_id"], ["id"]) - - -# def upgrade() -> None: -# bind = op.get_bind() -# sess = Session(bind=bind) -# inspector = sa.inspect(bind) - -# if not inspector.has_table("gateways"): -# print("Fresh database detected. Skipping migration.") -# return - -# print("Existing installation detected. Starting data and schema migration...") - -# # ── STAGE 1: ADD NEW NULLABLE COLUMNS AS PLACEHOLDERS ───────────────── -# op.add_column("gateways", sa.Column("slug", sa.String(), nullable=True)) -# op.add_column("gateways", sa.Column("id_new", sa.String(36), nullable=True)) - -# op.add_column("tools", sa.Column("id_new", sa.String(36), nullable=True)) -# op.add_column("tools", sa.Column("original_name", sa.String(), nullable=True)) -# op.add_column("tools", sa.Column("original_name_slug", sa.String(), nullable=True)) -# op.add_column("tools", sa.Column("name_new", sa.String(), nullable=True)) -# op.add_column("tools", sa.Column("gateway_id_new", sa.String(36), nullable=True)) - -# op.add_column("resources", sa.Column("gateway_id_new", sa.String(36), nullable=True)) -# op.add_column("prompts", sa.Column("gateway_id_new", sa.String(36), nullable=True)) - -# op.add_column("servers", sa.Column("id_new", sa.String(36), nullable=True)) - -# op.add_column("server_tool_association", sa.Column("server_id_new", sa.String(36), nullable=True)) -# op.add_column("server_tool_association", sa.Column("tool_id_new", sa.String(36), nullable=True)) - -# op.add_column("tool_metrics", sa.Column("tool_id_new", sa.String(36), nullable=True)) - -# # Add columns for the new server dependencies -# op.add_column("server_metrics", sa.Column("server_id_new", sa.String(36), nullable=True)) -# op.add_column("server_resource_association", sa.Column("server_id_new", sa.String(36), nullable=True)) -# op.add_column("server_prompt_association", sa.Column("server_id_new", sa.String(36), nullable=True)) - - -# # ── STAGE 2: POPULATE THE NEW COLUMNS (DATA MIGRATION) ─────────────── -# gateways = sess.execute(sa.select(sa.text("id, name")).select_from(sa.text("gateways"))).all() -# for gid, gname in gateways: -# g_uuid = uuid.uuid4().hex -# sess.execute( -# sa.text("UPDATE gateways SET id_new=:u, slug=:s WHERE id=:i"), -# {"u": g_uuid, "s": slugify(gname), "i": gid}, -# ) - -# tools = sess.execute( -# sa.select(sa.text("id, name, gateway_id")).select_from(sa.text("tools")) -# ).all() -# for tid, tname, g_old in tools: -# t_uuid = uuid.uuid4().hex -# tool_slug = slugify(tname) -# sess.execute( -# sa.text( -# """ -# UPDATE tools -# SET id_new=:u, -# original_name=:on, -# original_name_slug=:ons, -# name_new = CASE -# WHEN :g IS NOT NULL THEN (SELECT slug FROM gateways WHERE id = :g) || :sep || :ons -# ELSE :ons -# END, -# gateway_id_new=(SELECT id_new FROM gateways WHERE id=:g) -# WHERE id=:i -# """ -# ), -# { -# "u": t_uuid, "on": tname, "ons": tool_slug, -# "sep": settings.gateway_tool_name_separator, "g": g_old, "i": tid, -# }, -# ) - -# servers = sess.execute(sa.select(sa.text("id")).select_from(sa.text("servers"))).all() -# for (sid,) in servers: -# sess.execute( -# sa.text("UPDATE servers SET id_new=:u WHERE id=:i"), -# {"u": uuid.uuid4().hex, "i": sid}, -# ) - -# # Populate all dependent tables -# resources = sess.execute(sa.select(sa.text("id, gateway_id")).select_from(sa.text("resources"))).all() -# for rid, g_old in resources: -# sess.execute(sa.text("UPDATE resources SET gateway_id_new=(SELECT id_new FROM gateways WHERE id=:g) WHERE id=:i"), {"g": g_old, "i": rid}) -# prompts = sess.execute(sa.select(sa.text("id, gateway_id")).select_from(sa.text("prompts"))).all() -# for pid, g_old in prompts: -# sess.execute(sa.text("UPDATE prompts SET gateway_id_new=(SELECT id_new FROM gateways WHERE id=:g) WHERE id=:i"), {"g": g_old, "i": pid}) -# sta = sess.execute(sa.select(sa.text("server_id, tool_id")).select_from(sa.text("server_tool_association"))).all() -# for s_old, t_old in sta: -# sess.execute(sa.text("UPDATE server_tool_association SET server_id_new=(SELECT id_new FROM servers WHERE id=:s), tool_id_new=(SELECT id_new FROM tools WHERE id=:t) WHERE server_id=:s AND tool_id=:t"), {"s": s_old, "t": t_old}) -# tool_metrics = sess.execute(sa.select(sa.text("id, tool_id")).select_from(sa.text("tool_metrics"))).all() -# for tmid, t_old in tool_metrics: -# sess.execute(sa.text("UPDATE tool_metrics SET tool_id_new=(SELECT id_new FROM tools WHERE id=:t) WHERE id=:i"), {"t": t_old, "i": tmid}) -# server_metrics = sess.execute(sa.select(sa.text("id, server_id")).select_from(sa.text("server_metrics"))).all() -# for smid, s_old in server_metrics: -# sess.execute(sa.text("UPDATE server_metrics SET server_id_new=(SELECT id_new FROM servers WHERE id=:s) WHERE id=:i"), {"s": s_old, "i": smid}) -# server_resource_assoc = sess.execute(sa.select(sa.text("server_id, resource_id")).select_from(sa.text("server_resource_association"))).all() -# for s_old, r_id in server_resource_assoc: -# sess.execute(sa.text("UPDATE server_resource_association SET server_id_new=(SELECT id_new FROM servers WHERE id=:s) WHERE server_id=:s AND resource_id=:r"), {"s": s_old, "r": r_id}) -# server_prompt_assoc = sess.execute(sa.select(sa.text("server_id, prompt_id")).select_from(sa.text("server_prompt_association"))).all() -# for s_old, p_id in server_prompt_assoc: -# sess.execute(sa.text("UPDATE server_prompt_association SET server_id_new=(SELECT id_new FROM servers WHERE id=:s) WHERE server_id=:s AND prompt_id=:p"), {"s": s_old, "p": p_id}) - -# sess.commit() - -# # ── STAGE 3: FINALIZE SCHEMA (CORRECTED ORDER) ─────────────────────── -# with op.batch_alter_table("server_tool_association") as batch_op: -# batch_op.drop_column("server_id") -# batch_op.drop_column("tool_id") -# batch_op.alter_column("server_id_new", new_column_name="server_id", nullable=False) -# batch_op.alter_column("tool_id_new", new_column_name="tool_id", nullable=False) -# batch_op.create_primary_key("pk_server_tool_association", ["server_id", "tool_id"]) - -# with op.batch_alter_table("server_resource_association") as batch_op: -# batch_op.drop_column("server_id") -# batch_op.alter_column("server_id_new", new_column_name="server_id", nullable=False) - -# with op.batch_alter_table("server_prompt_association") as batch_op: -# batch_op.drop_column("server_id") -# batch_op.alter_column("server_id_new", new_column_name="server_id", nullable=False) - -# with op.batch_alter_table("server_metrics") as batch_op: -# batch_op.drop_column("server_id") -# batch_op.alter_column("server_id_new", new_column_name="server_id", nullable=False) - -# with op.batch_alter_table("tool_metrics") as batch_op: -# batch_op.drop_column("tool_id") -# batch_op.alter_column("tool_id_new", new_column_name="tool_id", nullable=False) - -# with op.batch_alter_table("tools") as batch_op: -# batch_op.drop_column("id") -# batch_op.alter_column("id_new", new_column_name="id", nullable=False) -# batch_op.create_primary_key("pk_tools", ["id"]) -# batch_op.drop_column("gateway_id") -# batch_op.alter_column("gateway_id_new", new_column_name="gateway_id", nullable=True) -# batch_op.drop_column("name") -# batch_op.alter_column("name_new", new_column_name="name", nullable=False) -# batch_op.alter_column("original_name", nullable=False) -# batch_op.alter_column("original_name_slug", nullable=False) -# batch_op.create_unique_constraint("uq_tools_name", ["name"]) -# batch_op.create_unique_constraint("uq_gateway_id__original_name", ["gateway_id", "original_name"]) - -# with op.batch_alter_table("resources") as batch_op: -# batch_op.drop_column("gateway_id") -# batch_op.alter_column("gateway_id_new", new_column_name="gateway_id", nullable=True) - -# with op.batch_alter_table("prompts") as batch_op: -# batch_op.drop_column("gateway_id") -# batch_op.alter_column("gateway_id_new", new_column_name="gateway_id", nullable=True) - -# with op.batch_alter_table("gateways") as batch_op: -# batch_op.drop_column("id") -# batch_op.alter_column("id_new", new_column_name="id", nullable=False) -# batch_op.create_primary_key("pk_gateways", ["id"]) -# batch_op.alter_column("slug", nullable=False) -# batch_op.create_unique_constraint("uq_gateways_slug", ["slug"]) -# batch_op.create_unique_constraint("uq_gateways_url", ["url"]) - -# with op.batch_alter_table("servers") as batch_op: -# batch_op.drop_column("id") -# batch_op.alter_column("id_new", new_column_name="id", nullable=False) -# batch_op.create_primary_key("pk_servers", ["id"]) - -# # Finally, recreate all the foreign key constraints -# op.create_foreign_key("fk_tools_gateway_id", "tools", "gateways", ["gateway_id"], ["id"]) -# op.create_foreign_key("fk_resources_gateway_id", "resources", "gateways", ["gateway_id"], ["id"]) -# op.create_foreign_key("fk_prompts_gateway_id", "prompts", "gateways", ["gateway_id"], ["id"]) -# op.create_foreign_key("fk_server_tool_association_servers", "server_tool_association", "servers", ["server_id"], ["id"]) -# op.create_foreign_key("fk_server_tool_association_tools", "server_tool_association", "tools", ["tool_id"], ["id"]) -# op.create_foreign_key("fk_tool_metrics_tool_id", "tool_metrics", "tools", ["tool_id"], ["id"]) -# op.create_foreign_key("fk_server_metrics_server_id", "server_metrics", "servers", ["server_id"], ["id"]) -# op.create_foreign_key("fk_server_resource_association_server_id", "server_resource_association", "servers", ["server_id"], ["id"]) -# op.create_foreign_key("fk_server_prompt_association_server_id", "server_prompt_association", "servers", ["server_id"], ["id"]) - - -def downgrade() -> None: - """Revert database schema from UUID primary keys back to integers. - - This downgrade reverses the UUID migration but with significant limitations: - - Schema structure is restored but data is NOT preserved - - All UUID values and slug fields are lost - - Foreign key relationships are broken (columns will be NULL) - - Original integer IDs cannot be recovered - - The downgrade operates in reverse order of the upgrade: - - Stage 1 - Revert schema changes: - - Drops UUID-based constraints and keys - - Renames UUID columns back to temporary names - - Re-adds integer columns (empty/NULL) - - Stage 2 - Data migration (skipped): - - Original integer IDs cannot be restored from UUIDs - - Relationships cannot be reconstructed - - Stage 3 - Remove temporary columns: - - Drops all UUID and slug columns - - Leaves database with original schema but no data - - Warning: - This downgrade is destructive and should only be used if you need - to revert the schema structure. All data in affected tables will - need to be manually restored from backups. - - Examples: - >>> # Running the downgrade - >>> downgrade() # doctest: +SKIP - # Schema reverted but data is lost - """ - # ── STAGE 1 (REVERSE): Revert Schema to original state ───────────────── - # This reverses the operations from STAGE 3 of the upgrade. - # Data from the new columns will be lost, which is expected. - - with op.batch_alter_table("server_tool_association") as batch_op: - # Drop new constraints - batch_op.drop_constraint("fk_server_tool_association_tools", type_="foreignkey") - batch_op.drop_constraint("fk_server_tool_association_servers", type_="foreignkey") - batch_op.drop_constraint("pk_server_tool_association", type_="primarykey") - # Rename final columns back to temporary names - batch_op.alter_column("server_id", new_column_name="server_id_new") - batch_op.alter_column("tool_id", new_column_name="tool_id_new") - # Add back old integer columns (data is not restored) - batch_op.add_column(sa.Column("server_id", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("tool_id", sa.Integer(), nullable=True)) - - with op.batch_alter_table("tools") as batch_op: - # Drop new constraints - batch_op.drop_constraint("fk_tools_gateway_id", type_="foreignkey") - batch_op.drop_constraint("uq_gateway_id__original_name", type_="unique") - batch_op.drop_constraint("uq_tools_name", type_="unique") - batch_op.drop_constraint("pk_tools", type_="primarykey") - # Rename final columns back to temporary names - batch_op.alter_column("id", new_column_name="id_new") - batch_op.alter_column("gateway_id", new_column_name="gateway_id_new") - batch_op.alter_column("name", new_column_name="name_new") - # Add back old columns - batch_op.add_column(sa.Column("id", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("gateway_id", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("name", sa.String(), nullable=True)) - - with op.batch_alter_table("servers") as batch_op: - batch_op.drop_constraint("pk_servers", type_="primarykey") - batch_op.alter_column("id", new_column_name="id_new") - batch_op.add_column(sa.Column("id", sa.Integer(), nullable=True)) - - with op.batch_alter_table("gateways") as batch_op: - batch_op.drop_constraint("uq_gateways_url", type_="unique") - batch_op.drop_constraint("uq_gateways_slug", type_="unique") - batch_op.drop_constraint("pk_gateways", type_="primarykey") - batch_op.alter_column("id", new_column_name="id_new") - batch_op.add_column(sa.Column("id", sa.Integer(), nullable=True)) - - # ── STAGE 2 (REVERSE): Reverse Data Migration (No-Op for Schema) ────── - # Reversing the data population (e.g., creating integer PKs from UUIDs) - # is a complex, stateful operation and is omitted here. At this point, - # the original columns exist but are empty (NULL). - - # ── STAGE 3 (REVERSE): Drop the temporary/new columns ──────────────── - # This reverses the operations from STAGE 1 of the upgrade. - op.drop_column("server_tool_association", "tool_id_new") - op.drop_column("server_tool_association", "server_id_new") - op.drop_column("servers", "id_new") - op.drop_column("tools", "gateway_id_new") - op.drop_column("tools", "name_new") - op.drop_column("tools", "original_name_slug") - op.drop_column("tools", "original_name") - op.drop_column("tools", "id_new") - op.drop_column("gateways", "id_new") - op.drop_column("gateways", "slug") diff --git a/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py b/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py deleted file mode 100644 index 8876f3b4..00000000 --- a/mcpgateway/alembic/versions/e4fc04d1a442_add_annotations_to_tables.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -"""Add annotations to tables - -Revision ID: e4fc04d1a442 -Revises: b77ca9d2de7e -Create Date: 2025-06-27 21:45:35.099713 - -""" - -# Standard -from typing import Sequence, Union - -# Third-Party -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision: str = "e4fc04d1a442" -down_revision: Union[str, Sequence[str], None] = "b77ca9d2de7e" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """ - Applies the migration to add the 'annotations' column. - - This function adds a new column named 'annotations' of type JSON to the 'tool' - table. It includes a server-side default of an empty JSON object ('{}') to ensure - that existing rows get a non-null default value. - """ - bind = op.get_bind() - inspector = sa.inspect(bind) - - if not inspector.has_table("gateways"): - print("Fresh database detected. Skipping migration.") - return - - op.add_column("tools", sa.Column("annotations", sa.JSON(), server_default=sa.text("'{}'"), nullable=False)) - - -def downgrade() -> None: - """ - Reverts the migration by removing the 'annotations' column. - - This function provides a way to undo the migration, safely removing the - 'annotations' column from the 'tool' table. - """ - bind = op.get_bind() - inspector = sa.inspect(bind) - - if not inspector.has_table("gateways"): - print("Fresh database detected. Skipping migration.") - return - - op.drop_column("tools", "annotations") diff --git a/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py b/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py deleted file mode 100644 index 097535b4..00000000 --- a/mcpgateway/alembic/versions/e75490e949b1_add_improved_status_to_tables.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -"""Add enabled and reachable columns in tools and gateways tables and migrate data (is_active ➜ enabled,reachable). - -Revision ID: e75490e949b1 -Revises: e4fc04d1a442 -Create Date: 2025-07-02 17:12:40.678256 -""" - -# Standard -from typing import Sequence, Union - -# Third-Party -from alembic import op -import sqlalchemy as sa - -# Revision identifiers. -revision: str = "e75490e949b1" -down_revision: Union[str, Sequence[str], None] = "e4fc04d1a442" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade(): - """ - Renames 'is_active' to 'enabled' and adds a new 'reachable' column (default True) - in both 'tools' and 'gateways' tables. - """ - op.alter_column("tools", "is_active", new_column_name="enabled") - op.add_column("tools", sa.Column("reachable", sa.Boolean(), nullable=False, server_default=sa.true())) - - op.alter_column("gateways", "is_active", new_column_name="enabled") - op.add_column("gateways", sa.Column("reachable", sa.Boolean(), nullable=False, server_default=sa.true())) - - -def downgrade(): - """ - Reverts the changes by renaming 'enabled' back to 'is_active' - and dropping the 'reachable' column in both 'tools' and 'gateways' tables. - """ - op.alter_column("tools", "enabled", new_column_name="is_active") - op.drop_column("tools", "reachable") - - op.alter_column("gateways", "enabled", new_column_name="is_active") - op.drop_column("gateways", "reachable") diff --git a/mcpgateway/bootstrap_db.py b/mcpgateway/bootstrap_db.py deleted file mode 100644 index 0d7f4761..00000000 --- a/mcpgateway/bootstrap_db.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -"""Database bootstrap/upgrade entry-point for MCP Gateway. - -Copyright 2025 -SPDX-License-Identifier: Apache-2.0 -Authors: Madhav Kandukuri - -The script: - -1. Creates a synchronous SQLAlchemy ``Engine`` from ``settings.database_url``. -2. Looks for an *alembic.ini* two levels up from this file to drive migrations. -3. If the database is still empty (no ``gateways`` table), it: - - builds the base schema with ``Base.metadata.create_all()`` - - stamps the migration head so Alembic knows it is up-to-date -4. Otherwise, it applies any outstanding Alembic revisions. -5. Logs a **"Database ready"** message on success. - -It is intended to be invoked via ``python3 -m mcpgateway.bootstrap_db`` or -directly with ``python3 mcpgateway/bootstrap_db.py``. -""" - -# Standard -import asyncio -from importlib.resources import files - -# Third-Party -from alembic import command -from alembic.config import Config -from sqlalchemy import create_engine, inspect - -# First-Party -from mcpgateway.config import settings -from mcpgateway.db import Base -from mcpgateway.services.logging_service import LoggingService - -# Initialize logging service first -logging_service = LoggingService() -logger = logging_service.get_logger(__name__) - - -async def main() -> None: - """ - Bootstrap or upgrade the database schema, then log readiness. - - Runs `create_all()` + `alembic stamp head` on an empty DB, otherwise just - executes `alembic upgrade head`, leaving application data intact. - - Args: - None - """ - engine = create_engine(settings.database_url) - ini_path = files("mcpgateway").joinpath("alembic.ini") - cfg = Config(str(ini_path)) # path in container - cfg.attributes["configure_logger"] = True - - with engine.begin() as conn: - cfg.attributes["connection"] = conn - cfg.set_main_option("sqlalchemy.url", settings.database_url) - - insp = inspect(conn) - - if "gateways" not in insp.get_table_names(): - logger.info("Empty DB detected - creating baseline schema") - Base.metadata.create_all(bind=conn) - command.stamp(cfg, "head") - else: - command.upgrade(cfg, "head") - - logger.info("Database ready") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/mcpgateway/main.py b/mcpgateway/main.py index 2ba43f71..17aaf81d 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -52,7 +52,6 @@ # First-Party from mcpgateway import __version__ from mcpgateway.admin import admin_router -from mcpgateway.bootstrap_db import main as bootstrap_db from mcpgateway.cache import ResourceCache, SessionRegistry from mcpgateway.config import jsonpath_modifier, settings from mcpgateway.db import Prompt as DbPrompt @@ -113,14 +112,6 @@ # Wait for database to be ready before creating tables wait_for_db_ready(max_tries=int(settings.db_max_retries), interval=int(settings.db_retry_interval_ms) / 1000, sync=True) # Converting ms to s -# Create database tables -try: - loop = asyncio.get_running_loop() -except RuntimeError: - asyncio.run(bootstrap_db()) -else: - loop.create_task(bootstrap_db()) - # Initialize plugin manager as a singleton. plugin_manager: PluginManager | None = PluginManager(settings.plugin_config_file) if settings.plugins_enabled else None diff --git a/tests/unit/mcpgateway/test_ui_version.py b/tests/unit/mcpgateway/test_ui_version.py index 5d4b68b6..27ddf640 100644 --- a/tests/unit/mcpgateway/test_ui_version.py +++ b/tests/unit/mcpgateway/test_ui_version.py @@ -15,74 +15,50 @@ from typing import Dict # Third-Party +# Third‑Party import pytest from starlette.testclient import TestClient # First-Party +# First‑Party from mcpgateway.config import settings -from mcpgateway.main import app -# --------------------------------------------------------------------------- # -# Fixtures -# --------------------------------------------------------------------------- # -@pytest.fixture(scope="session") -def test_client() -> TestClient: - """Spin up the FastAPI test client once for the whole session.""" - return TestClient(app) +# ────────────────────────────────────────────── +# Fixtures (local to this test file) +# ────────────────────────────────────────────── +@pytest.fixture(scope="module") +def test_client(app_with_temp_db) -> TestClient: + """ + Build a TestClient against the FastAPI app that + app_with_temp_db returns (i.e. the one wired to + the temporary SQLite database). + """ + return TestClient(app_with_temp_db) @pytest.fixture() def auth_headers() -> Dict[str, str]: - """ - Build the auth headers expected by the gateway: - - * Authorization: Basic - * X-API-Key: user:pw (plain text) - """ creds = f"{settings.basic_auth_user}:{settings.basic_auth_password}" basic_b64 = base64.b64encode(creds.encode()).decode() - return { "Authorization": f"Basic {basic_b64}", "X-API-Key": creds, } -# --------------------------------------------------------------------------- # +# ────────────────────────────────────────────── # Tests -# --------------------------------------------------------------------------- # -# def test_version_partial_html(test_client: TestClient, auth_headers: Dict[str, str]): -# """ -# /version?partial=true must return an HTML fragment with core meta-info. -# """ -# resp = test_client.get("/version?partial=true", headers=auth_headers) -# assert resp.status_code == 200 -# assert "text/html" in resp.headers["content-type"] - -# html = resp.text -# # Very loose sanity checks - we only care that it is an HTML fragment -# # and that some well-known marker exists. -# assert "