lime-mcp-server-sdk 0.2.0__tar.gz

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,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ quality:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ - name: Install
26
+ run: pip install -e ".[dev]"
27
+ - name: Ruff
28
+ if: matrix.python-version == '3.12'
29
+ run: ruff check src tests
30
+ - name: Mypy
31
+ if: matrix.python-version == '3.12'
32
+ run: mypy src/lime_mcp_server
33
+ - name: Pytest
34
+ run: pytest --cov=lime_mcp_server --cov-fail-under=100
@@ -0,0 +1,22 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+ - run: pip install build
21
+ - run: python -m build
22
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .coverage
7
+ htmlcov/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .pytest_cache/
11
+ .venv/
@@ -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.
@@ -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,85 @@
1
+ # lime-mcp-server-sdk
2
+
3
+ JWT verification for **LIME MCP resource servers** ([ADR 0081](https://github.com/Mawyxx/LIME)).
4
+
5
+ Install:
6
+
7
+ ```bash
8
+ pip install lime-mcp-server-sdk
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from lime_mcp_server import TokenVerifier
15
+
16
+ verifier = TokenVerifier() # defaults: https://lime.pics, aud=mcp
17
+ result = verifier.verify(bearer_token)
18
+ if result.is_valid:
19
+ agent_uuid = result.agent_id # alias for claims["sub"]
20
+ ```
21
+
22
+ MCP OAuth JWT identity is claim **`sub`** (UUID). There is no separate `agent_id` claim.
23
+
24
+ ## Environment variables
25
+
26
+ | Variable | Default | Description |
27
+ |----------|---------|-------------|
28
+ | `LIME_BASE_URL` | `https://lime.pics` | LIME origin for OAuth metadata + JWKS |
29
+ | `LIME_OAUTH_AUDIENCE` | `mcp` | Expected JWT `aud` |
30
+ | `LIME_JWKS_CACHE_TTL_SECONDS` | `3600` | Metadata + JWKS cache TTL |
31
+ | `LIME_JWT_VERIFY_LEEWAY_SECONDS` | `120` | Clock skew leeway |
32
+ | `LIME_JWKS_MIN_REFRESH_SECONDS` | `60` | Min interval between forced JWKS refresh |
33
+
34
+ ## Development
35
+
36
+ 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).
37
+
38
+ ```bash
39
+ cd sdk/lime-mcp-server-sdk
40
+ pip install -e ".[dev]"
41
+ ruff check src tests
42
+ mypy src/lime_mcp_server
43
+ pytest --cov=lime_mcp_server --cov-fail-under=100
44
+ ```
45
+
46
+ Live integration (optional):
47
+
48
+ ```bash
49
+ LIME_MCP_SERVER_INTEGRATION=1 LIME_AGENT_TOKEN=at_... pytest tests/integration/ -v
50
+ ```
51
+
52
+ ## Publish (standalone repo)
53
+
54
+ From monorepo workspace (after local QA):
55
+
56
+ ```bash
57
+ cd sdk/lime-mcp-server-sdk
58
+ git push -u origin main
59
+ git tag v0.2.0
60
+ git push origin v0.2.0
61
+ ```
62
+
63
+ GitHub Actions on tag `v*` publishes to PyPI. One-time setup:
64
+
65
+ 1. Create project `lime-mcp-server-sdk` on [pypi.org](https://pypi.org/manage/projects/)
66
+ 2. PyPI → **Publishing** → **Add a new pending publisher**:
67
+ - Owner: `Mawyxx`, repo: `lime-mcp-server-sdk`, workflow: `publish.yml`, environment: `pypi`
68
+ 3. GitHub repo → **Settings → Environments** → create **`pypi`**
69
+ 4. Push tag: `git push origin v0.2.0` (or re-tag and force-push)
70
+
71
+ Until PyPI is live, install from GitHub:
72
+
73
+ ```bash
74
+ pip install "lime-mcp-server-sdk @ git+https://github.com/Mawyxx/lime-mcp-server-sdk.git@v0.2.0"
75
+ ```
76
+
77
+ ## Changelog
78
+
79
+ ### 0.2.0
80
+
81
+ - Remove framework adapters (`LimeMcpTokenVerifier`, `[mcp]` extra). Core-only wheel.
82
+
83
+ ### 0.1.0
84
+
85
+ - Initial release: `TokenVerifier`, `TokenValidationResult`, JWKS cache.
@@ -0,0 +1,79 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lime-mcp-server-sdk"
7
+ version = "0.2.0"
8
+ description = "JWT verification for LIME MCP resource servers"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Mawyxx" }]
13
+ keywords = ["lime", "mcp", "jwt", "oauth", "resource-server"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "PyJWT>=2.8,<3",
27
+ "cryptography>=42.0,<45",
28
+ "httpx>=0.27,<0.28",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=8.0,<9.0",
34
+ "pytest-cov>=5.0,<6.0",
35
+ "ruff>=0.6,<1.0",
36
+ "mypy>=1.11,<2.0",
37
+ "respx>=0.21,<0.22",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/Mawyxx/lime-mcp-server-sdk"
42
+ Repository = "https://github.com/Mawyxx/lime-mcp-server-sdk"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/lime_mcp_server"]
46
+
47
+ [tool.ruff]
48
+ line-length = 100
49
+ target-version = "py310"
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "I", "UP", "B"]
53
+
54
+ [tool.mypy]
55
+ python_version = "3.12"
56
+ mypy_path = "src"
57
+ packages = ["lime_mcp_server"]
58
+ strict = true
59
+ warn_return_any = true
60
+ warn_unused_configs = true
61
+
62
+ [tool.pytest.ini_options]
63
+ testpaths = ["tests"]
64
+ pythonpath = ["."]
65
+ addopts = "-ra"
66
+ markers = [
67
+ "integration: live LIME network tests (LIME_MCP_SERVER_INTEGRATION=1)",
68
+ ]
69
+
70
+ [tool.coverage.run]
71
+ branch = false
72
+ source = ["lime_mcp_server"]
73
+
74
+ [tool.coverage.report]
75
+ fail_under = 100
76
+ show_missing = true
77
+ exclude_lines = [
78
+ "pragma: no cover",
79
+ ]
@@ -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