nvidia-nat-mcp 1.3.0a20250930__py3-none-any.whl → 1.3.0a20251002__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/plugins/mcp/client_base.py +91 -18
- nat/plugins/mcp/client_impl.py +8 -1
- {nvidia_nat_mcp-1.3.0a20250930.dist-info → nvidia_nat_mcp-1.3.0a20251002.dist-info}/METADATA +2 -2
- {nvidia_nat_mcp-1.3.0a20250930.dist-info → nvidia_nat_mcp-1.3.0a20251002.dist-info}/RECORD +7 -7
- {nvidia_nat_mcp-1.3.0a20250930.dist-info → nvidia_nat_mcp-1.3.0a20251002.dist-info}/WHEEL +0 -0
- {nvidia_nat_mcp-1.3.0a20250930.dist-info → nvidia_nat_mcp-1.3.0a20251002.dist-info}/entry_points.txt +0 -0
- {nvidia_nat_mcp-1.3.0a20250930.dist-info → nvidia_nat_mcp-1.3.0a20251002.dist-info}/top_level.txt +0 -0
nat/plugins/mcp/client_base.py
CHANGED
@@ -59,6 +59,8 @@ class AuthAdapter(httpx.Auth):
|
|
59
59
|
self.auth_provider = auth_provider
|
60
60
|
# each adapter instance has its own lock to avoid unnecessary delays for multiple clients
|
61
61
|
self._lock = anyio.Lock()
|
62
|
+
# Track whether we're currently in an interactive authentication flow
|
63
|
+
self.is_authenticating = False
|
62
64
|
|
63
65
|
async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
|
64
66
|
"""Add authentication headers to the request using NAT auth provider."""
|
@@ -85,11 +87,21 @@ class AuthAdapter(httpx.Auth):
|
|
85
87
|
# 4. The auth headers are revoked
|
86
88
|
# 5. Auth config on the MCP server has changed
|
87
89
|
# In this case we attempt to re-run discovery and authentication
|
90
|
+
|
91
|
+
# Signal that we're entering interactive auth flow
|
92
|
+
self.is_authenticating = True
|
93
|
+
logger.debug("Starting authentication flow due to 401 response")
|
94
|
+
|
88
95
|
auth_headers = await self._get_auth_headers(request=request, response=response)
|
89
96
|
request.headers.update(auth_headers)
|
90
97
|
yield request # Retry the request
|
91
98
|
except Exception as e:
|
92
99
|
logger.info("Failed to refresh auth after 401: %s", e)
|
100
|
+
raise
|
101
|
+
finally:
|
102
|
+
# Signal that auth flow is complete
|
103
|
+
self.is_authenticating = False
|
104
|
+
logger.debug("Authentication flow completed")
|
93
105
|
return
|
94
106
|
|
95
107
|
def _get_session_id_from_tool_call_request(self, request: httpx.Request) -> tuple[str | None, bool]:
|
@@ -148,12 +160,19 @@ class MCPBaseClient(ABC):
|
|
148
160
|
Args:
|
149
161
|
transport (str): The type of client to use ('sse', 'stdio', or 'streamable-http')
|
150
162
|
auth_provider (AuthProviderBase | None): Optional authentication provider for Bearer token injection
|
163
|
+
tool_call_timeout (timedelta): Timeout for tool calls when authentication is not required
|
164
|
+
auth_flow_timeout (timedelta): Extended timeout for tool calls that may require interactive authentication
|
165
|
+
reconnect_enabled (bool): Whether to automatically reconnect on connection failures
|
166
|
+
reconnect_max_attempts (int): Maximum number of reconnection attempts
|
167
|
+
reconnect_initial_backoff (float): Initial backoff delay in seconds for reconnection attempts
|
168
|
+
reconnect_max_backoff (float): Maximum backoff delay in seconds for reconnection attempts
|
151
169
|
"""
|
152
170
|
|
153
171
|
def __init__(self,
|
154
172
|
transport: str = 'streamable-http',
|
155
173
|
auth_provider: AuthProviderBase | None = None,
|
156
|
-
tool_call_timeout: timedelta = timedelta(seconds=
|
174
|
+
tool_call_timeout: timedelta = timedelta(seconds=60),
|
175
|
+
auth_flow_timeout: timedelta = timedelta(seconds=300),
|
157
176
|
reconnect_enabled: bool = True,
|
158
177
|
reconnect_max_attempts: int = 2,
|
159
178
|
reconnect_initial_backoff: float = 0.5,
|
@@ -173,6 +192,7 @@ class MCPBaseClient(ABC):
|
|
173
192
|
self._httpx_auth = AuthAdapter(auth_provider) if auth_provider else None
|
174
193
|
|
175
194
|
self._tool_call_timeout = tool_call_timeout
|
195
|
+
self._auth_flow_timeout = auth_flow_timeout
|
176
196
|
|
177
197
|
# Reconnect configuration
|
178
198
|
self._reconnect_enabled = reconnect_enabled
|
@@ -267,12 +287,25 @@ class MCPBaseClient(ABC):
|
|
267
287
|
async def _with_reconnect(self, coro):
|
268
288
|
"""
|
269
289
|
Execute an awaited operation, reconnecting once on errors.
|
290
|
+
Does not reconnect if the error occurs during an active authentication flow.
|
270
291
|
"""
|
271
292
|
try:
|
272
293
|
return await coro()
|
273
294
|
except Exception as e:
|
295
|
+
# Check if error happened during active authentication flow
|
296
|
+
if self._httpx_auth and self._httpx_auth.is_authenticating:
|
297
|
+
# Provide specific error message for authentication timeouts
|
298
|
+
if isinstance(e, TimeoutError):
|
299
|
+
logger.error("Timeout during user authentication flow - user may have abandoned authentication")
|
300
|
+
raise RuntimeError(
|
301
|
+
"Authentication timed out. User did not complete authentication in browser within "
|
302
|
+
f"{self._auth_flow_timeout.total_seconds()} seconds.") from e
|
303
|
+
else:
|
304
|
+
logger.error("Error during authentication flow: %s", e)
|
305
|
+
raise
|
306
|
+
|
307
|
+
# Normal error - attempt reconnect if enabled
|
274
308
|
if self._reconnect_enabled:
|
275
|
-
logger.warning("MCP Client operation failed. Attempting reconnect: %s", e)
|
276
309
|
try:
|
277
310
|
await self._reconnect()
|
278
311
|
except Exception as reconnect_err:
|
@@ -281,6 +314,47 @@ class MCPBaseClient(ABC):
|
|
281
314
|
return await coro()
|
282
315
|
raise
|
283
316
|
|
317
|
+
async def _has_cached_auth_token(self) -> bool:
|
318
|
+
"""
|
319
|
+
Check if we have a cached, non-expired authentication token.
|
320
|
+
|
321
|
+
Returns:
|
322
|
+
bool: True if we have a valid cached token, False if authentication may be needed
|
323
|
+
"""
|
324
|
+
if not self._auth_provider:
|
325
|
+
return True # No auth needed
|
326
|
+
|
327
|
+
try:
|
328
|
+
# Check if OAuth2 provider has tokens cached
|
329
|
+
if hasattr(self._auth_provider, '_auth_code_provider'):
|
330
|
+
provider = self._auth_provider._auth_code_provider
|
331
|
+
if provider and hasattr(provider, '_authenticated_tokens'):
|
332
|
+
# Check if we have at least one non-expired token
|
333
|
+
for auth_result in provider._authenticated_tokens.values():
|
334
|
+
if not auth_result.is_expired():
|
335
|
+
return True
|
336
|
+
|
337
|
+
return False
|
338
|
+
except Exception:
|
339
|
+
# If we can't check, assume we need auth to be safe
|
340
|
+
return False
|
341
|
+
|
342
|
+
async def _get_tool_call_timeout(self) -> timedelta:
|
343
|
+
"""
|
344
|
+
Determine the appropriate timeout for a tool call based on authentication state.
|
345
|
+
|
346
|
+
Returns:
|
347
|
+
timedelta: auth_flow_timeout if authentication may be needed, tool_call_timeout otherwise
|
348
|
+
"""
|
349
|
+
if self._auth_provider:
|
350
|
+
has_token = await self._has_cached_auth_token()
|
351
|
+
timeout = self._tool_call_timeout if has_token else self._auth_flow_timeout
|
352
|
+
if not has_token:
|
353
|
+
logger.debug("Using extended timeout (%s) for potential interactive authentication", timeout)
|
354
|
+
return timeout
|
355
|
+
else:
|
356
|
+
return self._tool_call_timeout
|
357
|
+
|
284
358
|
@mcp_exception_handler
|
285
359
|
async def get_tools(self) -> dict[str, MCPToolClient]:
|
286
360
|
"""
|
@@ -313,8 +387,7 @@ class MCPBaseClient(ABC):
|
|
313
387
|
tool_name=tool.name,
|
314
388
|
tool_description=tool.description,
|
315
389
|
tool_input_schema=tool.inputSchema,
|
316
|
-
parent_client=self
|
317
|
-
tool_call_timeout=self._tool_call_timeout)
|
390
|
+
parent_client=self)
|
318
391
|
for tool in response.tools
|
319
392
|
}
|
320
393
|
|
@@ -361,12 +434,7 @@ class MCPBaseClient(ABC):
|
|
361
434
|
async def _call_tool_with_meta():
|
362
435
|
params = CallToolRequestParams(name=tool_name, arguments=args, **{"_meta": {"session_id": session_id}})
|
363
436
|
req = ClientRequest(CallToolRequest(params=params))
|
364
|
-
|
365
|
-
# auth is enabled.
|
366
|
-
if self._auth_provider and self._tool_call_timeout.total_seconds() < 300:
|
367
|
-
timeout = timedelta(seconds=300)
|
368
|
-
else:
|
369
|
-
timeout = self._tool_call_timeout
|
437
|
+
timeout = await self._get_tool_call_timeout()
|
370
438
|
return await self._session.send_request(req, CallToolResult, request_read_timeout_seconds=timeout)
|
371
439
|
|
372
440
|
return await self._with_reconnect(_call_tool_with_meta)
|
@@ -376,7 +444,8 @@ class MCPBaseClient(ABC):
|
|
376
444
|
|
377
445
|
async def _call_tool():
|
378
446
|
session = self._session
|
379
|
-
|
447
|
+
timeout = await self._get_tool_call_timeout()
|
448
|
+
return await session.call_tool(tool_name, tool_args, read_timeout_seconds=timeout)
|
380
449
|
|
381
450
|
return await self._with_reconnect(_call_tool)
|
382
451
|
|
@@ -391,13 +460,15 @@ class MCPSSEClient(MCPBaseClient):
|
|
391
460
|
|
392
461
|
def __init__(self,
|
393
462
|
url: str,
|
394
|
-
tool_call_timeout: timedelta = timedelta(seconds=
|
463
|
+
tool_call_timeout: timedelta = timedelta(seconds=60),
|
464
|
+
auth_flow_timeout: timedelta = timedelta(seconds=300),
|
395
465
|
reconnect_enabled: bool = True,
|
396
466
|
reconnect_max_attempts: int = 2,
|
397
467
|
reconnect_initial_backoff: float = 0.5,
|
398
468
|
reconnect_max_backoff: float = 50.0):
|
399
469
|
super().__init__("sse",
|
400
470
|
tool_call_timeout=tool_call_timeout,
|
471
|
+
auth_flow_timeout=auth_flow_timeout,
|
401
472
|
reconnect_enabled=reconnect_enabled,
|
402
473
|
reconnect_max_attempts=reconnect_max_attempts,
|
403
474
|
reconnect_initial_backoff=reconnect_initial_backoff,
|
@@ -440,13 +511,15 @@ class MCPStdioClient(MCPBaseClient):
|
|
440
511
|
command: str,
|
441
512
|
args: list[str] | None = None,
|
442
513
|
env: dict[str, str] | None = None,
|
443
|
-
tool_call_timeout: timedelta = timedelta(seconds=
|
514
|
+
tool_call_timeout: timedelta = timedelta(seconds=60),
|
515
|
+
auth_flow_timeout: timedelta = timedelta(seconds=300),
|
444
516
|
reconnect_enabled: bool = True,
|
445
517
|
reconnect_max_attempts: int = 2,
|
446
518
|
reconnect_initial_backoff: float = 0.5,
|
447
519
|
reconnect_max_backoff: float = 50.0):
|
448
520
|
super().__init__("stdio",
|
449
521
|
tool_call_timeout=tool_call_timeout,
|
522
|
+
auth_flow_timeout=auth_flow_timeout,
|
450
523
|
reconnect_enabled=reconnect_enabled,
|
451
524
|
reconnect_max_attempts=reconnect_max_attempts,
|
452
525
|
reconnect_initial_backoff=reconnect_initial_backoff,
|
@@ -497,7 +570,8 @@ class MCPStreamableHTTPClient(MCPBaseClient):
|
|
497
570
|
def __init__(self,
|
498
571
|
url: str,
|
499
572
|
auth_provider: AuthProviderBase | None = None,
|
500
|
-
tool_call_timeout: timedelta = timedelta(seconds=
|
573
|
+
tool_call_timeout: timedelta = timedelta(seconds=60),
|
574
|
+
auth_flow_timeout: timedelta = timedelta(seconds=300),
|
501
575
|
reconnect_enabled: bool = True,
|
502
576
|
reconnect_max_attempts: int = 2,
|
503
577
|
reconnect_initial_backoff: float = 0.5,
|
@@ -505,6 +579,7 @@ class MCPStreamableHTTPClient(MCPBaseClient):
|
|
505
579
|
super().__init__("streamable-http",
|
506
580
|
auth_provider=auth_provider,
|
507
581
|
tool_call_timeout=tool_call_timeout,
|
582
|
+
auth_flow_timeout=auth_flow_timeout,
|
508
583
|
reconnect_enabled=reconnect_enabled,
|
509
584
|
reconnect_max_attempts=reconnect_max_attempts,
|
510
585
|
reconnect_initial_backoff=reconnect_initial_backoff,
|
@@ -550,14 +625,12 @@ class MCPToolClient:
|
|
550
625
|
parent_client: MCPBaseClient,
|
551
626
|
tool_name: str,
|
552
627
|
tool_description: str | None,
|
553
|
-
tool_input_schema: dict | None = None
|
554
|
-
tool_call_timeout: timedelta = timedelta(seconds=5)):
|
628
|
+
tool_input_schema: dict | None = None):
|
555
629
|
self._session = session
|
556
630
|
self._tool_name = tool_name
|
557
631
|
self._tool_description = tool_description
|
558
632
|
self._input_schema = (model_from_mcp_schema(self._tool_name, tool_input_schema) if tool_input_schema else None)
|
559
633
|
self._parent_client = parent_client
|
560
|
-
self._tool_call_timeout = tool_call_timeout
|
561
634
|
|
562
635
|
if self._parent_client is None:
|
563
636
|
raise RuntimeError("MCPToolClient initialized without a parent client.")
|
@@ -643,7 +716,7 @@ class MCPToolClient:
|
|
643
716
|
result = await self._parent_client.call_tool_with_meta(self._tool_name, tool_args, session_id)
|
644
717
|
else:
|
645
718
|
logger.info("Calling tool %s with arguments %s", self._tool_name, tool_args)
|
646
|
-
result = await self.
|
719
|
+
result = await self._parent_client.call_tool(self._tool_name, tool_args)
|
647
720
|
|
648
721
|
output = []
|
649
722
|
for res in result.content:
|
nat/plugins/mcp/client_impl.py
CHANGED
@@ -139,6 +139,10 @@ class MCPClientConfig(FunctionGroupBaseConfig, name="mcp_client"):
|
|
139
139
|
tool_call_timeout: timedelta = Field(
|
140
140
|
default=timedelta(seconds=60),
|
141
141
|
description="Timeout (in seconds) for the MCP tool call. Defaults to 60 seconds.")
|
142
|
+
auth_flow_timeout: timedelta = Field(
|
143
|
+
default=timedelta(seconds=300),
|
144
|
+
description="Timeout (in seconds) for the MCP auth flow. When the tool call requires interactive \
|
145
|
+
authentication, this timeout is used. Defaults to 300 seconds.")
|
142
146
|
reconnect_enabled: bool = Field(
|
143
147
|
default=True,
|
144
148
|
description="Whether to enable reconnecting to the MCP server if the connection is lost. \
|
@@ -196,7 +200,8 @@ async def mcp_client_function_group(config: MCPClientConfig, _builder: Builder):
|
|
196
200
|
client = MCPStdioClient(config.server.command,
|
197
201
|
config.server.args,
|
198
202
|
config.server.env,
|
199
|
-
config.tool_call_timeout,
|
203
|
+
tool_call_timeout=config.tool_call_timeout,
|
204
|
+
auth_flow_timeout=config.auth_flow_timeout,
|
200
205
|
reconnect_enabled=config.reconnect_enabled,
|
201
206
|
reconnect_max_attempts=config.reconnect_max_attempts,
|
202
207
|
reconnect_initial_backoff=config.reconnect_initial_backoff,
|
@@ -204,6 +209,7 @@ async def mcp_client_function_group(config: MCPClientConfig, _builder: Builder):
|
|
204
209
|
elif config.server.transport == "sse":
|
205
210
|
client = MCPSSEClient(str(config.server.url),
|
206
211
|
tool_call_timeout=config.tool_call_timeout,
|
212
|
+
auth_flow_timeout=config.auth_flow_timeout,
|
207
213
|
reconnect_enabled=config.reconnect_enabled,
|
208
214
|
reconnect_max_attempts=config.reconnect_max_attempts,
|
209
215
|
reconnect_initial_backoff=config.reconnect_initial_backoff,
|
@@ -212,6 +218,7 @@ async def mcp_client_function_group(config: MCPClientConfig, _builder: Builder):
|
|
212
218
|
client = MCPStreamableHTTPClient(str(config.server.url),
|
213
219
|
auth_provider=auth_provider,
|
214
220
|
tool_call_timeout=config.tool_call_timeout,
|
221
|
+
auth_flow_timeout=config.auth_flow_timeout,
|
215
222
|
reconnect_enabled=config.reconnect_enabled,
|
216
223
|
reconnect_max_attempts=config.reconnect_max_attempts,
|
217
224
|
reconnect_initial_backoff=config.reconnect_initial_backoff,
|
{nvidia_nat_mcp-1.3.0a20250930.dist-info → nvidia_nat_mcp-1.3.0a20251002.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: nvidia-nat-mcp
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.0a20251002
|
4
4
|
Summary: Subpackage for MCP client integration in NeMo Agent toolkit
|
5
5
|
Keywords: ai,rag,agents,mcp
|
6
6
|
Classifier: Programming Language :: Python
|
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.13
|
10
10
|
Requires-Python: <3.14,>=3.11
|
11
11
|
Description-Content-Type: text/markdown
|
12
|
-
Requires-Dist: nvidia-nat==v1.3.
|
12
|
+
Requires-Dist: nvidia-nat==v1.3.0a20251002
|
13
13
|
Requires-Dist: mcp~=1.14
|
14
14
|
|
15
15
|
<!--
|
@@ -1,7 +1,7 @@
|
|
1
1
|
nat/meta/pypi.md,sha256=GyV4DI1d9ThgEhnYTQ0vh40Q9hPC8jN-goLnRiFDmZ8,1498
|
2
2
|
nat/plugins/mcp/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv-tZzbU9W-izgx9aMEQg,680
|
3
|
-
nat/plugins/mcp/client_base.py,sha256=
|
4
|
-
nat/plugins/mcp/client_impl.py,sha256=
|
3
|
+
nat/plugins/mcp/client_base.py,sha256=x_mgrCORXqYfJcYZg8zd0wm1AN1uFl51A7bYye0X-rc,30151
|
4
|
+
nat/plugins/mcp/client_impl.py,sha256=FGWlpzyBDR2tNkV9Ek7brSX9EWh98kfAGCr49zMzDSU,13657
|
5
5
|
nat/plugins/mcp/exception_handler.py,sha256=4JVdZDJL4LyumZEcMIEBK2LYC6djuSMzqUhQDZZ6dUo,7648
|
6
6
|
nat/plugins/mcp/exceptions.py,sha256=EGVOnYlui8xufm8dhJyPL1SUqBLnCGOTvRoeyNcmcWE,5980
|
7
7
|
nat/plugins/mcp/register.py,sha256=HOT2Wl2isGuyFc7BUTi58-BbjI5-EtZMZo7stsv5pN4,831
|
@@ -12,8 +12,8 @@ nat/plugins/mcp/auth/auth_flow_handler.py,sha256=2JgK0aH-5ouQCd2ov0lDMJAD5ZWIQJ7
|
|
12
12
|
nat/plugins/mcp/auth/auth_provider.py,sha256=OfxPCEaXuhP8anOdrTRH-_E78CrbJtzW6i81_kebpDk,19321
|
13
13
|
nat/plugins/mcp/auth/auth_provider_config.py,sha256=vhU47Vcp_30M8tWu0FumbJ6pdUnFbBZm-ABdNlup__U,3821
|
14
14
|
nat/plugins/mcp/auth/register.py,sha256=yzphsn1I4a5G39_IacbuX0ZQqGM8fevvTUM_B94UXKE,1211
|
15
|
-
nvidia_nat_mcp-1.3.
|
16
|
-
nvidia_nat_mcp-1.3.
|
17
|
-
nvidia_nat_mcp-1.3.
|
18
|
-
nvidia_nat_mcp-1.3.
|
19
|
-
nvidia_nat_mcp-1.3.
|
15
|
+
nvidia_nat_mcp-1.3.0a20251002.dist-info/METADATA,sha256=IxP-J_3VJWx9SXiCSgQOsrBdagj6i24bx_XK8KZeI-0,1997
|
16
|
+
nvidia_nat_mcp-1.3.0a20251002.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
17
|
+
nvidia_nat_mcp-1.3.0a20251002.dist-info/entry_points.txt,sha256=rYvUp4i-klBr3bVNh7zYOPXret704vTjvCk1qd7FooI,97
|
18
|
+
nvidia_nat_mcp-1.3.0a20251002.dist-info/top_level.txt,sha256=8-CJ2cP6-f0ZReXe5Hzqp-5pvzzHz-5Ds5H2bGqh1-U,4
|
19
|
+
nvidia_nat_mcp-1.3.0a20251002.dist-info/RECORD,,
|
File without changes
|
{nvidia_nat_mcp-1.3.0a20250930.dist-info → nvidia_nat_mcp-1.3.0a20251002.dist-info}/entry_points.txt
RENAMED
File without changes
|
{nvidia_nat_mcp-1.3.0a20250930.dist-info → nvidia_nat_mcp-1.3.0a20251002.dist-info}/top_level.txt
RENAMED
File without changes
|