Skip to content

Commit 86fc08f

Browse files
Support client-id-only MCP OAuth2 and refine Outlook auth example config (#1885)
This PR improves MCP OAuth2 manual authentication and adds as Outlook auth example configuration using static client registration. - Optional client secret in manual OAuth2 - mcp_oauth2 now allows manual mode with client_id and optional client_secret (public-client compatible). - Runtime behavior remains consistent: when client_id is provided, manual registration is used instead of dynamic registration. - Added explicit config documentation that client_id takes precedence over dynamic registration. - Makes streamable-http the default transport - Adds an Outlook MCP auth example config file as a quick reference. ## By Submitting this PR I confirm: - I am familiar with the [Contributing Guidelines](https://github.com/NVIDIA/NeMo-Agent-Toolkit/blob/develop/docs/source/resources/contributing/index.md). - We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. - Any contribution which contains commits that are not Signed-Off will not be accepted. - When the PR is ready for review, new or existing tests cover these changes. - When the PR is ready for review, the documentation is up to date with these changes. ## Summary by CodeRabbit * **New Features** * Added an Outlook OAuth2 example configuration for per-user MCP authentication and a new default LLM/workflow example. * MCP server transport now defaults to "streamable-http", reducing required configuration. * **Behavior Changes** * OAuth2 validation updated so a provided client_id takes precedence and allows manual registration without requiring a client_secret. * **Tests** * Added unit tests validating the OAuth2 validation behavior and the new transport default. Authors: - Anuradha Karuppiah (https://github.com/AnuradhaKaruppiah) Approvers: - David Gardner (https://github.com/dagardner-nv) - Yuchen Zhang (https://github.com/yczhang-nv) URL: #1885
1 parent 3e9b632 commit 86fc08f

File tree

5 files changed

+107
-12
lines changed

5 files changed

+107
-12
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
# This is a static OAuth2 example for Outlook MCP authentication.
18+
# It demonstrates manual OAuth2 (no DCR) using a pre-registered client_id.
19+
# Ensure NAT_REDIRECT_URI is registered or allowlisted for NAT_CORPORATE_MCP_OUTLOOK_CLIENT_ID,
20+
# or the OAuth flow will fail with "redirect_uri not allowed for this client".
21+
22+
function_groups:
23+
mcp_outlook:
24+
_type: per_user_mcp_client
25+
server:
26+
url: ${NAT_CORPORATE_MCP_OUTLOOK_URL}
27+
auth_provider: mcp_oauth2_outlook
28+
29+
authentication:
30+
mcp_oauth2_outlook:
31+
_type: mcp_oauth2
32+
server_url: ${NAT_CORPORATE_MCP_OUTLOOK_URL}
33+
redirect_uri: ${NAT_REDIRECT_URI:-http://localhost:8000/auth/redirect}
34+
client_id: ${NAT_CORPORATE_MCP_OUTLOOK_CLIENT_ID}
35+
36+
llms:
37+
nim_llm:
38+
_type: nim
39+
model_name: nvidia/nemotron-3-nano-30b-a3b
40+
temperature: 0.0
41+
max_tokens: 1024
42+
chat_template_kwargs:
43+
enable_thinking: false
44+
45+
workflow:
46+
_type: per_user_react_agent
47+
tool_names:
48+
- mcp_outlook
49+
llm_name: nim_llm
50+
verbose: true
51+
retry_parsing_errors: true
52+
max_retries: 3

packages/nvidia_nat_mcp/src/nat/plugins/mcp/auth/auth_provider_config.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ class MCPOAuth2ProviderConfig(AuthProviderBaseConfig, name="mcp_oauth2"):
2828
2929
Supported modes:
3030
- Endpoints discovery + Dynamic Client Registration (DCR) (enable_dynamic_registration=True, no client_id)
31-
- Endpoints discovery + Manual Client Registration (client_id + client_secret provided)
31+
- Endpoints discovery + Manual Client Registration (client_id with optional client_secret)
32+
33+
Precedence:
34+
- If client_id is provided, manual registration mode is used even when
35+
enable_dynamic_registration is True.
3236
"""
3337
server_url: HttpUrl = Field(
3438
...,
@@ -39,8 +43,9 @@ class MCPOAuth2ProviderConfig(AuthProviderBaseConfig, name="mcp_oauth2"):
3943
client_id: str | None = Field(default=None, description="OAuth2 client ID for pre-registered clients")
4044
client_secret: OptionalSecretStr = Field(default=None,
4145
description="OAuth2 client secret for pre-registered clients")
42-
enable_dynamic_registration: bool = Field(default=True,
43-
description="Enable OAuth2 Dynamic Client Registration (RFC 7591)")
46+
enable_dynamic_registration: bool = Field(
47+
default=True,
48+
description="Enable OAuth2 Dynamic Client Registration (RFC 7591). Ignored when client_id is provided.")
4449
client_name: str = Field(default="NAT MCP Client", description="OAuth2 client name for dynamic registration")
4550

4651
# OAuth2 flow configuration
@@ -74,20 +79,20 @@ def validate_auth_config(self):
7479
# if default_user_id is not provided, use the server_url as the default user id
7580
if not self.default_user_id:
7681
self.default_user_id = str(self.server_url)
82+
# Manual registration + MCP discovery (public and confidential clients).
83+
# NOTE: client_id takes precedence over enable_dynamic_registration.
84+
if self.client_id:
85+
# Has pre-registered client ID; client_secret is optional.
86+
pass
7787
# Dynamic registration + MCP discovery
78-
if self.enable_dynamic_registration and not self.client_id:
88+
elif self.enable_dynamic_registration:
7989
# Pure dynamic registration - no explicit credentials needed
8090
pass
8191

82-
# Manual registration + MCP discovery
83-
elif self.client_id and self.client_secret:
84-
# Has credentials but will discover URLs from MCP server
85-
pass
86-
8792
# Invalid configuration
8893
else:
8994
raise ValueError("Must provide either: "
90-
"1) enable_dynamic_registration=True (dynamic), or "
91-
"2) client_id + client_secret (hybrid)")
95+
"1) enable_dynamic_registration=True without client_id (dynamic), or "
96+
"2) client_id with optional client_secret (manual)")
9297

9398
return self

packages/nvidia_nat_mcp/src/nat/plugins/mcp/client/client_config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ class MCPServerConfig(BaseModel):
4040
streamable-http is the recommended default for HTTP-based connections.
4141
"""
4242
transport: Literal["stdio", "sse", "streamable-http"] = Field(
43-
..., description="Transport type to connect to the MCP server (stdio, sse, or streamable-http)")
43+
default="streamable-http",
44+
description="Transport type to connect to the MCP server (stdio, sse, or streamable-http)")
4445
url: HttpUrl | None = Field(default=None,
4546
description="URL of the MCP server (for sse or streamable-http transport)")
4647
command: str | None = Field(default=None,

packages/nvidia_nat_mcp/tests/client/test_mcp_auth_provider.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,35 @@ def mock_credentials() -> OAuth2Credentials:
7878
)
7979

8080

81+
# --------------------------------------------------------------------------- #
82+
# MCPOAuth2ProviderConfig Tests
83+
# --------------------------------------------------------------------------- #
84+
85+
86+
class TestMCPOAuth2ProviderConfig:
87+
"""Test MCP OAuth2 provider config validation."""
88+
89+
def test_validate_allows_public_client_without_secret(self):
90+
"""Manual mode should allow a pre-registered public client without client_secret."""
91+
config = MCPOAuth2ProviderConfig(
92+
server_url="https://example.com/mcp", # type: ignore
93+
redirect_uri="https://example.com/callback", # type: ignore
94+
client_id="public_client_id",
95+
enable_dynamic_registration=False,
96+
)
97+
98+
assert config.client_id == "public_client_id"
99+
100+
def test_validate_rejects_no_client_id_when_dynamic_registration_disabled(self):
101+
"""Validation should fail when DCR is disabled and no client_id is provided."""
102+
with pytest.raises(ValueError, match="enable_dynamic_registration=True without client_id"):
103+
MCPOAuth2ProviderConfig(
104+
server_url="https://example.com/mcp", # type: ignore
105+
redirect_uri="https://example.com/callback", # type: ignore
106+
enable_dynamic_registration=False,
107+
)
108+
109+
81110
# --------------------------------------------------------------------------- #
82111
# DiscoverOAuth2Endpoints Tests
83112
# --------------------------------------------------------------------------- #

packages/nvidia_nat_mcp/tests/client/test_mcp_client_base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,14 @@ def test_custom_headers_default_none(self):
825825

826826
assert config.custom_headers is None
827827

828+
def test_transport_defaults_to_streamable_http(self):
829+
"""Test that transport defaults to streamable-http when omitted."""
830+
from nat.plugins.mcp.client.client_config import MCPServerConfig
831+
832+
config = MCPServerConfig(url="http://localhost:8080/mcp")
833+
834+
assert config.transport == "streamable-http"
835+
828836

829837
if __name__ == "__main__":
830838

0 commit comments

Comments
 (0)