diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
index 469ea86ad4b..92b5fb6552f 100644
--- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
+++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
@@ -4529,6 +4529,7 @@ class MCPServerManager:
extra_headers: dict[str, str] | None = None,
add_prefix: bool = True,
raw_headers: dict[str, str] | None = None,
+ raise_on_error: bool = False,
) -> list[Prompt]:
try:
headers: Final = (
@@ -4561,6 +4562,8 @@ class MCPServerManager:
items: Final = await self._prompt_discovery_cache.get(key, fetch)
return self._create_prefixed_prompts(items, server, add_prefix=add_prefix)
except Exception as error:
+ if raise_on_error:
+ raise_classified_list_failure(error, server.name, suppress_challenge=server.is_dcr_bridge)
verbose_logger.warning("Failed to get prompts from server %s: %s", server.name, error)
return []
@@ -4572,6 +4575,7 @@ class MCPServerManager:
extra_headers: dict[str, str] | None = None,
add_prefix: bool = True,
raw_headers: dict[str, str] | None = None,
+ raise_on_error: bool = False,
) -> list[Resource]:
try:
headers: Final = (
@@ -4604,6 +4608,8 @@ class MCPServerManager:
items: Final = await self._resource_discovery_cache.get(key, fetch)
return self._create_prefixed_resources(items, server, add_prefix=add_prefix)
except Exception as error:
+ if raise_on_error:
+ raise_classified_list_failure(error, server.name, suppress_challenge=server.is_dcr_bridge)
verbose_logger.warning("Failed to get resources from server %s: %s", server.name, error)
return []
@@ -4615,6 +4621,7 @@ class MCPServerManager:
extra_headers: dict[str, str] | None = None,
add_prefix: bool = True,
raw_headers: dict[str, str] | None = None,
+ raise_on_error: bool = False,
) -> list[ResourceTemplate]:
try:
headers: Final = (
@@ -4647,6 +4654,8 @@ class MCPServerManager:
items: Final = await self._template_discovery_cache.get(key, fetch)
return self._create_prefixed_resource_templates(items, server, add_prefix=add_prefix)
except Exception as error:
+ if raise_on_error:
+ raise_classified_list_failure(error, server.name, suppress_challenge=server.is_dcr_bridge)
verbose_logger.warning("Failed to get resource_templates from server %s: %s", server.name, error)
return []
diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
index 6a0ab5bdec5..31704aed0e4 100644
--- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
+++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py
@@ -5,13 +5,13 @@ from dataclasses import dataclass
from datetime import datetime
from traceback import walk_tb
from types import MappingProxyType
-from typing import TYPE_CHECKING, Any, Final, Literal
+from typing import TYPE_CHECKING, Annotated, Any, Final, Literal
from uuid import uuid4
import anyio
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
-from pydantic import ValidationError
+from pydantic import BaseModel, ValidationError
from starlette.datastructures import Headers
from litellm._logging import verbose_logger
@@ -169,12 +169,16 @@ def _known_connection_error_message(exc: BaseException, url: str | None, timeout
if MCP_AVAILABLE:
from mcp.shared.exceptions import McpError
+ from mcp.types import Prompt, Resource, ResourceTemplate
from mcp.types import Tool as MCPTool
from litellm.experimental_mcp_client.client import MCPClient, as_mcp_read_timeout
from litellm.llms.litellm_proxy.skills.skill_search import (
DEFAULT_SKILL_SEARCH_TOP_K,
)
+ from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import (
+ MCPRequestHandler,
+ )
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
_UPSTREAM_OAUTH_DISCOVERY_AUTH_TYPES,
global_mcp_server_manager,
@@ -195,6 +199,21 @@ if MCP_AVAILABLE:
fire_mcp_tool_call_failure_logging,
)
+ class ListMCPPromptsRestAPIResponse(BaseModel):
+ prompts: list[Prompt]
+
+ class ListMCPResourcesRestAPIResponse(BaseModel):
+ resources: list[Resource]
+ resource_templates: list[ResourceTemplate]
+
+ @dataclass(frozen=True, slots=True)
+ class _CatalogServerContext:
+ server: MCPServer
+ user_api_key_dict: UserAPIKeyAuth
+ mcp_auth_header: dict[str, str] | str | None
+ extra_headers: dict[str, str] | None
+ raw_headers: dict[str, str]
+
########################################################
############ MCP Server REST API Routes #################
async def _safe_fire_mcp_tool_call_logging(
@@ -1055,6 +1074,107 @@ if MCP_AVAILABLE:
"message": f"An unexpected error occurred: {e}",
}
+ async def _resolve_catalog_server_context(
+ request: Request,
+ server_id: str,
+ user_api_key_dict: UserAPIKeyAuth,
+ ) -> _CatalogServerContext:
+ acting_auth: Final = await acting_user_auth(user_api_key_dict)
+ _, canonical_server_id = await _resolve_allowed_mcp_servers_with_ip_filter(request, acting_auth, server_id)
+ server: Final = global_mcp_server_manager.get_mcp_server_by_id(canonical_server_id)
+ if server is None:
+ raise HTTPException(
+ status_code=404,
+ detail={
+ "error": "server_not_found",
+ "message": f"MCP server '{server_id}' was not found",
+ },
+ )
+ mcp_auth_header, mcp_server_auth_headers, raw_headers = _extract_mcp_headers_from_request(
+ request, MCPRequestHandler
+ )
+ return _CatalogServerContext(
+ server=server,
+ user_api_key_dict=acting_auth,
+ mcp_auth_header=_get_server_auth_header(server, mcp_server_auth_headers, mcp_auth_header),
+ extra_headers=await _get_user_oauth_extra_headers(server, acting_auth),
+ raw_headers=raw_headers,
+ )
+
+ def _catalog_list_http_exception(
+ error: MCPServerListError, server: MCPServer, catalog: Literal["prompts", "resources"]
+ ) -> HTTPException:
+ fault: Final = classify_list_exception(error)
+ verbose_logger.info("Listing %s from %s failed with a %s fault", catalog, server.name, fault.tag)
+ return HTTPException(
+ status_code=list_fault_http_status(fault),
+ detail={
+ "error": fault.tag,
+ "message": f"Failed to list {catalog} from server {get_server_prefix(server)}",
+ },
+ )
+
+ @router.get("/prompts/list", dependencies=[Depends(user_api_key_auth)])
+ async def list_prompts_rest_api(
+ request: Request,
+ server_id: Annotated[str, Query(description="The MCP server id, name, or alias to list prompts for")],
+ user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
+ ) -> ListMCPPromptsRestAPIResponse:
+ """List the prompts one MCP server exposes, with names as the upstream server reports them.
+
+ An upstream failure relays its classified HTTP status, the same as ``/mcp-rest/tools/list``."""
+ context: Final = await _resolve_catalog_server_context(request, server_id, user_api_key_dict)
+ try:
+ prompts: Final = await global_mcp_server_manager.get_prompts_from_server(
+ context.server,
+ user_api_key_auth=context.user_api_key_dict,
+ mcp_auth_header=context.mcp_auth_header,
+ extra_headers=context.extra_headers,
+ add_prefix=False,
+ raw_headers=context.raw_headers,
+ raise_on_error=True,
+ )
+ except MCPUpstreamAuthError as e:
+ raise _relay_upstream_auth_http_exception(e, request) from e
+ except MCPServerListError as e:
+ raise _catalog_list_http_exception(e, context.server, "prompts") from e
+ return ListMCPPromptsRestAPIResponse(prompts=prompts)
+
+ @router.get("/resources/list", dependencies=[Depends(user_api_key_auth)])
+ async def list_resources_rest_api(
+ request: Request,
+ server_id: Annotated[str, Query(description="The MCP server id, name, or alias to list resources for")],
+ user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
+ ) -> ListMCPResourcesRestAPIResponse:
+ """List the resources and resource templates one MCP server exposes."""
+ context: Final = await _resolve_catalog_server_context(request, server_id, user_api_key_dict)
+ try:
+ resources, resource_templates = await asyncio.gather(
+ global_mcp_server_manager.get_resources_from_server(
+ context.server,
+ user_api_key_auth=context.user_api_key_dict,
+ mcp_auth_header=context.mcp_auth_header,
+ extra_headers=context.extra_headers,
+ add_prefix=False,
+ raw_headers=context.raw_headers,
+ raise_on_error=True,
+ ),
+ global_mcp_server_manager.get_resource_templates_from_server(
+ context.server,
+ user_api_key_auth=context.user_api_key_dict,
+ mcp_auth_header=context.mcp_auth_header,
+ extra_headers=context.extra_headers,
+ add_prefix=False,
+ raw_headers=context.raw_headers,
+ raise_on_error=True,
+ ),
+ )
+ except MCPUpstreamAuthError as e:
+ raise _relay_upstream_auth_http_exception(e, request) from e
+ except MCPServerListError as e:
+ raise _catalog_list_http_exception(e, context.server, "resources") from e
+ return ListMCPResourcesRestAPIResponse(resources=resources, resource_templates=resource_templates)
+
@router.post("/tools/call", dependencies=[Depends(user_api_key_auth)])
async def call_tool_rest_api(
request: Request,
diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json
index 584b1e05b89..3571179cd08 100644
--- a/litellm/proxy/_lazy_openapi_snapshot.json
+++ b/litellm/proxy/_lazy_openapi_snapshot.json
@@ -21804,6 +21804,44 @@
"mcp_app": {
"components": {
"schemas": {
+ "Annotations": {
+ "additionalProperties": true,
+ "properties": {
+ "audience": {
+ "anyOf": [
+ {
+ "items": {
+ "enum": [
+ "user",
+ "assistant"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Audience"
+ },
+ "priority": {
+ "anyOf": [
+ {
+ "maximum": 1.0,
+ "minimum": 0.0,
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Priority"
+ }
+ },
+ "title": "Annotations",
+ "type": "object"
+ },
"HTTPValidationError": {
"properties": {
"detail": {
@@ -21817,6 +21855,86 @@
"title": "HTTPValidationError",
"type": "object"
},
+ "Icon": {
+ "additionalProperties": true,
+ "description": "An icon for display in user interfaces.",
+ "properties": {
+ "mimeType": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Mimetype"
+ },
+ "sizes": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Sizes"
+ },
+ "src": {
+ "title": "Src",
+ "type": "string"
+ }
+ },
+ "required": [
+ "src"
+ ],
+ "title": "Icon",
+ "type": "object"
+ },
+ "ListMCPPromptsRestAPIResponse": {
+ "properties": {
+ "prompts": {
+ "items": {
+ "$ref": "#/components/schemas/Prompt"
+ },
+ "title": "Prompts",
+ "type": "array"
+ }
+ },
+ "required": [
+ "prompts"
+ ],
+ "title": "ListMCPPromptsRestAPIResponse",
+ "type": "object"
+ },
+ "ListMCPResourcesRestAPIResponse": {
+ "properties": {
+ "resource_templates": {
+ "items": {
+ "$ref": "#/components/schemas/ResourceTemplate"
+ },
+ "title": "Resource Templates",
+ "type": "array"
+ },
+ "resources": {
+ "items": {
+ "$ref": "#/components/schemas/Resource"
+ },
+ "title": "Resources",
+ "type": "array"
+ }
+ },
+ "required": [
+ "resources",
+ "resource_templates"
+ ],
+ "title": "ListMCPResourcesRestAPIResponse",
+ "type": "object"
+ },
"MCPCredentials": {
"properties": {
"audience": {
@@ -22628,6 +22746,311 @@
"title": "NewMCPServerRequest",
"type": "object"
},
+ "Prompt": {
+ "additionalProperties": true,
+ "description": "A prompt or prompt template that the server offers.",
+ "properties": {
+ "_meta": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Meta"
+ },
+ "arguments": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/PromptArgument"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Arguments"
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description"
+ },
+ "icons": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Icon"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Icons"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Title"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "title": "Prompt",
+ "type": "object"
+ },
+ "PromptArgument": {
+ "additionalProperties": true,
+ "description": "An argument for a prompt template.",
+ "properties": {
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "required": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Required"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "title": "PromptArgument",
+ "type": "object"
+ },
+ "Resource": {
+ "additionalProperties": true,
+ "description": "A known resource that the server is capable of reading.",
+ "properties": {
+ "_meta": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Meta"
+ },
+ "annotations": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Annotations"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description"
+ },
+ "icons": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Icon"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Icons"
+ },
+ "mimeType": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Mimetype"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "size": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Size"
+ },
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Title"
+ },
+ "uri": {
+ "format": "uri",
+ "minLength": 1,
+ "title": "Uri",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "uri"
+ ],
+ "title": "Resource",
+ "type": "object"
+ },
+ "ResourceTemplate": {
+ "additionalProperties": true,
+ "description": "A template description for resources available on the server.",
+ "properties": {
+ "_meta": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Meta"
+ },
+ "annotations": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Annotations"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description"
+ },
+ "icons": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Icon"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Icons"
+ },
+ "mimeType": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Mimetype"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Title"
+ },
+ "uriTemplate": {
+ "title": "Uritemplate",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "uriTemplate"
+ ],
+ "title": "ResourceTemplate",
+ "type": "object"
+ },
"ValidationError": {
"properties": {
"ctx": {
@@ -22799,6 +23222,106 @@
]
}
},
+ "/mcp-rest/prompts/list": {
+ "get": {
+ "description": "List the prompts one MCP server exposes, with names as the upstream server reports them.\n\nAn upstream failure relays its classified HTTP status, the same as ``/mcp-rest/tools/list``.",
+ "operationId": "list_prompts_rest_api_mcp_rest_prompts_list_get_2",
+ "parameters": [
+ {
+ "description": "The MCP server id, name, or alias to list prompts for",
+ "in": "query",
+ "name": "server_id",
+ "required": true,
+ "schema": {
+ "description": "The MCP server id, name, or alias to list prompts for",
+ "title": "Server Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListMCPPromptsRestAPIResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "security": [
+ {
+ "APIKeyHeader": []
+ }
+ ],
+ "summary": "List Prompts Rest Api",
+ "tags": [
+ "mcp_app"
+ ]
+ }
+ },
+ "/mcp-rest/resources/list": {
+ "get": {
+ "description": "List the resources and resource templates one MCP server exposes.",
+ "operationId": "list_resources_rest_api_mcp_rest_resources_list_get_2",
+ "parameters": [
+ {
+ "description": "The MCP server id, name, or alias to list resources for",
+ "in": "query",
+ "name": "server_id",
+ "required": true,
+ "schema": {
+ "description": "The MCP server id, name, or alias to list resources for",
+ "title": "Server Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListMCPResourcesRestAPIResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "security": [
+ {
+ "APIKeyHeader": []
+ }
+ ],
+ "summary": "List Resources Rest Api",
+ "tags": [
+ "mcp_app"
+ ]
+ }
+ },
"/mcp-rest/test/connection": {
"post": {
"description": "Test if we can connect to the provided MCP server before adding it",
@@ -31550,6 +32073,44 @@
"mcp_rest": {
"components": {
"schemas": {
+ "Annotations": {
+ "additionalProperties": true,
+ "properties": {
+ "audience": {
+ "anyOf": [
+ {
+ "items": {
+ "enum": [
+ "user",
+ "assistant"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Audience"
+ },
+ "priority": {
+ "anyOf": [
+ {
+ "maximum": 1.0,
+ "minimum": 0.0,
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Priority"
+ }
+ },
+ "title": "Annotations",
+ "type": "object"
+ },
"HTTPValidationError": {
"properties": {
"detail": {
@@ -31563,6 +32124,86 @@
"title": "HTTPValidationError",
"type": "object"
},
+ "Icon": {
+ "additionalProperties": true,
+ "description": "An icon for display in user interfaces.",
+ "properties": {
+ "mimeType": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Mimetype"
+ },
+ "sizes": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Sizes"
+ },
+ "src": {
+ "title": "Src",
+ "type": "string"
+ }
+ },
+ "required": [
+ "src"
+ ],
+ "title": "Icon",
+ "type": "object"
+ },
+ "ListMCPPromptsRestAPIResponse": {
+ "properties": {
+ "prompts": {
+ "items": {
+ "$ref": "#/components/schemas/Prompt"
+ },
+ "title": "Prompts",
+ "type": "array"
+ }
+ },
+ "required": [
+ "prompts"
+ ],
+ "title": "ListMCPPromptsRestAPIResponse",
+ "type": "object"
+ },
+ "ListMCPResourcesRestAPIResponse": {
+ "properties": {
+ "resource_templates": {
+ "items": {
+ "$ref": "#/components/schemas/ResourceTemplate"
+ },
+ "title": "Resource Templates",
+ "type": "array"
+ },
+ "resources": {
+ "items": {
+ "$ref": "#/components/schemas/Resource"
+ },
+ "title": "Resources",
+ "type": "array"
+ }
+ },
+ "required": [
+ "resources",
+ "resource_templates"
+ ],
+ "title": "ListMCPResourcesRestAPIResponse",
+ "type": "object"
+ },
"MCPCredentials": {
"properties": {
"audience": {
@@ -32374,6 +33015,311 @@
"title": "NewMCPServerRequest",
"type": "object"
},
+ "Prompt": {
+ "additionalProperties": true,
+ "description": "A prompt or prompt template that the server offers.",
+ "properties": {
+ "_meta": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Meta"
+ },
+ "arguments": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/PromptArgument"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Arguments"
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description"
+ },
+ "icons": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Icon"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Icons"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Title"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "title": "Prompt",
+ "type": "object"
+ },
+ "PromptArgument": {
+ "additionalProperties": true,
+ "description": "An argument for a prompt template.",
+ "properties": {
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "required": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Required"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "title": "PromptArgument",
+ "type": "object"
+ },
+ "Resource": {
+ "additionalProperties": true,
+ "description": "A known resource that the server is capable of reading.",
+ "properties": {
+ "_meta": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Meta"
+ },
+ "annotations": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Annotations"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description"
+ },
+ "icons": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Icon"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Icons"
+ },
+ "mimeType": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Mimetype"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "size": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Size"
+ },
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Title"
+ },
+ "uri": {
+ "format": "uri",
+ "minLength": 1,
+ "title": "Uri",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "uri"
+ ],
+ "title": "Resource",
+ "type": "object"
+ },
+ "ResourceTemplate": {
+ "additionalProperties": true,
+ "description": "A template description for resources available on the server.",
+ "properties": {
+ "_meta": {
+ "anyOf": [
+ {
+ "additionalProperties": true,
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Meta"
+ },
+ "annotations": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Annotations"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description"
+ },
+ "icons": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/Icon"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Icons"
+ },
+ "mimeType": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Mimetype"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Title"
+ },
+ "uriTemplate": {
+ "title": "Uritemplate",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "uriTemplate"
+ ],
+ "title": "ResourceTemplate",
+ "type": "object"
+ },
"ValidationError": {
"properties": {
"ctx": {
@@ -32417,6 +33363,106 @@
}
},
"paths": {
+ "/mcp-rest/prompts/list": {
+ "get": {
+ "description": "List the prompts one MCP server exposes, with names as the upstream server reports them.\n\nAn upstream failure relays its classified HTTP status, the same as ``/mcp-rest/tools/list``.",
+ "operationId": "list_prompts_rest_api_mcp_rest_prompts_list_get",
+ "parameters": [
+ {
+ "description": "The MCP server id, name, or alias to list prompts for",
+ "in": "query",
+ "name": "server_id",
+ "required": true,
+ "schema": {
+ "description": "The MCP server id, name, or alias to list prompts for",
+ "title": "Server Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListMCPPromptsRestAPIResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "security": [
+ {
+ "APIKeyHeader": []
+ }
+ ],
+ "summary": "List Prompts Rest Api",
+ "tags": [
+ "mcp_rest"
+ ]
+ }
+ },
+ "/mcp-rest/resources/list": {
+ "get": {
+ "description": "List the resources and resource templates one MCP server exposes.",
+ "operationId": "list_resources_rest_api_mcp_rest_resources_list_get",
+ "parameters": [
+ {
+ "description": "The MCP server id, name, or alias to list resources for",
+ "in": "query",
+ "name": "server_id",
+ "required": true,
+ "schema": {
+ "description": "The MCP server id, name, or alias to list resources for",
+ "title": "Server Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListMCPResourcesRestAPIResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "security": [
+ {
+ "APIKeyHeader": []
+ }
+ ],
+ "summary": "List Resources Rest Api",
+ "tags": [
+ "mcp_rest"
+ ]
+ }
+ },
"/mcp-rest/test/connection": {
"post": {
"description": "Test if we can connect to the provided MCP server before adding it",
diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py
index 76a51627d0c..1f0a885a0b7 100644
--- a/litellm/proxy/_types.py
+++ b/litellm/proxy/_types.py
@@ -529,6 +529,8 @@ class LiteLLMRoutes(enum.Enum):
"/mcp/tools/call",
"/mcp-rest/tools/list",
"/mcp-rest/tools/call",
+ "/mcp-rest/prompts/list",
+ "/mcp-rest/resources/list",
"/v1/mcp/tools",
"/introspect",
]
diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py
index d449ad06642..1462dde8cc2 100644
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py
@@ -3931,6 +3931,39 @@ class TestMCPServerManager:
mock_client.list_resource_templates.assert_awaited_once()
assert result == expected_templates
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize(
+ ("manager_method", "client_method"),
+ [
+ ("get_prompts_from_server", "list_prompts"),
+ ("get_resources_from_server", "list_resources"),
+ ("get_resource_templates_from_server", "list_resource_templates"),
+ ],
+ )
+ async def test_catalog_fetch_failure_is_swallowed_unless_raise_on_error(self, manager_method, client_method):
+ """Catalog fetches stay best-effort for the MCP protocol aggregate (empty list) but a
+ single-server caller that opts in gets the classified fault instead of empty-success."""
+ manager = MCPServerManager()
+ server = MCPServer(
+ server_id="server-1",
+ name="alias-server",
+ alias="alias-server",
+ server_name="alias-server",
+ url="https://example.com",
+ transport=MCPTransport.http,
+ )
+ mock_client = AsyncMock()
+ setattr(mock_client, client_method, AsyncMock(side_effect=httpx.ConnectError("connection refused")))
+ mock_client.discovery_auth_fingerprint = AsyncMock(return_value="test-credential-hash")
+
+ with patch.object(manager, "_create_mcp_client", new_callable=AsyncMock, return_value=mock_client):
+ assert await getattr(manager, manager_method)(server, user_api_key_auth=None) == []
+ with pytest.raises(MCPServerListError) as exc_info:
+ await getattr(manager, manager_method)(server, user_api_key_auth=None, raise_on_error=True)
+
+ assert exc_info.value.fault == ServerListFault(tag="unreachable")
+ assert exc_info.value.server_name == server.name
+
@pytest.mark.asyncio
async def test_read_resource_from_server_success(self):
manager = MCPServerManager()
diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py
index 4ec4ae31ca6..fda8b1620e4 100644
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py
@@ -2366,6 +2366,191 @@ class TestListToolsRestAPI:
assert result["error"] is None
+class TestListPromptsAndResourcesRestAPI:
+ """LIT-2011: the dashboard lists a server's prompts and resources through /mcp-rest, gated
+ by the same server admission and upstream credential resolution as /mcp-rest/tools/list."""
+
+ pytestmark = pytest.mark.asyncio
+
+ @staticmethod
+ def _stub_server() -> MCPServer:
+ server = MCPServer(server_id="catalog-server-id", name="catalog-server", transport=MCPTransport.http)
+ server.alias = "catalog"
+ server.server_name = "catalog-server"
+ server.available_on_public_internet = True
+ return server
+
+ def _grant(self, monkeypatch, server: MCPServer, allowed: list[str]) -> None:
+ async def fake_contexts(user_api_key_auth):
+ return [user_api_key_auth]
+
+ async def fake_get_allowed_mcp_servers(*args, **kwargs):
+ return allowed
+
+ monkeypatch.setattr(rest_endpoints, "build_effective_auth_contexts", fake_contexts, raising=False)
+ monkeypatch.setattr(
+ rest_endpoints.global_mcp_server_manager,
+ "get_allowed_mcp_servers",
+ fake_get_allowed_mcp_servers,
+ raising=False,
+ )
+ monkeypatch.setattr(
+ rest_endpoints.global_mcp_server_manager,
+ "get_mcp_server_by_id",
+ lambda sid: server if sid == server.server_id else None,
+ raising=False,
+ )
+ monkeypatch.setattr(
+ rest_endpoints.global_mcp_server_manager,
+ "get_mcp_server_by_name",
+ lambda name, client_ip=None: server if name == server.alias else None,
+ raising=False,
+ )
+
+ @pytest.mark.parametrize("route_name", ["list_prompts_rest_api", "list_resources_rest_api"])
+ async def test_rejects_server_outside_caller_grant(self, monkeypatch, route_name):
+ server = self._stub_server()
+ self._grant(monkeypatch, server, allowed=["some-other-server"])
+ upstream = AsyncMock()
+ monkeypatch.setattr(rest_endpoints.global_mcp_server_manager, "get_prompts_from_server", upstream)
+ monkeypatch.setattr(rest_endpoints.global_mcp_server_manager, "get_resources_from_server", upstream)
+ monkeypatch.setattr(rest_endpoints.global_mcp_server_manager, "get_resource_templates_from_server", upstream)
+
+ request = _build_request(path=f"/mcp-rest/{route_name}", method="GET")
+ with pytest.raises(HTTPException) as exc_info:
+ await getattr(rest_endpoints, route_name)(
+ request, server_id=server.server_id, user_api_key_dict=UserAPIKeyAuth()
+ )
+
+ assert exc_info.value.status_code == 403
+ assert exc_info.value.detail["error"] == "access_denied"
+ upstream.assert_not_awaited()
+
+ async def test_lists_prompts_with_upstream_names_and_server_credential(self, monkeypatch):
+ from mcp.types import Prompt, PromptArgument
+
+ server = self._stub_server()
+ self._grant(monkeypatch, server, allowed=[server.server_id])
+ upstream_prompts = [
+ Prompt(
+ name="summarize",
+ description="Summarize text",
+ arguments=[PromptArgument(name="text", required=True)],
+ )
+ ]
+ get_prompts = AsyncMock(return_value=upstream_prompts)
+ monkeypatch.setattr(rest_endpoints.global_mcp_server_manager, "get_prompts_from_server", get_prompts)
+
+ request = _build_request(
+ headers={"x-mcp-catalog-authorization": "Bearer per-server-token"},
+ path="/mcp-rest/prompts/list",
+ method="GET",
+ )
+ result = await rest_endpoints.list_prompts_rest_api(
+ request, server_id=server.alias, user_api_key_dict=UserAPIKeyAuth(user_id="user-123")
+ )
+
+ assert [prompt.name for prompt in result.prompts] == ["summarize"]
+ assert result.prompts[0].arguments[0].name == "text"
+ get_prompts.assert_awaited_once()
+ call = get_prompts.await_args
+ assert call.args[0] is server
+ assert call.kwargs["add_prefix"] is False
+ assert call.kwargs["mcp_auth_header"] == {"Authorization": "Bearer per-server-token"}
+ assert call.kwargs["raw_headers"]["x-mcp-catalog-authorization"] == "Bearer per-server-token"
+ assert call.kwargs["user_api_key_auth"].user_id == "user-123"
+
+ async def test_lists_resources_and_templates_for_allowed_server(self, monkeypatch):
+ from mcp.types import Resource, ResourceTemplate
+
+ server = self._stub_server()
+ self._grant(monkeypatch, server, allowed=[server.server_id])
+ get_resources = AsyncMock(return_value=[Resource(name="readme", uri="demo://readme", mimeType="text/markdown")])
+ get_templates = AsyncMock(
+ return_value=[ResourceTemplate(name="user_profile", uriTemplate="demo://users/{user_id}/profile")]
+ )
+ monkeypatch.setattr(rest_endpoints.global_mcp_server_manager, "get_resources_from_server", get_resources)
+ monkeypatch.setattr(
+ rest_endpoints.global_mcp_server_manager, "get_resource_templates_from_server", get_templates
+ )
+
+ request = _build_request(path="/mcp-rest/resources/list", method="GET")
+ result = await rest_endpoints.list_resources_rest_api(
+ request, server_id=server.server_id, user_api_key_dict=UserAPIKeyAuth()
+ )
+
+ assert [str(resource.uri) for resource in result.resources] == ["demo://readme"]
+ assert result.resources[0].mimeType == "text/markdown"
+ assert [template.uriTemplate for template in result.resource_templates] == ["demo://users/{user_id}/profile"]
+ for upstream in (get_resources, get_templates):
+ upstream.assert_awaited_once()
+ assert upstream.await_args.args[0] is server
+ assert upstream.await_args.kwargs["add_prefix"] is False
+
+ @pytest.mark.parametrize(
+ ("route_name", "manager_method", "catalog"),
+ [
+ ("list_prompts_rest_api", "get_prompts_from_server", "prompts"),
+ ("list_resources_rest_api", "get_resources_from_server", "resources"),
+ ("list_resources_rest_api", "get_resource_templates_from_server", "resources"),
+ ],
+ )
+ async def test_upstream_fault_relays_truthful_status_instead_of_empty_success(
+ self, monkeypatch, route_name, manager_method, catalog
+ ):
+ """A broken upstream must answer like /mcp-rest/tools/list does (a gateway status), not as
+ an empty catalog the dashboard would render as "this server has no prompts"."""
+ from litellm.proxy._experimental.mcp_server.exceptions import MCPServerListError
+ from litellm.proxy._experimental.mcp_server.faults.list_outcomes import ServerListFault
+
+ server = self._stub_server()
+ self._grant(monkeypatch, server, allowed=[server.server_id])
+ failing = AsyncMock(side_effect=MCPServerListError(ServerListFault(tag="unreachable"), server.name))
+ for method in ("get_prompts_from_server", "get_resources_from_server", "get_resource_templates_from_server"):
+ monkeypatch.setattr(
+ rest_endpoints.global_mcp_server_manager,
+ method,
+ failing if method == manager_method else AsyncMock(return_value=[]),
+ )
+
+ request = _build_request(path=f"/mcp-rest/{catalog}/list", method="GET")
+ with pytest.raises(HTTPException) as exc_info:
+ await getattr(rest_endpoints, route_name)(
+ request, server_id=server.server_id, user_api_key_dict=UserAPIKeyAuth()
+ )
+
+ assert exc_info.value.status_code == 502
+ assert exc_info.value.detail == {
+ "error": "unreachable",
+ "message": f"Failed to list {catalog} from server catalog",
+ }
+ assert failing.await_args.kwargs["raise_on_error"] is True
+
+ async def test_upstream_auth_challenge_is_relayed_for_catalog_routes(self, monkeypatch):
+ from litellm.proxy._experimental.mcp_server.exceptions import MCPUpstreamAuthError
+
+ server = self._stub_server()
+ self._grant(monkeypatch, server, allowed=[server.server_id])
+ challenge = 'Bearer resource_metadata="https://upstream.example/.well-known/oauth-protected-resource"'
+ monkeypatch.setattr(
+ rest_endpoints.global_mcp_server_manager,
+ "get_prompts_from_server",
+ AsyncMock(
+ side_effect=MCPUpstreamAuthError(status_code=401, www_authenticate=challenge, server_name=server.name)
+ ),
+ )
+
+ request = _build_request(path="/mcp-rest/prompts/list", method="GET")
+ with pytest.raises(HTTPException) as exc_info:
+ await rest_endpoints.list_prompts_rest_api(
+ request, server_id=server.server_id, user_api_key_dict=UserAPIKeyAuth()
+ )
+
+ assert exc_info.value.status_code == 401
+ assert exc_info.value.headers is not None
+ assert "www-authenticate" in {key.lower() for key in exc_info.value.headers}
+
+
class TestCallToolRestAPI:
pytestmark = pytest.mark.asyncio
diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py
index 72c59223549..563d7151de1 100644
--- a/tests/test_litellm/proxy/auth/test_route_checks.py
+++ b/tests/test_litellm/proxy/auth/test_route_checks.py
@@ -495,6 +495,8 @@ def test_virtual_key_llm_api_routes_rejects_mcp_multi_segment_admin_subpaths(
("/mcp/tools/call", "POST"),
("/mcp-rest/tools/list", "GET"),
("/mcp-rest/tools/call", "POST"),
+ ("/mcp-rest/prompts/list", "GET"),
+ ("/mcp-rest/resources/list", "GET"),
("/v1/mcp/tools", "GET"),
],
)
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPCatalogSections.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPCatalogSections.tsx
new file mode 100644
index 00000000000..a0445f75fb4
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPCatalogSections.tsx
@@ -0,0 +1,154 @@
+import React from "react";
+import { FileText, MessageSquareText, type LucideIcon } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
+import type { MCPPrompt, MCPResource, MCPResourceTemplate } from "@/components/mcp_tools/types";
+
+interface CatalogSectionProps {
+ title: string;
+ icon: LucideIcon;
+ count: number;
+ isLoading: boolean;
+ errorMessage?: string | null;
+ emptyText: string;
+ children: React.ReactNode;
+}
+
+const CatalogSection = ({
+ title,
+ icon: Icon,
+ count,
+ isLoading,
+ errorMessage,
+ emptyText,
+ children,
+}: CatalogSectionProps) => {
+ const isSettled = !isLoading && !errorMessage;
+ return (
+
+ Loading {title.toLowerCase()}... Error: {errorMessage} {emptyText} {prompt.description}
+ {resource.uri}
+ {resource.description}
+ {template.uriTemplate}
+ {template.description}{prompt.name}
+ {prompt.description && (
+ {resource.title ?? resource.name}
+ {resource.mimeType && (
+ {template.title ?? template.name}
+