Skip to content

Commit 708dc31

Browse files
committed
Add displayName and editing UUIDs for virtual servers
Signed-off-by: Mihai Criveti <[email protected]>
1 parent 07953af commit 708dc31

File tree

8 files changed

+208
-18
lines changed

8 files changed

+208
-18
lines changed

mcpgateway/alembic/versions/733159a4fa74_add_display_name_to_tools.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,32 @@
2222

2323

2424
def upgrade() -> None:
25-
"""Add display_name column to tools table."""
25+
"""Add display_name column to tools table and populate smart defaults for existing tools."""
2626
bind = op.get_bind()
2727
inspector = sa.inspect(bind)
2828

29+
def generate_display_name(technical_name: str) -> str:
30+
"""Generate display name from technical name.
31+
32+
Args:
33+
technical_name: The technical tool name
34+
35+
Returns:
36+
str: Human-readable display name
37+
"""
38+
# Standard
39+
import re
40+
41+
if not technical_name:
42+
return ""
43+
# Replace underscores, hyphens, and dots with spaces
44+
display_name = re.sub(r"[_\-\.]+", " ", technical_name)
45+
# Remove extra whitespace and title case
46+
display_name = " ".join(display_name.split())
47+
if display_name:
48+
display_name = display_name.title()
49+
return display_name
50+
2951
# Check if this is a fresh database without existing tables
3052
if not inspector.has_table("tools"):
3153
print("Tools table not found. Skipping display_name migration.")
@@ -34,8 +56,27 @@ def upgrade() -> None:
3456
# Check if column already exists
3557
tools_columns = [col["name"] for col in inspector.get_columns("tools")]
3658
if "display_name" not in tools_columns:
59+
# Add the column first
3760
op.add_column("tools", sa.Column("display_name", sa.String(), nullable=True))
3861
print("Added display_name column to tools table.")
62+
63+
# Populate smart displayName for existing tools that don't have one
64+
connection = bind
65+
result = connection.execute(sa.text("SELECT id, original_name, custom_name FROM tools WHERE display_name IS NULL"))
66+
tools_to_update = result.fetchall()
67+
68+
for tool in tools_to_update:
69+
# Use custom_name if available, otherwise original_name
70+
source_name = tool.custom_name if tool.custom_name else tool.original_name
71+
smart_display_name = generate_display_name(source_name)
72+
73+
# Update only tools without existing display_name
74+
connection.execute(sa.text("UPDATE tools SET display_name = :display_name WHERE id = :tool_id AND display_name IS NULL"), {"display_name": smart_display_name, "tool_id": tool.id})
75+
76+
if tools_to_update:
77+
print(f"Populated smart displayName for {len(tools_to_update)} existing tools.")
78+
79+
connection.commit()
3980
else:
4081
print("display_name column already exists in tools table.")
4182

mcpgateway/schemas.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,9 @@ def validate_name(cls, v: str) -> str:
372372
Returns:
373373
str: Value if validated as safe
374374
375+
Raises:
376+
ValueError: When displayName contains unsafe content or exceeds length limits
377+
375378
Examples:
376379
>>> from mcpgateway.schemas import ToolCreate
377380
>>> ToolCreate.validate_name('valid_tool')
@@ -394,6 +397,9 @@ def validate_url(cls, v: str) -> str:
394397
Returns:
395398
str: Value if validated as safe
396399
400+
Raises:
401+
ValueError: When displayName contains unsafe content or exceeds length limits
402+
397403
Examples:
398404
>>> from mcpgateway.schemas import ToolCreate
399405
>>> ToolCreate.validate_url('https://example.com')
@@ -445,6 +451,9 @@ def validate_display_name(cls, v: Optional[str]) -> Optional[str]:
445451
Returns:
446452
str: Value if validated as safe
447453
454+
Raises:
455+
ValueError: When displayName contains unsafe content or exceeds length limits
456+
448457
Examples:
449458
>>> from mcpgateway.schemas import ToolCreate
450459
>>> ToolCreate.validate_display_name('My Custom Tool')
@@ -857,6 +866,9 @@ def validate_display_name(cls, v: Optional[str]) -> Optional[str]:
857866
Returns:
858867
str: Value if validated as safe
859868
869+
Raises:
870+
ValueError: When displayName contains unsafe content or exceeds length limits
871+
860872
Examples:
861873
>>> from mcpgateway.schemas import ToolUpdate
862874
>>> ToolUpdate.validate_display_name('My Custom Tool')
@@ -2866,6 +2878,9 @@ def validate_id(cls, v: Optional[str]) -> Optional[str]:
28662878
Returns:
28672879
str: Value if validated as safe
28682880
2881+
Raises:
2882+
ValueError: When displayName contains unsafe content or exceeds length limits
2883+
28692884
Examples:
28702885
>>> from mcpgateway.schemas import ServerCreate
28712886
>>> ServerCreate.validate_id('550e8400-e29b-41d4-a716-446655440000')
@@ -2987,6 +3002,9 @@ def validate_id(cls, v: Optional[str]) -> Optional[str]:
29873002
Returns:
29883003
str: Value if validated as safe
29893004
3005+
Raises:
3006+
ValueError: When displayName contains unsafe content or exceeds length limits
3007+
29903008
Examples:
29913009
>>> from mcpgateway.schemas import ServerUpdate
29923010
>>> ServerUpdate.validate_id('550e8400-e29b-41d4-a716-446655440000')

