nvidia-nat-mcp 1.3.0a20250929__py3-none-any.whl → 1.3.0rc1__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.
@@ -127,7 +127,7 @@ class MCPAuthenticationFlowHandler(ConsoleAuthenticationFlowHandler):
127
127
  try:
128
128
  token = await asyncio.wait_for(flow_state.future, timeout=timeout)
129
129
  logger.info("MCP authentication successful, token obtained")
130
- except asyncio.TimeoutError as exc:
130
+ except TimeoutError as exc:
131
131
  logger.error("MCP authentication timed out")
132
132
  raise RuntimeError(f"MCP authentication timed out ({timeout} seconds). Please try again.") from exc
133
133
  finally:
@@ -15,7 +15,7 @@
15
15
 
16
16
  import logging
17
17
  from collections.abc import Awaitable
18
- from typing import Callable
18
+ from collections.abc import Callable
19
19
  from urllib.parse import urljoin
20
20
  from urllib.parse import urlparse
21
21
 
@@ -321,6 +321,10 @@ class MCPOAuth2Provider(AuthProviderBase[MCPOAuth2ProviderConfig]):
321
321
 
322
322
  Otherwise, performs standard authentication flow.
323
323
  """
324
+ if not user_id:
325
+ # MCP tool calls cannot be made without an authorized user
326
+ raise RuntimeError("User is not authorized to call the tool")
327
+
324
328
  response = kwargs.get('response')
325
329
  if response and response.status_code == 401:
326
330
  await self._discover_and_register(response=response)
@@ -120,11 +120,8 @@ class AuthAdapter(httpx.Auth):
120
120
  session_id, is_tool_call = self._get_session_id_from_tool_call_request(request)
121
121
 
122
122
  if is_tool_call:
123
- # Tool call requests should use the session id if it exists, default user id can be used if allowed
124
- if self.auth_provider.config.allow_default_user_id_for_tool_calls:
125
- user_id = session_id or self.auth_provider.config.default_user_id
126
- else:
127
- user_id = session_id
123
+ # Tool call requests should use the session id
124
+ user_id = session_id
128
125
  else:
129
126
  # Non-tool call requests should use the session id if it exists and fallback to default user id
130
127
  user_id = session_id or self.auth_provider.config.default_user_id
@@ -184,6 +181,10 @@ class MCPBaseClient(ABC):
184
181
  self._reconnect_max_backoff = reconnect_max_backoff
185
182
  self._reconnect_lock: asyncio.Lock = asyncio.Lock()
186
183
 
184
+ @property
185
+ def auth_provider(self) -> AuthProviderBase | None:
186
+ return self._auth_provider
187
+
187
188
  @property
188
189
  def transport(self) -> str:
189
190
  return self._transport
@@ -280,7 +281,8 @@ class MCPBaseClient(ABC):
280
281
  return await coro()
281
282
  raise
282
283
 
283
- async def get_tools(self):
284
+ @mcp_exception_handler
285
+ async def get_tools(self) -> dict[str, MCPToolClient]:
284
286
  """
285
287
  Retrieve a dictionary of all tools served by the MCP server.
286
288
  Uses unauthenticated session for discovery.
@@ -288,7 +290,16 @@ class MCPBaseClient(ABC):
288
290
 
289
291
  async def _get_tools():
290
292
  session = self._session
291
- return await session.list_tools()
293
+ try:
294
+ # Add timeout to the list_tools call.
295
+ # This is needed because MCP SDK does not support timeout for list_tools()
296
+ with anyio.fail_after(self._tool_call_timeout.total_seconds()):
297
+ tools = await session.list_tools()
298
+ except TimeoutError as e:
299
+ from nat.plugins.mcp.exceptions import MCPTimeoutError
300
+ raise MCPTimeoutError(self.server_name, e)
301
+
302
+ return tools
292
303
 
293
304
  try:
294
305
  response = await self._with_reconnect(_get_tools)
@@ -536,7 +547,7 @@ class MCPToolClient:
536
547
 
