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.
@@ -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=5),
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
- # We will increase the timeout to 5 minutes if the tool call timeout is less than 5 min and
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
- return await session.call_tool(tool_name, tool_args, read_timeout_seconds=self._tool_call_timeout)
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=5),
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=5),
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=5),
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._session.call_tool(self._tool_name, tool_args)
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:
@@ -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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nvidia-nat-mcp
3
- Version: 1.3.0a20250930
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.0a20250930
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=UPdQ4vxe4MXVTF05jJx20dn8JPTvQN1ugZ-dups3-44,26418
4
- nat/plugins/mcp/client_impl.py,sha256=sjSFIJeD6LkexN5IbDq_OuoKW52ulpp26LpwaUjOpwg,13142
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.0a20250930.dist-info/METADATA,sha256=CBJk-2cOfsLjvFNIieoKpxaMHYtAeRFBWfzvj1Ti1Ns,1997
16
- nvidia_nat_mcp-1.3.0a20250930.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- nvidia_nat_mcp-1.3.0a20250930.dist-info/entry_points.txt,sha256=rYvUp4i-klBr3bVNh7zYOPXret704vTjvCk1qd7FooI,97
18
- nvidia_nat_mcp-1.3.0a20250930.dist-info/top_level.txt,sha256=8-CJ2cP6-f0ZReXe5Hzqp-5pvzzHz-5Ds5H2bGqh1-U,4
19
- nvidia_nat_mcp-1.3.0a20250930.dist-info/RECORD,,
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,,