Conversation
…t types + dynamic client registration
| Extended OAuthClientProvider that handles both JSON and form-encoded token responses. | ||
| Tries JSON first (standard), then falls back to form-encoded format. | ||
|
|
||
| See https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/client/auth/oauth2.py#L397. |
There was a problem hiding this comment.
Extend the base class from the SDK.
There was a problem hiding this comment.
So all the methods in this class are private methods with an underscore in front of them.
If we are overriding private methods in this implementation we should have a lot more comments surrounding what the entry points are that we are overriding and why we need to hack things in this particular manner.
This level of private method extension is highly likely to be snatched away from us without a moment's notice and we would have to be prepared for dealing with the scrambles that ensue.
That all said, if this really is the only way to do what you need, perhaps it's worth petitioning the MCP people to make this part of the API properly extensible so it has lasting support. Someone somewhere used that underscore prefix for a reason.
There was a problem hiding this comment.
Great point about the private methods - you're absolutely right that this is fragile and could break without warning. Let me explain the situation and our options:
I wanted to use the SDK's OAuth providers directly, but they're designed primarily for user authorization flows (Authorization Code grant) rather than pure machine-to-machine scenarios. The modifications needed to make them work for M2M are:
- Token response format handling - Supporting both JSON and form-encoded responses
- User-provided endpoints - The SDK only uses discovered endpoints, doesn't allow manual override
client_secret_postbug - Token exchange fails with this auth method in the SDK- Refresh token flow - Assumes Authorization Code context, not suitable for M2M
The SDK's architecture forces us to override private methods (_initialize(), _perform_authorization()) because:
- These are the only extension points for customizing the auth flow
- There are no public/protected hooks for injecting custom logic
- The class hierarchy isn't designed for the level of customization we need
I agree this isn't sustainable long-term. Here's what I propose:
Option 1: Upstream Fix
- Open an issue in the MCP SDK repo explaining our M2M use case
- Submit a PR with:
- Proper extension points (protected methods or hooks)
- M2M-specific improvements
- Better separation between user auth and M2M flows
Pros: Benefits the entire community, sustainable long-term
Cons: Unknown timeline for acceptance, may require significant rework of the SDK's OAuth module (the code there is messy and probably needs refactoring anyway)
Option 2: Custom Implementation
Build our own OAuth client that:
- Implements the exact flow we need
- Doesn't rely on SDK internals
- Gives us full control
Pros: Complete control, no dependency on SDK changes
Cons: We own the maintenance burden, have to keep up with OAuth spec changes
Option 3: Hybrid Approach
- Immediately: Open SDK issue + PR to make this properly extensible
- Short-term: Keep current implementation with extensive documentation
- Medium-term: Switch to SDK if accepted, or migrate to custom implementation if not
Regardless of direction, I have added comprehensive comments to our current implementation:
Does this approach work for you? Please let me know what you think.
Note:
The OAuth code in the SDK feels like it needs a broader refactor anyway - the current architecture mixes concerns between grant types and doesn't have clean extension points.
| self.context.update_token_expiry(token_response) | ||
| await self.context.storage.set_tokens(token_response) | ||
|
|
||
| async def _parse_form_token_response(self, response: httpx.Response) -> OAuthToken: |
There was a problem hiding this comment.
Add a method to parse text token response.
neuro_san/internals/run_context/langchain/mcp/file_token_storage.py
Outdated
Show resolved
Hide resolved
| if os.name == "nt": | ||
| base_path = os.path.join(os.getenv("APPDATA", os.path.expanduser("~")), "mcp-auth") | ||
| else: | ||
| base_path = os.path.join(os.path.expanduser("~"), ".mcp-auth") |
There was a problem hiding this comment.
Default to .mcp-auth at home directory.
| parsed: ParseResult = urlparse(mcp_server_url) | ||
| if not parsed.hostname: | ||
| raise ValueError(f"Invalid MCP server URL: {mcp_server_url}") | ||
| mcp_server_host: str = parsed.hostname |
There was a problem hiding this comment.
Use hostname of the MCP server for the subfolder under the main directory.
| self.mcp_info_path: PosixPath = Path(base_path).expanduser() / mcp_server_host | ||
| self.mcp_info_path.mkdir(parents=True, exist_ok=True) | ||
| self.tokens_file: PosixPath = self.mcp_info_path / "tokens.json" | ||
| self.client_info_file: PosixPath = self.mcp_info_path / "client_info.json" |
There was a problem hiding this comment.
Client credentials will be in client_info.json and token in tokens.json. Users can manually put client infos (id, secret, redirect_uri, grant type) here.
neuro_san/internals/run_context/langchain/mcp/oauth_callback_handler.py
Outdated
Show resolved
Hide resolved
| html_content = """ | ||
| <html><body style="font-family: sans-serif; text-align: center; padding: 50px;"> | ||
| <h1>Authentication Successful!</h1> | ||
| <p>You can close this window and return to your application.</p> |
There was a problem hiding this comment.
HTML page to redirect user to after authentication.
| self.timeout = timeout | ||
| self.callback_handler: Optional[OauthCallbackHandler] = None | ||
|
|
||
| async def get_auth(self) -> OAuthClientProvider: |
There was a problem hiding this comment.
Create auth based on client info.
client credentials -> authorization code -> dynamic client registration
| self._load_mcp_servers_info() | ||
| return self._mcp_servers_info.get(server_url, {}) | ||
|
|
||
| def _prepare_headers( |
There was a problem hiding this comment.
Refactor LangchainMcpAdapter.
|
|
||
| return None | ||
|
|
||
| def _create_oauth_provider(self, server_url: str) -> OauthProviderFactory: |
There was a problem hiding this comment.
Add method to create OAuth provider.
|
|
||
| # Get OAuth endpoints from server config (optional - will be discovered if not provided) | ||
| auth_endpoint = server_info.get("authorization_endpoint") | ||
| token_endpoint = server_info.get("token_endpoint") |
There was a problem hiding this comment.
Authorization and token endpoints can be taken from MCP_SERVERS_INFO_FILE.
|
|
||
| # Get callback port from environment or use default | ||
| callback_port_env = os.environ.get("AGENT_MCP_CALLBACK_PORT") | ||
| callback_port = int(callback_port_env) if callback_port_env else 3000 |
There was a problem hiding this comment.
The callback port can be set by AGENT_MCP_CALLBACK_PORT. Default to 3000.
| if not file_path: | ||
| file_path = os.environ.get("MCP_SERVERS_INFO_FILE") | ||
| # "MCP_SERVERS_INFO_FILE" will be deprecated in neuro-san==0.7. | ||
| file_path = os.environ.get("AGENT_MCP_SERVERS_INFO_FILE") or os.environ.get("MCP_SERVERS_INFO_FILE") |
There was a problem hiding this comment.
Add AGENT_MCP_SERVERS_INFO_FILE for consistency. MCP_SERVERS_INFO_FILE is still valid but should be deprecated and removed soon.
There was a problem hiding this comment.
Ah. Nice backwards compatibility.
There was a problem hiding this comment.
Pull request overview
Adds machine-to-machine OAuth support for connecting to authenticated MCP servers, including refresh-token and client-credentials grant flows, plus configuration via sly_data and an MCP info file.
Changes:
- Introduces OAuth token storage backed by
sly_dataand new OAuth providers forclient_credentialsandrefresh_tokenflows. - Updates
LangChainMcpAdapterto load MCP configuration fromAGENT_MCP_INFO_FILE(with backward-compatible fallback) and to attach headers/auth when creating MCP clients. - Adds/updates documentation, Docker env vars, requirements pinning, and comprehensive unit tests for the new auth components.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
neuro_san/internals/run_context/langchain/mcp/langchain_mcp_adapter.py |
Adds MCP info loading, header/auth preparation, tool filtering/tagging, and OAuth error handling. |
neuro_san/internals/run_context/langchain/mcp/mcp_info_restorer.py |
Replaces old restorer with unified MCP info file loader + new env var. |
neuro_san/internals/run_context/langchain/mcp/sly_data_token_storage.py |
Implements token/client-info storage backed by sly_data dict references. |
neuro_san/internals/run_context/langchain/mcp/client_credentials_oauth_provider.py |
Adds client-credentials OAuth provider based on MCP SDK internals with token endpoint override support. |
neuro_san/internals/run_context/langchain/mcp/refresh_token_oauth_provider.py |
Adds refresh-token OAuth provider adapted for M2M flows. |
neuro_san/internals/run_context/langchain/mcp/oauth_provider_factory.py |
Chooses refresh-token vs client-credentials provider based on available data. |
neuro_san/internals/run_context/langchain/core/base_tool_factory.py |
Passes sly_data through to MCP adapter so tokens can be updated in-place. |
requirements.txt |
Pins MCP SDK dependency to support OAuth flows. |
neuro_san/deploy/Dockerfile |
Adds AGENT_MCP_INFO_FILE and AGENT_MCP_TIMEOUT_SECONDS env vars (+ deprecation notes). |
docs/mcp_authentication.md |
New guide documenting the auth flows and configuration precedence. |
docs/agent_hocon_reference.md |
Replaces inline MCP auth section with a link to the new guide. |
tests/.../test_*.py |
Adds unit tests for token storage, providers, factory, and adapter integration behavior. |
Comments suppressed due to low confidence (6)
neuro_san/internals/run_context/langchain/mcp/oauth_provider_factory.py:90
- Factory defaults
token_endpoint_auth_methodtoclient_secret_basiceven whenclient_secretis missing (e.g., credentials only containclient_id). This will likely produce an invalid auth header/body. Validate required fields based ontoken_endpoint_auth_method(or default auth_method toNonewhen no secret is provided) and fail fast with a clear error/log.
return ClientCredentialsOauthProvider(
server_url=self.server_url,
storage=self.storage,
client_id=credentials.get("client_id"),
client_secret=credentials.get("client_secret"),
token_endpoint=self.token_endpoint,
token_endpoint_auth_method=credentials.get("token_endpoint_auth_method", "client_secret_basic"),
scopes=credentials.get("scope"),
timeout=self.timeout
)
neuro_san/internals/run_context/langchain/mcp/mcp_info_restorer.py:74
McpInfoRestorer.restore()is annotated to returnDict[str, Any], but it returnsNonewhen no env var/file is provided. Either change the return type toOptional[Dict[str, Any]]or return an empty dict to match the annotation/interface expectations.
neuro_san/internals/run_context/langchain/mcp/sly_data_token_storage.py:68tokens.expires_incan be absent/None per OAuth specs, but the log message formats it with%d, which raisesTypeErrorand triggers the error path (and logs a misleading failure). Use a safe format (e.g.,%s) or guard for None before logging.
try:
# Clear and update token in-place
self.tokens.clear()
self.tokens.update(tokens.model_dump(mode="json"))
self.logger.info("Tokens saved (expires in %d s)", tokens.expires_in)
except (AttributeError, TypeError) as errors:
neuro_san/internals/run_context/langchain/mcp/oauth_provider_factory.py:75
get_auth()is typed to returnAuth, but it returnsNonewhen no credentials are available. Update the return type toOptional[Auth](and adjust docstring/call sites if needed) to avoid type confusion for callers.
async def get_auth(self) -> Auth:
"""
Get appropriate OAuth provider based on stored credentials and tokens.
Flow implementation:
- Refresh token flow (if refresh token is available)
- Client credentials flow (no refresh token, but client credentials are available)
"""
credentials: Dict[str, Any] = await self.storage.get_client_info_dict()
tokens: Dict[str, Any] = await self.storage.get_tokens_dict()
refresh_token: str = tokens.get("refresh_token")
if credentials:
# If there is a refresh token, prioritize refresh token flow to reuse existing authorization
if refresh_token:
return await self._create_refresh_token_provider(credentials)
return await self._create_client_credentials_provider(credentials)
# No client credentials, no auth provider can be created
return None
neuro_san/internals/run_context/langchain/mcp/client_credentials_oauth_provider.py:87
client_secretis typed as required (str), butOauthProviderFactorypassescredentials.get("client_secret"), which can beNone, and the docs allow omittingclient_secretwhentoken_endpoint_auth_methodisNone. Consider makingclient_secretOptional[str]and explicitly handling the "no secret" case (including validation when auth_method requires a secret).
def __init__(
self,
server_url: str,
storage: TokenStorage,
client_id: str,
client_secret: str,
token_endpoint: str = None,
token_endpoint_auth_method: Literal["client_secret_basic", "client_secret_post", None] = "client_secret_basic",
scopes: str | None = None,
timeout: float = 300.0
tests/neuro_san/internals/run_context/langchain/mcp/test_client_credentials_oauth_provider.py:276
- The comment says the provided endpoint should NOT be used, but the assertion verifies the provided endpoint is used. Update the comment to match the test behavior (provided endpoint preferred over discovered).
# Should use discovered endpoint, not the provided one
assert str(request.url) == "https://fallback.auth.com/token"
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
neuro_san/internals/run_context/langchain/mcp/langchain_mcp_adapter.py
Outdated
Show resolved
Hide resolved
neuro_san/internals/run_context/langchain/mcp/mcp_info_restorer.py
Outdated
Show resolved
Hide resolved
|
MCP OAuth Authorization Implementation
Overview
Neuro-san supports machine-to-machine authentication for MCP servers that require credentials before granting access to tools.
This guide explains the available authentication methods and how to configure them.
Authentication Methods
Neuro-san supports three authentication methods, applied in the following priority order:
Headers (highest priority)
Authorizationfield withBearer <token_value>Refresh Token (fallback if headers unavailable or invalid)
Client Credentials (lowest priority)
Configuration Methods
Authentication data can be provided in two ways: through
sly_dataor via an environment variable configuration file.Method 1: Using
sly_dataPass authentication credentials directly in the
sly_dataobject.You can specify different credentials for different MCP URLs.
Available Fields:
http_headers- HTTP headers for authenticationmcp_client_info- Client credentials for token exchangemcp_tokens- Token information (only available via sly_data)Example:
{ "http_headers": { "<MCP_URL_1>": { "Authorization": "Bearer <token_value>" } }, "mcp_client_info": { "<MCP_URL_2>": { "client_id": "<client_id>", "client_secret": "<client_secret>", # Optional if token_endpoint_auth_method is None "token_endpoint_auth_method": "client_secret_post", # Optional, default: "client_secret_basic" "scope": "<scope>" # Optional, default: None } }, "mcp_tokens": { "<MCP_URL_3>": { "access_token": "<access_token>", "refresh_token": "<refresh_token>" } } }Method 2: Using Configuration File
Set the
AGENT_MCP_INFO_FILEenvironment variable to point to a HOCON configuration file.Important Notes:
MCP_SERVERS_INFO_FILEis deprecated and will be removed in version 0.7.0Source files can easily be committed to version control, and checking in secrets is a serious security risk.
If these configuration files need to be committed, use HOCON substitution (e.g., environment variable references)
instead of hardcoding secret values.
Example Configuration:
{ "mcp_server_url_1": { "http_headers": { "Authorization": "Bearer <token>" }, "mcp_client_info": { "client_id": "<client_id>", "client_secret": "<client_secret>", "token_endpoint_auth_method": "client_secret_post", "scope": "<scope>" }, "mcp_server_info": { "token_endpoint": "https://example.com/token" # Optional, default: discovery endpoint or base_url/token }, "auth_timeout": 300.0, # Optional timeout in seconds, default: 300.0 "tools": ["tool_1", "tool_2"] # Optional tool filtering } }Alternative Environment Variables:
AGENT_MCP_TIMEOUT_SECONDS- Can be used instead of theauth_timeoutfieldConfiguration Precedence Rules
When authentication data exists in multiple locations, the following precedence applies:
Field Reference
http_headersAuthorizationBearer <token>mcp_client_infoclient_idclient_secrettoken_endpoint_auth_methodclient_secret_basicscope*Required unless
token_endpoint_auth_methodis Nonemcp_tokensaccess_tokenrefresh_tokenmcp_server_infotoken_endpointbase_url/tokenUPDATE Mar 4, 2026
mcp_authentication.mdmcp_info_hocon_reference.mdUPDATE Feb 27, 2026
Convert draft to PR
Consolidate MCP configuration:
AGENT_MCP_CLIENTS_INFOandAGENT_MCP_TOKENS.MCP_SERVERS_INFO_FILEis deprecated and will be removed in version 0.7.0.sly_data.Remove
ExtendedOauthClientProvider:Update token endpoint resolution:
Add unit tests for new classes:
SlyDataTokenStorageClientCredentialsOauthProviderRefreshTokenOauthProviderOauthProviderFactoryAdd documentation:
mcp_authentication.mdto document the new authentication flow and configuration structure.UPDATE Feb 20,2026
Rework OAuth Implementation to Machine-to-Machine Authentication
Based on feedback from @d1donlydfink and @andreidenissov-cog, refactors the OAuth implementation to focus exclusively on machine-to-machine (M2M) authentication flows.
Changes
Removed
AuthorizationCodeOauthProvider- no longer needed for M2M flowsAdded
RefreshTokenOauthProvider- handles token refresh for M2M flowsAuthorization Flow Priority
Tests
Note
Jan 28, 2026
Authorization Flow Priority
The implementation supports multiple OAuth flows with the following priority order:
Client Credentials Flow (Machine-to-Machine)
Dynamic Client Registration (Fallback)
Architecture Overview
The OAuth flow is primarily handled by the MCP SDK with custom extensions to support additional use cases.
Class Hierarchy
Base Classes
OAuthClientProvider(from MCP SDK)ExtendedOAuthClientProvider(extendsOAuthClientProvider)Specialized Providers
ClientCredentialsOAuthProvider(extendsExtendedOAuthClientProvider)AuthorizationCodeOAuthProvider(extendsExtendedOAuthClientProvider)Storage and Factory
FileTokenStorage(extendsTokenStoragefrom SDK)OAuthProviderFactoryclient_credentials→ClientCredentialsOAuthProviderAuthorizationCodeOAuthProviderExtendedOAuthClientProvider(dynamic registration)Tested MCP servers