mcpgateway/services/gateway_service.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
from mcpgateway.services.oauth_manager import OAuthManager
8383
from mcpgateway.services.tool_service import ToolService
8484
from mcpgateway.utils.create_slug import slugify
85+
from mcpgateway.utils.display_name import generate_display_name
8586
from mcpgateway.utils.retry_manager import ResilientHttpClient
8687
from mcpgateway.utils.services_auth import decode_auth, encode_auth
8788

@@ -469,7 +470,7 @@ async def register_gateway(
469470
original_name=tool.name,
470471
custom_name=tool.name,
471472
custom_name_slug=slugify(tool.name),
472-
display_name=tool.name,
473+
display_name=generate_display_name(tool.name),
473474
url=normalized_url,
474475
description=tool.description,
475476
integration_type="MCP", # Gateway-discovered tools are MCP type
@@ -656,7 +657,7 @@ async def fetch_tools_after_oauth(self, db: Session, gateway_id: str) -> Dict[st
656657
original_name=tool.name,
657658
custom_name=tool.name,
658659
custom_name_slug=slugify(tool.name),
659-
display_name=tool.name,
660+
display_name=generate_display_name(tool.name),
660661
url=gateway.url.rstrip("/"),
661662
description=tool.description,
662663
integration_type="MCP", # Gateway-discovered tools are MCP type
@@ -842,7 +843,7 @@ async def update_gateway(self, db: Session, gateway_id: str, gateway_update: Gat
842843
original_name=tool.name,
843844
custom_name=tool.custom_name,
844845
custom_name_slug=slugify(tool.custom_name),
845-
display_name=tool.custom_name,
846+
display_name=generate_display_name(tool.custom_name),
846847
url=gateway.url,
847848
description=tool.description,
848849
integration_type="MCP", # Gateway-discovered tools are MCP type
@@ -1031,7 +1032,7 @@ async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bo
10311032
original_name=tool.name,
10321033
custom_name=tool.custom_name,
10331034
custom_name_slug=slugify(tool.custom_name),
1034-
display_name=tool.custom_name,
1035+
display_name=generate_display_name(tool.custom_name),
10351036
url=gateway.url,
10361037
description=tool.description,
10371038
integration_type="MCP", # Gateway-discovered tools are MCP type

mcpgateway/services/tool_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from mcpgateway.services.logging_service import LoggingService
4949
from mcpgateway.services.oauth_manager import OAuthManager
5050
from mcpgateway.utils.create_slug import slugify
51+
from mcpgateway.utils.display_name import generate_display_name
5152
from mcpgateway.utils.metrics_common import build_top_performers
5253
from mcpgateway.utils.passthrough_headers import get_passthrough_headers
5354
from mcpgateway.utils.retry_manager import ResilientHttpClient
@@ -1340,7 +1341,7 @@ async def create_tool_from_a2a_agent(
13401341
# Create tool entry for the A2A agent
13411342
tool_data = ToolCreate(
13421343
name=tool_name,
1343-
displayName=f"A2A: {agent.name}",
1344+
displayName=generate_display_name(agent.name),
13441345
url=agent.endpoint_url,
13451346
description=f"A2A Agent: {agent.description or agent.name}",
13461347
integration_type="A2A", # Special integration type for A2A agents

mcpgateway/static/admin.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5469,7 +5469,10 @@ async function viewTool(toolId) {
54695469
}
54705470
};
54715471

5472-
setTextSafely(".tool-display-name", tool.displayName || tool.customName || tool.name);
5472+
setTextSafely(
5473+
".tool-display-name",
5474+
tool.displayName || tool.customName || tool.name,
5475+
);
54735476
setTextSafely(".tool-name", tool.name);
54745477
setTextSafely(".tool-url", tool.url);
54755478
setTextSafely(".tool-type", tool.integrationType);

mcpgateway/utils/display_name.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# -*- coding: utf-8 -*-
2+
"""Location: ./mcpgateway/utils/display_name.py
3+
Copyright 2025
4+
SPDX-License-Identifier: Apache-2.0
5+
Authors: Mihai Criveti
6+
7+
Display Name Utilities.
8+
This module provides utilities for converting technical tool names to user-friendly display names.
9+
10+
Examples:
11+
>>> from mcpgateway.utils.display_name import generate_display_name
12+
>>> generate_display_name("duckduckgo_search")
13+
'Duckduckgo Search'
14+
>>> generate_display_name("weather-api")
15+
'Weather Api'
16+
>>> generate_display_name("get_user.profile")
17+
'Get User Profile'
18+
"""
19+
20+
# Standard
21+
import re
22+
23+
24+
def generate_display_name(technical_name: str) -> str:
25+
"""Convert technical tool name to human-readable display name.
26+
27+
Converts underscores, hyphens, and dots to spaces, then capitalizes the first letter.
28+
29+
Args:
30+
technical_name: The technical tool name (e.g., "duckduckgo_search")
31+
32+
Returns:
33+
str: Human-readable display name (e.g., "Duckduckgo Search")
34+
35+
Examples:
36+
>>> generate_display_name("duckduckgo_search")
37+
'Duckduckgo Search'
38+
>>> generate_display_name("weather-api")
39+
'Weather Api'
40+
>>> generate_display_name("get_user.profile")
41+
'Get User Profile'
42+
>>> generate_display_name("simple_tool")
43+
'Simple Tool'
44+
>>> generate_display_name("UPPER_CASE")
45+
'Upper Case'
46+
>>> generate_display_name("mixed_Case-Name.test")
47+
'Mixed Case Name Test'
48+
>>> generate_display_name("")
49+
''
50+
>>> generate_display_name("single")
51+
'Single'
52+
>>> generate_display_name("multiple___underscores")
53+
'Multiple Underscores'
54+
>>> generate_display_name("tool_with-mixed.separators")
55+
'Tool With Mixed Separators'
56+
"""
57+
if not technical_name:
58+
return ""
59+
60+
# Replace underscores, hyphens, and dots with spaces
61+
display_name = re.sub(r"[_\-\.]+", " ", technical_name)
62+
63+
# Remove extra whitespace and capitalize first letter
64+
display_name = " ".join(display_name.split()) # Normalize whitespace
65+
66+
if display_name:
67+
# Capitalize each word (title case)
68+
display_name = display_name.title()
69+
70+
return display_name

mcpgateway/validators.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@
5151
import html
5252
import logging
5353
import re
54-
import uuid
5554
from urllib.parse import urlparse
55+
import uuid
5656

5757
# First-Party
5858
from mcpgateway.config import settings
@@ -283,17 +283,17 @@ def validate_tool_name(cls, value: str) -> str:
283283
@classmethod
284284
def validate_uuid(cls, value: str, field_name: str = "UUID") -> str:
285285
"""Validate UUID format
286-
286+
287287
Args:
288288
value (str): Value to validate
289289
field_name (str): Name of field being validated
290-
290+
291291
Returns:
292292
str: Value if validated as safe
293-
293+
294294
Raises:
295295
ValueError: When value is not a valid UUID
296-
296+
297297
Examples:
298298
>>> SecurityValidator.validate_uuid('550e8400-e29b-41d4-a716-446655440000')
299299
'550e8400-e29b-41d4-a716-446655440000'
@@ -304,7 +304,7 @@ def validate_uuid(cls, value: str, field_name: str = "UUID") -> str:
304304
"""
305305
if not value:
306306
return value
307-
307+
308308
try:
309309
# Validate UUID format by attempting to parse it
310310
uuid_obj = uuid.UUID(value)

0 commit comments

Comments
 (0)