nvidia-nat 1.3.0a20250923__py3-none-any.whl → 1.3.0a20250924__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/agent/react_agent/register.py +12 -1
- nat/agent/reasoning_agent/reasoning_agent.py +2 -2
- nat/agent/rewoo_agent/register.py +12 -1
- nat/agent/tool_calling_agent/register.py +28 -8
- nat/builder/builder.py +33 -24
- nat/builder/eval_builder.py +14 -9
- nat/builder/function.py +108 -52
- nat/builder/workflow_builder.py +89 -79
- nat/cli/commands/info/info.py +16 -6
- nat/cli/commands/mcp/__init__.py +14 -0
- nat/cli/commands/mcp/mcp.py +786 -0
- nat/cli/entrypoint.py +2 -1
- nat/control_flow/router_agent/register.py +1 -1
- nat/control_flow/sequential_executor.py +6 -7
- nat/eval/evaluate.py +2 -1
- nat/eval/trajectory_evaluator/register.py +1 -1
- nat/experimental/decorators/experimental_warning_decorator.py +26 -5
- nat/experimental/test_time_compute/functions/plan_select_execute_function.py +2 -2
- nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +1 -1
- nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +1 -1
- nat/experimental/test_time_compute/models/strategy_base.py +2 -2
- nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +3 -3
- nat/front_ends/mcp/mcp_front_end_plugin_worker.py +4 -4
- nat/front_ends/simple_base/simple_front_end_plugin_base.py +1 -1
- nat/profiler/decorators/function_tracking.py +33 -1
- nat/profiler/parameter_optimization/prompt_optimizer.py +2 -2
- nat/runtime/loader.py +1 -1
- {nvidia_nat-1.3.0a20250923.dist-info → nvidia_nat-1.3.0a20250924.dist-info}/METADATA +1 -1
- {nvidia_nat-1.3.0a20250923.dist-info → nvidia_nat-1.3.0a20250924.dist-info}/RECORD +34 -33
- nat/cli/commands/info/list_mcp.py +0 -461
- {nvidia_nat-1.3.0a20250923.dist-info → nvidia_nat-1.3.0a20250924.dist-info}/WHEEL +0 -0
- {nvidia_nat-1.3.0a20250923.dist-info → nvidia_nat-1.3.0a20250924.dist-info}/entry_points.txt +0 -0
- {nvidia_nat-1.3.0a20250923.dist-info → nvidia_nat-1.3.0a20250924.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
- {nvidia_nat-1.3.0a20250923.dist-info → nvidia_nat-1.3.0a20250924.dist-info}/licenses/LICENSE.md +0 -0
- {nvidia_nat-1.3.0a20250923.dist-info → nvidia_nat-1.3.0a20250924.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,786 @@
|
|
|
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 asyncio
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any
|
|
21
|
+
from typing import Literal
|
|
22
|
+
from typing import cast
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
|
|
27
|
+
from nat.cli.commands.start import start_command
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group(name=__name__, invoke_without_command=False, help="MCP-related commands.")
|
|
33
|
+
def mcp_command():
|
|
34
|
+
"""
|
|
35
|
+
MCP-related commands.
|
|
36
|
+
"""
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# nat mcp serve: reuses the start/mcp frontend command
|
|
41
|
+
mcp_command.add_command(start_command.get_command(None, "mcp"), name="serve") # type: ignore
|
|
42
|
+
|
|
43
|
+
# Suppress verbose logs from mcp.client.sse and httpx
|
|
44
|
+
logging.getLogger("mcp.client.sse").setLevel(logging.WARNING)
|
|
45
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from nat.plugins.mcp.exception_handler import format_mcp_error
|
|
49
|
+
from nat.plugins.mcp.exceptions import MCPError
|
|
50
|
+
except ImportError:
|
|
51
|
+
# Fallback for when MCP client package is not installed
|
|
52
|
+
MCPError = Exception
|
|
53
|
+
|
|
54
|
+
def format_mcp_error(error, include_traceback=False):
|
|
55
|
+
click.echo(f"Error: {error}", err=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_transport_cli_args(transport: str, command: str | None, args: str | None, env: str | None) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Validate transport and parameter combinations, returning False if invalid.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
transport: The transport type ('sse', 'stdio', or 'streamable-http')
|
|
64
|
+
command: Command for stdio transport
|
|
65
|
+
args: Arguments for stdio transport
|
|
66
|
+
env: Environment variables for stdio transport
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
bool: True if valid, False if invalid (error message already displayed)
|
|
70
|
+
"""
|
|
71
|
+
if transport == 'stdio':
|
|
72
|
+
if not command:
|
|
73
|
+
click.echo("--command is required when using stdio client type", err=True)
|
|
74
|
+
return False
|
|
75
|
+
elif transport in ['sse', 'streamable-http']:
|
|
76
|
+
if command or args or env:
|
|
77
|
+
click.echo("--command, --args, and --env are not allowed when using sse or streamable-http client type",
|
|
78
|
+
err=True)
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MCPPingResult(BaseModel):
|
|
84
|
+
"""Result of an MCP server ping request.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
url (str): The MCP server URL that was pinged
|
|
88
|
+
status (str): Health status - 'healthy', 'unhealthy', or 'unknown'
|
|
89
|
+
response_time_ms (float | None): Response time in milliseconds, None if request failed completely
|
|
90
|
+
error (str | None): Error message if the ping failed, None if successful
|
|
91
|
+
"""
|
|
92
|
+
url: str
|
|
93
|
+
status: str
|
|
94
|
+
response_time_ms: float | None
|
|
95
|
+
error: str | None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def format_tool(tool: Any) -> dict[str, str | None]:
|
|
99
|
+
"""Format an MCP tool into a dictionary for display.
|
|
100
|
+
|
|
101
|
+
Extracts name, description, and input schema from various MCP tool object types
|
|
102
|
+
and normalizes them into a consistent dictionary format for CLI display.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
tool (Any): MCPToolClient or raw MCP Tool object (uses Any due to different types)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
dict[str, str | None]: Dictionary with name, description, and input_schema as keys
|
|
109
|
+
"""
|
|
110
|
+
name = getattr(tool, 'name', None)
|
|
111
|
+
description = getattr(tool, 'description', '')
|
|
112
|
+
input_schema = getattr(tool, 'input_schema', None) or getattr(tool, 'inputSchema', None)
|
|
113
|
+
|
|
114
|
+
# Normalize schema to JSON string
|
|
115
|
+
if input_schema is None:
|
|
116
|
+
return {
|
|
117
|
+
"name": name,
|
|
118
|
+
"description": description,
|
|
119
|
+
"input_schema": None,
|
|
120
|
+
}
|
|
121
|
+
elif hasattr(input_schema, "schema_json"):
|
|
122
|
+
schema_str = input_schema.schema_json(indent=2)
|
|
123
|
+
elif hasattr(input_schema, "model_json_schema"):
|
|
124
|
+
schema_str = json.dumps(input_schema.model_json_schema(), indent=2)
|
|
125
|
+
elif isinstance(input_schema, dict):
|
|
126
|
+
schema_str = json.dumps(input_schema, indent=2)
|
|
127
|
+
else:
|
|
128
|
+
# Final fallback: attempt to dump stringified version wrapped as JSON string
|
|
129
|
+
schema_str = json.dumps({"raw": str(input_schema)}, indent=2)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"name": name,
|
|
133
|
+
"description": description,
|
|
134
|
+
"input_schema": schema_str,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def print_tool(tool_dict: dict[str, str | None], detail: bool = False) -> None:
|
|
139
|
+
"""Print a formatted tool to the console with optional detailed information.
|
|
140
|
+
|
|
141
|
+
Outputs tool information in a user-friendly format to stdout. When detail=True
|
|
142
|
+
or when description/schema are available, shows full information with separator.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
tool_dict (dict[str, str | None]): Dictionary containing tool information with name, description, and
|
|
146
|
+
input_schema as keys
|
|
147
|
+
detail (bool, optional): Whether to force detailed output. Defaults to False.
|
|
148
|
+
"""
|
|
149
|
+
click.echo(f"Tool: {tool_dict.get('name', 'Unknown')}")
|
|
150
|
+
if detail or tool_dict.get('input_schema') or tool_dict.get('description'):
|
|
151
|
+
click.echo(f"Description: {tool_dict.get('description', 'No description available')}")
|
|
152
|
+
if tool_dict.get("input_schema"):
|
|
153
|
+
click.echo("Input Schema:")
|
|
154
|
+
click.echo(tool_dict.get("input_schema"))
|
|
155
|
+
else:
|
|
156
|
+
click.echo("Input Schema: None")
|
|
157
|
+
click.echo("-" * 60)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def list_tools_via_function_group(
|
|
161
|
+
command: str | None,
|
|
162
|
+
url: str | None,
|
|
163
|
+
tool_name: str | None = None,
|
|
164
|
+
transport: str = 'sse',
|
|
165
|
+
args: list[str] | None = None,
|
|
166
|
+
env: dict[str, str] | None = None,
|
|
167
|
+
) -> list[dict[str, str | None]]:
|
|
168
|
+
"""List tools by constructing the mcp_client function group and introspecting functions.
|
|
169
|
+
|
|
170
|
+
Mirrors the behavior of list_mcp.py but routes through the registered function group to ensure
|
|
171
|
+
parity with workflow configuration.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
# Ensure the registration side-effects are loaded
|
|
175
|
+
from nat.builder.workflow_builder import WorkflowBuilder
|
|
176
|
+
from nat.plugins.mcp.client_impl import MCPClientConfig
|
|
177
|
+
from nat.plugins.mcp.client_impl import MCPServerConfig
|
|
178
|
+
except ImportError:
|
|
179
|
+
click.echo(
|
|
180
|
+
"MCP client functionality requires nvidia-nat-mcp package. Install with: uv pip install nvidia-nat-mcp",
|
|
181
|
+
err=True)
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
if args is None:
|
|
185
|
+
args = []
|
|
186
|
+
|
|
187
|
+
# Build server config according to transport
|
|
188
|
+
server_cfg = MCPServerConfig(
|
|
189
|
+
transport=cast(Literal["stdio", "sse", "streamable-http"], transport),
|
|
190
|
+
url=cast(Any, url) if transport in ('sse', 'streamable-http') else None,
|
|
191
|
+
command=command if transport == 'stdio' else None,
|
|
192
|
+
args=args if transport == 'stdio' else None,
|
|
193
|
+
env=env if transport == 'stdio' else None,
|
|
194
|
+
)
|
|
195
|
+
group_cfg = MCPClientConfig(server=server_cfg)
|
|
196
|
+
|
|
197
|
+
tools: list[dict[str, str | None]] = []
|
|
198
|
+
|
|
199
|
+
async with WorkflowBuilder() as builder: # type: ignore
|
|
200
|
+
group = await builder.add_function_group("mcp_client", group_cfg)
|
|
201
|
+
|
|
202
|
+
# Access functions exposed by the group
|
|
203
|
+
fns = group.get_accessible_functions()
|
|
204
|
+
|
|
205
|
+
def to_tool_entry(full_name: str, fn_obj) -> dict[str, str | None]:
|
|
206
|
+
# full_name like "mcp_client.<tool>"
|
|
207
|
+
name = full_name.split(".", 1)[1] if "." in full_name else full_name
|
|
208
|
+
schema = getattr(fn_obj, 'input_schema', None)
|
|
209
|
+
if schema is None:
|
|
210
|
+
schema_str = None
|
|
211
|
+
elif hasattr(schema, "schema_json"):
|
|
212
|
+
schema_str = schema.schema_json(indent=2)
|
|
213
|
+
elif hasattr(schema, "model_json_schema"):
|
|
214
|
+
try:
|
|
215
|
+
schema_str = json.dumps(schema.model_json_schema(), indent=2)
|
|
216
|
+
except Exception:
|
|
217
|
+
schema_str = None
|
|
218
|
+
else:
|
|
219
|
+
schema_str = None
|
|
220
|
+
return {"name": name, "description": getattr(fn_obj, 'description', ''), "input_schema": schema_str}
|
|
221
|
+
|
|
222
|
+
if tool_name:
|
|
223
|
+
full = f"mcp_client.{tool_name}"
|
|
224
|
+
fn = fns.get(full)
|
|
225
|
+
if fn is not None:
|
|
226
|
+
tools.append(to_tool_entry(full, fn))
|
|
227
|
+
else:
|
|
228
|
+
for full, fn in fns.items():
|
|
229
|
+
tools.append(to_tool_entry(full, fn))
|
|
230
|
+
|
|
231
|
+
return tools
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def list_tools_direct(command, url, tool_name=None, transport='sse', args=None, env=None):
|
|
235
|
+
"""List MCP tools using direct MCP protocol with structured exception handling.
|
|
236
|
+
|
|
237
|
+
Bypasses MCPBuilder and uses raw MCP ClientSession and SSE client directly.
|
|
238
|
+
Converts raw exceptions to structured MCPErrors for consistent user experience.
|
|
239
|
+
Used when --direct flag is specified in CLI.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
url (str): MCP server URL to connect to
|
|
243
|
+
tool_name (str | None, optional): Specific tool name to retrieve.
|
|
244
|
+
If None, retrieves all available tools. Defaults to None.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
list[dict[str, str | None]]: List of formatted tool dictionaries, each containing name, description, and
|
|
248
|
+
input_schema as keys
|
|
249
|
+
|
|
250
|
+
Note:
|
|
251
|
+
This function handles ExceptionGroup by extracting the most relevant exception
|
|
252
|
+
and converting it to MCPError for consistent error reporting.
|
|
253
|
+
"""
|
|
254
|
+
if args is None:
|
|
255
|
+
args = []
|
|
256
|
+
from mcp import ClientSession
|
|
257
|
+
from mcp.client.sse import sse_client
|
|
258
|
+
from mcp.client.stdio import StdioServerParameters
|
|
259
|
+
from mcp.client.stdio import stdio_client
|
|
260
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
if transport == 'stdio':
|
|
264
|
+
|
|
265
|
+
def get_stdio_client():
|
|
266
|
+
return stdio_client(server=StdioServerParameters(command=command, args=args, env=env))
|
|
267
|
+
|
|
268
|
+
client = get_stdio_client
|
|
269
|
+
elif transport == 'streamable-http':
|
|
270
|
+
|
|
271
|
+
def get_streamable_http_client():
|
|
272
|
+
return streamablehttp_client(url=url)
|
|
273
|
+
|
|
274
|
+
client = get_streamable_http_client
|
|
275
|
+
else:
|
|
276
|
+
|
|
277
|
+
def get_sse_client():
|
|
278
|
+
return sse_client(url=url)
|
|
279
|
+
|
|
280
|
+
client = get_sse_client
|
|
281
|
+
|
|
282
|
+
async with client() as ctx:
|
|
283
|
+
read, write = (ctx[0], ctx[1]) if isinstance(ctx, tuple) else ctx
|
|
284
|
+
async with ClientSession(read, write) as session:
|
|
285
|
+
await session.initialize()
|
|
286
|
+
response = await session.list_tools()
|
|
287
|
+
|
|
288
|
+
tools = []
|
|
289
|
+
for tool in response.tools:
|
|
290
|
+
if tool_name:
|
|
291
|
+
if tool.name == tool_name:
|
|
292
|
+
tools.append(format_tool(tool))
|
|
293
|
+
else:
|
|
294
|
+
tools.append(format_tool(tool))
|
|
295
|
+
|
|
296
|
+
if tool_name and not tools:
|
|
297
|
+
click.echo(f"[INFO] Tool '{tool_name}' not found.")
|
|
298
|
+
return tools
|
|
299
|
+
except Exception as e:
|
|
300
|
+
# Convert raw exceptions to structured MCPError for consistency
|
|
301
|
+
try:
|
|
302
|
+
from nat.plugins.mcp.exception_handler import convert_to_mcp_error
|
|
303
|
+
from nat.plugins.mcp.exception_handler import extract_primary_exception
|
|
304
|
+
except ImportError:
|
|
305
|
+
# Fallback when MCP client package is not installed
|
|
306
|
+
def convert_to_mcp_error(exception, url):
|
|
307
|
+
return Exception(f"Error connecting to {url}: {exception}")
|
|
308
|
+
|
|
309
|
+
def extract_primary_exception(exceptions):
|
|
310
|
+
return exceptions[0] if exceptions else Exception("Unknown error")
|
|
311
|
+
|
|
312
|
+
if isinstance(e, ExceptionGroup):
|
|
313
|
+
primary_exception = extract_primary_exception(list(e.exceptions))
|
|
314
|
+
mcp_error = convert_to_mcp_error(primary_exception, url)
|
|
315
|
+
else:
|
|
316
|
+
mcp_error = convert_to_mcp_error(e, url)
|
|
317
|
+
|
|
318
|
+
format_mcp_error(mcp_error, include_traceback=False)
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def ping_mcp_server(url: str,
|
|
323
|
+
timeout: int,
|
|
324
|
+
transport: str = 'streamable-http',
|
|
325
|
+
command: str | None = None,
|
|
326
|
+
args: list[str] | None = None,
|
|
327
|
+
env: dict[str, str] | None = None) -> MCPPingResult:
|
|
328
|
+
"""Ping an MCP server to check if it's responsive.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
url (str): MCP server URL to ping
|
|
332
|
+
timeout (int): Timeout in seconds for the ping request
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
MCPPingResult: Structured result with status, response_time, and any error info
|
|
336
|
+
"""
|
|
337
|
+
from mcp.client.session import ClientSession
|
|
338
|
+
from mcp.client.sse import sse_client
|
|
339
|
+
from mcp.client.stdio import StdioServerParameters
|
|
340
|
+
from mcp.client.stdio import stdio_client
|
|
341
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
342
|
+
|
|
343
|
+
async def _ping_operation():
|
|
344
|
+
# Select transport
|
|
345
|
+
if transport == 'stdio':
|
|
346
|
+
stdio_args_local: list[str] = args or []
|
|
347
|
+
if not command:
|
|
348
|
+
raise RuntimeError("--command is required for stdio transport")
|
|
349
|
+
client_ctx = stdio_client(server=StdioServerParameters(command=command, args=stdio_args_local, env=env))
|
|
350
|
+
elif transport == 'sse':
|
|
351
|
+
client_ctx = sse_client(url)
|
|
352
|
+
else: # streamable-http
|
|
353
|
+
client_ctx = streamablehttp_client(url=url)
|
|
354
|
+
|
|
355
|
+
async with client_ctx as ctx:
|
|
356
|
+
read, write = (ctx[0], ctx[1]) if isinstance(ctx, tuple) else ctx
|
|
357
|
+
async with ClientSession(read, write) as session:
|
|
358
|
+
await session.initialize()
|
|
359
|
+
|
|
360
|
+
start_time = time.time()
|
|
361
|
+
await session.send_ping()
|
|
362
|
+
end_time = time.time()
|
|
363
|
+
response_time_ms = round((end_time - start_time) * 1000, 2)
|
|
364
|
+
|
|
365
|
+
return MCPPingResult(url=url, status="healthy", response_time_ms=response_time_ms, error=None)
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
# Apply timeout to the entire ping operation
|
|
369
|
+
return await asyncio.wait_for(_ping_operation(), timeout=timeout)
|
|
370
|
+
|
|
371
|
+
except asyncio.TimeoutError:
|
|
372
|
+
return MCPPingResult(url=url,
|
|
373
|
+
status="unhealthy",
|
|
374
|
+
response_time_ms=None,
|
|
375
|
+
error=f"Timeout after {timeout} seconds")
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
return MCPPingResult(url=url, status="unhealthy", response_time_ms=None, error=str(e))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@mcp_command.group(name="client", invoke_without_command=False, help="MCP client commands.")
|
|
382
|
+
def mcp_client_command():
|
|
383
|
+
"""
|
|
384
|
+
MCP client commands.
|
|
385
|
+
"""
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@mcp_client_command.group(name="tool", invoke_without_command=False, help="Inspect and call MCP tools.")
|
|
390
|
+
def mcp_client_tool_group():
|
|
391
|
+
"""
|
|
392
|
+
MCP client tool commands.
|
|
393
|
+
"""
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@mcp_client_tool_group.command(name="list", help="List tool names (default), or show details with --detail or --tool.")
|
|
398
|
+
@click.option('--direct', is_flag=True, help='Bypass MCPBuilder and use direct MCP protocol')
|
|
399
|
+
@click.option(
|
|
400
|
+
'--url',
|
|
401
|
+
default='http://localhost:9901/mcp',
|
|
402
|
+
show_default=True,
|
|
403
|
+
help='MCP server URL (e.g. http://localhost:8080/mcp for streamable-http, http://localhost:8080/sse for sse)')
|
|
404
|
+
@click.option('--transport',
|
|
405
|
+
type=click.Choice(['sse', 'stdio', 'streamable-http']),
|
|
406
|
+
default='streamable-http',
|
|
407
|
+
show_default=True,
|
|
408
|
+
help='Type of client to use (default: streamable-http, backwards compatible with sse)')
|
|
409
|
+
@click.option('--command', help='For stdio: The command to run (e.g. mcp-server)')
|
|
410
|
+
@click.option('--args', help='For stdio: Additional arguments for the command (space-separated)')
|
|
411
|
+
@click.option('--env', help='For stdio: Environment variables in KEY=VALUE format (space-separated)')
|
|
412
|
+
@click.option('--tool', default=None, help='Get details for a specific tool by name')
|
|
413
|
+
@click.option('--detail', is_flag=True, help='Show full details for all tools')
|
|
414
|
+
@click.option('--json-output', is_flag=True, help='Output tool metadata in JSON format')
|
|
415
|
+
@click.pass_context
|
|
416
|
+
def mcp_client_tool_list(ctx, direct, url, transport, command, args, env, tool, detail, json_output):
|
|
417
|
+
"""List MCP tool names (default) or show detailed tool information.
|
|
418
|
+
|
|
419
|
+
Use --detail for full output including descriptions and input schemas.
|
|
420
|
+
If --tool is provided, always shows full output for that specific tool.
|
|
421
|
+
Use --direct to bypass MCPBuilder and use raw MCP protocol.
|
|
422
|
+
Use --json-output to get structured JSON data instead of formatted text.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
ctx (click.Context): Click context object for command invocation
|
|
426
|
+
direct (bool): Whether to bypass MCPBuilder and use direct MCP protocol
|
|
427
|
+
url (str): MCP server URL to connect to (default: http://localhost:9901/mcp)
|
|
428
|
+
tool (str | None): Optional specific tool name to retrieve detailed info for
|
|
429
|
+
detail (bool): Whether to show full details (description + schema) for all tools
|
|
430
|
+
json_output (bool): Whether to output tool metadata in JSON format instead of text
|
|
431
|
+
|
|
432
|
+
Examples:
|
|
433
|
+
nat mcp client tool list # List tool names only
|
|
434
|
+
nat mcp client tool list --detail # Show all tools with full details
|
|
435
|
+
nat mcp client tool list --tool my_tool # Show details for specific tool
|
|
436
|
+
nat mcp client tool list --json-output # Get JSON format output
|
|
437
|
+
nat mcp client tool list --direct --url http://... # Use direct protocol with custom URL
|
|
438
|
+
"""
|
|
439
|
+
if ctx.invoked_subcommand is not None:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
if not validate_transport_cli_args(transport, command, args, env):
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
if transport in ['sse', 'streamable-http']:
|
|
446
|
+
if not url:
|
|
447
|
+
click.echo("[ERROR] --url is required when using sse or streamable-http client type", err=True)
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
stdio_args = args.split() if args else []
|
|
451
|
+
stdio_env = dict(var.split('=', 1) for var in env.split()) if env else None
|
|
452
|
+
|
|
453
|
+
fetcher = list_tools_direct if direct else list_tools_via_function_group
|
|
454
|
+
tools = asyncio.run(fetcher(command, url, tool, transport, stdio_args, stdio_env))
|
|
455
|
+
|
|
456
|
+
if json_output:
|
|
457
|
+
click.echo(json.dumps(tools, indent=2))
|
|
458
|
+
elif tool:
|
|
459
|
+
for tool_dict in (tools or []):
|
|
460
|
+
print_tool(tool_dict, detail=True)
|
|
461
|
+
elif detail:
|
|
462
|
+
for tool_dict in (tools or []):
|
|
463
|
+
print_tool(tool_dict, detail=True)
|
|
464
|
+
else:
|
|
465
|
+
for tool_dict in (tools or []):
|
|
466
|
+
click.echo(tool_dict.get('name', 'Unknown tool'))
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@mcp_client_command.command(name="ping", help="Ping an MCP server to check if it's responsive.")
|
|
470
|
+
@click.option(
|
|
471
|
+
'--url',
|
|
472
|
+
default='http://localhost:9901/mcp',
|
|
473
|
+
show_default=True,
|
|
474
|
+
help='MCP server URL (e.g. http://localhost:8080/mcp for streamable-http, http://localhost:8080/sse for sse)')
|
|
475
|
+
@click.option('--transport',
|
|
476
|
+
type=click.Choice(['sse', 'stdio', 'streamable-http']),
|
|
477
|
+
default='streamable-http',
|
|
478
|
+
show_default=True,
|
|
479
|
+
help='Type of client to use for ping')
|
|
480
|
+
@click.option('--command', help='For stdio: The command to run (e.g. mcp-server)')
|
|
481
|
+
@click.option('--args', help='For stdio: Additional arguments for the command (space-separated)')
|
|
482
|
+
@click.option('--env', help='For stdio: Environment variables in KEY=VALUE format (space-separated)')
|
|
483
|
+
@click.option('--timeout', default=60, show_default=True, help='Timeout in seconds for ping request')
|
|
484
|
+
@click.option('--json-output', is_flag=True, help='Output ping result in JSON format')
|
|
485
|
+
def mcp_client_ping(url: str,
|
|
486
|
+
transport: str,
|
|
487
|
+
command: str | None,
|
|
488
|
+
args: str | None,
|
|
489
|
+
env: str | None,
|
|
490
|
+
timeout: int,
|
|
491
|
+
json_output: bool) -> None:
|
|
492
|
+
"""Ping an MCP server to check if it's responsive.
|
|
493
|
+
|
|
494
|
+
This command sends a ping request to the MCP server and measures the response time.
|
|
495
|
+
It's useful for health checks and monitoring server availability.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
url (str): MCP server URL to ping (default: http://localhost:9901/mcp)
|
|
499
|
+
timeout (int): Timeout in seconds for the ping request (default: 60)
|
|
500
|
+
json_output (bool): Whether to output the result in JSON format
|
|
501
|
+
|
|
502
|
+
Examples:
|
|
503
|
+
nat mcp client ping # Ping default server
|
|
504
|
+
nat mcp client ping --url http://custom-server:9901/mcp # Ping custom server
|
|
505
|
+
nat mcp client ping --timeout 10 # Use 10 second timeout
|
|
506
|
+
nat mcp client ping --json-output # Get JSON format output
|
|
507
|
+
"""
|
|
508
|
+
# Validate combinations similar to list command
|
|
509
|
+
if not validate_transport_cli_args(transport, command, args, env):
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
stdio_args = args.split() if args else []
|
|
513
|
+
stdio_env = dict(var.split('=', 1) for var in env.split()) if env else None
|
|
514
|
+
|
|
515
|
+
result = asyncio.run(ping_mcp_server(url, timeout, transport, command, stdio_args, stdio_env))
|
|
516
|
+
|
|
517
|
+
if json_output:
|
|
518
|
+
click.echo(result.model_dump_json(indent=2))
|
|
519
|
+
elif result.status == "healthy":
|
|
520
|
+
click.echo(f"Server at {result.url} is healthy (response time: {result.response_time_ms}ms)")
|
|
521
|
+
else:
|
|
522
|
+
click.echo(f"Server at {result.url} {result.status}: {result.error}")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
async def call_tool_direct(command: str | None,
|
|
526
|
+
url: str | None,
|
|
527
|
+
tool_name: str,
|
|
528
|
+
transport: str,
|
|
529
|
+
args: list[str] | None,
|
|
530
|
+
env: dict[str, str] | None,
|
|
531
|
+
tool_args: dict[str, Any] | None) -> str:
|
|
532
|
+
"""Call an MCP tool directly via the selected transport.
|
|
533
|
+
|
|
534
|
+
Bypasses the WorkflowBuilder and talks to the MCP server using the raw
|
|
535
|
+
protocol client for the given transport. Aggregates tool outputs into a
|
|
536
|
+
plain string suitable for terminal display. Converts transport/protocol
|
|
537
|
+
exceptions into a structured MCPError for consistency.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
command (str | None): For ``stdio`` transport, the command to execute.
|
|
541
|
+
url (str | None): For ``sse`` or ``streamable-http`` transports, the server URL.
|
|
542
|
+
tool_name (str): Name of the tool to call.
|
|
543
|
+
transport (str): One of ``'stdio'``, ``'sse'``, or ``'streamable-http'``.
|
|
544
|
+
args (list[str] | None): For ``stdio`` transport, additional command arguments.
|
|
545
|
+
env (dict[str, str] | None): For ``stdio`` transport, environment variables.
|
|
546
|
+
tool_args (dict[str, Any] | None): JSON-serializable arguments passed to the tool.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
str: Concatenated textual output from the tool invocation.
|
|
550
|
+
|
|
551
|
+
Raises:
|
|
552
|
+
MCPError: If the connection, initialization, or tool call fails. When the
|
|
553
|
+
MCP client package is not installed, a generic ``Exception`` is raised
|
|
554
|
+
with an MCP-like error message.
|
|
555
|
+
RuntimeError: If required parameters for the chosen transport are missing
|
|
556
|
+
or if the tool returns an error response.
|
|
557
|
+
"""
|
|
558
|
+
from mcp import ClientSession
|
|
559
|
+
from mcp.client.sse import sse_client
|
|
560
|
+
from mcp.client.stdio import StdioServerParameters
|
|
561
|
+
from mcp.client.stdio import stdio_client
|
|
562
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
563
|
+
from mcp.types import TextContent
|
|
564
|
+
|
|
565
|
+
try:
|
|
566
|
+
if transport == 'stdio':
|
|
567
|
+
if not command:
|
|
568
|
+
raise RuntimeError("--command is required for stdio transport")
|
|
569
|
+
|
|
570
|
+
def get_stdio_client():
|
|
571
|
+
return stdio_client(server=StdioServerParameters(command=command, args=args or [], env=env))
|
|
572
|
+
|
|
573
|
+
client = get_stdio_client
|
|
574
|
+
elif transport == 'streamable-http':
|
|
575
|
+
|
|
576
|
+
def get_streamable_http_client():
|
|
577
|
+
if not url:
|
|
578
|
+
raise RuntimeError("--url is required for streamable-http transport")
|
|
579
|
+
return streamablehttp_client(url=url)
|
|
580
|
+
|
|
581
|
+
client = get_streamable_http_client
|
|
582
|
+
else:
|
|
583
|
+
|
|
584
|
+
def get_sse_client():
|
|
585
|
+
if not url:
|
|
586
|
+
raise RuntimeError("--url is required for sse transport")
|
|
587
|
+
return sse_client(url=url)
|
|
588
|
+
|
|
589
|
+
client = get_sse_client
|
|
590
|
+
|
|
591
|
+
async with client() as ctx:
|
|
592
|
+
read, write = (ctx[0], ctx[1]) if isinstance(ctx, tuple) else ctx
|
|
593
|
+
async with ClientSession(read, write) as session:
|
|
594
|
+
await session.initialize()
|
|
595
|
+
result = await session.call_tool(tool_name, tool_args or {})
|
|
596
|
+
|
|
597
|
+
outputs: list[str] = []
|
|
598
|
+
for content in result.content:
|
|
599
|
+
if isinstance(content, TextContent):
|
|
600
|
+
outputs.append(content.text)
|
|
601
|
+
else:
|
|
602
|
+
outputs.append(str(content))
|
|
603
|
+
|
|
604
|
+
# If the result indicates an error, raise to surface in CLI
|
|
605
|
+
if getattr(result, "isError", False):
|
|
606
|
+
raise RuntimeError("\n".join(outputs) or f"Tool call '{tool_name}' returned an error")
|
|
607
|
+
|
|
608
|
+
return "\n".join(outputs)
|
|
609
|
+
except Exception as e:
|
|
610
|
+
# Convert raw exceptions to structured MCPError for consistency
|
|
611
|
+
try:
|
|
612
|
+
from nat.plugins.mcp.exception_handler import convert_to_mcp_error
|
|
613
|
+
from nat.plugins.mcp.exception_handler import extract_primary_exception
|
|
614
|
+
except ImportError:
|
|
615
|
+
# Fallback when MCP client package is not installed
|
|
616
|
+
def convert_to_mcp_error(exception: Exception, url: str):
|
|
617
|
+
return Exception(f"Error connecting to {url}: {exception}")
|
|
618
|
+
|
|
619
|
+
def extract_primary_exception(exceptions):
|
|
620
|
+
return exceptions[0] if exceptions else Exception("Unknown error")
|
|
621
|
+
|
|
622
|
+
endpoint = url or (f"stdio:{command}" if transport == 'stdio' else "unknown")
|
|
623
|
+
if isinstance(e, ExceptionGroup):
|
|
624
|
+
primary_exception = extract_primary_exception(list(e.exceptions))
|
|
625
|
+
mcp_error = convert_to_mcp_error(primary_exception, endpoint)
|
|
626
|
+
else:
|
|
627
|
+
mcp_error = convert_to_mcp_error(e, endpoint)
|
|
628
|
+
raise mcp_error from e
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
async def call_tool_and_print(command: str | None,
|
|
632
|
+
url: str | None,
|
|
633
|
+
tool_name: str,
|
|
634
|
+
transport: str,
|
|
635
|
+
args: list[str] | None,
|
|
636
|
+
env: dict[str, str] | None,
|
|
637
|
+
tool_args: dict[str, Any] | None,
|
|
638
|
+
direct: bool) -> str:
|
|
639
|
+
"""Call an MCP tool either directly or via the function group and return output.
|
|
640
|
+
|
|
641
|
+
When ``direct`` is True, uses the raw MCP protocol client (bypassing the
|
|
642
|
+
builder). Otherwise, constructs the ``mcp_client`` function group and
|
|
643
|
+
invokes the corresponding function, mirroring workflow configuration.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
command (str | None): For ``stdio`` transport, the command to execute.
|
|
647
|
+
url (str | None): For ``sse`` or ``streamable-http`` transports, the server URL.
|
|
648
|
+
tool_name (str): Name of the tool to call.
|
|
649
|
+
transport (str): One of ``'stdio'``, ``'sse'``, or ``'streamable-http'``.
|
|
650
|
+
args (list[str] | None): For ``stdio`` transport, additional command arguments.
|
|
651
|
+
env (dict[str, str] | None): For ``stdio`` transport, environment variables.
|
|
652
|
+
tool_args (dict[str, Any] | None): JSON-serializable arguments passed to the tool.
|
|
653
|
+
direct (bool): If True, bypass WorkflowBuilder and use direct MCP client.
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
str: Stringified tool output suitable for terminal display. May be an
|
|
657
|
+
empty string when the MCP client package is not installed and ``direct``
|
|
658
|
+
is False.
|
|
659
|
+
|
|
660
|
+
Raises:
|
|
661
|
+
RuntimeError: If the tool is not found when using the function group.
|
|
662
|
+
MCPError: Propagated from ``call_tool_direct`` when direct mode fails.
|
|
663
|
+
"""
|
|
664
|
+
if direct:
|
|
665
|
+
return await call_tool_direct(command, url, tool_name, transport, args, env, tool_args)
|
|
666
|
+
|
|
667
|
+
try:
|
|
668
|
+
from nat.builder.workflow_builder import WorkflowBuilder
|
|
669
|
+
from nat.plugins.mcp.client_impl import MCPClientConfig
|
|
670
|
+
from nat.plugins.mcp.client_impl import MCPServerConfig
|
|
671
|
+
except ImportError:
|
|
672
|
+
click.echo(
|
|
673
|
+
"MCP client functionality requires nvidia-nat-mcp package. Install with: uv pip install nvidia-nat-mcp",
|
|
674
|
+
err=True)
|
|
675
|
+
return ""
|
|
676
|
+
|
|
677
|
+
server_cfg = MCPServerConfig(
|
|
678
|
+
transport=cast(Literal["stdio", "sse", "streamable-http"], transport),
|
|
679
|
+
url=cast(Any, url) if transport in ('sse', 'streamable-http') else None,
|
|
680
|
+
command=command if transport == 'stdio' else None,
|
|
681
|
+
args=args if transport == 'stdio' else None,
|
|
682
|
+
env=env if transport == 'stdio' else None,
|
|
683
|
+
)
|
|
684
|
+
group_cfg = MCPClientConfig(server=server_cfg)
|
|
685
|
+
|
|
686
|
+
async with WorkflowBuilder() as builder: # type: ignore
|
|
687
|
+
group = await builder.add_function_group("mcp_client", group_cfg)
|
|
688
|
+
fns = group.get_accessible_functions()
|
|
689
|
+
full = f"mcp_client.{tool_name}"
|
|
690
|
+
fn = fns.get(full)
|
|
691
|
+
if fn is None:
|
|
692
|
+
raise RuntimeError(f"Tool '{tool_name}' not found")
|
|
693
|
+
# The group exposes a Function that we can invoke with kwargs
|
|
694
|
+
result = await fn.acall_invoke(**(tool_args or {}))
|
|
695
|
+
# Ensure string output for terminal
|
|
696
|
+
return str(result)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@mcp_client_tool_group.command(name="call", help="Call a tool by name with optional arguments.")
|
|
700
|
+
@click.argument('tool_name', nargs=1, required=True)
|
|
701
|
+
@click.option('--direct', is_flag=True, help='Bypass MCPBuilder and use direct MCP protocol')
|
|
702
|
+
@click.option(
|
|
703
|
+
'--url',
|
|
704
|
+
default='http://localhost:9901/mcp',
|
|
705
|
+
show_default=True,
|
|
706
|
+
help='MCP server URL (e.g. http://localhost:8080/mcp for streamable-http, http://localhost:8080/sse for sse)')
|
|
707
|
+
@click.option('--transport',
|
|
708
|
+
type=click.Choice(['sse', 'stdio', 'streamable-http']),
|
|
709
|
+
default='streamable-http',
|
|
710
|
+
show_default=True,
|
|
711
|
+
help='Type of client to use (default: streamable-http, backwards compatible with sse)')
|
|
712
|
+
@click.option('--command', help='For stdio: The command to run (e.g. mcp-server)')
|
|
713
|
+
@click.option('--args', help='For stdio: Additional arguments for the command (space-separated)')
|
|
714
|
+
@click.option('--env', help='For stdio: Environment variables in KEY=VALUE format (space-separated)')
|
|
715
|
+
@click.option('--json-args', default=None, help='Pass tool args as a JSON object string')
|
|
716
|
+
def mcp_client_tool_call(tool_name: str,
|
|
717
|
+
direct: bool,
|
|
718
|
+
url: str | None,
|
|
719
|
+
transport: str,
|
|
720
|
+
command: str | None,
|
|
721
|
+
args: str | None,
|
|
722
|
+
env: str | None,
|
|
723
|
+
json_args: str | None) -> None:
|
|
724
|
+
"""Call an MCP tool by name with optional JSON arguments.
|
|
725
|
+
|
|
726
|
+
Validates transport parameters, parses ``--json-args`` into a dictionary,
|
|
727
|
+
invokes the tool (either directly or via the function group), and prints
|
|
728
|
+
the resulting output to stdout. Errors are formatted consistently with
|
|
729
|
+
other MCP CLI commands.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
tool_name (str): Name of the tool to call.
|
|
733
|
+
direct (bool): If True, bypass WorkflowBuilder and use the direct MCP client.
|
|
734
|
+
url (str | None): For ``sse`` or ``streamable-http`` transports, the server URL.
|
|
735
|
+
transport (str): One of ``'stdio'``, ``'sse'``, or ``'streamable-http'``.
|
|
736
|
+
command (str | None): For ``stdio`` transport, the command to execute.
|
|
737
|
+
args (str | None): For ``stdio`` transport, space-separated command arguments.
|
|
738
|
+
env (str | None): For ``stdio`` transport, space-separated ``KEY=VALUE`` pairs.
|
|
739
|
+
json_args (str | None): JSON object string with tool arguments (e.g. '{"q": "hello"}').
|
|
740
|
+
|
|
741
|
+
Examples:
|
|
742
|
+
nat mcp client tool call echo --json-args '{"text": "Hello"}'
|
|
743
|
+
nat mcp client tool call search --direct --url http://localhost:9901/mcp \
|
|
744
|
+
--json-args '{"query": "NVIDIA"}'
|
|
745
|
+
nat mcp client tool call run --transport stdio --command mcp-server \
|
|
746
|
+
--args "--flag1 --flag2" --env "ENV1=V1 ENV2=V2" --json-args '{}'
|
|
747
|
+
"""
|
|
748
|
+
# Validate transport args
|
|
749
|
+
if not validate_transport_cli_args(transport, command, args, env):
|
|
750
|
+
return
|
|
751
|
+
|
|
752
|
+
# Parse stdio params
|
|
753
|
+
stdio_args = args.split() if args else []
|
|
754
|
+
stdio_env = dict(var.split('=', 1) for var in env.split()) if env else None
|
|
755
|
+
|
|
756
|
+
# Parse tool args
|
|
757
|
+
arg_obj: dict[str, Any] = {}
|
|
758
|
+
if json_args:
|
|
759
|
+
try:
|
|
760
|
+
parsed = json.loads(json_args)
|
|
761
|
+
if not isinstance(parsed, dict):
|
|
762
|
+
click.echo("[ERROR] --json-args must be a JSON object", err=True)
|
|
763
|
+
return
|
|
764
|
+
arg_obj.update(parsed)
|
|
765
|
+
except json.JSONDecodeError as e:
|
|
766
|
+
click.echo(f"[ERROR] Failed to parse --json-args: {e}", err=True)
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
try:
|
|
770
|
+
output = asyncio.run(
|
|
771
|
+
call_tool_and_print(
|
|
772
|
+
command=command,
|
|
773
|
+
url=url,
|
|
774
|
+
tool_name=tool_name,
|
|
775
|
+
transport=transport,
|
|
776
|
+
args=stdio_args,
|
|
777
|
+
env=stdio_env,
|
|
778
|
+
tool_args=arg_obj,
|
|
779
|
+
direct=direct,
|
|
780
|
+
))
|
|
781
|
+
if output:
|
|
782
|
+
click.echo(output)
|
|
783
|
+
except MCPError as e:
|
|
784
|
+
format_mcp_error(e, include_traceback=False)
|
|
785
|
+
except Exception as e:
|
|
786
|
+
click.echo(f"[ERROR] {e}", err=True)
|