mcpify-cli 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.
- mcpify/__init__.py +7 -0
- mcpify/cli.py +59 -0
- mcpify/generator.py +130 -0
- mcpify/server.py +72 -0
- mcpify_cli-0.1.0.dist-info/METADATA +106 -0
- mcpify_cli-0.1.0.dist-info/RECORD +9 -0
- mcpify_cli-0.1.0.dist-info/WHEEL +4 -0
- mcpify_cli-0.1.0.dist-info/entry_points.txt +2 -0
- mcpify_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
mcpify/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""mcpify — turn any OpenAPI/REST API into an MCP server in one command."""
|
|
2
|
+
|
|
3
|
+
from .generator import Operation, parse_spec
|
|
4
|
+
from .server import build_server, serve
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["Operation", "parse_spec", "build_server", "serve", "__version__"]
|
mcpify/cli.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""mcpify command-line interface.
|
|
2
|
+
|
|
3
|
+
mcpify <openapi-spec> [--base-url URL] [--auth "Header: value"]
|
|
4
|
+
mcpify list <openapi-spec> # preview the tools without serving
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import asyncio
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from .generator import parse_spec
|
|
14
|
+
from .server import serve
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="mcpify",
|
|
20
|
+
description="Turn any OpenAPI/REST API into an MCP server in one command.",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("spec", help="URL or path to an OpenAPI 3.x JSON/YAML document")
|
|
23
|
+
parser.add_argument("--base-url", help="override the server base URL from the spec")
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--auth",
|
|
26
|
+
help='auth header sent with every request, e.g. "Authorization: Bearer XYZ" '
|
|
27
|
+
"(or set MCPIFY_AUTH)",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--list",
|
|
31
|
+
action="store_true",
|
|
32
|
+
help="print the generated tools and exit (no server)",
|
|
33
|
+
)
|
|
34
|
+
return parser
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv: list[str] | None = None) -> int:
|
|
38
|
+
args = _build_parser().parse_args(argv)
|
|
39
|
+
|
|
40
|
+
if args.list:
|
|
41
|
+
base_url, operations = parse_spec(args.spec)
|
|
42
|
+
print(f"Base URL: {base_url}")
|
|
43
|
+
print(f"Generated {len(operations)} tools:\n")
|
|
44
|
+
for op in operations:
|
|
45
|
+
params = ", ".join(op.parameters) or "—"
|
|
46
|
+
print(f" • {op.name} [{op.method.upper()} {op.path}]")
|
|
47
|
+
print(f" {op.description}")
|
|
48
|
+
print(f" params: {params}")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
asyncio.run(serve(args.spec, base_url=args.base_url, auth_header=args.auth))
|
|
53
|
+
except KeyboardInterrupt:
|
|
54
|
+
return 0
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
sys.exit(main())
|
mcpify/generator.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Turn an OpenAPI spec into a list of callable MCP tools.
|
|
2
|
+
|
|
3
|
+
The generator reads an OpenAPI 3.x document, walks every operation, and produces
|
|
4
|
+
one :class:`Operation` per endpoint. Each Operation knows how to build its JSON
|
|
5
|
+
schema (for the MCP tool definition) and how to execute itself as an HTTP request.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import urljoin
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Operation:
|
|
21
|
+
"""A single API endpoint exposed as an MCP tool."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
method: str
|
|
25
|
+
path: str
|
|
26
|
+
description: str
|
|
27
|
+
# parameter name -> {"in": "query"|"path"|"header", "schema": {...}, "required": bool}
|
|
28
|
+
parameters: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
29
|
+
request_body_schema: dict[str, Any] | None = None
|
|
30
|
+
|
|
31
|
+
def input_schema(self) -> dict[str, Any]:
|
|
32
|
+
"""JSON schema describing this tool's arguments (MCP `inputSchema`)."""
|
|
33
|
+
properties: dict[str, Any] = {}
|
|
34
|
+
required: list[str] = []
|
|
35
|
+
for pname, meta in self.parameters.items():
|
|
36
|
+
properties[pname] = dict(meta.get("schema") or {"type": "string"})
|
|
37
|
+
properties[pname].setdefault("description", f"{meta['in']} parameter")
|
|
38
|
+
if meta.get("required"):
|
|
39
|
+
required.append(pname)
|
|
40
|
+
if self.request_body_schema is not None:
|
|
41
|
+
properties["body"] = self.request_body_schema
|
|
42
|
+
required.append("body")
|
|
43
|
+
return {"type": "object", "properties": properties, "required": required}
|
|
44
|
+
|
|
45
|
+
def build_request(self, base_url: str, args: dict[str, Any]) -> httpx.Request:
|
|
46
|
+
"""Construct an httpx.Request for this operation from tool arguments."""
|
|
47
|
+
path = self.path
|
|
48
|
+
query: dict[str, Any] = {}
|
|
49
|
+
headers: dict[str, Any] = {}
|
|
50
|
+
for pname, meta in self.parameters.items():
|
|
51
|
+
if pname not in args:
|
|
52
|
+
continue
|
|
53
|
+
location = meta["in"]
|
|
54
|
+
value = args[pname]
|
|
55
|
+
if location == "path":
|
|
56
|
+
path = path.replace("{" + pname + "}", str(value))
|
|
57
|
+
elif location == "query":
|
|
58
|
+
query[pname] = value
|
|
59
|
+
elif location == "header":
|
|
60
|
+
headers[pname] = str(value)
|
|
61
|
+
url = urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
|
|
62
|
+
json_body = args.get("body") if self.request_body_schema is not None else None
|
|
63
|
+
return httpx.Request(
|
|
64
|
+
self.method.upper(), url, params=query or None, headers=headers or None,
|
|
65
|
+
json=json_body,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_document(source: str) -> dict[str, Any]:
|
|
70
|
+
"""Load an OpenAPI document from a URL or a local file path."""
|
|
71
|
+
if source.startswith(("http://", "https://")):
|
|
72
|
+
text = httpx.get(source, timeout=30, follow_redirects=True).text
|
|
73
|
+
else:
|
|
74
|
+
with open(source, encoding="utf-8") as fh:
|
|
75
|
+
text = fh.read()
|
|
76
|
+
try:
|
|
77
|
+
return json.loads(text)
|
|
78
|
+
except json.JSONDecodeError:
|
|
79
|
+
return yaml.safe_load(text)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _slug(method: str, path: str) -> str:
|
|
83
|
+
"""Derive a stable, readable tool name from method + path."""
|
|
84
|
+
parts = [p for p in path.strip("/").split("/") if p]
|
|
85
|
+
cleaned = [p.strip("{}") for p in parts] or ["root"]
|
|
86
|
+
return f"{method.lower()}_{'_'.join(cleaned)}".replace("-", "_")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_spec(source: str) -> tuple[str, list[Operation]]:
|
|
90
|
+
"""Parse an OpenAPI spec into (base_url, operations).
|
|
91
|
+
|
|
92
|
+
:param source: URL or file path to an OpenAPI 3.x JSON/YAML document.
|
|
93
|
+
:returns: the inferred server base URL and the list of operations.
|
|
94
|
+
"""
|
|
95
|
+
doc = _load_document(source)
|
|
96
|
+
servers = doc.get("servers") or [{"url": "/"}]
|
|
97
|
+
base_url = servers[0].get("url", "/")
|
|
98
|
+
|
|
99
|
+
operations: list[Operation] = []
|
|
100
|
+
for path, path_item in (doc.get("paths") or {}).items():
|
|
101
|
+
shared_params = path_item.get("parameters", [])
|
|
102
|
+
for method, op in path_item.items():
|
|
103
|
+
if method.lower() not in {"get", "post", "put", "patch", "delete"}:
|
|
104
|
+
continue
|
|
105
|
+
op = op or {}
|
|
106
|
+
name = op.get("operationId") or _slug(method, path)
|
|
107
|
+
description = op.get("summary") or op.get("description") or f"{method.upper()} {path}"
|
|
108
|
+
|
|
109
|
+
parameters: dict[str, dict[str, Any]] = {}
|
|
110
|
+
for param in [*shared_params, *op.get("parameters", [])]:
|
|
111
|
+
if "name" not in param:
|
|
112
|
+
continue
|
|
113
|
+
parameters[param["name"]] = {
|
|
114
|
+
"in": param.get("in", "query"),
|
|
115
|
+
"schema": param.get("schema", {"type": "string"}),
|
|
116
|
+
"required": param.get("required", param.get("in") == "path"),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
body_schema = None
|
|
120
|
+
content = (op.get("requestBody") or {}).get("content", {})
|
|
121
|
+
if "application/json" in content:
|
|
122
|
+
body_schema = content["application/json"].get("schema", {"type": "object"})
|
|
123
|
+
|
|
124
|
+
operations.append(
|
|
125
|
+
Operation(
|
|
126
|
+
name=name, method=method, path=path, description=description,
|
|
127
|
+
parameters=parameters, request_body_schema=body_schema,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return base_url, operations
|
mcpify/server.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Run the generated tools as an MCP server over stdio.
|
|
2
|
+
|
|
3
|
+
Uses the low-level MCP Python SDK so we can register tools dynamically at runtime
|
|
4
|
+
(one per OpenAPI operation) rather than via decorators known at import time.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from mcp.server import Server
|
|
15
|
+
from mcp.server.stdio import stdio_server
|
|
16
|
+
from mcp.types import TextContent, Tool
|
|
17
|
+
|
|
18
|
+
from .generator import Operation, parse_spec
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_server(source: str, base_url: str | None = None, auth_header: str | None = None) -> Server:
|
|
22
|
+
"""Create an MCP Server exposing every operation in the OpenAPI spec as a tool.
|
|
23
|
+
|
|
24
|
+
:param source: URL or path to the OpenAPI document.
|
|
25
|
+
:param base_url: override the server URL inferred from the spec.
|
|
26
|
+
:param auth_header: optional ``"Header: value"`` sent with every request
|
|
27
|
+
(defaults to the ``MCPIFY_AUTH`` env var). Useful for API keys / bearer tokens.
|
|
28
|
+
"""
|
|
29
|
+
inferred_base, operations = parse_spec(source)
|
|
30
|
+
effective_base = base_url or inferred_base
|
|
31
|
+
by_name: dict[str, Operation] = {op.name: op for op in operations}
|
|
32
|
+
|
|
33
|
+
auth_header = auth_header or os.environ.get("MCPIFY_AUTH")
|
|
34
|
+
default_headers: dict[str, str] = {}
|
|
35
|
+
if auth_header and ":" in auth_header:
|
|
36
|
+
key, _, value = auth_header.partition(":")
|
|
37
|
+
default_headers[key.strip()] = value.strip()
|
|
38
|
+
|
|
39
|
+
server = Server("mcpify")
|
|
40
|
+
|
|
41
|
+
@server.list_tools()
|
|
42
|
+
async def list_tools() -> list[Tool]:
|
|
43
|
+
return [
|
|
44
|
+
Tool(name=op.name, description=op.description, inputSchema=op.input_schema())
|
|
45
|
+
for op in operations
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
@server.call_tool()
|
|
49
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
50
|
+
op = by_name.get(name)
|
|
51
|
+
if op is None:
|
|
52
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
53
|
+
request = op.build_request(effective_base, arguments or {})
|
|
54
|
+
for header, value in default_headers.items():
|
|
55
|
+
request.headers.setdefault(header, value)
|
|
56
|
+
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
|
|
57
|
+
response = await client.send(request)
|
|
58
|
+
try:
|
|
59
|
+
payload = json.dumps(response.json(), indent=2)
|
|
60
|
+
except Exception:
|
|
61
|
+
payload = response.text
|
|
62
|
+
prefix = f"HTTP {response.status_code} {request.method} {request.url}\n"
|
|
63
|
+
return [TextContent(type="text", text=prefix + payload)]
|
|
64
|
+
|
|
65
|
+
return server
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def serve(source: str, base_url: str | None = None, auth_header: str | None = None) -> None:
|
|
69
|
+
"""Run the MCP server over stdio until the client disconnects."""
|
|
70
|
+
server = build_server(source, base_url=base_url, auth_header=auth_header)
|
|
71
|
+
async with stdio_server() as (read, write):
|
|
72
|
+
await server.run(read, write, server.create_initialization_options())
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcpify-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Turn any OpenAPI/REST API into an MCP server in one command.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Amanbig/mcpify
|
|
6
|
+
Project-URL: Issues, https://github.com/Amanbig/mcpify/issues
|
|
7
|
+
Author: Amanpreet Singh
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai-agents,claude,mcp,model-context-protocol,openapi,rest
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: httpx>=0.27
|
|
13
|
+
Requires-Dist: mcp>=1.2.0
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# mcpify
|
|
18
|
+
|
|
19
|
+
**Turn any REST API into an MCP server in one command.** Point `mcpify` at an OpenAPI
|
|
20
|
+
spec and every endpoint becomes a tool your AI agent (Claude, Cursor, Windsurf, …) can
|
|
21
|
+
call — no glue code, no per-endpoint wrappers.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mcpify https://api.example.com/openapi.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That's it. Every operation in the spec is now a live MCP tool.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
[MCP](https://modelcontextprotocol.io) is how AI agents call real tools. But wiring an
|
|
34
|
+
existing API into MCP means hand-writing a tool wrapper for every endpoint — tedious and
|
|
35
|
+
stale the moment the API changes. Almost every API already publishes an **OpenAPI spec**.
|
|
36
|
+
`mcpify` reads that spec and generates the whole MCP server at runtime, so:
|
|
37
|
+
|
|
38
|
+
- **Zero per-endpoint code** — N endpoints → N tools, automatically.
|
|
39
|
+
- **Always in sync** — regenerate from the spec; no wrappers to maintain.
|
|
40
|
+
- **Auth-aware** — pass an API key / bearer token once, applied to every call.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pipx install mcpify-cli # or: uvx mcpify-cli ...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Use
|
|
49
|
+
|
|
50
|
+
Preview the tools a spec produces (no server):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
mcpify --list examples/petstore-mini.yaml
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run it as an MCP server (stdio):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
mcpify https://petstore3.swagger.io/api/v3/openapi.json
|
|
60
|
+
mcpify ./openapi.yaml --base-url https://staging.internal.api
|
|
61
|
+
mcpify ./openapi.yaml --auth "Authorization: Bearer $TOKEN" # or set MCPIFY_AUTH
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Plug it into Claude
|
|
65
|
+
|
|
66
|
+
Add to your MCP client config (Claude Desktop / Claude Code `mcp.json`):
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"petstore": {
|
|
72
|
+
"command": "mcpify",
|
|
73
|
+
"args": ["https://petstore3.swagger.io/api/v3/openapi.json"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Restart the client and ask: *"list the available pets"* — Claude calls the API directly.
|
|
80
|
+
|
|
81
|
+
## How it works
|
|
82
|
+
|
|
83
|
+
1. Load the OpenAPI 3.x document (URL or file, JSON or YAML).
|
|
84
|
+
2. Walk every path/method into an `Operation` with a generated JSON input schema.
|
|
85
|
+
3. Serve them over MCP; each tool call is mapped to a live HTTP request (path, query,
|
|
86
|
+
header params and JSON bodies all handled) and the response is returned to the agent.
|
|
87
|
+
|
|
88
|
+
## Supported
|
|
89
|
+
|
|
90
|
+
- OpenAPI 3.x, JSON or YAML, from a URL or local file
|
|
91
|
+
- `GET` / `POST` / `PUT` / `PATCH` / `DELETE`
|
|
92
|
+
- Path, query, and header parameters; JSON request bodies
|
|
93
|
+
- A single global auth header (API key or bearer token)
|
|
94
|
+
|
|
95
|
+
## Roadmap
|
|
96
|
+
|
|
97
|
+
- Per-operation filtering (`--only`, `--tag`) to expose a subset of a large API
|
|
98
|
+
- `$ref` resolution for fully-expanded body schemas
|
|
99
|
+
- Swagger 2.0 specs
|
|
100
|
+
- SSE / HTTP transport in addition to stdio
|
|
101
|
+
|
|
102
|
+
PRs welcome.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
mcpify/__init__.py,sha256=kY2ZrUkiYuyisi8YhlLwAvngLmzP8Y_wraQQLJO3lYY,265
|
|
2
|
+
mcpify/cli.py,sha256=B-t4SijrVmUhCGFeqKEjvawAd70DzEYTWupY-SaMrVU,1756
|
|
3
|
+
mcpify/generator.py,sha256=N08Ag3IhpCDfWj1ogN6gVLGJbbDlbrscOFIxCAGRd-I,5189
|
|
4
|
+
mcpify/server.py,sha256=S4Sp0fBVe11HT56nnGOFBnwlEKyI7E41wunR_orIhTg,2854
|
|
5
|
+
mcpify_cli-0.1.0.dist-info/METADATA,sha256=BKyEOyCRMxpi1Jmw_tHXt1IcbkEoM8E7POGyX-EfLWE,3050
|
|
6
|
+
mcpify_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
mcpify_cli-0.1.0.dist-info/entry_points.txt,sha256=0Fwp_BJIUQ83w1fvqzN7tq9IC-AQoy5fNa4xIE2jyaI,43
|
|
8
|
+
mcpify_cli-0.1.0.dist-info/licenses/LICENSE,sha256=JvzcQW9bofgZRff1cvwH2Pvn-UMecsIJcfSBXyZJRb0,1072
|
|
9
|
+
mcpify_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amanpreet Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|