Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
705a1cb
db.py update
rakdutta1 Aug 14, 2025
b57c0fd
edit-tool
rakdutta1 Aug 18, 2025
a76bf1e
edit-tool
rakdutta1 Aug 18, 2025
53e3bb2
edit-tool
rakdutta1 Aug 18, 2025
d109e29
edit-tool
rakdutta1 Aug 18, 2025
9cd14d4
edit-tool
rakdutta1 Aug 19, 2025
b21583f
edit-tool
rakdutta1 Aug 19, 2025
eb6b499
doc test
rakdutta1 Aug 19, 2025
fb982d0
pytest
rakdutta1 Aug 19, 2025
476b4b6
pytest
rakdutta1 Aug 19, 2025
a61a1e0
revert alembic with main version
rakdutta1 Aug 19, 2025
627ad19
138 view realtime logs in UI and export logs (CSV, JSON) (#747)
crivetimihai Aug 14, 2025
7213a4d
749 reverse proxy (#750)
crivetimihai Aug 14, 2025
d566c62
(fix) Added missing prompts/get (#748)
imolloy Aug 14, 2025
c0ab091
Adds RPC endpoints and updates RPC response and error handling (#746)
madhav165 Aug 14, 2025
fea1f2b
753 fix tool invocation invalid method (#754)
crivetimihai Aug 15, 2025
ee86360
fix: suppress bandit security warnings with appropriate nosec comment…
crivetimihai Aug 15, 2025
1c27f57
Add agents file
crivetimihai Aug 15, 2025
b73023d
pylint (#759)
crivetimihai Aug 15, 2025
53fd0f8
Remove redundant title in readme. (#757)
vinodmut Aug 15, 2025
89fac0e
Update documentation with fixed image tag
crivetimihai Aug 16, 2025
24626ca
256 fuzz testing (#760)
crivetimihai Aug 16, 2025
50330dd
344 cors security headers (#761)
crivetimihai Aug 17, 2025
ad07b16
feat: Bulk Import Tools modal wiring #737 (#739)
vk-playground Aug 17, 2025
dfaafe6
Implemented configuration export (#764)
crivetimihai Aug 17, 2025
ebb9749
185 186 import export (#769)
crivetimihai Aug 17, 2025
1d5746f
fix: local network address translation in discovery module (#767)
araujof Aug 17, 2025
67390a7
Well known (#770)
crivetimihai Aug 17, 2025
263f170
Update docs with jsonrpc tutorial (#772)
crivetimihai Aug 18, 2025
49b5151
137 metadata timestamps (#776)
crivetimihai Aug 18, 2025
f353fb4
feat #262: MCP Langchain Agent (#781)
vk-playground Aug 18, 2025
a7e944e
Cleanup pr
crivetimihai Aug 18, 2025
2e52306
Cleanup pr
crivetimihai Aug 18, 2025
720e033
Issue 587/rest tool error (#778)
nmveeresh Aug 18, 2025
51df810
edit column header (#777)
shoummu1 Aug 18, 2025
e3913e6
Test case update (#775)
MohanLaksh Aug 18, 2025
48b719a
feat: add plugins cli, external plugin support, plugin template (#722)
araujof Aug 18, 2025
8a565b4
feat: Experimental Oauth 2.0 support in gateway (#768)
shams858 Aug 19, 2025
4221c43
Fix pre-commit hooks
crivetimihai Aug 19, 2025
36b5717
744 annotations (#784)
crivetimihai Aug 19, 2025
4e9c471
fix: plugins template (#783)
araujof Aug 19, 2025
e8e03d1
edit-tool
rakdutta1 Aug 18, 2025
7dbde16
edit-tool
rakdutta1 Aug 18, 2025
a9edb9e
doc test
rakdutta1 Aug 19, 2025
e915674
edit-tool
rakdutta1 Aug 19, 2025
2184a41
Merge branch 'main' into fix-715
rakdutta Aug 19, 2025
a7b2ae6
web lint
rakdutta1 Aug 19, 2025
ee313e1
flake8 fix
rakdutta1 Aug 19, 2025
48ec9d6
pytest fix
rakdutta1 Aug 19, 2025
a01d49d
revert with main
rakdutta1 Aug 19, 2025
3c62bde
flake fix
rakdutta1 Aug 19, 2025
6ba26c3
revert with main
rakdutta1 Aug 19, 2025
ad64796
alembic
rakdutta1 Aug 19, 2025
2cd54e8
alembic change
rakdutta1 Aug 19, 2025
2461d7d
flake8 fix
rakdutta1 Aug 20, 2025
af2f19e
remove addtional line
rakdutta1 Aug 20, 2025
b77992b
alembic
rakdutta1 Aug 20, 2025
31c8f5c
Merge remote-tracking branch 'origin/main' into fix-715
crivetimihai Aug 20, 2025
7ab9290
Rebase and fix
crivetimihai Aug 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 42 additions & 32 deletions mcpgateway/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1512,7 +1512,7 @@ async def admin_ui(
>>> mock_tool = ToolRead(
... id="t1", name="T1", original_name="T1", url="http://t1.com", description="d",
... created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc),
... enabled=True, reachable=True, gateway_slug="default", original_name_slug="t1",
... enabled=True, reachable=True, gateway_slug="default", custom_name_slug="t1",
... request_type="GET", integration_type="MCP", headers={}, input_schema={},
... annotations={}, jsonpath_filter=None, auth=None, execution_count=0,
... metrics=ToolMetrics(
Expand All @@ -1521,6 +1521,7 @@ async def admin_ui(
... avg_response_time=0.0, last_execution_time=None
... ),
... gateway_id=None,
... customName="T1",
... tags=[]
... )
>>> server_service.list_servers = AsyncMock(return_value=[mock_server])
Expand Down Expand Up @@ -1633,34 +1634,35 @@ async def admin_list_tools(
>>> mock_user = "test_user"
>>>
>>> # Mock tool data
>>> mock_tool = ToolRead(
... id="tool-1",
... name="Test Tool",
... original_name="TestTool",
... url="http://test.com/tool",
... description="A test tool",
... request_type="HTTP",
... integration_type="MCP",
... headers={},
... input_schema={},
... annotations={},
... jsonpath_filter=None,
... auth=None,
... created_at=datetime.now(timezone.utc),
... updated_at=datetime.now(timezone.utc),
... enabled=True,
... reachable=True,
... gateway_id=None,
... execution_count=0,
... metrics=ToolMetrics(
... total_executions=5, successful_executions=5, failed_executions=0,
... failure_rate=0.0, min_response_time=0.1, max_response_time=0.5,
... avg_response_time=0.3, last_execution_time=datetime.now(timezone.utc)
... ),
... gateway_slug="default",
... original_name_slug="test-tool",
... tags=[]
... ) # Added gateway_id=None
>>> mock_tool = ToolRead(
... id="tool-1",
... name="Test Tool",
... original_name="TestTool",
... url="http://test.com/tool",
... description="A test tool",
... request_type="HTTP",
... integration_type="MCP",
... headers={},
... input_schema={},
... annotations={},
... jsonpath_filter=None,
... auth=None,
... created_at=datetime.now(timezone.utc),
... updated_at=datetime.now(timezone.utc),
... enabled=True,
... reachable=True,
... gateway_id=None,
... execution_count=0,
... metrics=ToolMetrics(
... total_executions=5, successful_executions=5, failed_executions=0,
... failure_rate=0.0, min_response_time=0.1, max_response_time=0.5,
... avg_response_time=0.3, last_execution_time=datetime.now(timezone.utc)
... ),
... gateway_slug="default",
... custom_name_slug="test-tool",
... customName="Test Tool",
... tags=[]
... ) # Added gateway_id=None
>>>
>>> # Mock the tool_service.list_tools method
>>> original_list_tools = tool_service.list_tools
Expand All @@ -1686,7 +1688,8 @@ async def admin_list_tools(
... failure_rate=0.0, min_response_time=0.0, max_response_time=0.0,
... avg_response_time=0.0, last_execution_time=None
... ),
... gateway_slug="default", original_name_slug="inactive-tool",
... gateway_slug="default", custom_name_slug="inactive-tool",
... customName="Inactive Tool",
... tags=[]
... )
>>> tool_service.list_tools = AsyncMock(return_value=[mock_tool, mock_inactive_tool])
Expand Down Expand Up @@ -1772,7 +1775,8 @@ async def admin_get_tool(tool_id: str, db: Session = Depends(get_db), user: str
... failure_rate=0.0, min_response_time=0.0, max_response_time=0.0, avg_response_time=0.0,
... last_execution_time=None
... ),
... gateway_slug="default", original_name_slug="get-tool",
... gateway_slug="default", custom_name_slug="get-tool",
... customName="Get Tool",
... tags=[]
... )
>>>
Expand Down Expand Up @@ -2088,6 +2092,7 @@ async def admin_edit_tool(
>>> # Happy path: Edit tool successfully
>>> form_data_success = FormData([
... ("name", "Updated_Tool"),
... ("customName", "ValidToolName"),
... ("url", "http://updated.com"),
... ("requestType", "GET"),
... ("integrationType", "REST"),
Expand All @@ -2110,6 +2115,7 @@ async def admin_edit_tool(
>>> # Edge case: Edit tool with inactive checkbox checked
>>> form_data_inactive = FormData([
... ("name", "Inactive_Edit"),
... ("customName", "ValidToolName"),
... ("url", "http://inactive.com"),
... ("is_inactive_checked", "true"),
... ("requestType", "GET"),
Expand All @@ -2128,6 +2134,7 @@ async def admin_edit_tool(
>>> # Error path: Tool name conflict (simulated with IntegrityError)
>>> form_data_conflict = FormData([
... ("name", "Conflicting_Name"),
... ("customName", "Conflicting_Name"),
... ("url", "http://conflict.com"),
... ("requestType", "GET"),
... ("integrationType", "REST")
Expand All @@ -2146,6 +2153,7 @@ async def admin_edit_tool(
>>> # Error path: ToolError raised
>>> form_data_tool_error = FormData([
... ("name", "Tool_Error"),
... ("customName", "Tool_Error"),
... ("url", "http://toolerror.com"),
... ("requestType", "GET"),
... ("integrationType", "REST")
Expand All @@ -2164,6 +2172,7 @@ async def admin_edit_tool(
>>> # Error path: Pydantic Validation Error
>>> form_data_validation_error = FormData([
... ("name", "Bad_URL"),
... ("customName","Bad_Custom_Name"),
... ("url", "not-a-valid-url"),
... ("requestType", "GET"),
... ("integrationType", "REST")
Expand All @@ -2181,6 +2190,7 @@ async def admin_edit_tool(
>>> # Error path: Unexpected exception
>>> form_data_unexpected = FormData([
... ("name", "Crash_Tool"),
... ("customName", "Crash_Tool"),
... ("url", "http://crash.com"),
... ("requestType", "GET"),
... ("integrationType", "REST")
Expand All @@ -2202,13 +2212,13 @@ async def admin_edit_tool(
"""
LOGGER.debug(f"User {user} is editing tool ID {tool_id}")
form = await request.form()

# Parse tags from comma-separated string
tags_str = str(form.get("tags", ""))
tags: list[str] = [tag.strip() for tag in tags_str.split(",") if tag.strip()] if tags_str else []

tool_data: dict[str, Any] = {
"name": form.get("name"),
"custom_name": form.get("customName"),
"url": form.get("url"),
"description": form.get("description"),
"headers": json.loads(form.get("headers") or "{}"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""merge_a2a_and_custom_name_changes

Revision ID: 1fc1795f6983
Revises: add_a2a_agents_and_metrics, c9dd86c0aac9
Create Date: 2025-08-20 19:04:40.589538

"""

# Standard
from typing import Sequence, Union

# revision identifiers, used by Alembic.
revision: str = "1fc1795f6983"
down_revision: Union[str, Sequence[str], None] = ("add_a2a_agents_and_metrics", "c9dd86c0aac9")
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""


def downgrade() -> None:
"""Downgrade schema."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""remove original_name_slug and added custom_name

Revision ID: c9dd86c0aac9
Revises: add_oauth_tokens_table
Create Date: 2025-08-19 15:15:26.509036

"""

# Standard
from typing import Sequence, Union

# Third-Party
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "c9dd86c0aac9"
down_revision: Union[str, Sequence[str], None] = "add_oauth_tokens_table"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# Remove original_name_slug column
op.alter_column("tools", "original_name_slug", new_column_name="custom_name_slug")

# Add custom_name column
op.add_column("tools", sa.Column("custom_name", sa.String(), nullable=True))
op.execute("UPDATE tools SET custom_name = original_name")
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# Remove custom_name column
op.drop_column("tools", "custom_name")

# Add original_name_slug column back
op.alter_column("tools", "custom_name_slug", new_column_name="original_name_slug")
# ### end Alembic commands ###
46 changes: 30 additions & 16 deletions mcpgateway/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,6 @@ class Tool(Base):

id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex)
original_name: Mapped[str] = mapped_column(String, nullable=False)
original_name_slug: Mapped[str] = mapped_column(String, nullable=False)
url: Mapped[str] = mapped_column(String, nullable=True)
description: Mapped[Optional[str]]
integration_type: Mapped[str] = mapped_column(default="MCP")
Expand Down Expand Up @@ -403,6 +402,10 @@ class Tool(Base):
auth_type: Mapped[Optional[str]] = mapped_column(default=None) # "basic", "bearer", or None
auth_value: Mapped[Optional[str]] = mapped_column(default=None)

# custom_name,custom_name_slug
custom_name: Mapped[Optional[str]] = mapped_column(String, nullable=False)
custom_name_slug: Mapped[Optional[str]] = mapped_column(String, nullable=False)

# Federation relationship with a local gateway
gateway_id: Mapped[Optional[str]] = mapped_column(ForeignKey("gateways.id"))
# gateway_slug: Mapped[Optional[str]] = mapped_column(ForeignKey("gateways.slug"))
Expand Down Expand Up @@ -431,15 +434,15 @@ def name(self):
if self._computed_name: # pylint: disable=no-member
return self._computed_name # orm column, resolved at runtime

original_slug = slugify(self.original_name) # pylint: disable=no-member
custom_name_slug = slugify(self.custom_name_slug) # pylint: disable=no-member

# Gateway present → prepend its slug and the configured separator
if self.gateway_id: # pylint: disable=no-member
gateway_slug = slugify(self.gateway.name) # pylint: disable=no-member
return f"{gateway_slug}{settings.gateway_tool_name_separator}{original_slug}"
return f"{gateway_slug}{settings.gateway_tool_name_separator}{custom_name_slug}"

# No gateway → only the original name slug
return original_slug
return custom_name_slug

@name.setter
def name(self, value):
Expand Down Expand Up @@ -1123,7 +1126,7 @@ def failure_rate(self) -> float:
float: The failure rate as a value between 0 and 1.

Examples:
>>> tool = Tool(original_name="test_tool", original_name_slug="test-tool", input_schema={})
>>> tool = Tool(custom_name="test_tool", custom_name_slug="test-tool", input_schema={})
>>> tool.failure_rate # No metrics yet
0.0
>>> tool.metrics = [
Expand Down Expand Up @@ -1285,7 +1288,7 @@ def update_tool_names_on_gateway_update(_mapper, connection, target):
stmt = (
tools_table.update()
.where(tools_table.c.gateway_id == target.id)
.values(name=new_gateway_slug + separator + tools_table.c.original_name_slug)
.values(name=new_gateway_slug + separator + tools_table.c.custom_name_slug)
.execution_options(synchronize_session=False) # Important for bulk updates
)

Expand Down Expand Up @@ -1617,18 +1620,29 @@ def set_a2a_agent_slug(_mapper, _conn, target):


@event.listens_for(Tool, "before_insert")
def set_tool_name(_mapper, _conn, target):
"""Set the computed name for a Tool before insert.
@event.listens_for(Tool, "before_update")
def set_custom_name_and_slug(mapper, connection, target):
"""
Event listener to set custom_name, custom_name_slug, and name for Tool before insert/update.

- Sets custom_name to original_name if not provided.
- Calculates custom_name_slug from custom_name using slugify.
- Updates name to gateway_slug + separator + custom_name_slug.

Args:
_mapper: Mapper
_conn: Connection
target: Target Tool instance
mapper: SQLAlchemy mapper for the Tool model.
connection: Database connection.
target: The Tool instance being inserted or updated.
"""

sep = settings.gateway_tool_name_separator
gateway_slug = target.gateway.slug if target.gateway_id else ""
# Set custom_name to original_name if not provided
if not target.custom_name:
target.custom_name = target.original_name
# Always update custom_name_slug from custom_name
target.custom_name_slug = slugify(target.custom_name)
# Update name field
gateway_slug = slugify(target.gateway.name) if target.gateway else ""
if gateway_slug:
target.name = f"{gateway_slug}{sep}{slugify(target.original_name)}"
sep = settings.gateway_tool_name_separator
target.name = f"{gateway_slug}{sep}{target.custom_name_slug}"
else:
target.name = slugify(target.original_name)
target.name = target.custom_name_slug
17 changes: 16 additions & 1 deletion mcpgateway/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ class ToolUpdate(BaseModelWithConfigDict):
"""

name: Optional[str] = Field(None, description="Unique name for the tool")
custom_name: Optional[str] = Field(None, description="Custom name for the tool")
url: Optional[Union[str, AnyHttpUrl]] = Field(None, description="Tool endpoint URL")
description: Optional[str] = Field(None, description="Tool description")
integration_type: Optional[Literal["REST", "MCP", "A2A"]] = Field(None, description="Tool integration type")
Expand Down Expand Up @@ -668,6 +669,19 @@ def validate_name(cls, v: str) -> str:
"""
return SecurityValidator.validate_tool_name(v)

@field_validator("custom_name")
@classmethod
def validate_custom_name(cls, v: str) -> str:
"""Ensure custom tool names follow MCP naming conventions

Args:
v (str): Value to validate

Returns:
str: Value if validated as safe
"""
return SecurityValidator.validate_tool_name(v)

@field_validator("url")
@classmethod
def validate_url(cls, v: str) -> str:
Expand Down Expand Up @@ -864,7 +878,8 @@ class ToolRead(BaseModelWithConfigDict):
metrics: ToolMetrics
name: str
gateway_slug: str
original_name_slug: str
custom_name: str
custom_name_slug: str
tags: List[str] = Field(default_factory=list, description="Tags for categorizing the tool")

# Comprehensive metadata for audit tracking
Expand Down
Loading
Loading