Skip to content

Consistent duplicate constraints/working in tools, prompts and resources#2507

Merged
crivetimihai merged 1 commit intomainfrom
resources_prompts_constraints
Jan 27, 2026
Merged

Consistent duplicate constraints/working in tools, prompts and resources#2507
crivetimihai merged 1 commit intomainfrom
resources_prompts_constraints

Conversation

@kevalmahajan
Copy link
Copy Markdown
Member

@kevalmahajan kevalmahajan commented Jan 26, 2026

🐛 Bug-fix PR

Closes #2352

Reference: (Continuation of #2351)

📌 Summary

Updates the unique constraints for Resources and Prompts tables to support Gateway-level namespacing. Previously, these entities enforced uniqueness globally per Team/Owner (team_id, owner_email, uri/name). This prevented users from registering the same Gateway (e.g., using one-time auth) multiple times with different names, as the Resources/Prompts would collide.

This adds consistency with the working of tools, prompts and resources.
Handles both scenarios, prompts and resources added with gateway and without gateway/directly.

🔁 Reproduction Steps

  1. Enable "one-time auth" for Gateway registration.
  2. Register a Gateway (e.g., http://example.com as "Gateway A") containing Resources and Prompts.
  3. Register the same Gateway again (e.g., http://example.com as "Gateway B") for the same Team/User.
  4. Observed Error: Registration fails with an IntegrityError (Unique Constraint Violation) for Resources (on uri) or Prompts (on name).
  5. Expected Behavior: Registration succeeds; Resources/Prompts are duplicated but namespaced to their respective Gateway (similar to how Tools work).

🐞 Root Cause

Database Schema: The Resource table had a unique constraint on (team_id, owner_email, uri). The Prompt table had a unique constraint on (team_id, owner_email, name).

Logic: When a second Gateway attempted to sync the same Resources/Prompts, it tried to insert records with the same identifier for the same Team/Owner. Unlike Tools, which compute a namespaced name, Resources/Prompts utilize their raw identifiers, causing a collision at the database level.

💡 Fix Description

1. Updated Constraints: Modified mcpgateway/db.py to replace the restrictive team-level constraints with composite constraints that include gateway_id.

# Resources: 
UniqueConstraint("team_id", "owner_email", "gateway_id", "uri")
# Prompts: 
UniqueConstraint("team_id", "owner_email", "gateway_id", "name")
  1. Added a Partial Unique Index specifically for local items (WHERE gateway_id IS NULL) to both Resources and Prompts:
Scope Constraint / Index Type Columns Included Condition Purpose
Global Unique Constraint (team_id, owner_email, gateway_id, identifier) Handles federated items; allows duplicates across different gateways
Local Unique Index (team_id, owner_email, identifier) WHERE gateway_id IS NULL Strictly enforces uniqueness for local (non-gateway) items

3. Migration: Added a new Alembic migration script (b1b2b3b4b5b6_fix_constraints.py) to apply these schema changes.

4. Result:

  1. Resources and Prompts are now unique per Gateway within a Team when added with one_time_auth constraint while registering a gateway.
  2. This maintains data integrity (preventing duplicates within a single Gateway) while allowing the same content to exist across multiple Gateways owned by the same Team.
  3. When one-time-auth is disabled, it properly follows, the public/team/private level constraints for exact same gateway and highlights duplicate error in that case.

🧪 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

Copy link
Copy Markdown
Member

@brian-hussey brian-hussey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is fine, I've suggested a couple of changes from a readability point of view.

I tested the DB upgrade and downgrade path to this from rev this-2 to this-1 and this, and then back down to -1 and -2 and everything worked well.

@kevalmahajan
Copy link
Copy Markdown
Member Author

Hi @brian-hussey,

Since we use make black for code formatting, it will automatically reformat the code. Any manual changes made solely for readability will be reverted when we run make black to maintain consistent formatting across the codebase.

@ramcysiddique
Copy link
Copy Markdown
Contributor

@kevalmahajan Tested the fix in local, and seems to be working fine.

@brian-hussey
Copy link
Copy Markdown
Member

Hi @brian-hussey,

Since we use make black for code formatting, it will automatically reformat the code. Any manual changes made solely for readability will be reverted when we run make black to maintain consistent formatting across the codebase.

Ah cool, thanks. I'll resolve the conversation so.

Copy link
Copy Markdown
Member

@brian-hussey brian-hussey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested changes locally, up 2 versions to this and down 2 versions.
Everything worked as expected.

@kevalmahajan kevalmahajan added the wxo wxo integration label Jan 27, 2026
@crivetimihai crivetimihai force-pushed the resources_prompts_constraints branch from 7b9ed73 to a526ea8 Compare January 27, 2026 13:21
@crivetimihai
Copy link
Copy Markdown
Member

Changes Made During Review

1. Rebased onto main

  • Branch rebased onto current main (8257613)
  • Squashed 5 commits into 1 clean commit

2. Fixed Migration Idempotency (Critical)

The original migration was not idempotent and would fail when:

  • Running against a DB created via Base.metadata.create_all() (tests use this)
  • Re-running migrations in CI
  • Partially applied migrations

Changes made:

  • Added constraint_exists() helper to check unique constraints before drop/create
  • Added index_exists() helper to check indexes before drop/create
  • Added table existence checks (inspector.get_table_names())
  • Made both upgrade() and downgrade() fully idempotent
  • Follows existing patterns from f3a3a3d901b8_remove_gateway_url_unique_constraint.py

3. Code Review Summary

Design is sound:

  • Adding gateway_id to composite constraints allows same uri/name across different gateways
  • Partial unique index for gateway_id IS NULL enforces uniqueness for local items
  • Consistent with how prompts already handle this (event listeners compute namespaced names)

No security issues identified.

No performance concerns - constraint changes are schema-level.

Ready for Testing

Please run:

make test

@crivetimihai crivetimihai force-pushed the resources_prompts_constraints branch from a526ea8 to 9bcd4d9 Compare January 27, 2026 13:25
@crivetimihai
Copy link
Copy Markdown
Member

Additional Fixes (v2)

Addressed remaining issues:

1. Fixed exception handling in constraint_exists/index_exists

  • Changed from return True to return False on exception
  • Now if introspection fails, creates are attempted instead of skipped
  • All create/drop operations wrapped in try/except to handle "already exists" gracefully

2. Fixed early return bug

  • Refactored to use _upgrade_resources() and _upgrade_prompts() helper functions
  • Each table is now processed independently
  • Missing resources table no longer skips prompts migration

Summary of Changes

Issue Fix
Introspection failure skipped creates Return False → attempt create, catch errors
Early return skipped prompts Independent helper functions for each table
All operations Wrapped in try/except for graceful handling

Ready for testing: make test

crivetimihai

This comment was marked as outdated.

Updates unique constraints for Resources and Prompts tables to support
Gateway-level namespacing. Previously, these entities enforced uniqueness
globally per Team/Owner (team_id, owner_email, uri/name). This prevented
users from registering the same Gateway multiple times with different names.

Changes:
- Add gateway_id to unique constraints for resources and prompts
- Add partial unique indexes for local items (where gateway_id IS NULL)
- Make migration idempotent with proper existence checks

Closes #2352

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai crivetimihai force-pushed the resources_prompts_constraints branch from 9bcd4d9 to c0e20dd Compare January 27, 2026 13:33
@crivetimihai
Copy link
Copy Markdown
Member

Final Review Complete ✅

Changes Made

  1. Rebased & Squashed - 5 commits → 1 clean commit on latest main
  2. Made migration idempotent - Added proper existence checks for all constraints/indexes
  3. Fixed exception handling - Returns False on introspection failure so creates are attempted
  4. Fixed early return bug - Each table processed independently
  5. Added docstrings - Fixed flake8 DAR101 errors

Code Quality

  • make flake8 passes
  • make black isort applied
  • make ruff passes
  • ✅ Related tests pass (resource_service, prompt_service)

Follow-up

Regression tests for the new constraint behavior tracked in #2520

Ready to Merge

The PR is ready for final approval and merge.

@crivetimihai
Copy link
Copy Markdown
Member

Additional Follow-up Issue

Created #2522 to track the silent exception handling concern in the migration. The current implementation swallows all exceptions which could mask real schema failures.

Recommendation: Address #2522 before deploying to production environments where migration failures need to be surfaced.

@crivetimihai
Copy link
Copy Markdown
Member

✅ Manual Database Constraint Testing

Tested the implementation against a running PostgreSQL instance via docker-compose.

Verified Constraints

Resources table:

SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'resources'::regclass AND contype = 'u';

              conname               |              pg_get_constraintdef              
------------------------------------+------------------------------------------------
 uq_team_owner_gateway_uri_resource | UNIQUE (team_id, owner_email, gateway_id, uri)
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'resources' AND indexname LIKE '%local%';

            indexname             |  indexdef                                                                   
----------------------------------+-----------------------------------------------------------------------------
 uq_team_owner_uri_resource_local | CREATE UNIQUE INDEX ... WHERE (gateway_id IS NULL)

Prompts table: Same pattern verified ✅

Test Results

Test SQL Expected Actual
1. Duplicate with same gateway INSERT ... gateway_id='fast_time', uri='time://formats' ❌ Reject ERROR: duplicate key value violates unique constraint "uq_team_owner_gateway_uri_resource"
2. Same URI, different gateway INSERT ... gateway_id='fast_test', uri='time://formats' ✅ Allow INSERT 0 1
3. Create local resource INSERT ... gateway_id=NULL, uri='local://test' ✅ Allow INSERT 0 1
4. Duplicate local resource INSERT ... gateway_id=NULL, uri='local://test' ❌ Reject ERROR: duplicate key value violates unique constraint "uq_team_owner_uri_resource_local"

How to Reproduce

# Start the stack
docker compose up -d

# Connect to PostgreSQL
docker exec mcp-context-forge-postgres-1 psql -U postgres -d mcp

# Run test queries (see above)

Conclusion

The gateway namespacing constraints are working correctly:

  • ✅ Same resource/prompt URI can exist across different gateways
  • ✅ Duplicate within same gateway is rejected
  • ✅ Local items (gateway_id=NULL) still enforce uniqueness via partial index

@crivetimihai crivetimihai merged commit 674105c into main Jan 27, 2026
53 checks passed
@crivetimihai crivetimihai deleted the resources_prompts_constraints branch January 27, 2026 13:49
hughhennelly pushed a commit to hughhennelly/mcp-context-forge that referenced this pull request Feb 8, 2026
IBM#2507)

Updates unique constraints for Resources and Prompts tables to support
Gateway-level namespacing. Previously, these entities enforced uniqueness
globally per Team/Owner (team_id, owner_email, uri/name). This prevented
users from registering the same Gateway multiple times with different names.

Changes:
- Add gateway_id to unique constraints for resources and prompts
- Add partial unique indexes for local items (where gateway_id IS NULL)
- Make migration idempotent with proper existence checks

Closes IBM#2352

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: hughhennnelly <hughhennelly06@gmail.com>
kcostell06 pushed a commit to kcostell06/mcp-context-forge that referenced this pull request Feb 24, 2026
IBM#2507)

Updates unique constraints for Resources and Prompts tables to support
Gateway-level namespacing. Previously, these entities enforced uniqueness
globally per Team/Owner (team_id, owner_email, uri/name). This prevented
users from registering the same Gateway multiple times with different names.

Changes:
- Add gateway_id to unique constraints for resources and prompts
- Add partial unique indexes for local items (where gateway_id IS NULL)
- Make migration idempotent with proper existence checks

Closes IBM#2352

Signed-off-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

wxo wxo integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Multiple gateway import failing with unique constraint error for resources

4 participants