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.
- nat/plugins/mcp/auth/auth_flow_handler.py +1 -1
- nat/plugins/mcp/auth/auth_provider.py +5 -1
- nat/plugins/mcp/client_base.py +54 -24
- nat/plugins/mcp/client_impl.py +66 -4
- nat/plugins/mcp/exception_handler.py +1 -1
- {nvidia_nat_mcp-1.3.0a20250929.dist-info → nvidia_nat_mcp-1.3.0rc1.dist-info}/METADATA +2 -2
- nvidia_nat_mcp-1.3.0rc1.dist-info/RECORD +19 -0
- nvidia_nat_mcp-1.3.0a20250929.dist-info/RECORD +0 -19
- {nvidia_nat_mcp-1.3.0a20250929.dist-info → nvidia_nat_mcp-1.3.0rc1.dist-info}/WHEEL +0 -0
- {nvidia_nat_mcp-1.3.0a20250929.dist-info → nvidia_nat_mcp-1.3.0rc1.dist-info}/entry_points.txt +0 -0
- {nvidia_nat_mcp-1.3.0a20250929.dist-info → nvidia_nat_mcp-1.3.0rc1.dist-info}/top_level.txt +0 -0
@@ -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
|
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
|
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)
|
nat/plugins/mcp/client_base.py
CHANGED
@@ -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
|
124
|
-
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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
|
-
|
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:
|
663
|
+
result_str = f"MCPToolClient tool call failed: {e.original_exception}"
|
634
664
|
|
635
665
|
return result_str
|
nat/plugins/mcp/client_impl.py
CHANGED
@@ -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 =
|
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=
|
204
|
-
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,
|
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.
|
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.
|
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,,
|
File without changes
|
{nvidia_nat_mcp-1.3.0a20250929.dist-info → nvidia_nat_mcp-1.3.0rc1.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|