mcp2cli 0.1.0__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.
- mcp2cli/__init__.py +0 -0
- mcp2cli/main.py +281 -0
- mcp2cli-0.1.0.dist-info/METADATA +107 -0
- mcp2cli-0.1.0.dist-info/RECORD +7 -0
- mcp2cli-0.1.0.dist-info/WHEEL +4 -0
- mcp2cli-0.1.0.dist-info/entry_points.txt +2 -0
- mcp2cli-0.1.0.dist-info/licenses/LICENSE +19 -0
mcp2cli/__init__.py
ADDED
|
File without changes
|
mcp2cli/main.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
9
|
+
|
|
10
|
+
from mcp.types import Tool, LoggingMessageNotification, SamplingMessage
|
|
11
|
+
from mcp.client.session import ClientSession
|
|
12
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
13
|
+
from mcp.client.stdio import stdio_client, StdioServerParameters
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MCPCLI:
|
|
20
|
+
"""
|
|
21
|
+
A CLI for MCP servers, dynamically generating commands from tool definitions.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config_path: str = "mcp.json"):
|
|
25
|
+
"""
|
|
26
|
+
Initializes the MCPCLI, loading server configurations and setting up the Typer app.
|
|
27
|
+
"""
|
|
28
|
+
self.config_path = config_path
|
|
29
|
+
self.servers: Dict[str, Dict[str, Any]] = {}
|
|
30
|
+
self.tools: Dict[str, List[str]] = {} # server_name -> list of tool_names
|
|
31
|
+
self.tool_to_servers: Dict[str, List[str]] = {} # tool_name -> list of server_names
|
|
32
|
+
self._app = typer.Typer(rich_markup_mode="rich", add_help_option=True, pretty_exceptions_show_locals=False)
|
|
33
|
+
self._app.callback(invoke_without_command=True)(self.main)
|
|
34
|
+
|
|
35
|
+
def _show_help(self):
|
|
36
|
+
"""
|
|
37
|
+
Displays all available tools from all servers in a table, grouped by server.
|
|
38
|
+
"""
|
|
39
|
+
table = Table(title="[bold]Available MCP Tools[/bold]")
|
|
40
|
+
table.add_column("Server", style="cyan", no_wrap=True)
|
|
41
|
+
table.add_column("Tool Name", style="magenta")
|
|
42
|
+
table.add_column("Description", style="green")
|
|
43
|
+
|
|
44
|
+
for server_name, tools in sorted(self.tools.items()):
|
|
45
|
+
if tools:
|
|
46
|
+
server_config = self.servers.get(server_name, {})
|
|
47
|
+
for i, tool_name in enumerate(sorted(tools)):
|
|
48
|
+
tool_def = next((t for t in server_config.get("tools", []) if t.name == tool_name), None)
|
|
49
|
+
description = tool_def.description if tool_def else ""
|
|
50
|
+
if i == 0:
|
|
51
|
+
table.add_row(f"[bold]{server_name}[/bold]", tool_name, description, end_section=True if len(tools) == 1 else False)
|
|
52
|
+
else:
|
|
53
|
+
table.add_row("", tool_name, description, end_section=True if i == len(tools) - 1 else False)
|
|
54
|
+
|
|
55
|
+
console.print(table)
|
|
56
|
+
|
|
57
|
+
def main(self, ctx: typer.Context):
|
|
58
|
+
"""
|
|
59
|
+
A CLI for MCP servers.
|
|
60
|
+
"""
|
|
61
|
+
if ctx.invoked_subcommand is None:
|
|
62
|
+
self._show_help()
|
|
63
|
+
|
|
64
|
+
async def _get_client_session(self, server_name: str):
|
|
65
|
+
server_config = self.servers[server_name]
|
|
66
|
+
|
|
67
|
+
if "url" in server_config:
|
|
68
|
+
return streamablehttp_client(server_config["url"])
|
|
69
|
+
elif "command" in server_config:
|
|
70
|
+
command_parts = server_config["command"].split()
|
|
71
|
+
command = command_parts[0]
|
|
72
|
+
args = command_parts[1:] + server_config.get("args", [])
|
|
73
|
+
env = server_config.get("env", {})
|
|
74
|
+
params = StdioServerParameters(command=command, args=args, env=env)
|
|
75
|
+
return stdio_client(params)
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(f"Unknown or invalid transport configuration for server {server_name}")
|
|
78
|
+
|
|
79
|
+
async def _load_server_tools(self, server_name: str, server_config: Dict[str, Any]):
|
|
80
|
+
session_context = None
|
|
81
|
+
try:
|
|
82
|
+
session_context = await self._get_client_session(server_name)
|
|
83
|
+
tools = []
|
|
84
|
+
if 'command' in server_config:
|
|
85
|
+
async with session_context as client_streams:
|
|
86
|
+
read, write = client_streams
|
|
87
|
+
async with ClientSession(read, write) as session:
|
|
88
|
+
await session.initialize()
|
|
89
|
+
tool_list = await session.list_tools()
|
|
90
|
+
tools = tool_list.tools
|
|
91
|
+
else:
|
|
92
|
+
async with session_context as (read, write, _):
|
|
93
|
+
async with ClientSession(read, write) as session:
|
|
94
|
+
await session.initialize()
|
|
95
|
+
tool_list = await session.list_tools()
|
|
96
|
+
tools = tool_list.tools
|
|
97
|
+
|
|
98
|
+
self.servers[server_name]["tools"] = tools
|
|
99
|
+
tool_names = [tool.name for tool in tools]
|
|
100
|
+
self.tools[server_name] = tool_names
|
|
101
|
+
|
|
102
|
+
for tool in tools:
|
|
103
|
+
if tool.name not in self.tool_to_servers:
|
|
104
|
+
self.tool_to_servers[tool.name] = []
|
|
105
|
+
self.tool_to_servers[tool.name].append(server_name)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
console.print(f"Error connecting to server '{server_name}': {e}", style="bold red")
|
|
108
|
+
import traceback
|
|
109
|
+
traceback.print_exc()
|
|
110
|
+
|
|
111
|
+
def _add_tool_commands(self):
|
|
112
|
+
for tool_name, server_names in self.tool_to_servers.items():
|
|
113
|
+
first_server_name = server_names[0]
|
|
114
|
+
server_config = self.servers[first_server_name]
|
|
115
|
+
tool_def = next((t for t in server_config.get("tools", []) if t.name == tool_name), None)
|
|
116
|
+
|
|
117
|
+
if tool_def:
|
|
118
|
+
command_callback = self._create_command_callback(tool_name, tool_def, len(server_names) > 1)
|
|
119
|
+
self._app.command(name=tool_name, help=tool_def.description)(command_callback)
|
|
120
|
+
|
|
121
|
+
async def _load_all_servers(self):
|
|
122
|
+
if not os.path.exists(self.config_path):
|
|
123
|
+
console.print(f"Error: Configuration file not found at {self.config_path}", style="bold red")
|
|
124
|
+
raise typer.Exit(code=1)
|
|
125
|
+
|
|
126
|
+
with open(self.config_path, "r") as f:
|
|
127
|
+
config = json.load(f)
|
|
128
|
+
|
|
129
|
+
self.servers = config.get("mcpServers", {})
|
|
130
|
+
for name, conf in self.servers.items():
|
|
131
|
+
try:
|
|
132
|
+
await self._load_server_tools(name, conf)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
console.print(f"Failed to load server '{name}': {e}", style="bold red")
|
|
135
|
+
|
|
136
|
+
def _create_command_callback(self, tool_name: str, tool_def: Tool, needs_server_option: bool):
|
|
137
|
+
|
|
138
|
+
def callback(
|
|
139
|
+
ctx: typer.Context,
|
|
140
|
+
server_name: Optional[str] = typer.Option(None, "--server-name", help="Specify the server for the command."),
|
|
141
|
+
**kwargs
|
|
142
|
+
):
|
|
143
|
+
async def async_main():
|
|
144
|
+
chosen_server = server_name
|
|
145
|
+
if not chosen_server:
|
|
146
|
+
server_options = self.tool_to_servers.get(tool_name, [])
|
|
147
|
+
if len(server_options) == 1:
|
|
148
|
+
chosen_server = server_options[0]
|
|
149
|
+
else:
|
|
150
|
+
console.print(f"Tool '{tool_name}' is available on multiple servers: {server_options}. Please specify one with --server-name.", style="bold red")
|
|
151
|
+
raise typer.Exit(code=1)
|
|
152
|
+
|
|
153
|
+
params = {k: v for k, v in kwargs.items() if k not in ['server_name'] and v is not None}
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
server_config = self.servers[chosen_server]
|
|
157
|
+
session_context = await self._get_client_session(chosen_server)
|
|
158
|
+
|
|
159
|
+
async def process_result(session):
|
|
160
|
+
await session.initialize()
|
|
161
|
+
call_result = await session.call_tool(tool_name, params)
|
|
162
|
+
|
|
163
|
+
# If the result is async iterable (streaming)
|
|
164
|
+
if hasattr(call_result, "__aiter__"):
|
|
165
|
+
final_result = None
|
|
166
|
+
async for item in call_result:
|
|
167
|
+
if isinstance(item, LoggingMessageNotification):
|
|
168
|
+
if item.params and hasattr(item.params, 'data') and isinstance(item.params.data, str):
|
|
169
|
+
console.print(item.params.data, end="")
|
|
170
|
+
elif isinstance(item, SamplingMessage) and item.role == "assistant":
|
|
171
|
+
if hasattr(item.content, 'text'):
|
|
172
|
+
final_result = item.content.text
|
|
173
|
+
|
|
174
|
+
if final_result is not None:
|
|
175
|
+
# Try to coerce to int/float if possible
|
|
176
|
+
try:
|
|
177
|
+
result_val = int(final_result)
|
|
178
|
+
except (ValueError, TypeError):
|
|
179
|
+
try:
|
|
180
|
+
result_val = float(final_result)
|
|
181
|
+
except (ValueError, TypeError):
|
|
182
|
+
result_val = final_result
|
|
183
|
+
console.print(json.dumps({"result": result_val}))
|
|
184
|
+
else:
|
|
185
|
+
# Non-streaming result (e.g., CallToolResult)
|
|
186
|
+
result_val = None
|
|
187
|
+
# Prefer structuredContent if available
|
|
188
|
+
if hasattr(call_result, "structuredContent") and call_result.structuredContent is not None:
|
|
189
|
+
sc = call_result.structuredContent
|
|
190
|
+
if isinstance(sc, dict) and "result" in sc:
|
|
191
|
+
result_val = sc["result"]
|
|
192
|
+
else:
|
|
193
|
+
result_val = sc
|
|
194
|
+
elif hasattr(call_result, "content") and call_result.content:
|
|
195
|
+
# content may be list of TextContent objects
|
|
196
|
+
first = call_result.content[0] if isinstance(call_result.content, list) else call_result.content
|
|
197
|
+
if hasattr(first, "text"):
|
|
198
|
+
try:
|
|
199
|
+
result_val = int(first.text)
|
|
200
|
+
except (ValueError, TypeError):
|
|
201
|
+
try:
|
|
202
|
+
result_val = float(first.text)
|
|
203
|
+
except (ValueError, TypeError):
|
|
204
|
+
result_val = first.text
|
|
205
|
+
else:
|
|
206
|
+
# Fallback: the object itself might be JSON serialisable
|
|
207
|
+
result_val = call_result
|
|
208
|
+
|
|
209
|
+
console.print(json.dumps({"result": result_val}))
|
|
210
|
+
|
|
211
|
+
if 'command' in server_config:
|
|
212
|
+
async with session_context as client_streams:
|
|
213
|
+
read, write = client_streams
|
|
214
|
+
async with ClientSession(read, write) as session:
|
|
215
|
+
await process_result(session)
|
|
216
|
+
else:
|
|
217
|
+
async with session_context as (read, write, _):
|
|
218
|
+
async with ClientSession(read, write) as session:
|
|
219
|
+
await process_result(session)
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
console.print(f"Error calling tool {tool_name} on server {chosen_server}: {e}", style="bold red")
|
|
223
|
+
|
|
224
|
+
asyncio.run(async_main())
|
|
225
|
+
|
|
226
|
+
# Dynamically create the signature for Typer
|
|
227
|
+
sig_params = [
|
|
228
|
+
inspect.Parameter('ctx', inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
if needs_server_option:
|
|
232
|
+
sig_params.append(inspect.Parameter('server_name', inspect.Parameter.KEYWORD_ONLY, annotation=Optional[str], default=typer.Option(None, "--server-name", help="Specify the server for the command.")))
|
|
233
|
+
else:
|
|
234
|
+
sig_params.append(inspect.Parameter('server_name', inspect.Parameter.KEYWORD_ONLY, annotation=Optional[str], default=typer.Option(None, "--server-name", help="Specify the server for the command.", hidden=True)))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
for p_name, p_schema in tool_def.inputSchema.get("properties", {}).items():
|
|
238
|
+
schema_type = p_schema.get("type", "string")
|
|
239
|
+
if schema_type == "integer":
|
|
240
|
+
param_type = int
|
|
241
|
+
elif schema_type == "number":
|
|
242
|
+
param_type = float
|
|
243
|
+
elif schema_type == "boolean":
|
|
244
|
+
param_type = bool
|
|
245
|
+
else:
|
|
246
|
+
param_type = str
|
|
247
|
+
|
|
248
|
+
sig_params.append(
|
|
249
|
+
inspect.Parameter(
|
|
250
|
+
p_name,
|
|
251
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
252
|
+
annotation=param_type,
|
|
253
|
+
default=typer.Option(None, f"--{p_name}", help=p_schema.get("description"))
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
callback.__signature__ = inspect.Signature(parameters=sig_params)
|
|
258
|
+
return callback
|
|
259
|
+
|
|
260
|
+
async def _prepare_cli(self):
|
|
261
|
+
await self._load_all_servers()
|
|
262
|
+
self._add_tool_commands()
|
|
263
|
+
|
|
264
|
+
def run(self):
|
|
265
|
+
"""
|
|
266
|
+
Loads all servers and runs the Typer application.
|
|
267
|
+
"""
|
|
268
|
+
self._app()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def run_cli():
|
|
272
|
+
"""Initializes and runs the MCP-CLI application."""
|
|
273
|
+
cli = MCPCLI()
|
|
274
|
+
try:
|
|
275
|
+
asyncio.run(cli._prepare_cli())
|
|
276
|
+
cli.run()
|
|
277
|
+
except Exception as e:
|
|
278
|
+
console.print(f"Failed to prepare CLI: {e}", style="bold red")
|
|
279
|
+
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
run_cli()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp2cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A command-line tool to interact with Model Context Protocol (MCP) servers.
|
|
5
|
+
Project-URL: Homepage, https://github.com/PsychArch/mcp2cli
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/PsychArch/mcp2cli/issues
|
|
7
|
+
Author-email: PsychArch <psycharch@users.noreply.github.com>
|
|
8
|
+
License: Copyright (c) 2025 PsychArch
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
29
|
+
Classifier: Operating System :: OS Independent
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
32
|
+
Requires-Python: >=3.11
|
|
33
|
+
Requires-Dist: mcp[cli]>=1.10.1
|
|
34
|
+
Requires-Dist: rich>=14.0.0
|
|
35
|
+
Requires-Dist: typer>=0.16.0
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# mcp2cli
|
|
39
|
+
|
|
40
|
+
A command-line interface (CLI) for interacting with Model Context Protocol (MCP) servers. It dynamically generates CLI commands from the tools exposed by an MCP server.
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
`mcp2cli` requires a `mcp.json` file in the current directory to define available MCP servers.
|
|
45
|
+
|
|
46
|
+
**`mcp.json` format:**
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"local_server": {
|
|
52
|
+
"command": "uv",
|
|
53
|
+
"args": ["run", "python", "examples/server.py", "--transport", "stdio"]
|
|
54
|
+
},
|
|
55
|
+
"remote_server": {
|
|
56
|
+
"url": "http://127.0.0.1:8000/mcp"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `command` and `args`: For servers managed by `mcp2cli` (e.g., via `stdio`).
|
|
63
|
+
- `url`: For remotely accessible servers (e.g., via `http`).
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
**List all available tools:**
|
|
68
|
+
```bash
|
|
69
|
+
uvx mcp2cli
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Execute a tool:**
|
|
73
|
+
```bash
|
|
74
|
+
# Format: uvx mcp2cli <tool_name> [tool_arguments]
|
|
75
|
+
uvx mcp2cli sum --a 5 --b 3
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Get help for a tool:**
|
|
79
|
+
```bash
|
|
80
|
+
uvx mcp2cli <tool_name> --help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Target a specific server** (if a tool is on multiple servers):
|
|
84
|
+
```bash
|
|
85
|
+
uvx mcp2cli <tool_name> --server-name <server_name>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Example Server
|
|
89
|
+
|
|
90
|
+
The project includes an example server in `examples/server.py`.
|
|
91
|
+
|
|
92
|
+
1. **Run the HTTP server:**
|
|
93
|
+
```bash
|
|
94
|
+
uv run python examples/server.py --transport http
|
|
95
|
+
```
|
|
96
|
+
2. **Configure `mcp.json`** to connect to it:
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"http_server": { "url": "http://127.0.0.1:8000/mcp" }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
3. **Use the CLI:**
|
|
105
|
+
```bash
|
|
106
|
+
uvx mcp2cli sum --a 10 --b 20
|
|
107
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mcp2cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mcp2cli/main.py,sha256=Ti2MhiMHNuTETy58yZMRk9lSgu8BmR1iu6rFX5CS2Wg,13072
|
|
3
|
+
mcp2cli-0.1.0.dist-info/METADATA,sha256=4IfvntGqwtoj_irXX8VYUYtMMCk4o-Nme7790RoYHAg,3412
|
|
4
|
+
mcp2cli-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
mcp2cli-0.1.0.dist-info/entry_points.txt,sha256=c7zbrVmtJ_GTIUAn0FzlM_L6CYG3bTZ20dwjzDMMwcU,49
|
|
6
|
+
mcp2cli-0.1.0.dist-info/licenses/LICENSE,sha256=6seH8A9HOZgZdiqgzNQGscugTmNfrG1A5TVOLm3dy08,1053
|
|
7
|
+
mcp2cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2025 PsychArch
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|