From 8bdc0433a9810f2f97b27de18af7b696cdc43d73 Mon Sep 17 00:00:00 2001 From: Veeresh K Date: Wed, 8 Oct 2025 10:45:16 +0000 Subject: [PATCH 1/8] added alembic script to resize url and slug columns to 191 Signed-off-by: Veeresh K --- ...2366_resize_url_and_slug_columns_to_191.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py diff --git a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py new file mode 100644 index 000000000..738d11c46 --- /dev/null +++ b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py @@ -0,0 +1,63 @@ +""""resize url and slug columns to 191" + +Revision ID: 878e287d2366 +Revises: 2f67b12600b4 +Create Date: 2025-10-08 09:08:35.363100 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision: str = '878e287d2366' +down_revision: Union[str, Sequence[str], None] = '2f67b12600b4' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # Truncate existing values longer than 191 chars + op.execute(""" + UPDATE gateways + SET slug = LEFT(slug, 191), + url = LEFT(url, 191) + WHERE LENGTH(slug) > 191 OR LENGTH(url) > 191; + """) + + # Resize columns to String(191) + op.alter_column( + 'gateways', + 'slug', + existing_type=sa.String(length=255), + type_=sa.String(length=191), + existing_nullable=False + ) + op.alter_column( + 'gateways', + 'url', + existing_type=sa.String(length=767), + type_=sa.String(length=191), + existing_nullable=False + ) + + +def downgrade() -> None: + """Downgrade schema.""" + op.alter_column( + 'gateways', + 'slug', + existing_type=sa.String(length=191), + type_=sa.String(length=255), + existing_nullable=False + ) + op.alter_column( + 'gateways', + 'url', + existing_type=sa.String(length=191), + type_=sa.String(length=767), + existing_nullable=False + ) \ No newline at end of file From 3cc26c35da82d723a6a1dcaba7f676e648c7663a Mon Sep 17 00:00:00 2001 From: Veeresh K Date: Thu, 9 Oct 2025 08:38:52 +0000 Subject: [PATCH 2/8] fixed pylint issues Signed-off-by: Veeresh K --- .../878e287d2366_resize_url_and_slug_columns_to_191.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py index 738d11c46..5ade0a196 100644 --- a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py +++ b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py @@ -9,7 +9,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import mysql # revision identifiers, used by Alembic. revision: str = '878e287d2366' @@ -60,4 +59,4 @@ def downgrade() -> None: existing_type=sa.String(length=191), type_=sa.String(length=767), existing_nullable=False - ) \ No newline at end of file + ) From 9954f5ac346bf5c26c1823aa597839205de83438 Mon Sep 17 00:00:00 2001 From: Veeresh K Date: Thu, 9 Oct 2025 12:51:55 +0000 Subject: [PATCH 3/8] added space for re run checks Signed-off-by: Veeresh K --- .../versions/878e287d2366_resize_url_and_slug_columns_to_191.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py index 5ade0a196..42d1166de 100644 --- a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py +++ b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py @@ -1,4 +1,4 @@ -""""resize url and slug columns to 191" +"""" resize url and slug columns to 191" Revision ID: 878e287d2366 Revises: 2f67b12600b4 From 0f5c804d5dc9431ab6c698725f71f75f0cb959d4 Mon Sep 17 00:00:00 2001 From: Veeresh K Date: Fri, 10 Oct 2025 04:39:50 +0000 Subject: [PATCH 4/8] fixed testcase Signed-off-by: Veeresh K --- .../test_translate_stdio_endpoint.py | 7 +- tests/unit/test_tool_service_output_schema.py | 81 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_tool_service_output_schema.py diff --git a/tests/unit/mcpgateway/test_translate_stdio_endpoint.py b/tests/unit/mcpgateway/test_translate_stdio_endpoint.py index 708d605ed..d61503d08 100644 --- a/tests/unit/mcpgateway/test_translate_stdio_endpoint.py +++ b/tests/unit/mcpgateway/test_translate_stdio_endpoint.py @@ -8,7 +8,7 @@ Tests for StdIOEndpoint class modifications to support dynamic environment variables. """ - +import sys import asyncio import json import logging @@ -271,13 +271,14 @@ async def test_multiple_env_vars(self, test_script, caplog): pubsub = _PubSub() - env_vars = { + env_vars = os.environ.copy() + env_vars.update({ "GITHUB_TOKEN": "github-token-123", "TENANT_ID": "acme-corp", "API_KEY": "api-key-456", "ENVIRONMENT": "production", "DEBUG": "false", - } + }) endpoint = StdIOEndpoint( "jq -cMn env", pubsub, env_vars) diff --git a/tests/unit/test_tool_service_output_schema.py b/tests/unit/test_tool_service_output_schema.py new file mode 100644 index 000000000..9aebf6f10 --- /dev/null +++ b/tests/unit/test_tool_service_output_schema.py @@ -0,0 +1,81 @@ +import asyncio +from unittest.mock import MagicMock + +import pytest + +from mcpgateway.services.tool_service import ToolService +from mcpgateway.models import TextContent + + +class FakeResponse: + def __init__(self, json_data, status_code=200): + self._json = json_data + self.status_code = status_code + + def json(self): + return self._json + + def raise_for_status(self): + return None + + +class FakeHttpClient: + def __init__(self, response: FakeResponse): + self._response = response + + async def request(self, method, url, json=None, headers=None): + return self._response + + async def get(self, url, params=None, headers=None): + return self._response + + +class DummyTool: + def __init__(self): + self.name = "dummy" + self.enabled = True + self.reachable = True + self.integration_type = "REST" + self.url = "http://example.local" + self.request_type = "POST" + self.headers = {} + self.auth_type = None + self.auth_value = None + self.jsonpath_filter = "" + # Provide an output_schema to trigger structured-content behavior + self.output_schema = {"type": "object", "properties": {"y": {"type": "number"}}} + # Minimal attributes expected by ToolService.invoke_tool + self.id = 1 + self.gateway_id = None + + +@pytest.mark.asyncio +async def test_invoke_tool_returns_structured_content_when_output_schema_present(): + svc = ToolService() + + # fake DB that returns our dummy tool for the select + db = MagicMock() + fake_tool = DummyTool() + # db.execute(...).scalar_one_or_none() should return the tool + m = MagicMock() + m.scalar_one_or_none.return_value = fake_tool + db.execute.return_value = m + + # Replace the http client with a fake response returning JSON + svc._http_client = FakeHttpClient(FakeResponse({"y": 10.0, "z": 20.0, "result": 30.0}, status_code=200)) + + result = await svc.invoke_tool(db, "dummy", {}) + + dumped = result.model_dump() + assert isinstance(dumped, dict) + # New behavior: when structuredContent is present and valid we remove + # the unstructured textual `content` entry and return the parsed object + # in `structuredContent` (clients should prefer structuredContent). + assert "structuredContent" in dumped + structured = dumped["structuredContent"] + assert isinstance(structured, dict) + assert structured.get("y") == 10.0 + assert structured.get("z") == 20.0 + assert structured.get("result") == 30.0 + # content may be empty when structuredContent is valid + assert dumped.get("content", []) == [] From 597a03d851fe171534220e67443b9bea17ba357e Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sat, 11 Oct 2025 02:33:48 +0100 Subject: [PATCH 5/8] Fix migration issues for VARCHAR(191) column resize - Update db.py SQLAlchemy model to match migration (String(191)) - Add dialect-specific SQL handling in migration (SQLite, PostgreSQL, MySQL) - Use CHAR_LENGTH for character-based truncation (not byte-based LENGTH in MySQL) - Ensure truncation works correctly across all supported database backends --- ...2366_resize_url_and_slug_columns_to_191.py | 44 ++++++++++++++++--- mcpgateway/db.py | 4 +- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py index 42d1166de..9d9350c7a 100644 --- a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py +++ b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py @@ -19,13 +19,43 @@ def upgrade() -> None: """Upgrade schema.""" - # Truncate existing values longer than 191 chars - op.execute(""" - UPDATE gateways - SET slug = LEFT(slug, 191), - url = LEFT(url, 191) - WHERE LENGTH(slug) > 191 OR LENGTH(url) > 191; - """) + # Get database dialect for dialect-specific operations + bind = op.get_bind() + dialect_name = bind.dialect.name + + # Truncate existing values longer than 191 chars using dialect-appropriate functions + if dialect_name == 'sqlite': + # SQLite uses SUBSTR and LENGTH + op.execute(""" + UPDATE gateways + SET slug = SUBSTR(slug, 1, 191), + url = SUBSTR(url, 1, 191) + WHERE LENGTH(slug) > 191 OR LENGTH(url) > 191; + """) + elif dialect_name == 'postgresql': + # PostgreSQL supports LEFT and CHAR_LENGTH + op.execute(""" + UPDATE gateways + SET slug = LEFT(slug, 191), + url = LEFT(url, 191) + WHERE CHAR_LENGTH(slug) > 191 OR CHAR_LENGTH(url) > 191; + """) + elif dialect_name == 'mysql': + # MySQL supports LEFT and CHAR_LENGTH (character-based, not byte-based) + op.execute(""" + UPDATE gateways + SET slug = LEFT(slug, 191), + url = LEFT(url, 191) + WHERE CHAR_LENGTH(slug) > 191 OR CHAR_LENGTH(url) > 191; + """) + else: + # Fallback for other databases + op.execute(""" + UPDATE gateways + SET slug = SUBSTR(slug, 1, 191), + url = SUBSTR(url, 1, 191) + WHERE LENGTH(slug) > 191 OR LENGTH(url) > 191; + """) # Resize columns to String(191) op.alter_column( diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 5e5e97afe..bf5b37507 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -2430,8 +2430,8 @@ class Gateway(Base): id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex) name: Mapped[str] = mapped_column(String(255), nullable=False) - slug: Mapped[str] = mapped_column(String(255), nullable=False) - url: Mapped[str] = mapped_column(String(767), nullable=False) + slug: Mapped[str] = mapped_column(String(191), nullable=False) + url: Mapped[str] = mapped_column(String(191), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) transport: Mapped[str] = mapped_column(String(20), default="SSE") capabilities: Mapped[Dict[str, Any]] = mapped_column(JSON) From 858080ee52a23819aa7b6259407444e0a91fe66d Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Sat, 11 Oct 2025 02:38:09 +0100 Subject: [PATCH 6/8] Add SQLite batch operations support for column type changes - SQLite doesn't support ALTER COLUMN TYPE directly - Use batch_alter_table for SQLite (creates temp table, copies data, renames) - Keep direct ALTER COLUMN for PostgreSQL and MySQL - Applies to both upgrade and downgrade operations - Fixes: sqlalchemy.exc.OperationalError with SQLite --- ...2366_resize_url_and_slug_columns_to_191.py | 94 +++++++++++++------ 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py index 9d9350c7a..1d18c40ee 100644 --- a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py +++ b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py @@ -58,35 +58,73 @@ def upgrade() -> None: """) # Resize columns to String(191) - op.alter_column( - 'gateways', - 'slug', - existing_type=sa.String(length=255), - type_=sa.String(length=191), - existing_nullable=False - ) - op.alter_column( - 'gateways', - 'url', - existing_type=sa.String(length=767), - type_=sa.String(length=191), - existing_nullable=False - ) + # SQLite requires batch operations for ALTER COLUMN + if dialect_name == 'sqlite': + with op.batch_alter_table('gateways', schema=None) as batch_op: + batch_op.alter_column( + 'slug', + existing_type=sa.String(length=255), + type_=sa.String(length=191), + existing_nullable=False + ) + batch_op.alter_column( + 'url', + existing_type=sa.String(length=767), + type_=sa.String(length=191), + existing_nullable=False + ) + else: + # PostgreSQL and MySQL support direct ALTER COLUMN + op.alter_column( + 'gateways', + 'slug', + existing_type=sa.String(length=255), + type_=sa.String(length=191), + existing_nullable=False + ) + op.alter_column( + 'gateways', + 'url', + existing_type=sa.String(length=767), + type_=sa.String(length=191), + existing_nullable=False + ) def downgrade() -> None: """Downgrade schema.""" - op.alter_column( - 'gateways', - 'slug', - existing_type=sa.String(length=191), - type_=sa.String(length=255), - existing_nullable=False - ) - op.alter_column( - 'gateways', - 'url', - existing_type=sa.String(length=191), - type_=sa.String(length=767), - existing_nullable=False - ) + # Get database dialect for dialect-specific operations + bind = op.get_bind() + dialect_name = bind.dialect.name + + # SQLite requires batch operations for ALTER COLUMN + if dialect_name == 'sqlite': + with op.batch_alter_table('gateways', schema=None) as batch_op: + batch_op.alter_column( + 'slug', + existing_type=sa.String(length=191), + type_=sa.String(length=255), + existing_nullable=False + ) + batch_op.alter_column( + 'url', + existing_type=sa.String(length=191), + type_=sa.String(length=767), + existing_nullable=False + ) + else: + # PostgreSQL and MySQL support direct ALTER COLUMN + op.alter_column( + 'gateways', + 'slug', + existing_type=sa.String(length=191), + type_=sa.String(length=255), + existing_nullable=False + ) + op.alter_column( + 'gateways', + 'url', + existing_type=sa.String(length=191), + type_=sa.String(length=767), + existing_nullable=False + ) From 1a308a09c836edb5454c5d6849709a55a514bd17 Mon Sep 17 00:00:00 2001 From: Veeresh K Date: Wed, 22 Oct 2025 12:51:39 +0000 Subject: [PATCH 7/8] changed down revision Signed-off-by: Veeresh K --- .../878e287d2366_resize_url_and_slug_columns_to_191.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py index 1d18c40ee..698fe2af4 100644 --- a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py +++ b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py @@ -1,7 +1,7 @@ """" resize url and slug columns to 191" Revision ID: 878e287d2366 -Revises: 2f67b12600b4 +Revises: 3c89a45f32e5 Create Date: 2025-10-08 09:08:35.363100 """ @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. revision: str = '878e287d2366' -down_revision: Union[str, Sequence[str], None] = '2f67b12600b4' +down_revision: Union[str, Sequence[str], None] = '3c89a45f32e5' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None From ac70bfab7b01be0576dea79c0540eabccb39942d Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 31 Oct 2025 10:30:12 +0530 Subject: [PATCH 8/8] Revises change after rebae Signed-off-by: rakdutta --- .../878e287d2366_resize_url_and_slug_columns_to_191.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py index 698fe2af4..a6abe3bab 100644 --- a/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py +++ b/mcpgateway/alembic/versions/878e287d2366_resize_url_and_slug_columns_to_191.py @@ -1,7 +1,7 @@ """" resize url and slug columns to 191" Revision ID: 878e287d2366 -Revises: 3c89a45f32e5 +Revises: h2b3c4d5e6f7 Create Date: 2025-10-08 09:08:35.363100 """ @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. revision: str = '878e287d2366' -down_revision: Union[str, Sequence[str], None] = '3c89a45f32e5' +down_revision: Union[str, Sequence[str], None] = 'h2b3c4d5e6f7' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None