nvidia-nat-mcp 1.3.0rc1__py3-none-any.whl → 1.3.0rc3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nat/meta/pypi.md CHANGED
@@ -19,9 +19,9 @@ limitations under the License.
19
19
 
20
20
 
21
21
  # NVIDIA NeMo Agent Toolkit MCP Subpackage
22
- Subpackage for MCP client integration in NeMo Agent toolkit.
22
+ Subpackage for MCP integration in NeMo Agent toolkit.
23
23
 
24
- This package provides MCP (Model Context Protocol) client functionality, allowing NeMo Agent toolkit workflows to connect to external MCP servers and use their tools as functions.
24
+ This package provides MCP (Model Context Protocol) functionality, allowing NeMo Agent toolkit workflows to connect to external MCP servers and use their tools as functions.
25
25
 
26
26
  ## Features
27
27
 
@@ -23,6 +23,7 @@ import httpx
23
23
  from pydantic import BaseModel
24
24
  from pydantic import Field
25
25
  from pydantic import HttpUrl
26
+ from pydantic import TypeAdapter
26
27
 
27
28
  from mcp.shared.auth import OAuthClientInformationFull
28
29
  from mcp.shared.auth import OAuthClientMetadata
@@ -65,7 +66,6 @@ class DiscoverOAuth2Endpoints:
65
66
  def __init__(self, config: MCPOAuth2ProviderConfig):
66
67
  self.config = config
67
68
  self._cached_endpoints: OAuth2Endpoints | None = None
68
- self._authenticated_servers: dict[str, AuthResult] = {}
69
69
 
70
70
  self._flow_handler: MCPAuthenticationFlowHandler = MCPAuthenticationFlowHandler()
71
71
 
@@ -192,11 +192,13 @@ class DiscoverOAuth2Endpoints:
192
192
  continue
193
193
  if meta.authorization_endpoint and meta.token_endpoint:
194
194
  logger.info("Discovered OAuth2 endpoints from %s", url)
195
- # this is bit of a hack to get the scopes supported by the auth server
195
+ # Convert AnyHttpUrl to HttpUrl using TypeAdapter
196
+ http_url_adapter = TypeAdapter(HttpUrl)
196
197
  return OAuth2Endpoints(
197
- authorization_url=str(meta.authorization_endpoint),
198
- token_url=str(meta.token_endpoint),
199
- registration_url=str(meta.registration_endpoint) if meta.registration_endpoint else None,
198
+ authorization_url=http_url_adapter.validate_python(str(meta.authorization_endpoint)),
199
+ token_url=http_url_adapter.validate_python(str(meta.token_endpoint)),
200
+ registration_url=http_url_adapter.validate_python(str(meta.registration_endpoint))
201
+ if meta.registration_endpoint else None,
200
202
  scopes=meta.scopes_supported,
201
203
  )
202
204
  except Exception as e:
@@ -283,8 +285,9 @@ class DynamicClientRegistration:
283
285
  class MCPOAuth2Provider(AuthProviderBase[MCPOAuth2ProviderConfig]):
284
286
  """MCP OAuth2 authentication provider that delegates to NAT framework."""
285
287
 
286
- def __init__(self, config: MCPOAuth2ProviderConfig):
288
+ def __init__(self, config: MCPOAuth2ProviderConfig, builder=None):
287
289
  super().__init__(config)
290
+ self._builder = builder
288
291
 
289
292
  # Discovery
290
293
  self._discoverer = DiscoverOAuth2Endpoints(config)
@@ -300,6 +303,19 @@ class MCPOAuth2Provider(AuthProviderBase[MCPOAuth2ProviderConfig]):
300
303
 
301
304
  self._auth_callback = None
302
305
 
306
+ # Initialize token storage
307
+ self._token_storage = None
308
+ self._token_storage_object_store_name = None
309
+
310
+ if self.config.token_storage_object_store:
311
+ # Store object store name, will be resolved later when builder context is available
312
+ self._token_storage_object_store_name = self.config.token_storage_object_store
313
+ logger.info(f"Configured to use object store '{self._token_storage_object_store_name}' for token storage")
314
+ else:
315
+ # Default: use in-memory token storage
316
+ from .token_storage import InMemoryTokenStorage
317
+ self._token_storage = InMemoryTokenStorage()
318
+
303
319
  def _set_custom_auth_callback(self,
304
320
  auth_callback: Callable[[OAuth2AuthCodeFlowProviderConfig, AuthFlowType],
305
321
  Awaitable[AuthenticatedContext]]):
