Skip to content

Commit e66b9a8

Browse files
committed
Fixes CIMD validation issue
1 parent 9a992d7 commit e66b9a8

10 files changed

Lines changed: 146 additions & 12 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"metadata": {
88
"description": "Rosetta 2.0 - Enterprise knowledge management system providing AI agents with unified access to instructions, workflows, skills, and business context",
9-
"version": "2.0.1"
9+
"version": "2.0.2"
1010
},
1111
"plugins": [
1212
{

.cursor-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"metadata": {
88
"description": "Rosetta 2.0 - Enterprise knowledge management system providing AI agents with unified access to instructions, workflows, skills, and business context",
9-
"version": "2.0.1"
9+
"version": "2.0.2"
1010
},
1111
"plugins": [
1212
{

DEVELOPER_GUIDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ venv/bin/pip install -r requirements.txt
274274
git config core.hooksPath .githooks
275275
```
276276

277+
Git does not automatically use the repository's `.githooks/` directory.
278+
Each developer must run `git config core.hooksPath .githooks` once in their local clone to enable the native pre-commit hook.
279+
277280
On Windows, use the matching root-venv interpreter and pip executable:
278281

279282
```powershell

agents/IMPLEMENTATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ For detailed change history, use git history and PRs instead of expanding this f
3434
- Added HTTP transport support on top of the existing `stdio` mode.
3535
- Added Redis-backed session and plan storage with in-memory fallbacks for local development.
3636
- Added OAuth/OIDC integration for HTTP deployments, including introspection-based validation and offline-refresh handling.
37+
- Added a FastMCP loopback redirect compatibility patch so CIMD-based OAuth clients using ephemeral localhost callback ports can complete HTTP authentication.
3738
- Added origin validation and cross-tool hardening around invalid inputs, malformed requests, and wrapper failures.
3839
- Added response-shape and schema cleanup so tool contracts are more predictable for coding agents.
3940

agents/MEMORY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ If an upstream composite action exposes a path override for an internal runtime
1919
### Clear Live Auth Environment Triggers In Unit Test Fixtures [ACTIVE]
2020
When env vars can trigger real authentication, add an autouse fixture that strips them so CI and local unit suites never reach shared services by accident.
2121

22+
### Approved Workaround Shape Wins Over Narrower Substitutions [ACTIVE]
23+
When a user explicitly approves a concrete workaround implementation shape, execute that shape or ask before deviating; do not silently replace it with a “safer” variant.
24+
2225
## What Worked
2326

2427
### Inspecting Upstream `action.yml` With `gh api` Separates Repo Fixes From Upstream Limits [ACTIVE]
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""FastMCP workarounds for loopback redirect URI validation.
2+
3+
This module applies a runtime monkey patch for FastMCP redirect validation so
4+
loopback callbacks can use ephemeral ports during OAuth flows.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import importlib
10+
import logging
11+
import sys
12+
from typing import TypeVar
13+
from urllib.parse import urlparse
14+
15+
logger = logging.getLogger(__name__)
16+
17+
_PATCHED_SENTINEL = "_ims_loopback_redirect_fix_patched"
18+
_REDIRECT_VALIDATION_MODULE = "fastmcp.server.auth.redirect_validation"
19+
_MODELS_MODULE = "fastmcp.server.auth.oauth_proxy.models"
20+
_LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "::1"}
21+
22+
T = TypeVar("T", bound=type)
23+
24+
25+
def with_loopback_redirect_fix(cls: T) -> T:
26+
"""Apply the approved FastMCP loopback redirect workaround once."""
27+
redirect_validation_module = sys.modules.get(_REDIRECT_VALIDATION_MODULE)
28+
if redirect_validation_module is None:
29+
try:
30+
redirect_validation_module = importlib.import_module(
31+
_REDIRECT_VALIDATION_MODULE
32+
)
33+
except ImportError:
34+
logger.warning(
35+
"loopback_redirect_fix: could not import %s; patch not applied",
36+
_REDIRECT_VALIDATION_MODULE,
37+
)
38+
return cls
39+
40+
if getattr(redirect_validation_module, _PATCHED_SENTINEL, False):
41+
logger.debug("loopback_redirect_fix: already applied")
42+
return cls
43+
44+
original_matches = getattr(redirect_validation_module, "matches_allowed_pattern", None)
45+
if original_matches is None:
46+
logger.warning(
47+
"loopback_redirect_fix: %s.matches_allowed_pattern missing; patch not applied",
48+
_REDIRECT_VALIDATION_MODULE,
49+
)
50+
return cls
51+
52+
def patched_matches_allowed_pattern(uri: str, pattern: str) -> bool:
53+
try:
54+
parsed = urlparse(uri)
55+
host = parsed.hostname
56+
if host and host.lower() in _LOOPBACK_HOSTS:
57+
uri_no_port = parsed._replace(netloc=host).geturl()
58+
pattern_parsed = urlparse(pattern)
59+
pattern_host = pattern_parsed.hostname or ""
60+
if pattern_host.lower() in _LOOPBACK_HOSTS:
61+
pattern_no_port = pattern_parsed._replace(
62+
netloc=pattern_host
63+
).geturl()
64+
return bool(original_matches(uri_no_port, pattern_no_port))
65+
except Exception:
66+
pass
67+
return bool(original_matches(uri, pattern))
68+
69+
setattr(
70+
redirect_validation_module,
71+
"matches_allowed_pattern",
72+
patched_matches_allowed_pattern,
73+
)
74+
setattr(redirect_validation_module, _PATCHED_SENTINEL, True)
75+
76+
models_module = sys.modules.get(_MODELS_MODULE)
77+
if models_module is None:
78+
try:
79+
models_module = importlib.import_module(_MODELS_MODULE)
80+
except ImportError:
81+
models_module = None
82+
83+
if models_module is not None:
84+
setattr(models_module, "matches_allowed_pattern", patched_matches_allowed_pattern)
85+
86+
logger.info(
87+
"loopback_redirect_fix: applied FastMCP localhost port-agnostic redirect matching"
88+
)
89+
return cls

ims-mcp-server/ims_mcp/auth/oauth.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from ims_mcp.config import RosettaConfig
1010

1111
from ims_mcp.auth.offline_refresh_fix import with_offline_refresh_fix
12+
from ims_mcp.auth.loopback_redirect_fix import with_loopback_redirect_fix
1213
from ims_mcp.constants import OAUTH_MODE_OIDC, TRANSPORT_HTTP
1314

1415

@@ -52,6 +53,7 @@ def build_oauth_provider(
5253
from fastmcp.server.auth.oidc_proxy import OIDCProxy
5354

5455
OIDCProxy = with_offline_refresh_fix(OIDCProxy)
56+
OIDCProxy = with_loopback_redirect_fix(OIDCProxy)
5557
extra_authorize_params: dict[str, str] | None = (
5658
{"scope": config.oauth_extra_scopes} if config.oauth_extra_scopes else None
5759
)
@@ -77,6 +79,7 @@ def build_oauth_provider(
7779
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
7880

7981
OAuthProxy = with_offline_refresh_fix(OAuthProxy)
82+
OAuthProxy = with_loopback_redirect_fix(OAuthProxy)
8083

8184
from ims_mcp.constants import INTROSPECTION_CACHE_TTL_SECONDS
8285

ims-mcp-server/tests/test_oauth.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Unit tests for the OAuth provider builder."""
22

3+
import pytest
34
from unittest.mock import patch
45

56
from ims_mcp.auth.oauth import build_oauth_provider
@@ -56,14 +57,12 @@ def test_raises_when_oauth_incomplete():
5657
oauth_authorization_endpoint="https://kc.example.com/auth",
5758
# missing token/introspection/client_id/client_secret
5859
)
59-
import pytest
6060
with pytest.raises(ValueError, match="requires OAuth configuration"):
6161
build_oauth_provider(cfg)
6262

6363

6464
def test_raises_when_all_empty():
6565
cfg = _make_config(transport="http")
66-
import pytest
6766
with pytest.raises(ValueError, match="requires OAuth configuration"):
6867
build_oauth_provider(cfg)
6968

@@ -373,3 +372,45 @@ def test_oidc_proxy_receives_base_url():
373372
provider = build_oauth_provider(cfg)
374373
assert provider is not None
375374
assert our_url in str(provider.base_url)
375+
376+
377+
def test_loopback_redirect_fix_accepts_localhost_with_different_port():
378+
from fastmcp.server.auth.cimd import CIMDDocument
379+
from fastmcp.server.auth.oauth_proxy.models import ProxyDCRClient
380+
from pydantic import AnyHttpUrl, AnyUrl
381+
382+
build_oauth_provider(_make_full_http_config())
383+
384+
client = ProxyDCRClient(
385+
client_id="https://claude.ai/oauth/claude-code-client-metadata",
386+
client_secret=None,
387+
redirect_uris=None,
388+
cimd_document=CIMDDocument(
389+
client_id=AnyHttpUrl("https://claude.ai/oauth/claude-code-client-metadata"),
390+
redirect_uris=["http://localhost:3000/callback"],
391+
),
392+
)
393+
394+
validated = client.validate_redirect_uri(AnyUrl("http://localhost:52605/callback"))
395+
assert str(validated) == "http://localhost:52605/callback"
396+
397+
398+
def test_loopback_redirect_fix_does_not_relax_non_loopback_hosts():
399+
from fastmcp.server.auth.cimd import CIMDDocument
400+
from fastmcp.server.auth.oauth_proxy.models import ProxyDCRClient
401+
from pydantic import AnyHttpUrl, AnyUrl
402+
403+
build_oauth_provider(_make_full_http_config())
404+
405+
client = ProxyDCRClient(
406+
client_id="https://example.com/client.json",
407+
client_secret=None,
408+
redirect_uris=None,
409+
cimd_document=CIMDDocument(
410+
client_id=AnyHttpUrl("https://example.com/client.json"),
411+
redirect_uris=["https://app.example.com:3000/callback"],
412+
),
413+
)
414+
415+
with pytest.raises(Exception, match="does not match CIMD redirect_uris"):
416+
client.validate_redirect_uri(AnyUrl("https://app.example.com:52605/callback"))

plugins/core-claude/.claude-plugin/plugin.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@
4646
"capabilities": [
4747
"list_instructions",
4848
"query_instructions",
49-
"get_context_instructions",
50-
"list_project_context",
51-
"query_project_context",
52-
"store_project_context"
49+
"get_context_instructions"
5350
],
5451
"authentication": "oauth",
5552
"datasets": [

plugins/core-cursor/.cursor-plugin/plugin.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@
4444
"capabilities": [
4545
"list_instructions",
4646
"query_instructions",
47-
"get_context_instructions",
48-
"list_project_context",
49-
"query_project_context",
50-
"store_project_context"
47+
"get_context_instructions"
5148
],
5249
"authentication": "oauth",
5350
"datasets": [

0 commit comments

Comments
 (0)