-
Notifications
You must be signed in to change notification settings - Fork 232
Description
🧭 Epic
Title: Comprehensive Tag Support System
Goal: Enable flexible tagging capabilities across all MCP Gateway entities (tools, resources, prompts, servers, gateways) to support dynamic categorization, filtering, and organization.
Why now: The Dynamic Virtual Server API depends on tag-based filtering. Currently, no entities support tags, limiting organizational flexibility and preventing implementation of advanced features like dynamic server composition and intelligent grouping.
Enables:
- [DESIGN]: Architecture Decisions and Discussions for AI Middleware and Plugin Framework (Enables #319) #313
- [Feature Request]: AI Middleware Integration / Plugin Framework for extensible gateway capabilities #319
- [Feature Request]: Dynamic Server Catalog via Rule, Regexp, Tags - or Embedding / LLM-Based Selection #123
🧭 Type of Feature
- Enhancement to existing functionality
- New feature or capability
- Security Related (requires review)
🙋♂️ User Story 1
As a: gateway admin
I want: to assign multiple tags to any tool, resource, or prompt
So that: I can organize entities by category, team, or purpose
✅ Acceptance Criteria
Scenario: Add tags to a tool
Given I have a tool with id "analytics-tool-1"
When I PATCH /tools/analytics-tool-1 with {"tags": ["analytics", "finance", "internal"]}
Then the tool has tags = ["analytics", "finance", "internal"]
And GET /tools/analytics-tool-1 returns the tags in the response
Scenario: Update existing tags
Given a tool with tags = ["old-tag"]
When I PATCH the tool with {"tags": ["new-tag", "another-tag"]}
Then the old tags are replaced with the new ones
🙋♂️ User Story 2
As a: API consumer
I want: to filter entities by one or more tags
So that: I can retrieve only relevant items
✅ Acceptance Criteria
Scenario: Filter tools by single tag
Given tools exist with various tags
When I GET /tools?tag=analytics
Then I only receive tools that have "analytics" in their tags array
Scenario: Filter by multiple tags (AND logic)
Given tools with different tag combinations
When I GET /tools?tag=analytics&tag=internal
Then I only receive tools that have BOTH "analytics" AND "internal" tags
Scenario: Empty result for non-existent tag
When I GET /tools?tag=nonexistent
Then I receive an empty array
🙋♂️ User Story 3
As a: server admin
I want: to tag servers and gateways for organizational purposes
So that: I can group related infrastructure components
✅ Acceptance Criteria
Scenario: Tag a server
Given a server with id "prod-server-1"
When I PATCH /servers/prod-server-1 with {"tags": ["production", "region-us-east"]}
Then the server is tagged appropriately
Scenario: Filter servers by tag
When I GET /servers?tag=production
Then I only see production servers
🙋♂️ User Story 4
As a: UI developer
I want: to see all unique tags across a resource type
So that: I can build tag clouds or filter interfaces
✅ Acceptance Criteria
Scenario: Get all tool tags
Given tools with tags: ["analytics", "finance"], ["analytics", "risk"], ["operations"]
When I GET /tools/tags
Then I receive ["analytics", "finance", "risk", "operations"] (unique, sorted)
Scenario: Tag frequency count
When I GET /tools/tags?include_count=true
Then I receive [{"tag": "analytics", "count": 2}, {"tag": "finance", "count": 1}, ...]
🙋♂️ User Story 5
As a: operations team member
I want: to bulk update tags on multiple entities
So that: I can reorganize without individual updates
✅ Acceptance Criteria
Scenario: Bulk add tag to tools
Given a list of tool IDs
When I POST /tools/bulk-tag with {"ids": ["tool1", "tool2"], "operation": "add", "tags": ["deprecated"]}
Then both tools have "deprecated" added to their existing tags
Scenario: Bulk remove tag
When I POST /tools/bulk-tag with {"ids": ["tool1", "tool2"], "operation": "remove", "tags": ["old-tag"]}
Then "old-tag" is removed from both tools' tags
🙋♂️ User Story 6
As a: admin
I want: tag validation and normalization
So that: tags remain consistent and searchable
✅ Acceptance Criteria
Scenario: Tag normalization
When I add tags ["Finance", "FINANCE", " finance "]
Then they are stored as ["finance"] (lowercase, trimmed, deduplicated)
Scenario: Invalid tag rejection
When I try to add tags ["", "a", "this-tag-is-way-too-long-and-exceeds-reasonable-limits"]
Then I receive a validation error
Scenario: Special character handling
When I add tags ["high-priority", "team:backend", "v2.0"]
Then they are accepted (alphanumeric, dash, colon, dot allowed)
🙋♂️ User Story 7
As a: dynamic server user
I want: to reference tags in dynamic server rules
So that: my virtual servers automatically include tagged entities
✅ Acceptance Criteria
Scenario: Dynamic rule using tags
Given tools tagged with "analytics"
And a dynamic server rule: type = "tool", filter = "$.tags[?(@ == 'analytics')]"
When I GET /dynamic/my-server/tools
Then I see all tools tagged "analytics"
Scenario: Complex tag queries
Given a rule: filter = "$.tags[?(@ == 'finance' || @ == 'risk')]"
Then tools with either "finance" OR "risk" tags are included
📐 Design Sketch
🗄️ Database Schema Changes
-- Add tags column to each entity table
ALTER TABLE tools ADD COLUMN tags JSON DEFAULT '[]';
ALTER TABLE resources ADD COLUMN tags JSON DEFAULT '[]';
ALTER TABLE prompts ADD COLUMN tags JSON DEFAULT '[]';
ALTER TABLE servers ADD COLUMN tags JSON DEFAULT '[]';
ALTER TABLE gateways ADD COLUMN tags JSON DEFAULT '[]';
-- Create indexes for tag queries (PostgreSQL GIN index example)
CREATE INDEX idx_tools_tags ON tools USING GIN (tags);
CREATE INDEX idx_resources_tags ON resources USING GIN (tags);
-- ... repeat for other tables
🧩 API Modifications
Entity Type | Tag Endpoints | Filter Support |
---|---|---|
Tools | PATCH /tools/{id} with tagsGET /tools/tags POST /tools/bulk-tag |
GET /tools?tag=... |
Resources | PATCH /resources/{id} with tagsGET /resources/tags POST /resources/bulk-tag |
GET /resources?tag=... |
Prompts | PATCH /prompts/{id} with tagsGET /prompts/tags POST /prompts/bulk-tag |
GET /prompts?tag=... |
Servers | PATCH /servers/{id} with tagsGET /servers/tags |
GET /servers?tag=... |
Gateways | PATCH /gateways/{id} with tagsGET /gateways/tags |
GET /gateways?tag=... |
🔧 Tag Validation Rules
class TagValidator:
MIN_LENGTH = 2
MAX_LENGTH = 50
ALLOWED_PATTERN = r'^[a-z0-9][a-z0-9\-\:\.]*[a-z0-9]$'
@staticmethod
def normalize(tag: str) -> str:
return tag.strip().lower()
@staticmethod
def validate(tag: str) -> bool:
normalized = TagValidator.normalize(tag)
return (
len(normalized) >= TagValidator.MIN_LENGTH and
len(normalized) <= TagValidator.MAX_LENGTH and
re.match(TagValidator.ALLOWED_PATTERN, normalized)
)
🎨 UI Implementation Details
📊 Tag Display in Lists (admin.html)
For each entity table (tools, resources, prompts, servers, gateways), add a new "Tags" column:
<!-- In table header -->
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Tags
</th>
<!-- In table body -->
<td class="px-6 py-4 whitespace-normal">
<div class="flex flex-wrap gap-1">
{% for tag in entity.tags %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
{{ tag }}
</span>
{% endfor %}
{% if not entity.tags %}
<span class="text-gray-500 dark:text-gray-400 text-xs">No tags</span>
{% endif %}
</div>
</td>
🏷️ Tag Input Component (admin.js)
Create a reusable tag input component with these features:
- Comma-separated input: Type tags separated by commas
- Tag pills display: Show selected tags as removable pills
- Autocomplete: Suggest existing tags as user types
- Validation: Real-time validation with error messages
- Keyboard navigation: Tab to complete, backspace to remove
class TagInput {
constructor(elementId, options = {}) {
this.element = document.getElementById(elementId);
this.tags = options.initialTags || [];
this.maxTags = options.maxTags || 20;
this.existingTags = options.existingTags || [];
this.onTagsChange = options.onTagsChange || (() => {});
this.init();
}
init() {
// Create wrapper with input and pills container
this.wrapper = document.createElement('div');
this.wrapper.className = 'tag-input-wrapper border rounded-md p-2';
this.pillsContainer = document.createElement('div');
this.pillsContainer.className = 'flex flex-wrap gap-1 mb-2';
this.input = document.createElement('input');
this.input.type = 'text';
this.input.placeholder = 'Add tags (comma-separated)...';
this.input.className = 'w-full outline-none';
// Set up event listeners
this.input.addEventListener('keydown', (e) => this.handleKeydown(e));
this.input.addEventListener('input', (e) => this.handleInput(e));
this.input.addEventListener('blur', () => this.addPendingTags());
this.wrapper.appendChild(this.pillsContainer);
this.wrapper.appendChild(this.input);
this.element.appendChild(this.wrapper);
this.renderTags();
}
addTag(tag) {
const normalized = this.normalizeTag(tag);
if (normalized && !this.tags.includes(normalized) && this.tags.length < this.maxTags) {
this.tags.push(normalized);
this.renderTags();
this.onTagsChange(this.tags);
return true;
}
return false;
}
removeTag(tag) {
this.tags = this.tags.filter(t => t !== tag);
this.renderTags();
this.onTagsChange(this.tags);
}
renderTags() {
this.pillsContainer.innerHTML = '';
this.tags.forEach(tag => {
const pill = document.createElement('span');
pill.className = 'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800';
pill.innerHTML = `
${escapeHtml(tag)}
<button type="button" class="ml-1 text-blue-600 hover:text-blue-800" data-tag="${escapeHtml(tag)}">
<svg class="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
`;
pill.querySelector('button').addEventListener('click', () => this.removeTag(tag));
this.pillsContainer.appendChild(pill);
});
}
}
✏️ Edit Modal Integration
For each edit modal, add a tags section:
<!-- In edit modal form -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Tags</label>
<div id="edit-{entity}-tags-container" class="mt-1"></div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Add tags to categorize this {entity}. Use lowercase letters, numbers, hyphens, and underscores.
</p>
</div>
🔍 Tag Filtering
Add a multi-select dropdown for tag filtering on each tab:
<!-- Above each entity table -->
<div class="flex items-center space-x-4 mb-4">
<div class="relative">
<button id="{entity}-tag-filter-btn" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
Filter by Tags
<span id="{entity}-tag-count" class="ml-2 px-2 py-0.5 text-xs bg-gray-200 rounded-full hidden">0</span>
</button>
<div id="{entity}-tag-filter-dropdown" class="absolute z-10 mt-2 w-64 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 hidden">
<div class="p-2">
<input type="text" placeholder="Search tags..." class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
</div>
<div class="max-h-60 overflow-y-auto">
<div id="{entity}-tag-list" class="py-1">
<!-- Populated dynamically -->
</div>
</div>
<div class="border-t px-3 py-2">
<button class="text-sm text-blue-600 hover:text-blue-800">Clear all</button>
</div>
</div>
</div>
<!-- Existing search and show inactive checkbox -->
</div>
📊 View Modal Enhancement
Update view modals to display tags prominently:
// In viewTool, viewResource, etc.
const renderTags = (tags) => {
if (!tags || tags.length === 0) {
return '<p><strong>Tags:</strong> <span class="text-gray-500">None</span></p>';
}
const tagBadges = tags.map(tag =>
`<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300 mr-1">
${escapeHtml(tag)}
</span>`
).join('');
return `
<div>
<strong>Tags:</strong>
<div class="mt-1 flex flex-wrap gap-1">
${tagBadges}
</div>
</div>
`;
};
🎯 Tag Management Tab
Add a new tab for centralized tag management:
<!-- In tab navigation -->
<a href="#tags" id="tab-tags" class="tab-link ...">
Tag Management
</a>
<!-- Tag management panel -->
<div id="tags-panel" class="tab-panel hidden">
<div class="bg-white shadow rounded-lg p-6 dark:bg-gray-800">
<h2 class="text-2xl font-bold dark:text-gray-200 mb-4">Tag Overview</h2>
<!-- Tag statistics grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded">
<h3 class="font-medium text-gray-900 dark:text-gray-100">Total Tags</h3>
<p class="text-2xl font-bold text-indigo-600" id="total-tags-count">0</p>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded">
<h3 class="font-medium text-gray-900 dark:text-gray-100">Most Used Tag</h3>
<p class="text-lg font-medium text-gray-700 dark:text-gray-300" id="most-used-tag">-</p>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded">
<h3 class="font-medium text-gray-900 dark:text-gray-100">Tagged Entities</h3>
<p class="text-2xl font-bold text-green-600" id="tagged-entities-count">0</p>
</div>
</div>
<!-- Tag cloud -->
<div class="mb-8">
<h3 class="text-lg font-medium mb-4">Tag Cloud</h3>
<div id="tag-cloud" class="flex flex-wrap gap-2">
<!-- Populated dynamically with varying sizes based on usage -->
</div>
</div>
<!-- Tag usage table -->
<div>
<h3 class="text-lg font-medium mb-4">Tag Usage Details</h3>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tag</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tools</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Resources</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Prompts</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Servers</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Gateways</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Total</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody id="tag-usage-tbody" class="bg-white divide-y divide-gray-200 dark:bg-gray-900">
<!-- Populated dynamically -->
</tbody>
</table>
</div>
</div>
</div>
🔄 Alternatives Considered
- Separate tags table: More complex joins, but better normalization → decided against for performance
- Tag hierarchies: Parent/child tags → too complex for initial implementation
- Predefined tag taxonomy: Restrictive for users → free-form tags more flexible
- Tags as separate service: Over-engineering → integrated approach simpler
📓 Additional Context
- Tags should be case-insensitive for queries but preserve original case for display
- Consider tag autocomplete API for UI (
/tags/autocomplete?prefix=...
) - Migration must handle existing data gracefully (default empty array)
- Performance consideration: Tag queries should use database indexes
- Future enhancement: Tag aliases (e.g., "ml" → "machine-learning")
🧭 Tasks
Area | Task | Notes | Priority |
---|---|---|---|
Database | [ ] Create migration to add tags columns | Use JSON column type | P1 |
[ ] Add database indexes for tag queries | Platform-specific (GIN for PostgreSQL) | P1 | |
[ ] Add tags field to all ORM models | Default to empty list | P1 | |
Schemas | [ ] Update Pydantic schemas (Create/Read/Update) | Add Optional[List[str]] tags field | P1 |
[ ] Create TagValidator class | Normalization and validation logic | P1 | |
[ ] Add tag field validators with security checks | Prevent XSS, SQL injection | P1 | |
[ ] Add BulkTagOperation schema | For bulk updates | P2 | |
API | [ ] Add tags to PATCH endpoints | All entity types | P1 |
[ ] Add tag filter to GET list endpoints | Query parameter support | P1 | |
[ ] Create /tags endpoints for each entity | List unique tags | P2 | |
[ ] Implement bulk tag operations | Add/remove/replace | P2 | |
Services | [ ] Update service layer for tag operations | CRUD + filtering | P1 |
[ ] Implement tag normalization | On write operations | P1 | |
[ ] Add tag aggregation queries | For tag clouds | P2 | |
Admin UI - Display | [ ] Add tag badges to entity list tables | Styled tag pills in admin.html | P1 |
[ ] Show tags in view modals | Display tags with proper styling | P1 | |
[ ] Add tag count indicators | Show number of tags per entity | P2 | |
[ ] Implement tag tooltips | Show full tag list on hover for truncated displays | P3 | |
Admin UI - Forms | [ ] Add tag input to Add Tool form | Multi-tag input with validation | P1 |
[ ] Add tag input to Add Resource form | Multi-tag input with validation | P1 | |
[ ] Add tag input to Add Prompt form | Multi-tag input with validation | P1 | |
[ ] Add tag input to Add Server form | Multi-tag input with validation | P1 | |
[ ] Add tag input to Add Gateway form | Multi-tag input with validation | P1 | |
Admin UI - Edit Modals | [ ] Add tag editor to Edit Tool modal | Editable tag list with add/remove | P1 |
[ ] Add tag editor to Edit Resource modal | Editable tag list with add/remove | P1 | |
[ ] Add tag editor to Edit Prompt modal | Editable tag list with add/remove | P1 | |
[ ] Add tag editor to Edit Server modal | Editable tag list with add/remove | P1 | |
[ ] Add tag editor to Edit Gateway modal | Editable tag list with add/remove | P1 | |
Admin UI - Filtering | [ ] Add tag filter dropdown to each tab | Multi-select tag filter | P2 |
[ ] Implement tag search/autocomplete | Quick tag selection | P2 | |
[ ] Add "clear filters" button | Reset all tag filters | P2 | |
[ ] Persist filter state in URL | Shareable filtered views | P3 | |
Admin UI - Tag Management | [ ] Create dedicated Tags tab | Central tag management interface | P2 |
[ ] Show tag usage statistics | Count of entities per tag | P2 | |
[ ] Bulk tag operations UI | Select multiple entities for tagging | P3 | |
[ ] Tag rename/merge functionality | Admin tag maintenance | P3 | |
Frontend - admin.js | [ ] Create tag input component | Reusable tag editor widget | P1 |
[ ] Add tag validation functions | Client-side validation | P1 | |
[ ] Implement tag autocomplete | Suggest existing tags | P2 | |
[ ] Add tag filter state management | Handle multi-tag filtering | P2 | |
[ ] Create tag badge rendering | Safe HTML generation | P1 | |
Frontend - Styling | [ ] Design tag pill styles | Colors, hover states, remove buttons | P1 |
[ ] Create tag input styles | Match existing form design | P1 | |
[ ] Add tag filter dropdown styles | Consistent with other filters | P2 | |
[ ] Responsive tag display | Handle overflow gracefully | P1 | |
Tests | [ ] Unit tests for tag validation | Edge cases, special characters | P1 |
[ ] Integration tests for tag filtering | Single and multiple tags | P1 | |
[ ] Security tests for tag input | XSS, injection attempts | P1 | |
[ ] Test bulk operations | Success and error cases | P2 | |
[ ] Performance tests for tag queries | Large datasets | P2 | |
[ ] UI tests for tag interactions | Add, remove, filter operations | P2 | |
Documentation | [ ] Update API documentation | New endpoints and parameters | P1 |
[ ] Add tagging guide | Best practices, examples | P2 | |
[ ] Document migration process | For existing deployments | P1 | |
[ ] Create UI usage guide | How to use tags in the admin interface | P2 |
🔗 Dependencies
- Database migration framework functional
- JSON column support in target database
- Full-text search capabilities (for future tag search)
🎯 Success Metrics
- All entity types support tags with consistent API
- Tag queries perform within 100ms for typical datasets
- Zero data loss during migration
- Dynamic server rules can filter by tags successfully
- Admin UI provides intuitive tag management
✅ Final Quality Checklist
Code Quality & Standards:
- Include updates to
schemas.py
with data validation, to ensure tags follow a strict format and approved list of characters, size, etc. Included with full test coverage including security testing. -
make flake8 ruff pylint
all pass -
mypy
passes for new code -
make lint-web
passes, no security issues -
make interrogate
maintained at 100% docstring coverage, using google docstring format, and arguments typing, with doctest -
make doctest
passes and maintained at > 60% -
make test
passes and maintained at > 82%, test cases included with > 80% coverage for new feature code -
make smoketest
passes -
make docker-run-ssl-host
passes -
make docker-stop compose-stop compose-rm compose-up
passes -
cd docs; make serve
shows no errors and new docs render correctly
Feature Completeness:
- All entity types (tools, resources, prompts, servers, gateways) support tags
- Tag validation prevents invalid/malicious input
- Tags are properly indexed for performance
- UI displays tags consistently across all views
- Tag filtering works for all entity types
- Bulk tag operations are functional
- Tag management interface is complete
- Documentation is comprehensive
Security Review:
- Tag input sanitization prevents XSS
- SQL injection prevention for tag queries
- Tag size limits enforced
- Special characters properly escaped
- API rate limiting for tag operations
Performance Validation:
- Tag queries optimized with indexes
- Bulk operations use efficient SQL
- UI renders large tag lists smoothly
- Autocomplete performs well with many tags
- No N+1 query problems
User Experience:
- Tag input is intuitive
- Validation messages are helpful
- Tag filtering is discoverable
- Bulk operations have confirmation
- Tag management provides clear overview