537
548
  def __init__(self,
538
549
  session: ClientSession,
539
- parent_client: "MCPBaseClient",
550
+ parent_client: MCPBaseClient,
540
551
  tool_name: str,
541
552
  tool_description: str | None,
542
553
  tool_input_schema: dict | None = None,
@@ -578,6 +589,32 @@ class MCPToolClient:
578
589
  """
579
590
  self._tool_description = description
580
591
 
592
+ def _get_session_id(self) -> str | None:
593
+ """
594
+ Get the session id from the context.
595
+ """
596
+ from nat.builder.context import Context as _Ctx
597
+
598
+ # get auth callback (for example: WebSocketAuthenticationFlowHandler). this is lazily set in the client
599
+ # on first tool call
600
+ auth_callback = _Ctx.get().user_auth_callback
601
+ if auth_callback and self._parent_client:
602
+ # set custom auth callback
603
+ self._parent_client.set_user_auth_callback(auth_callback)
604
+
605
+ # get session id from context, authentication is done per-websocket session for tool calls
606
+ session_id = None
607
+ cookies = getattr(_Ctx.get().metadata, "cookies", None)
608
+ if cookies:
609
+ session_id = cookies.get("nat-session")
610
+
611
+ if not session_id:
612
+ # use default user id if allowed
613
+ if self._parent_client.auth_provider and \
614
+ self._parent_client.auth_provider.config.allow_default_user_id_for_tool_calls:
615
+ session_id = self._parent_client.auth_provider.config.default_user_id
616
+ return session_id
617
+
581
618
  async def acall(self, tool_args: dict) -> str:
582
619
  """
583
620
  Call the MCP tool with the provided arguments.
@@ -589,25 +626,18 @@ class MCPToolClient:
589
626
  raise RuntimeError("No session available for tool call")
590
627
 
591
628
  # Extract context information
592
- session_id = None
593
629
  try:
594
- from nat.builder.context import Context as _Ctx
595
-
596
- # get auth callback (for example: WebSocketAuthenticationFlowHandler). this is lazily set in the client
597
- # on first tool call
598
- auth_callback = _Ctx.get().user_auth_callback
599
- if auth_callback and self._parent_client:
600
- # set custom auth callback
601
- self._parent_client.set_user_auth_callback(auth_callback)
602
-
603
- # get session id from context, authentication is done per-websocket session for tool calls
604
- cookies = getattr(_Ctx.get().metadata, "cookies", None)
605
- if cookies:
606
- session_id = cookies.get("nat-session")
630
+ session_id = self._get_session_id()
607
631
  except Exception:
608
- pass
632
+ session_id = None
609
633
 
610
634
  try:
635
+ # if auth is enabled and session id is not available return user is not authorized to call the tool
636
+ if self._parent_client.auth_provider and not session_id:
637
+ result_str = "User is not authorized to call the tool"
638
+ mcp_error: MCPError = convert_to_mcp_error(RuntimeError(result_str), self._parent_client.server_name)
639
+ raise mcp_error
640
+
611
641
  if session_id:
612
642
  logger.info("Calling tool %s with arguments %s for a user session", self._tool_name, tool_args)
613
643
  result = await self._parent_client.call_tool_with_meta(self._tool_name, tool_args, session_id)
@@ -630,6 +660,6 @@ class MCPToolClient:
630
660
 
631
661
  except MCPError as e:
632
662
  format_mcp_error(e, include_traceback=False)
633
- result_str = "MCPToolClient tool call failed: %s" % e.original_exception
663
+ result_str = f"MCPToolClient tool call failed: {e.original_exception}"
634
664
 
635
665
  return result_str
@@ -32,6 +32,50 @@ from nat.plugins.mcp.tool import mcp_tool_function
32
32
  logger = logging.getLogger(__name__)
33
33
 
34
34
 
35
+ class MCPFunctionGroup(FunctionGroup):
36
+ """
37
+ A specialized FunctionGroup for MCP clients that includes MCP-specific attributes
38
+ with proper type safety.
39
+ """
40
+
41
+ def __init__(self, *args, **kwargs):
42
+ super().__init__(*args, **kwargs)
43
+ # MCP client attributes with proper typing
44
+ self._mcp_client = None # Will be set to the actual MCP client instance
45
+ self._mcp_client_server_name: str | None = None
46
+ self._mcp_client_transport: str | None = None
47
+
48
+ @property
49
+ def mcp_client(self):
50
+ """Get the MCP client instance."""
51
+ return self._mcp_client
52
+
53
+ @mcp_client.setter
54
+ def mcp_client(self, client):
55
+ """Set the MCP client instance."""
56
+ self._mcp_client = client
57
+
58
+ @property
59
+ def mcp_client_server_name(self) -> str | None:
60
+ """Get the MCP client server name."""
61
+ return self._mcp_client_server_name
62
+
63
+ @mcp_client_server_name.setter
64
+ def mcp_client_server_name(self, server_name: str | None):
65
+ """Set the MCP client server name."""
66
+ self._mcp_client_server_name = server_name
67
+
68
+ @property
69
+ def mcp_client_transport(self) -> str | None:
70
+ """Get the MCP client transport type."""
71
+ return self._mcp_client_transport
72
+
73
+ @mcp_client_transport.setter
74
+ def mcp_client_transport(self, transport: str | None):
75
+ """Set the MCP client transport type."""
76
+ self._mcp_client_transport = transport
77
+
78
+
35
79
  class MCPToolOverrideConfig(BaseModel):
36
80
  """
37
81
  Configuration for overriding tool properties when exposing from MCP server.
@@ -177,10 +221,16 @@ async def mcp_client_function_group(config: MCPClientConfig, _builder: Builder):
177
221
 
178
222
  logger.info("Configured to use MCP server at %s", client.server_name)
179
223
 
180
- # Create the function group
181
- group = FunctionGroup(config=config)
224
+ # Create the MCP function group
225
+ group = MCPFunctionGroup(config=config)
182
226
 
183
227
  async with client:
228
+ # Expose the live MCP client on the function group instance so other components (e.g., HTTP endpoints)
229
+ # can reuse the already-established session instead of creating a new client per request.
230
+ group.mcp_client = client
231
+ group.mcp_client_server_name = client.server_name
232
+ group.mcp_client_transport = client.transport
233
+
184
234
  all_tools = await client.get_tools()
185
235
  tool_overrides = mcp_apply_tool_alias_and_description(all_tools, config.tool_overrides)
186
236
 
@@ -196,12 +246,24 @@ async def mcp_client_function_group(config: MCPClientConfig, _builder: Builder):
196
246
  # Create the tool function
197
247
  tool_fn = mcp_tool_function(tool)
198
248
 
249
+ # Normalize optional typing for linter/type-checker compatibility
250
+ single_fn = tool_fn.single_fn
251
+ if single_fn is None:
252
+ # Should not happen because mcp_tool_function always sets a single_fn
253
+ logger.warning("Skipping tool %s because single_fn is None", function_name)
254
+ continue
255
+
256
+ input_schema = tool_fn.input_schema
257
+ # Convert NoneType sentinel to None for FunctionGroup.add_function signature
258
+ if input_schema is type(None): # noqa: E721
259
+ input_schema = None
260
+
199
261
  # Add to group
200
262
  logger.info("Adding tool %s to group", function_name)
201
263
  group.add_function(name=function_name,
202
264
  description=description,
203
- fn=tool_fn.single_fn,
204
- input_schema=tool_fn.input_schema,
265
+ fn=single_fn,
266
+ input_schema=input_schema,
205
267
  converters=tool_fn.converters)
206
268
 
207
269
  yield group
@@ -94,7 +94,7 @@ def extract_primary_exception(exceptions: list[Exception]) -> Exception:
94
94
  """
95
95
  # Prioritize connection errors
96
96
  for exc in exceptions:
97
- if isinstance(exc, (httpx.ConnectError, ConnectionError)):
97
+ if isinstance(exc, httpx.ConnectError | ConnectionError):
98
98
  return exc
99
99
 
100
100
  # Then timeout errors
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nvidia-nat-mcp
3
- Version: 1.3.0a20250929
3
+ Version: 1.3.0rc1
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.0a20250929
12
+ Requires-Dist: nvidia-nat==v1.3.0-rc1
13
13
  Requires-Dist: mcp~=1.14
14
14
 
15
15
  <!--
@@ -0,0 +1,19 @@
1
+ nat/meta/pypi.md,sha256=GyV4DI1d9ThgEhnYTQ0vh40Q9hPC8jN-goLnRiFDmZ8,1498
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
5
+ nat/plugins/mcp/exception_handler.py,sha256=4JVdZDJL4LyumZEcMIEBK2LYC6djuSMzqUhQDZZ6dUo,7648
6
+ nat/plugins/mcp/exceptions.py,sha256=EGVOnYlui8xufm8dhJyPL1SUqBLnCGOTvRoeyNcmcWE,5980
7
+ nat/plugins/mcp/register.py,sha256=HOT2Wl2isGuyFc7BUTi58-BbjI5-EtZMZo7stsv5pN4,831
8
+ nat/plugins/mcp/tool.py,sha256=v3MFsiaLJy8Ourcfqa6ohtAE2Nn-vqpC6Q6gsCdJ28Q,6165
9
+ nat/plugins/mcp/utils.py,sha256=3fuzYpC14wrfMOTOGvY2KHWcxZvBWqrxdDZD17lhmC8,4055
10
+ nat/plugins/mcp/auth/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv-tZzbU9W-izgx9aMEQg,680
11
+ nat/plugins/mcp/auth/auth_flow_handler.py,sha256=2JgK0aH-5ouQCd2ov0lDMJAD5ZWIQJ7SVcXaLArxn6Y,6010
12
+ nat/plugins/mcp/auth/auth_provider.py,sha256=OfxPCEaXuhP8anOdrTRH-_E78CrbJtzW6i81_kebpDk,19321
13
+ nat/plugins/mcp/auth/auth_provider_config.py,sha256=vhU47Vcp_30M8tWu0FumbJ6pdUnFbBZm-ABdNlup__U,3821
14
+ nat/plugins/mcp/auth/register.py,sha256=yzphsn1I4a5G39_IacbuX0ZQqGM8fevvTUM_B94UXKE,1211
15
+ nvidia_nat_mcp-1.3.0rc1.dist-info/METADATA,sha256=NdfOVgW-bDqAZWInj84TQgDjH05tlBwEYO-mZdlwklM,1986
16
+ nvidia_nat_mcp-1.3.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ nvidia_nat_mcp-1.3.0rc1.dist-info/entry_points.txt,sha256=rYvUp4i-klBr3bVNh7zYOPXret704vTjvCk1qd7FooI,97
18
+ nvidia_nat_mcp-1.3.0rc1.dist-info/top_level.txt,sha256=8-CJ2cP6-f0ZReXe5Hzqp-5pvzzHz-5Ds5H2bGqh1-U,4
19
+ nvidia_nat_mcp-1.3.0rc1.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- nat/meta/pypi.md,sha256=GyV4DI1d9ThgEhnYTQ0vh40Q9hPC8jN-goLnRiFDmZ8,1498
2
- nat/plugins/mcp/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv-tZzbU9W-izgx9aMEQg,680
3
- nat/plugins/mcp/client_base.py,sha256=x6NHs3X2alyB6Wo4pjWZrGyuU7lLqGC_cLO9fuL4Zgw,25194
4
- nat/plugins/mcp/client_impl.py,sha256=M1gTMlp3RLhFaAHOvwkk38boFy05MixV_glrIEcMjvo,10759
5
- nat/plugins/mcp/exception_handler.py,sha256=JdPdZG1NgWpdRnIz7JTGHiJASS5wot9nJiD3SRWV4Kw,7649
6
- nat/plugins/mcp/exceptions.py,sha256=EGVOnYlui8xufm8dhJyPL1SUqBLnCGOTvRoeyNcmcWE,5980
7
- nat/plugins/mcp/register.py,sha256=HOT2Wl2isGuyFc7BUTi58-BbjI5-EtZMZo7stsv5pN4,831
8
- nat/plugins/mcp/tool.py,sha256=v3MFsiaLJy8Ourcfqa6ohtAE2Nn-vqpC6Q6gsCdJ28Q,6165
9
- nat/plugins/mcp/utils.py,sha256=3fuzYpC14wrfMOTOGvY2KHWcxZvBWqrxdDZD17lhmC8,4055
10
- nat/plugins/mcp/auth/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv-tZzbU9W-izgx9aMEQg,680
11
- nat/plugins/mcp/auth/auth_flow_handler.py,sha256=eRqRS2t-YzJ3kEFaG0PEC8DzctYzaJzr9XLZGkvuxq0,6018
12
- nat/plugins/mcp/auth/auth_provider.py,sha256=5TTaPlIXMgwrE4YcZ_HO9-GNBBFaDTdUKAa5fuALayI,19142
13
- nat/plugins/mcp/auth/auth_provider_config.py,sha256=vhU47Vcp_30M8tWu0FumbJ6pdUnFbBZm-ABdNlup__U,3821
14
- nat/plugins/mcp/auth/register.py,sha256=yzphsn1I4a5G39_IacbuX0ZQqGM8fevvTUM_B94UXKE,1211
15
- nvidia_nat_mcp-1.3.0a20250929.dist-info/METADATA,sha256=HceYMMAdbXe5IBiiXJIyrd5IdEUcWIHgjJtiW53BT4I,1997
16
- nvidia_nat_mcp-1.3.0a20250929.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- nvidia_nat_mcp-1.3.0a20250929.dist-info/entry_points.txt,sha256=rYvUp4i-klBr3bVNh7zYOPXret704vTjvCk1qd7FooI,97
18
- nvidia_nat_mcp-1.3.0a20250929.dist-info/top_level.txt,sha256=8-CJ2cP6-f0ZReXe5Hzqp-5pvzzHz-5Ds5H2bGqh1-U,4
19
- nvidia_nat_mcp-1.3.0a20250929.dist-info/RECORD,,