Skip to content

Fix MCP servers/gateway tags not persisting in DB#2250

Merged
crivetimihai merged 2 commits intomainfrom
2203_fix_tags_mcpservers
Jan 21, 2026
Merged

Fix MCP servers/gateway tags not persisting in DB#2250
crivetimihai merged 2 commits intomainfrom
2203_fix_tags_mcpservers

Conversation

@kevalmahajan
Copy link
Copy Markdown
Member

@kevalmahajan kevalmahajan commented Jan 20, 2026

🐛 Bug-fix PR

📌 Summary

Closes #2203

Fixed tags not being properly stored in the database for MCP servers/gateways. Tags were being validated and converted to list of dictionaries [{"id": "tag", "label": "Tag"}] by the Pydantic schema, but the database model type annotation incorrectly specified List[str], causing tags to be stored as empty lists [] instead of the expected format.

🔁 Reproduction Steps

  1. Navigate to MCP Servers tab in the UI
  2. Create or update an MCP server/gateway with tags (e.g., "dev", "git", "mcp")
  3. Check the database: SELECT tags FROM gateways;
  4. Bug:
    • Tags column shows [] (empty list) instead of [{"id": "dev", "label": "dev"}, ...]
    • Tags do not display in the UI

🐞 Root Cause

  1. The database model in mcpgateway/db.py had an incorrect type annotation:
tags: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=False)
  1. The validate_tags_field() function is used as a Pydantic field validator in both GatewayCreate and GatewayUpdate schemas. This function:

    1. Takes input: Optional[List[str]] - raw tag strings like ["dev", "git", "mcp"]
    2. Normalizes: Converts to lowercase, removes duplicates, validates format
    3. Returns: List[Dict[str, str]] - structured format like [{"id": "dev", "label": "dev"}, {"id": "git", "label": "git"}]
  2. The Problem:

    1. Pydantic validator returns: List[Dict[str, str]] → [{"id": "dev", "label": "dev"}]
    2. Database model expects: List[str] → ["dev", "git"]
    3. SQLAlchemy sees the type mismatch and fails to serialize the dictionaries properly
    4. Result: Empty list [] stored in database

💡 Fix Description

  1. Database Model Type Fix: mcpgateway/db.py

Updated the Gateway model's tags type annotation to match what validate_tags_field() actually returns:

# Before - incorrect type annotation
tags: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=False)

# After - correct type annotation matching validate_tags_field() output
tags: Mapped[List[Dict[str, str]]] = mapped_column(JSON, default=list, nullable=False)

This aligns the database model with:
1. The output of validate_tags_field()
2. The format used by virtual servers and tools (which were working correctly)
3. The UI's expectation of [{"id": "tag", "label": "Tag"}] format

  1. Service Layer Cleanup mcpgateway/services/gateway_service.py
    Only call the tag conversion in convert_gateway_to_read() method in the tags are List[str], otherwise the fucntion call is not needed. Previously, this method was calling validate_tags_field() again on already-validated tags from the database, which was redundant. Now it passes tags through directly with backward compatibility:
# Before - was re-validating already validated tags
if gateway.tags:
    gateway_dict["tags"] = validate_tags_field(gateway.tags)
else:
    gateway_dict["tags"] = []

# After - pass through directly with backward compatibility check
if gateway.tags:
    # Check if tags are old format (List[str]) or new format (List[Dict[str, str]])
    if isinstance(gateway.tags[0], str):
        # Convert old format to new format for backward compatibility
        gateway_dict["tags"] = validate_tags_field(gateway.tags)
    else:
        # Already in correct format, pass through
        gateway_dict["tags"] = gateway.tags
else:
    gateway_dict["tags"] = []

🧪 Verification

Check Command Status
Lint suite make lint
Unit tests make test
Coverage ≥ 90 % make coverage
Manual regression no longer fails steps / screenshots