@@ -308,7 +324,7 @@ class MCPOAuth2Provider(AuthProviderBase[MCPOAuth2ProviderConfig]):
308
324
  logger.info("Using custom authentication callback")
309
325
  self._auth_callback = auth_callback
310
326
  if self._auth_code_provider:
311
- self._auth_code_provider._set_custom_auth_callback(self._auth_callback)
327
+ self._auth_code_provider._set_custom_auth_callback(self._auth_callback) # type: ignore[arg-type]
312
328
 
313
329
  async def authenticate(self, user_id: str | None = None, **kwargs) -> AuthResult:
314
330
  """
@@ -374,6 +390,22 @@ class MCPOAuth2Provider(AuthProviderBase[MCPOAuth2ProviderConfig]):
374
390
  endpoints = self._cached_endpoints
375
391
  credentials = self._cached_credentials
376
392
 
393
+ # Resolve object store reference if needed
394
+ if self._token_storage_object_store_name and not self._token_storage:
395
+ try:
396
+ if not self._builder:
397
+ raise RuntimeError("Builder not available for resolving object store")
398
+ object_store = await self._builder.get_object_store_client(self._token_storage_object_store_name)
399
+ from .token_storage import ObjectStoreTokenStorage
400
+ self._token_storage = ObjectStoreTokenStorage(object_store)
401
+ logger.info(f"Initialized token storage with object store '{self._token_storage_object_store_name}'")
402
+ except Exception as e:
403
+ logger.warning(
404
+ f"Failed to resolve object store '{self._token_storage_object_store_name}' for token storage: {e}. "
405
+ "Falling back to in-memory storage.")
406
+ from .token_storage import InMemoryTokenStorage
407
+ self._token_storage = InMemoryTokenStorage()
408
+
377
409
  # Build the OAuth2 provider if not already built
378
410
  if self._auth_code_provider is None:
379
411
  scopes = self._effective_scopes
@@ -387,12 +419,12 @@ class MCPOAuth2Provider(AuthProviderBase[MCPOAuth2ProviderConfig]):
387
419
  scopes=scopes,
388
420
  use_pkce=bool(self.config.use_pkce),
389
421
  authorization_kwargs={"resource": str(self.config.server_url)})
390
- self._auth_code_provider = OAuth2AuthCodeFlowProvider(oauth2_config)
422
+ self._auth_code_provider = OAuth2AuthCodeFlowProvider(oauth2_config, token_storage=self._token_storage)
391
423
 
392
424
  # Use MCP-specific authentication method if available
393
425
  if hasattr(self._auth_code_provider, "_set_custom_auth_callback"):
394
- self._auth_code_provider._set_custom_auth_callback(self._auth_callback
395
- or self._flow_handler.authenticate)
426
+ callback = self._auth_callback or self._flow_handler.authenticate
427
+ self._auth_code_provider._set_custom_auth_callback(callback) # type: ignore[arg-type]
396
428
 
397
429
  # Auth code provider is responsible for per-user cache + refresh
398
430
  return await self._auth_code_provider.authenticate(user_id=user_id)
@@ -53,6 +53,11 @@ class MCPOAuth2ProviderConfig(AuthProviderBaseConfig, name="mcp_oauth2"):
53
53
  default_user_id: str | None = Field(default=None, description="Default user ID for authentication")
54
54
  allow_default_user_id_for_tool_calls: bool = Field(default=True, description="Allow default user ID for tool calls")
55
55
 
56
+ # Token storage configuration
57
+ token_storage_object_store: str | None = Field(
58
+ default=None,
59
+ description="Reference to object store for secure token storage. If None, uses in-memory storage.")
60
+
56
61
  @model_validator(mode="after")
57
62
  def validate_auth_config(self):
58
63
  """Validate authentication configuration for MCP-specific options."""
@@ -22,4 +22,4 @@ from nat.plugins.mcp.auth.auth_provider_config import MCPOAuth2ProviderConfig
22
22
  @register_auth_provider(config_type=MCPOAuth2ProviderConfig)
23
23
  async def mcp_oauth2_provider(authentication_provider: MCPOAuth2ProviderConfig, builder: Builder):
24
24
  """Register MCP OAuth2 authentication provider with NAT system."""
25
- yield MCPOAuth2Provider(authentication_provider)
25
+ yield MCPOAuth2Provider(authentication_provider, builder=builder)
@@ -0,0 +1,265 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import hashlib
17
+ import json
18
+ import logging
19
+ from abc import ABC
20
+ from abc import abstractmethod
21
+
22
+ from nat.data_models.authentication import AuthResult
23
+ from nat.data_models.authentication import BasicAuthCred
24
+ from nat.data_models.authentication import BearerTokenCred
25
+ from nat.data_models.authentication import CookieCred
26
+ from nat.data_models.authentication import HeaderCred
27
+ from nat.data_models.authentication import QueryCred
28
+ from nat.data_models.object_store import NoSuchKeyError
29
+ from nat.object_store.interfaces import ObjectStore
30
+ from nat.object_store.models import ObjectStoreItem
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class TokenStorageBase(ABC):
36
+ """
37
+ Abstract base class for token storage implementations.
38
+
39
+ Token storage implementations handle the secure persistence of authentication
40
+ tokens for MCP OAuth2 flows. Implementations can use various backends such as
41
+ object stores, databases, or in-memory storage.
42
+ """
43
+
44
+ @abstractmethod
45
+ async def store(self, user_id: str, auth_result: AuthResult) -> None:
46
+ """
47
+ Store an authentication result for a user.
48
+
49
+ Args:
50
+ user_id: The unique identifier for the user
51
+ auth_result: The authentication result to store
52
+ """
53
+ pass
54
+
55
+ @abstractmethod
56
+ async def retrieve(self, user_id: str) -> AuthResult | None:
57
+ """
58
+ Retrieve an authentication result for a user.
59
+
60
+ Args:
61
+ user_id: The unique identifier for the user
62
+
63
+ Returns:
64
+ The authentication result if found, None otherwise
65
+ """
66
+ pass
67
+
68
+ @abstractmethod
69
+ async def delete(self, user_id: str) -> None:
70
+ """
71
+ Delete an authentication result for a user.
72
+
73
+ Args:
74
+ user_id: The unique identifier for the user
75
+ """
76
+ pass
77
+
78
+ @abstractmethod
79
+ async def clear_all(self) -> None:
80
+ """
81
+ Clear all stored authentication results.
82
+ """
83
+ pass
84
+
85
+
86
+ class ObjectStoreTokenStorage(TokenStorageBase):
87
+ """
88
+ Token storage implementation backed by a NeMo Agent toolkit object store.
89
+
90
+ This implementation uses the object store infrastructure to persist tokens,
91
+ which provides encryption at rest, access controls, and persistence across
92
+ restarts when using backends like S3, MySQL, or Redis.
93
+ """
94
+
95
+ def __init__(self, object_store: ObjectStore):
96
+ """
97
+ Initialize the object store token storage.
98
+
99
+ Args:
100
+ object_store: The object store instance to use for token persistence
101
+ """
102
+ self._object_store = object_store
103
+
104
+ def _get_key(self, user_id: str) -> str:
105
+ """
106
+ Generate the object store key for a user's token.
107
+
108
+ Uses SHA256 hash to ensure the key is S3-compatible and doesn't
109
+ contain special characters like "://" that are invalid in object keys.
110
+
111
+ Args:
112
+ user_id: The user identifier
113
+
114
+ Returns:
115
+ The object store key
116
+ """
117
+ # Hash the user_id to create an S3-safe key
118
+ user_hash = hashlib.sha256(user_id.encode('utf-8')).hexdigest()
119
+ return f"tokens/{user_hash}"
120
+
121
+ async def store(self, user_id: str, auth_result: AuthResult) -> None:
122
+ """
123
+ Store an authentication result in the object store.
124
+
125
+ Args:
126
+ user_id: The unique identifier for the user
127
+ auth_result: The authentication result to store
128
+ """
129
+ key = self._get_key(user_id)
130
+
131
+ # Serialize the AuthResult to JSON with secrets exposed
132
+ # SecretStr values are masked by default, so we need to expose them manually
133
+ # Create a serializable dict with exposed secrets
134
+ auth_dict = auth_result.model_dump(mode='json')
135
+ # Manually expose SecretStr values in credentials
136
+ for i, cred_obj in enumerate(auth_result.credentials):
137
+ if isinstance(cred_obj, BearerTokenCred):
138
+ auth_dict['credentials'][i]['token'] = cred_obj.token.get_secret_value()
139
+ elif isinstance(cred_obj, BasicAuthCred):
140
+ auth_dict['credentials'][i]['username'] = cred_obj.username.get_secret_value()
141
+ auth_dict['credentials'][i]['password'] = cred_obj.password.get_secret_value()
142
+ elif isinstance(cred_obj, HeaderCred | QueryCred | CookieCred):
143
+ auth_dict['credentials'][i]['value'] = cred_obj.value.get_secret_value()
144
+
145
+ data = json.dumps(auth_dict).encode('utf-8')
146
+
147
+ # Prepare metadata
148
+ metadata = {}
149
+ if auth_result.token_expires_at:
150
+ metadata["expires_at"] = auth_result.token_expires_at.isoformat()
151
+
152
+ # Create the object store item
153
+ item = ObjectStoreItem(data=data, content_type="application/json", metadata=metadata if metadata else None)
154
+
155
+ # Store using upsert to handle both new and existing tokens
156
+ await self._object_store.upsert_object(key, item)
157
+
158
+ async def retrieve(self, user_id: str) -> AuthResult | None:
159
+ """
160
+ Retrieve an authentication result from the object store.
161
+
162
+ Args:
163
+ user_id: The unique identifier for the user
164
+
165
+ Returns:
166
+ The authentication result if found, None otherwise
167
+ """
168
+ key = self._get_key(user_id)
169
+
170
+ try:
171
+ item = await self._object_store.get_object(key)
172
+ # Deserialize the AuthResult from JSON
173
+ auth_result = AuthResult.model_validate_json(item.data)
174
+ return auth_result
175
+ except NoSuchKeyError:
176
+ return None
177
+ except Exception as e:
178
+ logger.error(f"Error deserializing token for user {user_id}: {e}", exc_info=True)
179
+ return None
180
+
181
+ async def delete(self, user_id: str) -> None:
182
+ """
183
+ Delete an authentication result from the object store.
184
+
185
+ Args:
186
+ user_id: The unique identifier for the user
187
+ """
188
+ key = self._get_key(user_id)
189
+
190
+ try:
191
+ await self._object_store.delete_object(key)
192
+ except NoSuchKeyError:
193
+ # Token doesn't exist, which is fine for delete operations
194
+ pass
195
+
196
+ async def clear_all(self) -> None:
197
+ """
198
+ Clear all stored authentication results.
199
+
200
+ Note: This implementation does not support clearing all tokens as the
201
+ object store interface doesn't provide a list operation. Individual
202
+ tokens must be deleted explicitly.
203
+ """
204
+ logger.warning("clear_all() is not supported for ObjectStoreTokenStorage")
205
+
206
+
207
+ class InMemoryTokenStorage(TokenStorageBase):
208
+ """
209
+ In-memory token storage using NeMo Agent toolkit's built-in object store.
210
+
211
+ This implementation uses the in-memory object store for token persistence,
212
+ which provides a secure default option that doesn't require external storage
213
+ configuration. Tokens are stored in memory and cleared when the process exits.
214
+ """
215
+
216
+ def __init__(self):
217
+ """
218
+ Initialize the in-memory token storage.
219
+ """
220
+ from nat.object_store.in_memory_object_store import InMemoryObjectStore
221
+
222
+ # Create a dedicated in-memory object store for tokens
223
+ self._object_store = InMemoryObjectStore()
224
+
225
+ # Wrap with ObjectStoreTokenStorage for the actual implementation
226
+ self._storage = ObjectStoreTokenStorage(self._object_store)
227
+ logger.debug("Initialized in-memory token storage")
228
+
229
+ async def store(self, user_id: str, auth_result: AuthResult) -> None:
230
+ """
231
+ Store an authentication result in memory.
232
+
233
+ Args:
234
+ user_id: The unique identifier for the user
235
+ auth_result: The authentication result to store
236
+ """
237
+ await self._storage.store(user_id, auth_result)
238
+
239
+ async def retrieve(self, user_id: str) -> AuthResult | None:
240
+ """
241
+ Retrieve an authentication result from memory.
242
+
243
+ Args:
244
+ user_id: The unique identifier for the user
245
+
246
+ Returns:
247
+ The authentication result if found, None otherwise
248
+ """
249
+ return await self._storage.retrieve(user_id)
250
+
251
+ async def delete(self, user_id: str) -> None:
252
+ """
253
+ Delete an authentication result from memory.
254
+
255
+ Args:
256
+ user_id: The unique identifier for the user
257
+ """
258
+ await self._storage.delete(user_id)
259
+
260
+ async def clear_all(self) -> None:
261
+ """
262
+ Clear all stored authentication results from memory.
263
+ """
264
+ # For in-memory storage, we can access the internal storage
265
+ self._object_store._store.clear()