diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index dd4cdb292..be5986b50 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -627,8 +627,8 @@ async def admin_add_server(request: Request, db: Session = Depends(get_db), user description=form.get("description"), icon=form.get("icon"), associated_tools=",".join(form.getlist("associatedTools")), - associated_resources=form.get("associatedResources"), - associated_prompts=form.get("associatedPrompts"), + associated_resources=",".join(form.getlist("associatedResources")), + associated_prompts=",".join(form.getlist("associatedPrompts")), tags=tags, ) except KeyError as e: @@ -785,8 +785,8 @@ async def admin_edit_server( description=form.get("description"), icon=form.get("icon"), associated_tools=",".join(form.getlist("associatedTools")), - associated_resources=form.get("associatedResources"), - associated_prompts=form.get("associatedPrompts"), + associated_resources=",".join(form.getlist("associatedResources")), + associated_prompts=",".join(form.getlist("associatedPrompts")), tags=tags, ) await server_service.update_server(db, server_id, server) @@ -3442,7 +3442,7 @@ async def admin_delete_resource(uri: str, request: Request, db: Session = Depend @admin_router.post("/resources/{resource_id}/toggle") async def admin_toggle_resource( - resource_id: int, + resource_id: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth), @@ -3456,7 +3456,7 @@ async def admin_toggle_resource( logs any errors that might occur during the status toggle operation. Args: - resource_id (int): The ID of the resource whose status to toggle. + resource_id (str): The ID of the resource whose status to toggle. request (Request): FastAPI request containing form data with the 'activate' field. db (Session): Database session dependency. user (str): Authenticated user dependency. @@ -3927,7 +3927,7 @@ async def admin_delete_prompt(name: str, request: Request, db: Session = Depends @admin_router.post("/prompts/{prompt_id}/toggle") async def admin_toggle_prompt( - prompt_id: int, + prompt_id: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth), @@ -3941,7 +3941,7 @@ async def admin_toggle_prompt( logs any errors that might occur during the status toggle operation. Args: - prompt_id (int): The ID of the prompt whose status to toggle. + prompt_id (str): The ID of the prompt whose status to toggle. request (Request): FastAPI request containing form data with the 'activate' field. db (Session): Database session dependency. user (str): Authenticated user dependency. diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 9aa0fca4c..02b2ee61a 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -126,7 +126,7 @@ def utc_now() -> datetime: def refresh_slugs_on_startup(): - """Refresh slugs for all gateways and names of tools on startup.""" + """Refresh slugs for all gateways and names of tools, resources and prompts on startup.""" with SessionLocal() as session: gateways = session.query(Gateway).all() @@ -143,6 +143,14 @@ def refresh_slugs_on_startup(): for tool in tools: session.expire(tool, ["gateway"]) + resources = session.query(Resource).all() + for resource in resources: + session.expire(resource, ["gateway"]) + + prompts = session.query(Prompt).all() + for prompt in prompts: + session.expire(prompt, ["gateway"]) + updated = False for tool in tools: if tool.gateway: @@ -152,6 +160,24 @@ def refresh_slugs_on_startup(): if tool.name != new_name: tool.name = new_name updated = True + + for resource in resources: + if resource.gateway: + new_name = f"{resource.gateway.slug}{settings.gateway_tool_name_separator}{slugify(resource.original_name)}" + else: + new_name = slugify(resource.original_name) + if resource.name != new_name: + resource.name = new_name + updated = True + + for prompt in prompts: + if prompt.gateway: + new_name = f"{prompt.gateway.slug}{settings.gateway_tool_name_separator}{slugify(prompt.original_name)}" + else: + new_name = slugify(prompt.original_name) + if prompt.name != new_name: + prompt.name = new_name + updated = True if updated: session.commit() @@ -173,7 +199,7 @@ class Base(DeclarativeBase): "server_resource_association", Base.metadata, Column("server_id", String, ForeignKey("servers.id"), primary_key=True), - Column("resource_id", Integer, ForeignKey("resources.id"), primary_key=True), + Column("resource_id", String, ForeignKey("resources.id"), primary_key=True), ) # Association table for servers and prompts @@ -181,7 +207,7 @@ class Base(DeclarativeBase): "server_prompt_association", Base.metadata, Column("server_id", String, ForeignKey("servers.id"), primary_key=True), - Column("prompt_id", Integer, ForeignKey("prompts.id"), primary_key=True), + Column("prompt_id", String, ForeignKey("prompts.id"), primary_key=True), ) # Association table for servers and A2A agents @@ -241,7 +267,7 @@ class ResourceMetric(Base): Attributes: id (int): Primary key. - resource_id (int): Foreign key linking to the resource. + resource_id (str): Foreign key linking to the resource. timestamp (datetime): The time when the invocation occurred. response_time (float): The response time in seconds. is_success (bool): True if the invocation succeeded, False otherwise. @@ -251,7 +277,7 @@ class ResourceMetric(Base): __tablename__ = "resource_metrics" id: Mapped[int] = mapped_column(primary_key=True) - resource_id: Mapped[int] = mapped_column(Integer, ForeignKey("resources.id"), nullable=False) + resource_id: Mapped[str] = mapped_column(Integer, ForeignKey("resources.id"), nullable=False) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) response_time: Mapped[float] = mapped_column(Float, nullable=False) is_success: Mapped[bool] = mapped_column(Boolean, nullable=False) @@ -293,7 +319,7 @@ class PromptMetric(Base): Attributes: id (int): Primary key. - prompt_id (int): Foreign key linking to the prompt. + prompt_id (str): Foreign key linking to the prompt. timestamp (datetime): The time when the invocation occurred. response_time (float): The response time in seconds. is_success (bool): True if the invocation succeeded, False otherwise. @@ -303,7 +329,7 @@ class PromptMetric(Base): __tablename__ = "prompt_metrics" id: Mapped[int] = mapped_column(primary_key=True) - prompt_id: Mapped[int] = mapped_column(Integer, ForeignKey("prompts.id"), nullable=False) + prompt_id: Mapped[str] = mapped_column(Integer, ForeignKey("prompts.id"), nullable=False) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) response_time: Mapped[float] = mapped_column(Float, nullable=False) is_success: Mapped[bool] = mapped_column(Boolean, nullable=False) @@ -645,9 +671,9 @@ class Resource(Base): __tablename__ = "resources" - id: Mapped[int] = mapped_column(primary_key=True) + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex) uri: Mapped[str] = mapped_column(unique=True) - name: Mapped[str] + original_name: Mapped[str] = mapped_column(String, nullable=False) description: Mapped[Optional[str]] mime_type: Mapped[Optional[str]] size: Mapped[Optional[int]] @@ -681,13 +707,20 @@ class Resource(Base): # Subscription tracking subscriptions: Mapped[List["ResourceSubscription"]] = relationship("ResourceSubscription", back_populates="resource", cascade="all, delete-orphan") + # custom_name,custom_name_slug, display_name + custom_name: Mapped[Optional[str]] = mapped_column(String, nullable=False) + custom_name_slug: Mapped[Optional[str]] = mapped_column(String, nullable=False) + display_name: Mapped[Optional[str]] = mapped_column(String, nullable=True) + gateway_id: Mapped[Optional[str]] = mapped_column(ForeignKey("gateways.id")) - gateway: Mapped["Gateway"] = relationship("Gateway", back_populates="resources") + gateway: Mapped["Gateway"] = relationship("Gateway", primaryjoin="Resource.gateway_id == Gateway.id", foreign_keys=[gateway_id], back_populates="resources") # federated_with = relationship("Gateway", secondary=resource_gateway_table, back_populates="federated_resources") # Many-to-many relationship with Servers servers: Mapped[List["Server"]] = relationship("Server", secondary=server_resource_association, back_populates="resources") + _computed_name = Column("name", String, unique=True) # Stored column + @property def content(self) -> ResourceContent: """ @@ -742,6 +775,69 @@ def content(self) -> ResourceContent: ) raise ValueError("Resource has no content") + @hybrid_property + def name(self): + """Return the display/lookup name. + + Returns: + str: Name to display + """ + if self._computed_name: # pylint: disable=no-member + return self._computed_name # orm column, resolved at runtime + + 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}{custom_name_slug}" + + # No gateway → only the original name slug + return custom_name_slug + + @name.setter + def name(self, value): + """Store an explicit value that overrides the calculated one. + + Args: + value (str): Value to set to _computed_name + """ + self._computed_name = value + + @name.expression + @classmethod + def name(cls): + """ + SQL expression used when the hybrid appears in a filter/order_by. + Simply forwards to the ``_computed_name`` column; the Python-side + reconstruction above is not needed on the SQL side. + + Returns: + str: computed name for SQL use + """ + return cls._computed_name + + __table_args__ = (UniqueConstraint("gateway_id", "original_name", name="uq_gateway_id__original_name"),) + + @hybrid_property + def gateway_slug(self): + """Always returns the current slug from the related Gateway + + Returns: + str: slug for Python use + """ + return self.gateway.slug if self.gateway else None + + @gateway_slug.expression + @classmethod + def gateway_slug(cls): + """For database queries - auto-joins to get current slug + + Returns: + str: slug for SQL use + """ + return select(Gateway.slug).where(Gateway.id == cls.gateway_id).scalar_subquery() + @property def execution_count(self) -> int: """ @@ -846,7 +942,7 @@ class ResourceSubscription(Base): __tablename__ = "resource_subscriptions" id: Mapped[int] = mapped_column(primary_key=True) - resource_id: Mapped[int] = mapped_column(ForeignKey("resources.id")) + resource_id: Mapped[str] = mapped_column(ForeignKey("resources.id")) subscriber_id: Mapped[str] # Client identifier created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) last_notification: Mapped[Optional[datetime]] = mapped_column(DateTime) @@ -874,8 +970,8 @@ class Prompt(Base): __tablename__ = "prompts" - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(unique=True) + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex) + original_name: Mapped[str] = mapped_column(String, nullable=False) description: Mapped[Optional[str]] template: Mapped[str] = mapped_column(Text) argument_schema: Mapped[Dict[str, Any]] = mapped_column(JSON) @@ -901,13 +997,83 @@ class Prompt(Base): metrics: Mapped[List["PromptMetric"]] = relationship("PromptMetric", back_populates="prompt", cascade="all, delete-orphan") + # custom_name,custom_name_slug, display_name + custom_name: Mapped[Optional[str]] = mapped_column(String, nullable=False) + custom_name_slug: Mapped[Optional[str]] = mapped_column(String, nullable=False) + display_name: Mapped[Optional[str]] = mapped_column(String, nullable=True) + gateway_id: Mapped[Optional[str]] = mapped_column(ForeignKey("gateways.id")) - gateway: Mapped["Gateway"] = relationship("Gateway", back_populates="prompts") + gateway: Mapped["Gateway"] = relationship("Gateway", primaryjoin="Prompt.gateway_id == Gateway.id", foreign_keys=[gateway_id], back_populates="prompts") # federated_with = relationship("Gateway", secondary=prompt_gateway_table, back_populates="federated_prompts") # Many-to-many relationship with Servers servers: Mapped[List["Server"]] = relationship("Server", secondary=server_prompt_association, back_populates="prompts") + _computed_name = Column("name", String, unique=True) # Stored column + + @hybrid_property + def name(self): + """Return the display/lookup name. + + Returns: + str: Name to display + """ + if self._computed_name: # pylint: disable=no-member + return self._computed_name # orm column, resolved at runtime + + 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}{custom_name_slug}" + + # No gateway → only the original name slug + return custom_name_slug + + @name.setter + def name(self, value): + """Store an explicit value that overrides the calculated one. + + Args: + value (str): Value to set to _computed_name + """ + self._computed_name = value + + @name.expression + @classmethod + def name(cls): + """ + SQL expression used when the hybrid appears in a filter/order_by. + Simply forwards to the ``_computed_name`` column; the Python-side + reconstruction above is not needed on the SQL side. + + Returns: + str: computed name for SQL use + """ + return cls._computed_name + + __table_args__ = (UniqueConstraint("gateway_id", "original_name", name="uq_gateway_id__original_name"),) + + @hybrid_property + def gateway_slug(self): + """Always returns the current slug from the related Gateway + + Returns: + str: slug for Python use + """ + return self.gateway.slug if self.gateway else None + + @gateway_slug.expression + @classmethod + def gateway_slug(cls): + """For database queries - auto-joins to get current slug + + Returns: + str: slug for SQL use + """ + return select(Gateway.slug).where(Gateway.id == cls.gateway_id).scalar_subquery() + def validate_arguments(self, args: Dict[str, str]) -> None: """ Validate prompt arguments against the argument schema. @@ -1304,6 +1470,48 @@ def update_tool_names_on_gateway_update(_mapper, connection, target): # 5. Execute the statement using the connection from the ongoing transaction. connection.execute(stmt) + print(f"Gateway name changed for ID {target.id}. Issuing bulk update for prompts.") + + # 2. Get a reference to the underlying database table for Prompts + prompts_table = Prompt.__table__ + + # 3. Prepare the new values + new_gateway_slug = slugify(target.name) + separator = settings.gateway_tool_name_separator + + # 4. Construct a single, powerful UPDATE statement using SQLAlchemy Core. + # This is highly efficient as it all happens in the database. + stmt = ( + prompts_table.update() + .where(prompts_table.c.gateway_id == target.id) + .values(name=new_gateway_slug + separator + prompts_table.c.custom_name_slug) + .execution_options(synchronize_session=False) # Important for bulk updates + ) + + # 5. Execute the statement using the connection from the ongoing transaction. + connection.execute(stmt) + + print(f"Gateway name changed for ID {target.id}. Issuing bulk update for resources.") + + # 2. Get a reference to the underlying database table for Prompts + resources_table = Resource.__table__ + + # 3. Prepare the new values + new_gateway_slug = slugify(target.name) + separator = settings.gateway_tool_name_separator + + # 4. Construct a single, powerful UPDATE statement using SQLAlchemy Core. + # This is highly efficient as it all happens in the database. + stmt = ( + resources_table.update() + .where(resources_table.c.gateway_id == target.id) + .values(name=new_gateway_slug + separator + resources_table.c.custom_name_slug) + .execution_options(synchronize_session=False) # Important for bulk updates + ) + + # 5. Execute the statement using the connection from the ongoing transaction. + connection.execute(stmt) + class A2AAgent(Base): """ @@ -1630,7 +1838,7 @@ def set_a2a_agent_slug(_mapper, _conn, target): @event.listens_for(Tool, "before_insert") @event.listens_for(Tool, "before_update") -def set_custom_name_and_slug(mapper, connection, target): # pylint: disable=unused-argument +def set_tool_custom_name_and_slug(mapper, connection, target): # pylint: disable=unused-argument """ Event listener to set custom_name, custom_name_slug, and name for Tool before insert/update. @@ -1659,3 +1867,69 @@ def set_custom_name_and_slug(mapper, connection, target): # pylint: disable=unu target.name = f"{gateway_slug}{sep}{target.custom_name_slug}" else: target.name = target.custom_name_slug + + +@event.listens_for(Prompt, "before_insert") +@event.listens_for(Prompt, "before_update") +def set_prompt_custom_name_and_slug(mapper, connection, target): # pylint: disable=unused-argument + """ + Event listener to set custom_name, custom_name_slug, and name for Prompt 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. + - Sets display_name to custom_name if not provided. + + Args: + mapper: SQLAlchemy mapper for the Prompt model. + connection: Database connection. + target: The Prompt instance being inserted or updated. + """ + # Set custom_name to original_name if not provided + if not target.custom_name: + target.custom_name = target.original_name + # Set display_name to custom_name if not provided + if not target.display_name: + target.display_name = target.custom_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: + sep = settings.gateway_tool_name_separator + target.name = f"{gateway_slug}{sep}{target.custom_name_slug}" + else: + target.name = target.custom_name_slug + + +@event.listens_for(Resource, "before_insert") +@event.listens_for(Resource, "before_update") +def set_resource_custom_name_and_slug(mapper, connection, target): # pylint: disable=unused-argument + """ + Event listener to set custom_name, custom_name_slug, and name for Resource 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. + - Sets display_name to custom_name if not provided. + + Args: + mapper: SQLAlchemy mapper for the Resource model. + connection: Database connection. + target: The Resource instance being inserted or updated. + """ + # Set custom_name to original_name if not provided + if not target.custom_name: + target.custom_name = target.original_name + # Set display_name to custom_name if not provided + if not target.display_name: + target.display_name = target.custom_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: + sep = settings.gateway_tool_name_separator + target.name = f"{gateway_slug}{sep}{target.custom_name_slug}" + else: + target.name = target.custom_name_slug diff --git a/mcpgateway/main.py b/mcpgateway/main.py index 768ac28e3..c5f277d53 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -1742,7 +1742,7 @@ async def list_resource_templates( @resource_router.post("/{resource_id}/toggle") async def toggle_resource_status( - resource_id: int, + resource_id: str, activate: bool = True, db: Session = Depends(get_db), user: str = Depends(require_auth), @@ -1751,7 +1751,7 @@ async def toggle_resource_status( Activate or deactivate a resource by its ID. Args: - resource_id (int): The ID of the resource. + resource_id (str): The ID of the resource. activate (bool): True to activate, False to deactivate. db (Session): Database session. user (str): Authenticated user. @@ -1983,7 +1983,7 @@ async def subscribe_resource(uri: str, user: str = Depends(require_auth)) -> Str ############### @prompt_router.post("/{prompt_id}/toggle") async def toggle_prompt_status( - prompt_id: int, + prompt_id: str, activate: bool = True, db: Session = Depends(get_db), user: str = Depends(require_auth), diff --git a/mcpgateway/schemas.py b/mcpgateway/schemas.py index 743424f3a..0f264432b 100644 --- a/mcpgateway/schemas.py +++ b/mcpgateway/schemas.py @@ -373,7 +373,7 @@ def validate_name(cls, v: str) -> str: str: Value if validated as safe Raises: - ValueError: When displayName contains unsafe content or exceeds length limits + ValueError: When name contains unsafe content or exceeds length limits Examples: >>> from mcpgateway.schemas import ToolCreate @@ -398,7 +398,7 @@ def validate_url(cls, v: str) -> str: str: Value if validated as safe Raises: - ValueError: When displayName contains unsafe content or exceeds length limits + ValueError: When url contains unsafe content or exceeds length limits Examples: >>> from mcpgateway.schemas import ToolCreate @@ -1141,6 +1141,7 @@ class ResourceCreate(BaseModel): uri: str = Field(..., description="Unique URI for the resource") name: str = Field(..., description="Human-readable resource name") + displayName: Optional[str] = Field(None, description="Display name for the resource (shown in UI)") # noqa: N815 description: Optional[str] = Field(None, description="Resource description") mime_type: Optional[str] = Field(None, description="Resource MIME type") template: Optional[str] = Field(None, description="URI template for parameterized resources") @@ -1206,6 +1207,35 @@ def validate_description(cls, v: Optional[str]) -> Optional[str]: raise ValueError(f"Description exceeds maximum length of {SecurityValidator.MAX_DESCRIPTION_LENGTH}") return SecurityValidator.sanitize_display_text(v, "Description") + @field_validator("displayName") + @classmethod + def validate_display_name(cls, v: Optional[str]) -> Optional[str]: + """Ensure display names display safely + + Args: + v (str): Value to validate + + Returns: + str: Value if validated as safe + + Raises: + ValueError: When displayName contains unsafe content or exceeds length limits + + Examples: + >>> from mcpgateway.schemas import ResourceCreate + >>> ResourceCreate.validate_display_name('My Custom Resource') + 'My Custom Resource' + >>> ResourceCreate.validate_display_name('') + Traceback (most recent call last): + ... + ValueError: ... + """ + if v is None: + return v + if len(v) > SecurityValidator.MAX_NAME_LENGTH: + raise ValueError(f"Display name exceeds maximum length of {SecurityValidator.MAX_NAME_LENGTH}") + return SecurityValidator.sanitize_display_text(v, "Display name") + @field_validator("mime_type") @classmethod def validate_mime_type(cls, v: Optional[str]) -> Optional[str]: @@ -1261,6 +1291,8 @@ class ResourceUpdate(BaseModelWithConfigDict): """ name: Optional[str] = Field(None, description="Human-readable resource name") + displayName: Optional[str] = Field(None, description="Display name for the resource (shown in UI)") # noqa: N815 + custom_name: Optional[str] = Field(None, description="Custom name for the resource") description: Optional[str] = Field(None, description="Resource description") mime_type: Optional[str] = Field(None, description="Resource MIME type") template: Optional[str] = Field(None, description="URI template for parameterized resources") @@ -1362,6 +1394,35 @@ def validate_content(cls, v: Optional[Union[str, bytes]]) -> Optional[Union[str, return v + @field_validator("displayName") + @classmethod + def validate_display_name(cls, v: Optional[str]) -> Optional[str]: + """Ensure display names display safely + + Args: + v (str): Value to validate + + Returns: + str: Value if validated as safe + + Raises: + ValueError: When displayName contains unsafe content or exceeds length limits + + Examples: + >>> from mcpgateway.schemas import ResourceUpdate + >>> ResourceUpdate.validate_display_name('My Custom Resource') + 'My Custom Resource' + >>> ResourceUpdate.validate_display_name('') + Traceback (most recent call last): + ... + ValueError: ... + """ + if v is None: + return v + if len(v) > SecurityValidator.MAX_NAME_LENGTH: + raise ValueError(f"Display name exceeds maximum length of {SecurityValidator.MAX_NAME_LENGTH}") + return SecurityValidator.sanitize_display_text(v, "Display name") + class ResourceRead(BaseModelWithConfigDict): """Schema for reading resource information. @@ -1374,7 +1435,7 @@ class ResourceRead(BaseModelWithConfigDict): - Metrics: Aggregated metrics for the resource invocations. """ - id: int + id: str uri: str name: str description: Optional[str] @@ -1384,6 +1445,10 @@ class ResourceRead(BaseModelWithConfigDict): updated_at: datetime is_active: bool metrics: ResourceMetrics + displayName: Optional[str] = Field(None, description="Display name for the resource (shown in UI)") # noqa: N815 + gateway_slug: str + custom_name: str + custom_name_slug: str tags: List[str] = Field(default_factory=list, description="Tags for categorizing the resource") # Comprehensive metadata for audit tracking @@ -1641,6 +1706,7 @@ class PromptCreate(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) name: str = Field(..., description="Unique name for the prompt") + displayName: Optional[str] = Field(None, description="Display name for the prompt (shown in UI)") # noqa: N815 description: Optional[str] = Field(None, description="Prompt description") template: str = Field(..., description="Prompt template text") arguments: List[PromptArgument] = Field(default_factory=list, description="List of arguments for the template") @@ -1692,6 +1758,35 @@ def validate_description(cls, v: Optional[str]) -> Optional[str]: raise ValueError(f"Description exceeds maximum length of {SecurityValidator.MAX_DESCRIPTION_LENGTH}") return SecurityValidator.sanitize_display_text(v, "Description") + @field_validator("displayName") + @classmethod + def validate_display_name(cls, v: Optional[str]) -> Optional[str]: + """Ensure display names display safely + + Args: + v (str): Value to validate + + Returns: + str: Value if validated as safe + + Raises: + ValueError: When displayName contains unsafe content or exceeds length limits + + Examples: + >>> from mcpgateway.schemas import PromptCreate + >>> PromptCreate.validate_display_name('My Custom Prompt') + 'My Custom Prompt' + >>> PromptCreate.validate_display_name('') + Traceback (most recent call last): + ... + ValueError: ... + """ + if v is None: + return v + if len(v) > SecurityValidator.MAX_NAME_LENGTH: + raise ValueError(f"Display name exceeds maximum length of {SecurityValidator.MAX_NAME_LENGTH}") + return SecurityValidator.sanitize_display_text(v, "Display name") + @field_validator("template") @classmethod def validate_template(cls, v: str) -> str: @@ -1755,6 +1850,8 @@ class PromptUpdate(BaseModelWithConfigDict): """ name: Optional[str] = Field(None, description="Unique name for the prompt") + displayName: Optional[str] = Field(None, description="Display name for the prompt (shown in UI)") # noqa: N815 + custom_name: Optional[str] = Field(None, description="Custom name for the prompt") description: Optional[str] = Field(None, description="Prompt description") template: Optional[str] = Field(None, description="Prompt template text") arguments: Optional[List[PromptArgument]] = Field(None, description="List of arguments for the template") @@ -1836,6 +1933,35 @@ def validate_arguments(cls, v: Dict[str, Any]) -> Dict[str, Any]: SecurityValidator.validate_json_depth(v) return v + @field_validator("displayName") + @classmethod + def validate_display_name(cls, v: Optional[str]) -> Optional[str]: + """Ensure display names display safely + + Args: + v (str): Value to validate + + Returns: + str: Value if validated as safe + + Raises: + ValueError: When displayName contains unsafe content or exceeds length limits + + Examples: + >>> from mcpgateway.schemas import PromptUpdate + >>> PromptUpdate.validate_display_name('My Custom Prompt') + 'My Custom Prompt' + >>> PromptUpdate.validate_display_name('') + Traceback (most recent call last): + ... + ValueError: ... + """ + if v is None: + return v + if len(v) > SecurityValidator.MAX_NAME_LENGTH: + raise ValueError(f"Display name exceeds maximum length of {SecurityValidator.MAX_NAME_LENGTH}") + return SecurityValidator.sanitize_display_text(v, "Display name") + class PromptRead(BaseModelWithConfigDict): """Schema for reading prompt information. @@ -1847,7 +1973,7 @@ class PromptRead(BaseModelWithConfigDict): - Metrics: Aggregated metrics for the prompt invocations. """ - id: int + id: str name: str description: Optional[str] template: str @@ -1855,6 +1981,10 @@ class PromptRead(BaseModelWithConfigDict): created_at: datetime updated_at: datetime is_active: bool + displayName: Optional[str] = Field(None, description="Display name for the prompt (shown in UI)") # noqa: N815 + gateway_slug: str + custom_name: str + custom_name_slug: str tags: List[str] = Field(default_factory=list, description="Tags for categorizing the prompt") metrics: PromptMetrics @@ -2879,7 +3009,7 @@ def validate_id(cls, v: Optional[str]) -> Optional[str]: str: Value if validated as safe Raises: - ValueError: When displayName contains unsafe content or exceeds length limits + ValueError: When id contains unsafe content or exceeds length limits Examples: >>> from mcpgateway.schemas import ServerCreate @@ -3107,8 +3237,8 @@ class ServerRead(BaseModelWithConfigDict): updated_at: datetime is_active: bool associated_tools: List[str] = [] - associated_resources: List[int] = [] - associated_prompts: List[int] = [] + associated_resources: List[str] = [] + associated_prompts: List[str] = [] associated_a2a_agents: List[str] = [] metrics: ServerMetrics tags: List[str] = Field(default_factory=list, description="Tags for categorizing the server") diff --git a/mcpgateway/services/export_service.py b/mcpgateway/services/export_service.py index 9204b5f3c..77e29b44c 100644 --- a/mcpgateway/services/export_service.py +++ b/mcpgateway/services/export_service.py @@ -326,6 +326,7 @@ async def _export_prompts(self, db: Session, tags: Optional[List[str]], include_ for prompt in prompts: prompt_data = { "name": prompt.name, + "displayName": prompt.displayName, "template": prompt.template, "description": prompt.description, "input_schema": {"type": "object", "properties": {}, "required": []}, @@ -366,6 +367,7 @@ async def _export_resources(self, db: Session, tags: Optional[List[str]], includ for resource in resources: resource_data = { "name": resource.name, + "displayName": resource.displayName, "uri": resource.uri, "description": resource.description, "mime_type": resource.mime_type, diff --git a/mcpgateway/services/gateway_service.py b/mcpgateway/services/gateway_service.py index af8f9607e..920646f74 100644 --- a/mcpgateway/services/gateway_service.py +++ b/mcpgateway/services/gateway_service.py @@ -496,7 +496,10 @@ async def register_gateway( db_resources = [ DbResource( uri=resource.uri, - name=resource.name, + original_name=resource.name, + custom_name=resource.name, + custom_name_slug=slugify(resource.name), + display_name=generate_display_name(resource.name), description=resource.description, mime_type=resource.mime_type, template=resource.template, @@ -514,7 +517,10 @@ async def register_gateway( # Create prompt DB models db_prompts = [ DbPrompt( - name=prompt.name, + original_name=prompt.name, + custom_name=prompt.name, + custom_name_slug=slugify(prompt.name), + display_name=generate_display_name(prompt.name), description=prompt.description, template=prompt.template if hasattr(prompt, "template") else "", argument_schema={}, # Use argument_schema instead of arguments @@ -863,7 +869,10 @@ async def update_gateway(self, db: Session, gateway_id: str, gateway_update: Gat gateway.resources.append( DbResource( uri=resource.uri, - name=resource.name, + original_name=resource.name, + custom_name=resource.custom_name, + custom_name_slug=slugify(resource.custom_name), + display_name=generate_display_name(resource.custom_name), description=resource.description, mime_type=resource.mime_type, template=resource.template, @@ -876,7 +885,10 @@ async def update_gateway(self, db: Session, gateway_id: str, gateway_update: Gat if not existing_prompt: gateway.prompts.append( DbPrompt( - name=prompt.name, + original_name=prompt.name, + custom_name=prompt.custom_name, + custom_name_slug=slugify(prompt.custom_name), + display_name=generate_display_name(prompt.custom_name), description=prompt.description, template=prompt.template if hasattr(prompt, "template") else "", argument_schema={}, # Use argument_schema instead of arguments @@ -886,7 +898,7 @@ async def update_gateway(self, db: Session, gateway_id: str, gateway_update: Gat gateway.capabilities = capabilities gateway.tools = [tool for tool in gateway.tools if tool.original_name in new_tool_names] # keep only still-valid rows gateway.resources = [resource for resource in gateway.resources if resource.uri in new_resource_uris] # keep only still-valid rows - gateway.prompts = [prompt for prompt in gateway.prompts if prompt.name in new_prompt_names] # keep only still-valid rows + gateway.prompts = [prompt for prompt in gateway.prompts if prompt.original_name in new_prompt_names] # keep only still-valid rows gateway.last_seen = datetime.now(timezone.utc) # Update tracking with new URL @@ -1052,7 +1064,10 @@ async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bo gateway.resources.append( DbResource( uri=resource.uri, - name=resource.name, + original_name=resource.name, + custom_name=resource.custom_name, + custom_name_slug=slugify(resource.custom_name), + display_name=generate_display_name(resource.custom_name), description=resource.description, mime_type=resource.mime_type, template=resource.template, @@ -1065,7 +1080,10 @@ async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bo if not existing_prompt: gateway.prompts.append( DbPrompt( - name=prompt.name, + original_name=prompt.name, + custom_name=prompt.custom_name, + custom_name_slug=slugify(prompt.custom_name), + display_name=generate_display_name(prompt.custom_name), description=prompt.description, template=prompt.template if hasattr(prompt, "template") else "", argument_schema={}, # Use argument_schema instead of arguments @@ -1075,7 +1093,7 @@ async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bo gateway.capabilities = capabilities gateway.tools = [tool for tool in gateway.tools if tool.original_name in new_tool_names] # keep only still-valid rows gateway.resources = [resource for resource in gateway.resources if resource.uri in new_resource_uris] # keep only still-valid rows - gateway.prompts = [prompt for prompt in gateway.prompts if prompt.name in new_prompt_names] # keep only still-valid rows + gateway.prompts = [prompt for prompt in gateway.prompts if prompt.original_name in new_prompt_names] # keep only still-valid rows gateway.last_seen = datetime.now(timezone.utc) except Exception as e: logger.warning(f"Failed to initialize reactivated gateway: {e}") diff --git a/mcpgateway/services/import_service.py b/mcpgateway/services/import_service.py index 7c3d845e9..a8701c7d3 100644 --- a/mcpgateway/services/import_service.py +++ b/mcpgateway/services/import_service.py @@ -1073,7 +1073,14 @@ def _convert_to_prompt_create(self, prompt_data: Dict[str, Any]) -> PromptCreate for prop_name, prop_data in properties.items(): arguments.append({"name": prop_name, "description": prop_data.get("description", ""), "required": prop_name in required_fields}) - return PromptCreate(name=prompt_data["name"], template=prompt_data["template"], description=prompt_data.get("description"), arguments=arguments, tags=prompt_data.get("tags", [])) + return PromptCreate( + name=prompt_data["name"], + displayName=prompt_data.get("displayName"), + template=prompt_data["template"], + description=prompt_data.get("description"), + arguments=arguments, + tags=prompt_data.get("tags", []), + ) def _convert_to_prompt_update(self, prompt_data: Dict[str, Any]) -> PromptUpdate: """Convert import data to PromptUpdate schema. @@ -1094,7 +1101,12 @@ def _convert_to_prompt_update(self, prompt_data: Dict[str, Any]) -> PromptUpdate arguments.append({"name": prop_name, "description": prop_data.get("description", ""), "required": prop_name in required_fields}) return PromptUpdate( - name=prompt_data.get("name"), template=prompt_data.get("template"), description=prompt_data.get("description"), arguments=arguments if arguments else None, tags=prompt_data.get("tags") + name=prompt_data.get("name"), + displayName=prompt_data.get("displayName"), + template=prompt_data.get("template"), + description=prompt_data.get("description"), + arguments=arguments if arguments else None, + tags=prompt_data.get("tags"), ) def _convert_to_resource_create(self, resource_data: Dict[str, Any]) -> ResourceCreate: @@ -1108,6 +1120,7 @@ def _convert_to_resource_create(self, resource_data: Dict[str, Any]) -> Resource """ return ResourceCreate( uri=resource_data["uri"], + displayName=resource_data.get("displayName"), name=resource_data["name"], description=resource_data.get("description"), mime_type=resource_data.get("mime_type"), @@ -1125,7 +1138,12 @@ def _convert_to_resource_update(self, resource_data: Dict[str, Any]) -> Resource ResourceUpdate schema object """ return ResourceUpdate( - name=resource_data.get("name"), description=resource_data.get("description"), mime_type=resource_data.get("mime_type"), content=resource_data.get("content"), tags=resource_data.get("tags") + name=resource_data.get("name"), + displayName=resource_data.get("displayName"), + description=resource_data.get("description"), + mime_type=resource_data.get("mime_type"), + content=resource_data.get("content"), + tags=resource_data.get("tags"), ) def get_import_status(self, import_id: str) -> Optional[ImportStatus]: diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index 9e2bb6380..0ebb62c47 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -37,6 +37,7 @@ from mcpgateway.plugins.framework import GlobalContext, PluginManager, PluginViolationError, PromptPosthookPayload, PromptPrehookPayload from mcpgateway.schemas import PromptCreate, PromptRead, PromptUpdate, TopPerformer from mcpgateway.services.logging_service import LoggingService +from mcpgateway.utils.create_slug import slugify from mcpgateway.utils.metrics_common import build_top_performers # Initialize logging service first @@ -218,7 +219,7 @@ def _convert_db_prompt(self, db_prompt: DbPrompt) -> Dict[str, Any]: avg_rt = (sum(m.response_time for m in db_prompt.metrics) / total) if total > 0 else None last_time = max((m.timestamp for m in db_prompt.metrics), default=None) if total > 0 else None - return { + prompt_dict = { "id": db_prompt.id, "name": db_prompt.name, "description": db_prompt.description, @@ -240,6 +241,16 @@ def _convert_db_prompt(self, db_prompt: DbPrompt) -> Dict[str, Any]: "tags": db_prompt.tags or [], } + display_name = getattr(db_prompt, "display_name", None) + custom_name = getattr(db_prompt, "custom_name", db_prompt.original_name) + prompt_dict["displayName"] = display_name or custom_name + prompt_dict["custom_name"] = custom_name + prompt_dict["gateway_slug"] = getattr(db_prompt, "gateway_slug", "") or "" + prompt_dict["custom_name_slug"] = getattr(db_prompt, "custom_name_slug", "") or "" + prompt_dict["tags"] = getattr(db_prompt, "tags", []) or [] + + return prompt_dict + async def register_prompt( self, db: Session, @@ -309,7 +320,9 @@ async def register_prompt( # Create DB model db_prompt = DbPrompt( - name=prompt.name, + original_name=prompt.name, + custom_name=prompt.name, + custom_name_slug=slugify(prompt.name), description=prompt.description, template=prompt.template, argument_schema=argument_schema, @@ -631,8 +644,10 @@ async def update_prompt(self, db: Session, name: str, prompt_update: PromptUpdat raise PromptNotFoundError(f"Prompt not found: {name}") - if prompt_update.name is not None: - prompt.name = prompt_update.name + if prompt_update.custom_name is not None: + prompt.custom_name = prompt_update.custom_name + if prompt_update.displayName is not None: + prompt.display_name = prompt_update.displayName if prompt_update.description is not None: prompt.description = prompt_update.description if prompt_update.template is not None: @@ -675,7 +690,7 @@ async def update_prompt(self, db: Session, name: str, prompt_update: PromptUpdat db.rollback() raise PromptError(f"Failed to update prompt: {str(e)}") - async def toggle_prompt_status(self, db: Session, prompt_id: int, activate: bool) -> PromptRead: + async def toggle_prompt_status(self, db: Session, prompt_id: str, activate: bool) -> PromptRead: """ Toggle the activation status of a prompt. diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index fe5bd0bd0..6f0683ab2 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -49,6 +49,7 @@ from mcpgateway.observability import create_span from mcpgateway.schemas import ResourceCreate, ResourceMetrics, ResourceRead, ResourceSubscription, ResourceUpdate, TopPerformer from mcpgateway.services.logging_service import LoggingService +from mcpgateway.utils.create_slug import slugify from mcpgateway.utils.metrics_common import build_top_performers # Plugin support imports (conditional) @@ -217,6 +218,17 @@ def _convert_resource_to_read(self, resource: DbResource) -> ResourceRead: "last_execution_time": last_time, } resource_dict["tags"] = resource.tags or [] + + resource_dict["name"] = resource.name + # Handle displayName with fallback and None checks + display_name = getattr(resource, "display_name", None) + custom_name = getattr(resource, "custom_name", resource.original_name) + resource_dict["displayName"] = display_name or custom_name + resource_dict["custom_name"] = custom_name + resource_dict["gateway_slug"] = getattr(resource, "gateway_slug", "") or "" + resource_dict["custom_name_slug"] = getattr(resource, "custom_name_slug", "") or "" + resource_dict["tags"] = getattr(resource, "tags", []) or [] + return ResourceRead.model_validate(resource_dict) async def register_resource( @@ -279,7 +291,9 @@ async def register_resource( # Create DB model db_resource = DbResource( uri=resource.uri, - name=resource.name, + original_name=resource.name, + custom_name=resource.name, + custom_name_slug=slugify(resource.name), description=resource.description, mime_type=mime_type, template=resource.template, @@ -545,7 +559,7 @@ async def read_resource(self, db: Session, uri: str, request_id: Optional[str] = # Return content return content - async def toggle_resource_status(self, db: Session, resource_id: int, activate: bool) -> ResourceRead: + async def toggle_resource_status(self, db: Session, resource_id: str, activate: bool) -> ResourceRead: """ Toggle the activation status of a resource. @@ -735,8 +749,10 @@ async def update_resource(self, db: Session, uri: str, resource_update: Resource raise ResourceNotFoundError(f"Resource not found: {uri}") # Update fields if provided - if resource_update.name is not None: - resource.name = resource_update.name + if resource_update.custom_name is not None: + resource.custom_name = resource_update.custom_name + if resource_update.displayName is not None: + resource.display_name = resource_update.displayName if resource_update.description is not None: resource.description = resource_update.description if resource_update.mime_type is not None: diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index 492635352..79fd31771 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -207,8 +207,8 @@ def _convert_server_to_read(self, server: DbServer) -> ServerRead: } # Also update associated IDs (if not already done) server_dict["associated_tools"] = [tool.name for tool in server.tools] if server.tools else [] - server_dict["associated_resources"] = [res.id for res in server.resources] if server.resources else [] - server_dict["associated_prompts"] = [prompt.id for prompt in server.prompts] if server.prompts else [] + server_dict["associated_resources"] = [res.name for res in server.resources] if server.resources else [] + server_dict["associated_prompts"] = [prompt.name for prompt in server.prompts] if server.prompts else [] server_dict["associated_a2a_agents"] = [agent.id for agent in server.a2a_agents] if server.a2a_agents else [] server_dict["tags"] = server.tags or [] return ServerRead.model_validate(server_dict) @@ -338,7 +338,7 @@ async def register_server(self, db: Session, server_in: ServerCreate) -> ServerR for resource_id in server_in.associated_resources: if resource_id.strip() == "": continue - resource_obj = db.get(DbResource, int(resource_id)) + resource_obj = db.get(DbResource, resource_id) if not resource_obj: raise ServerError(f"Resource with id {resource_id} does not exist.") db_server.resources.append(resource_obj) @@ -348,7 +348,7 @@ async def register_server(self, db: Session, server_in: ServerCreate) -> ServerR for prompt_id in server_in.associated_prompts: if prompt_id.strip() == "": continue - prompt_obj = db.get(DbPrompt, int(prompt_id)) + prompt_obj = db.get(DbPrompt, prompt_id) if not prompt_obj: raise ServerError(f"Prompt with id {prompt_id} does not exist.") db_server.prompts.append(prompt_obj) @@ -475,8 +475,8 @@ async def get_server(self, db: Session, server_id: str) -> ServerRead: "updated_at": server.updated_at, "is_active": server.is_active, "associated_tools": [tool.name for tool in server.tools], - "associated_resources": [res.id for res in server.resources], - "associated_prompts": [prompt.id for prompt in server.prompts], + "associated_resources": [res.name for res in server.resources], + "associated_prompts": [prompt.name for prompt in server.prompts], } logger.debug(f"Server Data: {server_data}") return self._convert_server_to_read(server) @@ -559,7 +559,7 @@ async def update_server(self, db: Session, server_id: str, server_update: Server if server_update.associated_resources is not None: server.resources = [] for resource_id in server_update.associated_resources: - resource_obj = db.get(DbResource, int(resource_id)) + resource_obj = db.get(DbResource, resource_id) if resource_obj: server.resources.append(resource_obj) @@ -567,7 +567,7 @@ async def update_server(self, db: Session, server_id: str, server_update: Server if server_update.associated_prompts is not None: server.prompts = [] for prompt_id in server_update.associated_prompts: - prompt_obj = db.get(DbPrompt, int(prompt_id)) + prompt_obj = db.get(DbPrompt, prompt_id) if prompt_obj: server.prompts.append(prompt_obj) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 3f5b13d47..b0b16aad2 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -4233,6 +4233,93 @@ function initToolSelect( checkboxes.forEach((cb) => cb.addEventListener("change", update)); } +function initResourceSelect( + selectId, + max = 6, + selectBtnId = null, + clearBtnId = null, +) { + const container = document.getElementById(selectId); + const clearBtn = clearBtnId ? document.getElementById(clearBtnId) : null; + const selectBtn = selectBtnId ? document.getElementById(selectBtnId) : null; + + if (!container) { + console.warn(`Resource select elements not found: ${selectId}`); + return; + } + + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + + function update() { + try { + const checked = Array.from(checkboxes).filter((cb) => cb.checked); + const count = checked.length; + } catch (error) { + console.error("Error updating resource select:", error); + } + } + + if (clearBtn) { + clearBtn.addEventListener("click", () => { + checkboxes.forEach((cb) => (cb.checked = false)); + update(); + }); + } + + if (selectBtn) { + selectBtn.addEventListener("click", () => { + checkboxes.forEach((cb) => (cb.checked = true)); + update(); + }); + } + + update(); // Initial render + checkboxes.forEach((cb) => cb.addEventListener("change", update)); +} + +function initPromptSelect( + selectId, + max = 6, + selectBtnId = null, + clearBtnId = null, +) { + const container = document.getElementById(selectId); + const clearBtn = clearBtnId ? document.getElementById(clearBtnId) : null; + const selectBtn = selectBtnId ? document.getElementById(selectBtnId) : null; + + if (!container) { + console.warn(`Prompt select elements not found: ${selectId}`); + return; + } + + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + + function update() { + try { + const checked = Array.from(checkboxes).filter((cb) => cb.checked); + const count = checked.length; + } catch (error) { + console.error("Error updating prompt select:", error); + } + } + + if (clearBtn) { + clearBtn.addEventListener("click", () => { + checkboxes.forEach((cb) => (cb.checked = false)); + update(); + }); + } + + if (selectBtn) { + selectBtn.addEventListener("click", () => { + checkboxes.forEach((cb) => (cb.checked = true)); + update(); + }); + } + + update(); // Initial render + checkboxes.forEach((cb) => cb.addEventListener("change", update)); +} // =================================================================== // INACTIVE ITEMS HANDLING // =================================================================== @@ -6694,8 +6781,10 @@ document.addEventListener("DOMContentLoaded", () => { // 1. Initialize CodeMirror editors first initializeCodeMirrorEditors(); - // 2. Initialize tool selects + // 2. Initialize tool, resource, prompt selects initializeToolSelects(); + initializeResourceSelects(); + initializePromptSelects(); // 3. Set up all event listeners initializeEventListeners(); @@ -6856,6 +6945,40 @@ function initializeToolSelects() { ); } +function initializeResourceSelects() { + console.log("Initializing resource selects..."); + + initResourceSelect( + "associatedResources", + 6, + "selectAllResourcesBtn", + "clearAllResourcesBtn", + ); + initResourceSelect( + "edit-server-resources", + 6, + "selectAllEditResourcesBtn", + "clearAllEditResourcesBtn", + ); +} + +function initializePromptSelects() { + console.log("Initializing prompt selects..."); + + initPromptSelect( + "associatedPrompts", + 6, + "selectAllPromptsBtn", + "clearAllPromptsBtn", + ); + initPromptSelect( + "edit-server-prompts", + 6, + "selectAllEditPromptsBtn", + "clearAllEditPromptsBtn", + ); +} + function initializeEventListeners() { console.log("Setting up event listeners..."); diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index 335fe01fb..810cfd386 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -959,16 +959,16 @@

- {% if server.associatedResources %} {{ - server.associatedResources | join(', ') }} {% else %} + {% if server.associatedResources %} {{ server.associatedResources | + map('string') | join(', ') }} {% else %} N/A {% endif %} - {% if server.associatedPrompts %} {{ - server.associatedPrompts | join(', ') }} {% else %} + {% if server.associatedPrompts %} {{ server.associatedPrompts | + map('string') | join(', ') }} {% else %} N/A {% endif %} @@ -1066,7 +1066,7 @@

@@ -1154,7 +1154,7 @@

class="text-gray-700 dark:text-gray-300" style="display: none" > - No tool found containing "" + No tool found containing ""

@@ -1190,29 +1190,125 @@

aria-live="polite" >

-
+
+ Associated Resources + +
+ {% for resource in resources %} + + {% endfor %} + +
+
+ + + +
-
+
+ Associated Prompts + +
+ {% for prompt in prompts %} + + {% endfor %} + +
+
+ + + +
@@ -2265,7 +2361,12 @@

- ID + S. No. + + + Gateway Name - {{ resource.id }} + {{ loop.index }} + + + {{ resource.gatewaySlug }} - ID + S. No. + + + Gateway Name - {{ prompt.id }} + {{ loop.index }} + + + {{ prompt.gatewaySlug }} >

-
+
- +
+ class="max-h-60 overflow-y-auto rounded-md border border-gray-600 shadow-sm p-3 bg-gray-50 dark:bg-gray-900" + > + {% for resource in resources %} + + {% endfor %} +
+
+ + + +
+ + +
+ + +
-
+
- +
+ class="max-h-60 overflow-y-auto rounded-md border border-gray-600 shadow-sm p-3 bg-gray-50 dark:bg-gray-900" + > + {% for prompt in prompts %} + + {% endfor %} +
+
+ + + +
+ + +
+ + +
// Add search functionality for filtering tools document.addEventListener('DOMContentLoaded', function() { - const searchBox = document.getElementById('searchTools'); + const toolSearchBox = document.getElementById('searchTools'); + const resourceSearchBox = document.getElementById('searchResources'); + const promptSearchBox = document.getElementById('searchPrompts'); + const toolItems = document.querySelectorAll('#associatedTools .tool-item'); + const resourceItems = document.querySelectorAll('#associatedResources .resource-item'); + const promptItems = document.querySelectorAll('#associatedPrompts .prompt-item'); + const noToolsMessage = document.getElementById('noToolsMessage'); - const searchQuerySpan = document.getElementById('searchQuery'); + const noResourcesMessage = document.getElementById('noResourcesMessage'); + const noPromptsMessage = document.getElementById('noPromptsMessage'); - searchBox.addEventListener('input', function() { + const toolSearchQuerySpan = document.getElementById('toolSearchQuery'); + const resourceSearchQuerySpan = document.getElementById('resourceSearchQuery'); + const promptSearchQuerySpan = document.getElementById('promptSearchQuery'); + + toolSearchBox.addEventListener('input', function() { const filter = this.value.toLowerCase(); let hasVisibleItems = false; @@ -5815,10 +6036,54 @@

if (hasVisibleItems) { noToolsMessage.style.display = 'none'; } else { - searchQuerySpan.textContent = filter; + toolSearchQuerySpan.textContent = filter; noToolsMessage.style.display = 'block'; } }); + + resourceSearchBox.addEventListener('input', function() { + const filter = this.value.toLowerCase(); + let hasVisibleItems = false; + + resourceItems.forEach(function(resourceItem) { + const resourceName = resourceItem.querySelector('span').textContent.toLowerCase(); + if (resourceName.includes(filter)) { + resourceItem.style.display = ''; + hasVisibleItems = true; + } else { + resourceItem.style.display = 'none'; + } + }); + + if (hasVisibleItems) { + noResourcesMessage.style.display = 'none'; + } else { + resourceSearchQuerySpan.textContent = filter; + noResourcesMessage.style.display = 'block'; + } + }); + + promptSearchBox.addEventListener('input', function() { + const filter = this.value.toLowerCase(); + let hasVisibleItems = false; + + promptItems.forEach(function(promptItem) { + const promptName = promptItem.querySelector('span').textContent.toLowerCase(); + if (promptName.includes(filter)) { + promptItem.style.display = ''; + hasVisibleItems = true; + } else { + promptItem.style.display = 'none'; + } + }); + + if (hasVisibleItems) { + noPromptsMessage.style.display = 'none'; + } else { + promptSearchQuerySpan.textContent = filter; + noPromptsMessage.style.display = 'block'; + } + }); });