📐 MCP Compliance (if relevant)

  • Matches current MCP spec
  • No breaking change to MCP clients

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • No secrets/credentials committed

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
@crivetimihai crivetimihai force-pushed the 2203_fix_tags_mcpservers branch from a247f59 to d3279e1 Compare January 21, 2026 01:14
@crivetimihai
Copy link
Copy Markdown
Member

Additional Fixes Applied

The original PR correctly identified that Gateway.tags type annotation needed to change from List[str] to List[Dict[str, str]] to match the Pydantic schema output. However, several additional issues were identified and fixed:

1. Tag Filtering (High Priority)

Problem: json_contains_expr() assumed tags were stored as List[str], so filtering by tags=["dev"] would never match dict-format tags like [{"id": "dev", "label": "dev"}].

Fix: Added new json_contains_tag_expr() function that handles both legacy string and new dict formats:

  • SQLite: Uses CASE WHEN type = 'object' THEN json_extract(value, '$.id') END to safely extract IDs from objects while avoiding "malformed JSON" errors on string elements
  • PostgreSQL: Uses col.contains([{"id": tag_value}])
  • MySQL: Uses JSON_CONTAINS with dict format

Updated all 6 services (gateway, tool, server, prompt, resource, a2a) to use this function.

2. Catalog Service Re-validation Loop (Medium Priority)

Problem: catalog_service.py unconditionally called validate_tags_field() on tags from DB. When tags were already dicts, str(dict) produced garbage strings that failed validation, resulting in empty tags.

Fix: Added format detection before calling validate_tags_field() - dicts now pass through unchanged.

3. Legacy Tag Handling in _prepare_gateway_for_read (Medium Priority)

Problem: The original PR removed tag conversion from _prepare_gateway_for_read(), but this method is still called from multiple read paths. Legacy List[str] tags would fail GatewayRead Pydantic validation.

Fix: Restored tag conversion with format detection - only converts if tags are strings.

Files Changed

  • mcpgateway/utils/sqlalchemy_modifier.py - New json_contains_tag_expr() function
  • mcpgateway/services/*.py (6 files) - Updated to use json_contains_tag_expr
  • mcpgateway/services/catalog_service.py - Fixed tag pass-through
  • mcpgateway/services/gateway_service.py - Fixed _prepare_gateway_for_read
  • tests/unit/mcpgateway/services/*.py (5 files) - Updated mocks

All tests pass. Bandit security checks pass.

Address issues with dict-format tag storage introduced by migration:

- Add json_contains_tag_expr() to handle both List[str] and List[Dict]
  tag formats in database queries (SQLite, PostgreSQL, MySQL)
- Update all services (gateway, tool, server, prompt, resource, a2a)
  to use json_contains_tag_expr for tag filtering
- Fix catalog_service.py to pass through dict-format tags without
  re-validation
- Restore legacy tag handling in _prepare_gateway_for_read for
  backward compatibility
- Update tests to mock json_contains_tag_expr instead of json_contains_expr

Closes #2203

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai crivetimihai force-pushed the 2203_fix_tags_mcpservers branch from d3279e1 to e7f2ef0 Compare January 21, 2026 09:06
@crivetimihai crivetimihai merged commit 1176ffc into main Jan 21, 2026
51 checks passed
@crivetimihai crivetimihai deleted the 2203_fix_tags_mcpservers branch January 21, 2026 09:18
kcostell06 pushed a commit to kcostell06/mcp-context-forge that referenced this pull request Feb 24, 2026
* fix tags in mcp servers

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* fix(tags): handle dict-format tags in filtering and read paths

Address issues with dict-format tag storage introduced by migration:

- Add json_contains_tag_expr() to handle both List[str] and List[Dict]
  tag formats in database queries (SQLite, PostgreSQL, MySQL)
- Update all services (gateway, tool, server, prompt, resource, a2a)
  to use json_contains_tag_expr for tag filtering
- Fix catalog_service.py to pass through dict-format tags without
  re-validation
- Restore legacy tag handling in _prepare_gateway_for_read for
  backward compatibility
- Update tests to mock json_contains_tag_expr instead of json_contains_expr

Closes IBM#2203

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Tags for MCP servers not saved

2 participants