lime-mcp-server-sdk 0.2.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.
@@ -0,0 +1,28 @@
1
+ """LIME MCP resource server SDK — JWT verification via Core JWKS."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from lime_mcp_server._cache import JwksCache
6
+ from lime_mcp_server._config import LimeConfig
7
+ from lime_mcp_server._envelope import FORBIDDEN_MCP_CLAIMS, unwrap_lime_data
8
+ from lime_mcp_server._jwt import verify_mcp_access_token
9
+ from lime_mcp_server._types import TokenValidationResult
10
+ from lime_mcp_server._verifier import TokenVerifier
11
+
12
+ __version__ = "0.2.0"
13
+
14
+ __all__ = [
15
+ "FORBIDDEN_MCP_CLAIMS",
16
+ "JwksCache",
17
+ "LimeConfig",
18
+ "TokenValidationResult",
19
+ "TokenVerifier",
20
+ "jwks_cache_ttl_seconds",
21
+ "unwrap_lime_data",
22
+ "verify_mcp_access_token",
23
+ ]
24
+
25
+
26
+ def jwks_cache_ttl_seconds() -> int:
27
+ """Default JWKS cache TTL from environment (compatibility helper)."""
28
+ return LimeConfig().cache_ttl
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import threading
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from lime_mcp_server._config import LimeConfig
12
+ from lime_mcp_server._envelope import JWKS_PATH, METADATA_PATH, unwrap_lime_data
13
+
14
+ logger = logging.getLogger("lime.mcp_server")
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class JwksSnapshot:
19
+ keys: list[dict[str, Any]]
20
+ issuer: str
21
+ fetched_at: float
22
+
23
+
24
+ class JwksCache:
25
+ """Thread-safe JWKS + OAuth metadata cache with stale fallback."""
26
+
27
+ def __init__(
28
+ self,
29
+ config: LimeConfig,
30
+ *,
31
+ http_client: httpx.Client | None = None,
32
+ ) -> None:
33
+ self._config = config
34
+ self._lock = threading.Lock()
35
+ self._snapshot: JwksSnapshot | None = None
36
+ self._last_forced_refresh_at: float = 0.0
37
+ self._owns_client = http_client is None
38
+ self._client = http_client or httpx.Client(
39
+ timeout=config.http_timeout,
40
+ trust_env=False,
41
+ headers={
42
+ "Accept": "application/json",
43
+ "User-Agent": config.user_agent,
44
+ },
45
+ )
46
+
47
+ def close(self) -> None:
48
+ if self._owns_client:
49
+ self._client.close()
50
+
51
+ def warm(self) -> None:
52
+ """Prefetch metadata and JWKS (non-fatal on failure)."""
53
+ try:
54
+ self.refresh(force=True)
55
+ except Exception:
56
+ logger.warning("JWKS cache warmup failed", exc_info=True)
57
+
58
+ def invalidate(self) -> None:
59
+ with self._lock:
60
+ self._snapshot = None
61
+
62
+ def refresh(self, *, force: bool = False) -> None:
63
+ """Refresh metadata + JWKS from LIME."""
64
+ now = time.monotonic()
65
+ with self._lock:
66
+ if force:
67
+ elapsed = now - self._last_forced_refresh_at
68
+ if elapsed < self._config.min_refresh_seconds:
69
+ if self._snapshot is not None:
70
+ return
71
+ self._last_forced_refresh_at = now
72
+ self._snapshot = self._fetch_snapshot()
73
+
74
+ def get_jwks(self, kid: str | None) -> tuple[list[dict[str, Any]], str]:
75
+ """Return JWKS keys and issuer; refresh on TTL expiry or kid mismatch."""
76
+ snapshot = self._current_snapshot()
77
+ if snapshot is None or self._is_expired(snapshot):
78
+ try:
79
+ self.refresh(force=False)
80
+ except Exception as exc:
81
+ if snapshot is not None:
82
+ logger.warning("JWKS refresh failed, using stale cache: %s", exc)
83
+ return snapshot.keys, snapshot.issuer
84
+ raise
85
+ snapshot = self._current_snapshot()
86
+ if snapshot is None:
87
+ raise RuntimeError("JWKS cache unavailable after refresh")
88
+
89
+ if kid is not None and not any(key.get("kid") == kid for key in snapshot.keys):
90
+ try:
91
+ self.refresh(force=True)
92
+ except Exception as exc:
93
+ logger.warning("JWKS kid-mismatch refresh failed: %s", exc)
94
+ if not any(key.get("kid") == kid for key in snapshot.keys):
95
+ raise
96
+ snapshot = self._current_snapshot()
97
+ if snapshot is None:
98
+ raise RuntimeError("JWKS cache unavailable after kid refresh")
99
+
100
+ return snapshot.keys, snapshot.issuer
101
+
102
+ def _current_snapshot(self) -> JwksSnapshot | None:
103
+ with self._lock:
104
+ return self._snapshot
105
+
106
+ def _is_expired(self, snapshot: JwksSnapshot) -> bool:
107
+ return (time.monotonic() - snapshot.fetched_at) >= self._config.cache_ttl
108
+
109
+ def _fetch_snapshot(self) -> JwksSnapshot:
110
+ metadata = self._fetch_metadata()
111
+ issuer = str(metadata.get("issuer", "")).strip()
112
+ if not issuer:
113
+ raise ValueError("metadata missing issuer")
114
+ jwks_uri = str(metadata.get("jwks_uri", ""))
115
+ keys = self._fetch_jwks(jwks_uri)
116
+ return JwksSnapshot(keys=keys, issuer=issuer, fetched_at=time.monotonic())
117
+
118
+ def _fetch_metadata(self) -> dict[str, Any]:
119
+ response = self._client.get(f"{self._config.base_url}{METADATA_PATH}")
120
+ if response.status_code != 200:
121
+ raise RuntimeError(f"oauth metadata HTTP {response.status_code}")
122
+ try:
123
+ body = response.json()
124
+ except ValueError as exc:
125
+ raise ValueError("metadata response must be JSON object") from exc
126
+ if not isinstance(body, dict):
127
+ raise ValueError("metadata response must be JSON object")
128
+ return unwrap_lime_data(body)
129
+
130
+ def _fetch_jwks(self, jwks_uri: str) -> list[dict[str, Any]]:
131
+ base = self._config.base_url
132
+ if jwks_uri.startswith(base):
133
+ path = jwks_uri[len(base) :]
134
+ elif jwks_uri.startswith("http"):
135
+ raise ValueError("cross-origin jwks_uri fetch not supported")
136
+ elif jwks_uri:
137
+ path = jwks_uri
138
+ else:
139
+ path = JWKS_PATH
140
+
141
+ response = self._client.get(f"{base}{path}")
142
+ if response.status_code != 200:
143
+ raise RuntimeError(f"jwks HTTP {response.status_code}")
144
+ try:
145
+ body = response.json()
146
+ except ValueError as exc:
147
+ raise ValueError("jwks response must be JSON object") from exc
148
+ if not isinstance(body, dict):
149
+ raise ValueError("jwks response must be JSON object")
150
+ data = unwrap_lime_data(body)
151
+ keys = data.get("keys")
152
+ if not isinstance(keys, list) or not keys:
153
+ raise ValueError("jwks missing keys")
154
+ return keys
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ def _env_int(name: str, default: str) -> int:
8
+ return int(os.environ.get(name, default))
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class LimeConfig:
13
+ """Configuration for MCP JWT verification against LIME Core JWKS."""
14
+
15
+ base_url: str = field(
16
+ default_factory=lambda: os.environ.get("LIME_BASE_URL", "https://lime.pics").rstrip("/"),
17
+ )
18
+ audience: str = field(
19
+ default_factory=lambda: os.environ.get("LIME_OAUTH_AUDIENCE", "mcp"),
20
+ )
21
+ cache_ttl: int = field(
22
+ default_factory=lambda: _env_int("LIME_JWKS_CACHE_TTL_SECONDS", "3600"),
23
+ )
24
+ leeway_seconds: int = field(
25
+ default_factory=lambda: _env_int("LIME_JWT_VERIFY_LEEWAY_SECONDS", "120"),
26
+ )
27
+ min_refresh_seconds: int = field(
28
+ default_factory=lambda: _env_int("LIME_JWKS_MIN_REFRESH_SECONDS", "60"),
29
+ )
30
+ allowed_algorithms: tuple[str, ...] = ("RS256",)
31
+ user_agent: str = field(
32
+ default_factory=lambda: os.environ.get("LIME_VERIFY_USER_AGENT", "curl/8.5.0"),
33
+ )
34
+ http_timeout: float = 30.0
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ METADATA_PATH = "/api/v1/modules/oauth/.well-known/oauth-authorization-server"
6
+ JWKS_PATH = "/api/v1/core/.well-known/jwks.json"
7
+
8
+ FORBIDDEN_MCP_CLAIMS = frozenset({"user_id", "passport_version", "request_id"})
9
+
10
+
11
+ def unwrap_lime_data(body: dict[str, Any]) -> dict[str, Any]:
12
+ """Unwrap LIME API envelope ``{ ok, data }``."""
13
+ if not body.get("ok"):
14
+ raise ValueError("LIME envelope not ok")
15
+ data = body.get("data")
16
+ if not isinstance(data, dict):
17
+ raise ValueError("LIME envelope missing data object")
18
+ return data
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, cast
4
+
5
+ import jwt
6
+ from jwt.algorithms import RSAAlgorithm
7
+
8
+ from lime_mcp_server._envelope import FORBIDDEN_MCP_CLAIMS
9
+
10
+
11
+ def verify_mcp_access_token(
12
+ token: str,
13
+ *,
14
+ issuer: str,
15
+ audience: str,
16
+ jwks_keys: list[dict[str, Any]],
17
+ leeway_seconds: int = 120,
18
+ allowed_algorithms: tuple[str, ...] = ("RS256",),
19
+ ) -> dict[str, Any]:
20
+ """Verify RS256 MCP access token against pre-fetched JWKS keys."""
21
+ header = jwt.get_unverified_header(token)
22
+ kid = header.get("kid")
23
+ matching = [key for key in jwks_keys if key.get("kid") == kid]
24
+ if not matching:
25
+ raise jwt.InvalidTokenError(f"no jwks key for kid={kid!r}")
26
+ public_key = cast(Any, RSAAlgorithm.from_jwk(matching[0]))
27
+ claims = jwt.decode(
28
+ token,
29
+ public_key,
30
+ algorithms=list(allowed_algorithms),
31
+ issuer=issuer,
32
+ audience=audience,
33
+ leeway=leeway_seconds,
34
+ )
35
+ for forbidden in FORBIDDEN_MCP_CLAIMS:
36
+ if forbidden in claims:
37
+ raise jwt.InvalidTokenError(f"forbidden claim: {forbidden}")
38
+ sub = claims.get("sub")
39
+ if not isinstance(sub, str) or not sub.strip():
40
+ raise jwt.InvalidTokenError("missing sub claim")
41
+ return claims
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class TokenValidationResult:
9
+ """Structured outcome of MCP JWT verification."""
10
+
11
+ is_valid: bool
12
+ claims: dict[str, Any] | None = None
13
+ error: str | None = None
14
+
15
+ @property
16
+ def agent_id(self) -> str | None:
17
+ """Agent UUID from ``sub`` claim (MCP OAuth has no separate ``agent_id`` claim)."""
18
+ if self.is_valid and self.claims:
19
+ sub = self.claims.get("sub")
20
+ if isinstance(sub, str) and sub.strip():
21
+ return sub
22
+ return None
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import jwt
6
+
7
+ from lime_mcp_server._cache import JwksCache
8
+ from lime_mcp_server._config import LimeConfig
9
+ from lime_mcp_server._jwt import verify_mcp_access_token
10
+ from lime_mcp_server._types import TokenValidationResult
11
+
12
+
13
+ class TokenVerifier:
14
+ """Verify LIME-issued MCP JWTs for external resource servers."""
15
+
16
+ def __init__(
17
+ self,
18
+ *,
19
+ base_url: str | None = None,
20
+ audience: str | None = None,
21
+ cache_ttl: int | None = None,
22
+ leeway_seconds: int | None = None,
23
+ min_refresh_seconds: int | None = None,
24
+ allowed_algorithms: tuple[str, ...] | None = None,
25
+ config: LimeConfig | None = None,
26
+ cache: JwksCache | None = None,
27
+ ) -> None:
28
+ defaults = config or LimeConfig()
29
+ self._config = LimeConfig(
30
+ base_url=(base_url or defaults.base_url).rstrip("/"),
31
+ audience=audience or defaults.audience,
32
+ cache_ttl=cache_ttl if cache_ttl is not None else defaults.cache_ttl,
33
+ leeway_seconds=(
34
+ leeway_seconds if leeway_seconds is not None else defaults.leeway_seconds
35
+ ),
36
+ min_refresh_seconds=(
37
+ min_refresh_seconds
38
+ if min_refresh_seconds is not None
39
+ else defaults.min_refresh_seconds
40
+ ),
41
+ allowed_algorithms=allowed_algorithms or defaults.allowed_algorithms,
42
+ user_agent=defaults.user_agent,
43
+ http_timeout=defaults.http_timeout,
44
+ )
45
+ self._cache = cache or JwksCache(self._config)
46
+ self._cache.warm()
47
+
48
+ @property
49
+ def config(self) -> LimeConfig:
50
+ return self._config
51
+
52
+ def verify(self, token: str) -> TokenValidationResult:
53
+ """Verify a Bearer MCP JWT and return a structured result."""
54
+ try:
55
+ kid = self._get_kid(token)
56
+ jwks_keys, issuer = self._cache.get_jwks(kid)
57
+ claims = verify_mcp_access_token(
58
+ token,
59
+ issuer=issuer,
60
+ audience=self._config.audience,
61
+ jwks_keys=jwks_keys,
62
+ leeway_seconds=self._config.leeway_seconds,
63
+ allowed_algorithms=self._config.allowed_algorithms,
64
+ )
65
+ return TokenValidationResult(is_valid=True, claims=claims, error=None)
66
+ except jwt.ExpiredSignatureError:
67
+ return TokenValidationResult(is_valid=False, error="Token expired")
68
+ except jwt.InvalidIssuerError:
69
+ return TokenValidationResult(is_valid=False, error="Invalid issuer")
70
+ except jwt.InvalidAudienceError:
71
+ return TokenValidationResult(is_valid=False, error="Invalid audience")
72
+ except jwt.InvalidTokenError as exc:
73
+ return TokenValidationResult(is_valid=False, error=f"Invalid token: {exc}")
74
+ except Exception as exc:
75
+ return TokenValidationResult(is_valid=False, error=f"Verification error: {exc}")
76
+
77
+ def refresh_cache(self) -> None:
78
+ self._cache.refresh(force=True)
79
+
80
+ def invalidate_cache(self) -> None:
81
+ self._cache.invalidate()
82
+
83
+ @staticmethod
84
+ def _get_kid(token: str) -> str | None:
85
+ try:
86
+ header = jwt.get_unverified_header(token)
87
+ kid = header.get("kid")
88
+ return str(kid) if kid is not None else None
89
+ except Exception:
90
+ return None
91
+
92
+ def close(self) -> None:
93
+ self._cache.close()
94
+
95
+ def __enter__(self) -> TokenVerifier:
96
+ return self
97
+
98
+ def __exit__(self, *_args: Any) -> None:
99
+ self.close()
File without changes
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: lime-mcp-server-sdk
3
+ Version: 0.2.0
4
+ Summary: JWT verification for LIME MCP resource servers
5
+ Project-URL: Homepage, https://github.com/Mawyxx/lime-mcp-server-sdk
6
+ Project-URL: Repository, https://github.com/Mawyxx/lime-mcp-server-sdk
7
+ Author: Mawyxx
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: jwt,lime,mcp,oauth,resource-server
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: cryptography<45,>=42.0
22
+ Requires-Dist: httpx<0.28,>=0.27
23
+ Requires-Dist: pyjwt<3,>=2.8
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy<2.0,>=1.11; extra == 'dev'
26
+ Requires-Dist: pytest-cov<6.0,>=5.0; extra == 'dev'
27
+ Requires-Dist: pytest<9.0,>=8.0; extra == 'dev'
28
+ Requires-Dist: respx<0.22,>=0.21; extra == 'dev'
29
+ Requires-Dist: ruff<1.0,>=0.6; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # lime-mcp-server-sdk
33
+
34
+ JWT verification for **LIME MCP resource servers** ([ADR 0081](https://github.com/Mawyxx/LIME)).
35
+
36
+ Install:
37
+
38
+ ```bash
39
+ pip install lime-mcp-server-sdk
40
+ ```
41
+
42
+ ## Quick start
43
+
44
+ ```python
45
+ from lime_mcp_server import TokenVerifier
46
+
47
+ verifier = TokenVerifier() # defaults: https://lime.pics, aud=mcp
48
+ result = verifier.verify(bearer_token)
49
+ if result.is_valid:
50
+ agent_uuid = result.agent_id # alias for claims["sub"]
51
+ ```
52
+
53
+ MCP OAuth JWT identity is claim **`sub`** (UUID). There is no separate `agent_id` claim.
54
+
55
+ ## Environment variables
56
+
57
+ | Variable | Default | Description |
58
+ |----------|---------|-------------|
59
+ | `LIME_BASE_URL` | `https://lime.pics` | LIME origin for OAuth metadata + JWKS |
60
+ | `LIME_OAUTH_AUDIENCE` | `mcp` | Expected JWT `aud` |
61
+ | `LIME_JWKS_CACHE_TTL_SECONDS` | `3600` | Metadata + JWKS cache TTL |
62
+ | `LIME_JWT_VERIFY_LEEWAY_SECONDS` | `120` | Clock skew leeway |
63
+ | `LIME_JWKS_MIN_REFRESH_SECONDS` | `60` | Min interval between forced JWKS refresh |
64
+
65
+ ## Development
66
+
67
+ Monorepo workspace: `sdk/lime-mcp-server-sdk/` (gitignored). Standalone repo: [github.com/Mawyxx/lime-mcp-server-sdk](https://github.com/Mawyxx/lime-mcp-server-sdk).
68
+
69
+ ```bash
70
+ cd sdk/lime-mcp-server-sdk
71
+ pip install -e ".[dev]"
72
+ ruff check src tests
73
+ mypy src/lime_mcp_server
74
+ pytest --cov=lime_mcp_server --cov-fail-under=100
75
+ ```
76
+
77
+ Live integration (optional):
78
+
79
+ ```bash
80
+ LIME_MCP_SERVER_INTEGRATION=1 LIME_AGENT_TOKEN=at_... pytest tests/integration/ -v
81
+ ```
82
+
83
+ ## Publish (standalone repo)
84
+
85
+ From monorepo workspace (after local QA):
86
+
87
+ ```bash
88
+ cd sdk/lime-mcp-server-sdk
89
+ git push -u origin main
90
+ git tag v0.2.0
91
+ git push origin v0.2.0
92
+ ```
93
+
94
+ GitHub Actions on tag `v*` publishes to PyPI. One-time setup:
95
+
96
+ 1. Create project `lime-mcp-server-sdk` on [pypi.org](https://pypi.org/manage/projects/)
97
+ 2. PyPI → **Publishing** → **Add a new pending publisher**:
98
+ - Owner: `Mawyxx`, repo: `lime-mcp-server-sdk`, workflow: `publish.yml`, environment: `pypi`
99
+ 3. GitHub repo → **Settings → Environments** → create **`pypi`**
100
+ 4. Push tag: `git push origin v0.2.0` (or re-tag and force-push)
101
+
102
+ Until PyPI is live, install from GitHub:
103
+
104
+ ```bash
105
+ pip install "lime-mcp-server-sdk @ git+https://github.com/Mawyxx/lime-mcp-server-sdk.git@v0.2.0"
106
+ ```
107
+
108
+ ## Changelog
109
+
110
+ ### 0.2.0
111
+
112
+ - Remove framework adapters (`LimeMcpTokenVerifier`, `[mcp]` extra). Core-only wheel.
113
+
114
+ ### 0.1.0
115
+
116
+ - Initial release: `TokenVerifier`, `TokenValidationResult`, JWKS cache.
@@ -0,0 +1,12 @@
1
+ lime_mcp_server/__init__.py,sha256=heAu1DPT4eTWFHvHVhrpSVibre-WPHi-ArLDCrB5F2k,826
2
+ lime_mcp_server/_cache.py,sha256=gWusqHrEldB99DctfzuKJQ17I9gfQA9JSXqihLO-HxE,5596
3
+ lime_mcp_server/_config.py,sha256=SfLSQXuWn3Eg1E73ySk9_IhA_6i6hIijykQg--tmlnc,1122
4
+ lime_mcp_server/_envelope.py,sha256=ahXMnqZbkiSWU-XELFQ7XrN-ylBH63wWN9SRxTR4XbY,603
5
+ lime_mcp_server/_jwt.py,sha256=_VR0DP-YEDBMi0qdRjlTEVOffF7XA9-9VTjk3yh6zZ8,1269
6
+ lime_mcp_server/_types.py,sha256=XLgvwtTvBe4DUOBkIZM3YLe1B8yiTxqoqAES-IpSa1c,632
7
+ lime_mcp_server/_verifier.py,sha256=tuhzKl3KncSbGsPVHbOu8VtydwKD-RzyVbNhmcyGS2k,3603
8
+ lime_mcp_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ lime_mcp_server_sdk-0.2.0.dist-info/METADATA,sha256=bFb7Fa2LzZ0kc4gsvLp6ooVfn9TzOqDYnSrPKB3etpc,3637
10
+ lime_mcp_server_sdk-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ lime_mcp_server_sdk-0.2.0.dist-info/licenses/LICENSE,sha256=c4XnKeH8FnU_zj8Hk7EZn-q2TVtygTdCBwF8im5NIhc,1063
12
+ lime_mcp_server_sdk-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mawyxx
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.