-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathresource_manager.py
More file actions
137 lines (114 loc) · 5 KB
/
Copy pathresource_manager.py
File metadata and controls
137 lines (114 loc) · 5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""Resource manager functionality."""
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from mcp_types import Annotations, Icon, InputRequiredResult
from pydantic import AnyUrl
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
from mcp.server.mcpserver.resources.base import Resource
from mcp.server.mcpserver.resources.templates import (
DEFAULT_RESOURCE_SECURITY,
ResourceSecurity,
ResourceSecurityError,
ResourceTemplate,
)
from mcp.server.mcpserver.utilities.logging import get_logger
if TYPE_CHECKING:
from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver.context import Context
logger = get_logger(__name__)
class ResourceManager:
"""Manages MCPServer resources."""
def __init__(self, warn_on_duplicate_resources: bool = True, *, resources: list[Resource] | None = None):
self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {}
self.warn_on_duplicate_resources = warn_on_duplicate_resources
for resource in resources or ():
self.add_resource(resource)
def add_resource(self, resource: Resource) -> Resource:
"""Add a resource to the manager.
Args:
resource: A Resource instance to add.
Returns:
The added resource. If a resource with the same URI already exists, returns the existing resource.
"""
logger.debug(
"Adding resource",
extra={"uri": resource.uri, "type": type(resource).__name__, "resource_name": resource.name},
)
existing = self._resources.get(str(resource.uri))
if existing:
if self.warn_on_duplicate_resources:
logger.warning(f"Resource already exists: {resource.uri}")
return existing
self._resources[str(resource.uri)] = resource
return resource
def add_template(
self,
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
icons: list[Icon] | None = None,
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
security: ResourceSecurity = DEFAULT_RESOURCE_SECURITY,
) -> ResourceTemplate:
"""Add a template from a function."""
template = ResourceTemplate.from_function(
fn,
uri_template=uri_template,
name=name,
title=title,
description=description,
mime_type=mime_type,
icons=icons,
annotations=annotations,
meta=meta,
security=security,
)
self._templates[template.uri_template] = template
return template
async def get_resource(
self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]
) -> Resource | InputRequiredResult:
"""Get resource by URI, checking concrete resources first, then templates.
A template function may return an `InputRequiredResult` instead of
resource content (the 2026-07-28 multi-round-trip flow); it is passed
through unchanged.
Raises:
ResourceNotFoundError: If no resource or template matches the URI.
ResourceError: If a matching template fails to create the resource.
Note:
Pydantic's ``AnyUrl`` normalises percent-encoding and
resolves ``..`` segments during validation, so a value
constructed as ``AnyUrl("file:///a/%2E%2E/b")`` arrives
here as ``file:///b``. The JSON-RPC protocol layer passes
raw ``str`` values and is unaffected, but internal callers
wrapping URIs in ``AnyUrl`` should be aware that security
checks see the already-normalised form.
"""
uri_str = str(uri)
logger.debug("Getting resource", extra={"uri": uri_str})
# First check concrete resources
if resource := self._resources.get(uri_str):
return resource
# Then check templates
for template in self._templates.values():
try:
params = template.matches(uri_str)
except ResourceSecurityError as e:
raise ResourceNotFoundError(f"Unknown resource: {uri}") from e
if params is not None:
return await template.create_resource(uri_str, params, context=context)
raise ResourceNotFoundError(f"Unknown resource: {uri}")
def list_resources(self) -> list[Resource]:
"""List all registered resources."""
logger.debug("Listing resources", extra={"count": len(self._resources)})
return list(self._resources.values())
def list_templates(self) -> list[ResourceTemplate]:
"""List all registered templates."""
logger.debug("Listing templates", extra={"count": len(self._templates)})
return list(self._templates.values())