Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
f28adf3
Remove the call to _clear_tool_defs_cache (doesn't appear to be neede…
dagardner-nv Aug 5, 2026
793e2fe
Use to_thread instead
dagardner-nv Aug 5, 2026
af52bf6
Bump minimum version of hermes-agent
dagardner-nv Aug 5, 2026
e8518ee
Update claude, codex and deepagents to handle args and env
dagardner-nv Aug 5, 2026
bc7a137
Update tests
dagardner-nv Aug 5, 2026
65c8ec9
Add E2E tests
dagardner-nv Aug 5, 2026
7bdd1c9
Ensure values for MCP transport
dagardner-nv Aug 5, 2026
ad66d64
Allow setting custom http headers for MCP
dagardner-nv Aug 6, 2026
080874f
First pass at auth
dagardner-nv Aug 6, 2026
da50ceb
Move the shutdown_mcp_servers call to a thread
dagardner-nv Aug 6, 2026
68fb796
Map the MCP Auth class into Python
dagardner-nv Aug 6, 2026
d46197f
Support MCP auth in adapters
dagardner-nv Aug 6, 2026
a4a805d
Consolidate common MCP Auth code
dagardner-nv Aug 6, 2026
3fa778c
Merge branch 'main' of github.com:NVIDIA/NeMo-Fabric into david-mcp-i…
dagardner-nv Aug 7, 2026
7d0e008
Remove shlex parsing
dagardner-nv Aug 7, 2026
36b23d9
Update add_mcp_server call
dagardner-nv Aug 7, 2026
ea2204f
Remove unused imports
dagardner-nv Aug 7, 2026
11d631c
Update tests and readme to match command handling
dagardner-nv Aug 7, 2026
001bcf4
Merge branch 'david-mcp-improvements' into david-mcp-auth
dagardner-nv Aug 7, 2026
af48f4b
Merge branch 'david-mcp-improvements' of github.com:dagardner-nv/nemo…
dagardner-nv Aug 7, 2026
9db4ae3
Update docs
dagardner-nv Aug 7, 2026
b77756a
NAT informed MCP auth improvements
dagardner-nv Aug 7, 2026
c0f6f6b
Merge branch 'main' of github.com:NVIDIA/NeMo-Fabric into david-mcp-i…
dagardner-nv Aug 7, 2026
2558adb
Merge branch 'david-mcp-improvements' of github.com:dagardner-nv/nemo…
dagardner-nv Aug 7, 2026
556d4e5
Cleanups
dagardner-nv Aug 7, 2026
8faaec5
Fix handling of localhost and 127.0.0.1
dagardner-nv Aug 7, 2026
58e3dfe
Merge branch 'main' of github.com:NVIDIA/NeMo-Fabric into david-mcp-auth
dagardner-nv Aug 10, 2026
2b9a15e
Update schema
dagardner-nv Aug 10, 2026
1e9552f
Resolve dependency conflict
dagardner-nv Aug 10, 2026
b8c49c6
Update api docs
dagardner-nv Aug 10, 2026
bbe79c8
Update tests
dagardner-nv Aug 10, 2026
c4eefaa
Remove validate_stdio_options as it is unneeded
dagardner-nv Aug 10, 2026
b241a2b
Fix windows spexific test error
dagardner-nv Aug 10, 2026
c27c2fb
WIP
dagardner-nv Aug 10, 2026
e193684
Ensure stdio auth isn't leaked
dagardner-nv Aug 10, 2026
556486a
Avoid out of date terminology
dagardner-nv Aug 10, 2026
fa808b5
fix: replace claude mcp login subprocess with in-process HTTP/SSE OAu…
dagardner-nv Aug 10, 2026
b10a71f
Use McpTransport for the McpServerPlan.transport field
dagardner-nv Aug 10, 2026
32b6e14
Validate the MCP transport field
dagardner-nv Aug 10, 2026
32fc0e6
Fix auth for claude
dagardner-nv Aug 10, 2026
70f4ab3
Update test
dagardner-nv Aug 11, 2026
d856e12
Merge branch 'main' of github.com:NVIDIA/NeMo-Fabric into david-mcp-auth
dagardner-nv Aug 11, 2026
669e389
Cleanup normalize_custom_headers
dagardner-nv Aug 11, 2026
c850c05
Fix merge error
dagardner-nv Aug 11, 2026
d16d5e3
Clip the oath timeout to the invokation timeout
dagardner-nv Aug 11, 2026
551d334
Ensure callbacks are closed in stop method
dagardner-nv Aug 11, 2026
d3585dc
wrap mcp related blocking calls in asyncio.to_thread to avoid blockin…
dagardner-nv Aug 11, 2026
1541f5c
set deny_unknown_fields
dagardner-nv Aug 11, 2026
d8b614f
Track auth on a per-mcp basis
dagardner-nv Aug 11, 2026
7c934a2
Track the timeout
dagardner-nv Aug 11, 2026
fa730b6
Ensure custom_headers in the Rust models is reflected in the Python m…
dagardner-nv Aug 11, 2026
72b08b7
Fix credential serialization
dagardner-nv Aug 11, 2026
55454ac
Merge branch 'main' of github.com:NVIDIA/NeMo-Fabric into david-mcp-auth
dagardner-nv Aug 11, 2026
7192332
Merge branch 'main' of github.com:NVIDIA/NeMo-Fabric into david-mcp-auth
dagardner-nv Aug 11, 2026
327c675
Fix merge error
dagardner-nv Aug 11, 2026
025213a
Merge branch 'main' of github.com:NVIDIA/NeMo-Fabric into david-mcp-auth
dagardner-nv Aug 11, 2026
087235e
First pass at only supporting oauth in the two adapters that support it
dagardner-nv Aug 11, 2026
1abaf94
Move the remaining code in mcp_auth.py into utils.py
dagardner-nv Aug 12, 2026
5a94add
Remove un-needed changes
dagardner-nv Aug 12, 2026
8effc0c
Remove un-needed changes
dagardner-nv Aug 12, 2026
a304b0e
Improve http header validation
dagardner-nv Aug 12, 2026
4505120
Fix cursor pagination issue per CR feedback
dagardner-nv Aug 12, 2026
812f9ef
Addressing CR feedback
dagardner-nv Aug 12, 2026
612c136
Update adapters/hermes/src/nemo_fabric_adapters/hermes/configuration.py
dagardner-nv Aug 12, 2026
61a5db9
Allow timeout to be configured
dagardner-nv Aug 12, 2026
01de7c1
Add an as_str method to mcp transport class
dagardner-nv Aug 12, 2026
8c149af
Fix comment
dagardner-nv Aug 12, 2026
3a7baaf
Add just recipe for building schemas
dagardner-nv Aug 12, 2026
144ca53
Update documentation/skills to include the just recipe
dagardner-nv Aug 12, 2026
46d03ff
Update docs
dagardner-nv Aug 12, 2026
76e1e0e
Require authorization_timeout_seconds>0
dagardner-nv Aug 12, 2026
9aaf4b1
Distinguish between timeout errors
dagardner-nv Aug 12, 2026
a9d0660
Include mcp server name and header name in error messages
dagardner-nv Aug 12, 2026
dda5ff6
Just use just
dagardner-nv Aug 12, 2026
86b29c7
Remove header variable expansion in adapters
dagardner-nv Aug 12, 2026
5d689ac
Fix claude environment variable handling
dagardner-nv Aug 12, 2026
3ea57cb
Catch any exception raised from validate_http_headers
dagardner-nv Aug 12, 2026
f2dbad0
Only use McpServerConfig.env for stdio
dagardner-nv Aug 12, 2026
6e1fe61
Document MCP auth
dagardner-nv Aug 12, 2026
b68a3a4
Forbid extras in McpAuthenticationConfig
dagardner-nv Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/contribute-adapter/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ uv sync --group adapter-tests
uv run --no-sync pytest tests/adapters/test_<name>*.py
just test-python
just lock-python && just wheels
cargo run -p nemo-fabric-core --example generate-schemas -- schemas
just schemas
cargo fmt --all -- --check && just test-rust
just docs
uv run pre-commit run --all-files --show-diff-on-failure
Expand Down
12 changes: 12 additions & 0 deletions adapter-contract/src/nemo_fabric_adapter_contract/codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ def encode_dataclass(instance: Any) -> dict[str, Any]:
continue
if item.metadata.get("omit_empty") and not value:
continue
if "omit_default" in item.metadata and value == item.metadata["omit_default"]:
continue
result[item.name] = _encode_value(value, path=(item.name,))
return result

Expand All @@ -187,6 +189,16 @@ def _decode_value(
if type(None) in arguments and value is None:
return None
options = tuple(option for option in arguments if option is not type(None))
if isinstance(value, Mapping) and "type" in value:
tagged_options = [
option
for option in options
if isinstance(option, type)
and get_origin(_resolved_type_hints(option).get("type")) is Literal
and value["type"] in get_args(_resolved_type_hints(option)["type"])
]
if len(tagged_options) == 1:
return _decode_value(tagged_options[0], value, path=path)
errors = []
for option in options:
try:
Expand Down
112 changes: 112 additions & 0 deletions adapter-contract/src/nemo_fabric_adapter_contract/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ def _empty_list():
return field(default_factory=list, metadata={"omit_empty": True})


def _default(value: Any):
return field(default=value, metadata={"omit_default": value})


def _json_value_field(*, default: Any = MISSING, omit_empty: bool = False):
metadata = {"json": True}
if omit_empty:
Expand Down Expand Up @@ -210,6 +214,108 @@ def _validate_tool_names(value: list[str] | None, field_name: str, label: str) -
)


class OAuthTokenEndpointAuthMethod(StrEnum):
"""OAuth client authentication method used at the token endpoint."""

NONE = "none"
CLIENT_SECRET_POST = "client_secret_post"
CLIENT_SECRET_BASIC = "client_secret_basic"


@dataclass(slots=True, kw_only=True)
class McpOAuth2Config(ContractModel):
"""OAuth 2.0 authorization-code authentication for an MCP server."""

type: Literal["oauth2"]
client_id: str | None = _optional()
client_secret_env: str | None = _optional()
scopes: list[str] = _empty_list()
redirect_uri: str | None = _optional()
enable_dynamic_registration: bool = _default(True)
client_name: str | None = _optional()
token_endpoint_auth_method: OAuthTokenEndpointAuthMethod | None = _optional()
authorization_timeout_seconds: int = _default(300)

@property
def scope(self) -> str | None:
"""Return scopes in the space-delimited form expected by OAuth clients."""

value = " ".join(self.scopes)
return value or None

def _validate(self) -> None:
for name in ("client_id", "client_secret_env", "redirect_uri", "client_name"):
if (value := getattr(self, name)) is not None:
_nonblank(value, name)
_validate_tool_names(self.scopes, "scopes", "authentication scope")
if self.client_secret_env is not None and self.client_id is None:
raise ContractValidationError(
"requires client_id", path=("client_secret_env",)
)
if not self.enable_dynamic_registration and self.client_id is None:
raise ContractValidationError(
"is required when dynamic registration is disabled",
path=("client_id",),
)
if (
self.token_endpoint_auth_method
in {
OAuthTokenEndpointAuthMethod.CLIENT_SECRET_BASIC,
OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
}
and self.client_id is not None
and self.client_secret_env is None
):
raise ContractValidationError(
"requires client_secret_env for a pre-registered client",
path=("token_endpoint_auth_method",),
)
if (
self.token_endpoint_auth_method is OAuthTokenEndpointAuthMethod.NONE
and self.client_secret_env is not None
):
raise ContractValidationError(
"'none' cannot be combined with client_secret_env",
path=("token_endpoint_auth_method",),
)
if not 1 <= self.authorization_timeout_seconds <= (1 << 64) - 1:
raise ContractValidationError(
f"must be greater than zero and less than or equal to {(1 << 64) - 1}",
path=("authorization_timeout_seconds",),
)
Comment thread
dagardner-nv marked this conversation as resolved.


@dataclass(slots=True, kw_only=True)
class McpServiceAccountConfig(ContractModel):
"""OAuth 2.0 client-credentials authentication for an MCP server."""

type: Literal["service_account"]
client_id: str
client_secret_env: str
token_url: str
scopes: list[str] = _empty_list()
token_endpoint_auth_method: OAuthTokenEndpointAuthMethod | None = _optional()
token_cache_buffer_seconds: int = _default(300)

def _validate(self) -> None:
for name in ("client_id", "client_secret_env", "token_url"):
_nonblank(getattr(self, name), name)
_validate_tool_names(self.scopes, "scopes", "authentication scope")
if self.token_endpoint_auth_method is OAuthTokenEndpointAuthMethod.NONE:
raise ContractValidationError(
"service_account requires client_secret_basic or client_secret_post",
path=("token_endpoint_auth_method",),
)
_bounded_int(
self.token_cache_buffer_seconds,
"token_cache_buffer_seconds",
(1 << 64) - 1,
)


McpAuthenticationConfig = McpOAuth2Config | McpServiceAccountConfig


@dataclass(slots=True, kw_only=True)
class AgentMcpServerConfig(AgentContractBlock):
"""One MCP server routed to the adapter target."""
Expand All @@ -218,12 +324,18 @@ class AgentMcpServerConfig(AgentContractBlock):
url: str
args: list[str] = _empty_list()
env: dict[str, str] = _empty_dict()
authentication: McpAuthenticationConfig | None = _optional()
custom_headers: dict[str, str] = _empty_dict()
Comment thread
dagardner-nv marked this conversation as resolved.
allowed_tools: list[str] | None = _optional()
blocked_tools: list[str] = _empty_list()

def _validate(self) -> None:
_nonblank(self.transport, "transport")
_nonblank(self.url, "url")
if self.transport != "stdio" and self.env:
raise ContractValidationError(
"env is only valid for stdio transport", path=("env",)
)
_validate_tool_names(self.allowed_tools, "allowed_tools", "MCP tool")
_validate_tool_names(self.blocked_tools, "blocked_tools", "MCP tool")
if self.allowed_tools is not None:
Expand Down
Loading
Loading