2222class ResourceManager :
2323 """Manages MCPServer resources with optional tenant-scoped storage.
2424
25- Resources and templates are stored in dicts keyed by
26- ``(tenant_id, uri_string)`` and ``(tenant_id, uri_template)``
27- respectively. This allows the same URI to exist independently under
28- different tenants. When ``tenant_id`` is ``None`` (the default),
25+ Resources and templates are stored in nested dicts:
26+ ``{tenant_id: {uri_string: Resource}}`` and
27+ ``{tenant_id: {uri_template: ResourceTemplate}}`` respectively.
28+ This allows the same URI to exist independently under different tenants
29+ with O(1) lookups per tenant. When ``tenant_id`` is ``None`` (the default),
2930 entries live in a global scope, preserving backward compatibility
3031 with single-tenant usage.
32+
33+ Note: This class is not thread-safe. It is designed to run within a
34+ single-threaded async event loop, where all synchronous mutations
35+ execute atomically. Do not share instances across OS threads without
36+ external synchronization.
3137 """
3238
3339 def __init__ (self , warn_on_duplicate_resources : bool = True ):
34- self ._resources : dict [tuple [ str | None , str ] , Resource ] = {}
35- self ._templates : dict [tuple [ str | None , str ] , ResourceTemplate ] = {}
40+ self ._resources : dict [str | None , dict [ str , Resource ] ] = {}
41+ self ._templates : dict [str | None , dict [ str , ResourceTemplate ] ] = {}
3642 self .warn_on_duplicate_resources = warn_on_duplicate_resources
3743
3844 def add_resource (self , resource : Resource , * , tenant_id : str | None = None ) -> Resource :
@@ -54,13 +60,14 @@ def add_resource(self, resource: Resource, *, tenant_id: str | None = None) -> R
5460 "resource_name" : resource .name ,
5561 },
5662 )
57- key = (tenant_id , str (resource .uri ))
58- existing = self ._resources .get (key )
63+ scope = self ._resources .setdefault (tenant_id , {})
64+ uri_str = str (resource .uri )
65+ existing = scope .get (uri_str )
5966 if existing :
6067 if self .warn_on_duplicate_resources :
6168 logger .warning (f"Resource already exists: { resource .uri } " )
6269 return existing
63- self . _resources [ key ] = resource
70+ scope [ uri_str ] = resource
6471 return resource
6572
6673 def add_template (
@@ -77,7 +84,12 @@ def add_template(
7784 * ,
7885 tenant_id : str | None = None ,
7986 ) -> ResourceTemplate :
80- """Add a template from a function, optionally scoped to a tenant."""
87+ """Add a template from a function, optionally scoped to a tenant.
88+
89+ Returns:
90+ The added template. If a template with the same URI template already
91+ exists, returns the existing template.
92+ """
8193 template = ResourceTemplate .from_function (
8294 fn ,
8395 uri_template = uri_template ,
@@ -89,9 +101,25 @@ def add_template(
89101 annotations = annotations ,
90102 meta = meta ,
91103 )
92- self ._templates [(tenant_id , template .uri_template )] = template
104+ scope = self ._templates .setdefault (tenant_id , {})
105+ existing = scope .get (template .uri_template )
106+ if existing :
107+ if self .warn_on_duplicate_resources :
108+ logger .warning (f"Resource template already exists: { template .uri_template } " )
109+ return existing
110+ scope [template .uri_template ] = template
93111 return template
94112
113+ def remove_resource (self , uri : AnyUrl | str , * , tenant_id : str | None = None ) -> None :
114+ """Remove a resource by URI, optionally scoped to a tenant."""
115+ uri_str = str (uri )
116+ scope = self ._resources .get (tenant_id , {})
117+ if uri_str not in scope :
118+ raise ValueError (f"Unknown resource: { uri } " )
119+ del scope [uri_str ]
120+ if not scope and tenant_id in self ._resources :
121+ del self ._resources [tenant_id ]
122+
95123 async def get_resource (
96124 self ,
97125 uri : AnyUrl | str ,
@@ -104,13 +132,12 @@ async def get_resource(
104132 logger .debug ("Getting resource" , extra = {"uri" : uri_str })
105133
106134 # First check concrete resources
107- if resource := self ._resources .get ((tenant_id , uri_str )):
135+ resource = self ._resources .get (tenant_id , {}).get (uri_str )
136+ if resource :
108137 return resource
109138
110139 # Then check templates for this tenant scope
111- for (tid , _ ), template in self ._templates .items ():
112- if tid != tenant_id :
113- continue
140+ for template in self ._templates .get (tenant_id , {}).values ():
114141 if params := template .matches (uri_str ):
115142 try :
116143 return await template .create_resource (uri_str , params , context = context )
@@ -121,12 +148,12 @@ async def get_resource(
121148
122149 def list_resources (self , * , tenant_id : str | None = None ) -> list [Resource ]:
123150 """List all registered resources for a given tenant scope."""
124- resources = [ r for ( tid , _ ), r in self ._resources .items () if tid == tenant_id ]
151+ resources = list ( self ._resources .get ( tenant_id , {}). values ())
125152 logger .debug ("Listing resources" , extra = {"count" : len (resources )})
126153 return resources
127154
128155 def list_templates (self , * , tenant_id : str | None = None ) -> list [ResourceTemplate ]:
129156 """List all registered templates for a given tenant scope."""
130- templates = [ t for ( tid , _ ), t in self ._templates .items () if tid == tenant_id ]
157+ templates = list ( self ._templates .get ( tenant_id , {}). values ())
131158 logger .debug ("Listing templates" , extra = {"count" : len (templates )})
132159 return templates
0 commit comments