Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions crates/rmcp/src/transport/auth.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ use reqwest::{Client as HttpClient, IntoUrl, StatusCode, Url, header::AUTHORIZAT
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error};
use tracing::{debug, error, warn};

const DEFAULT_EXCHANGE_URL: &str = "http://localhost";

Expand DownExpand Up@@ -102,7 +102,7 @@ pub enum AuthError {
pub struct AuthorizationMetadata {
pub authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
pub registration_endpoint: Option<String>,
pub issuer: Option<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
Expand DownExpand Up@@ -273,7 +273,7 @@ impl AuthorizationManager {
return Ok(metadata);
}

debug!("No valid .well-known endpoint found, falling back to default endpoints");
warn!("No valid .well-known endpoint found, falling back to default endpoints");

// fallback to default endpoints
let mut auth_base = self.base_url.clone();
Expand All@@ -290,7 +290,7 @@ impl AuthorizationManager {
Ok(AuthorizationMetadata {
authorization_endpoint: create_endpoint("authorize"),
token_endpoint: create_endpoint("token"),
registration_endpoint: create_endpoint("register"),
registration_endpoint: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that supporting DCR is a SHOULD in the MCP spec, If we cannot get metadata at all and we construct these defaults, should we continue to populate a default uri here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It aimed to achieve better compatibility with the RFC 8414 protocol, according to the rfc 8414 , it should be optional
image
.
And in the spec file ,the registrat should be alt in the picture, and user can setting the client ID by himself which we provide this interface ,
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub, for example, does not provide a value for registration_endpoint in their response, as they do not support DCR and require pre-registration of clients. (I had to make this same change in a fork, to support GitHub properly, and had not yet had an opportunity to open a PR here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

issuer: None,
jwks_uri: None,
scopes_supported: None,
Expand DownExpand Up@@ -323,12 +323,10 @@ impl AuthorizationManager {
let token_url = TokenUrl::new(metadata.token_endpoint.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?;

// debug!("token url: {:?}", token_url);
let client_id = ClientId::new(config.client_id);
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;

debug!("client_id: {:?}", client_id);
let mut client_builder = BasicClient::new(client_id.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url)
Expand All@@ -349,14 +347,16 @@ impl AuthorizationManager {
redirect_uri: &str,
) -> Result<OAuthClientConfig, AuthError> {
if self.metadata.is_none() {
error!("No authorization support detected");
return Err(AuthError::NoAuthorizationSupport);
}
Comment thread
jokemanfire marked this conversation as resolved.

let metadata = self.metadata.as_ref().unwrap();
let registration_url = metadata.registration_endpoint.clone();
let Some(registration_url) = metadata.registration_endpoint.as_ref() else {
return Err(AuthError::RegistrationFailed(
"Dynamic client registration not supported".to_string(),
));
};

debug!("registration url: {:?}", registration_url);
// prepare registration request
let registration_request = ClientRegistrationRequest {
client_name: name.to_string(),
Expand All@@ -369,8 +369,6 @@ impl AuthorizationManager {
response_types: vec!["code".to_string()],
};

debug!("registration request: {:?}", registration_request);

let response = match self
.http_client
.post(registration_url)
Expand All@@ -380,7 +378,6 @@ impl AuthorizationManager {
{
Ok(response) => response,
Err(e) => {
Comment thread
jokemanfire marked this conversation as resolved.
error!("Registration request failed: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"HTTP request error: {}",
e
Expand All@@ -395,7 +392,6 @@ impl AuthorizationManager {
Err(_) => "cannot get error details".to_string(),
};

error!("Registration failed: HTTP {} - {}", status, error_text);
return Err(AuthError::RegistrationFailed(format!(
"HTTP {}: {}",
status, error_text
Expand All@@ -406,7 +402,6 @@ impl AuthorizationManager {
let reg_response = match response.json::<ClientRegistrationResponse>().await {
Ok(response) => response,
Err(e) => {
error!("Failed to parse registration response: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"analyze response error: {}",
e
Expand DownExpand Up@@ -471,7 +466,6 @@ impl AuthorizationManager {
pkce_verifier,
csrf_token,
});
debug!("set authorization state: {:?}", self.state.read().await);

Ok(auth_url.to_string())
}
Expand DownExpand Up@@ -625,9 +619,9 @@ impl AuthorizationSession {
redirect_uri: &str,
client_name: Option<&str>,
) -> Result<Self, AuthError> {
// set redirect uri
// Default client config
let config = OAuthClientConfig {
client_id: "mcp-client".to_string(), // temporary id, will be updated by dynamic registration
client_id: "mcp-client".to_string(),
client_secret: None,
scopes: scopes.iter().map(|s| s.to_string()).collect(),
redirect_uri: redirect_uri.to_string(),
Expand All@@ -640,7 +634,10 @@ impl AuthorizationSession {
{
Ok(config) => config,
Err(e) => {
eprintln!("Dynamic registration failed: {}", e);
warn!(
"Dynamic registration failed: {}, fallback to default config",
e
);
// fallback to default config
config
}
Expand Down
2 changes: 1 addition & 1 deletion examples/servers/src/complex_auth_sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -525,7 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
issuer: Some(BIND_ADDRESS.to_string()),
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
additional_fields,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions crates/rmcp/src/transport/auth.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ use reqwest::{Client as HttpClient, IntoUrl, StatusCode, Url, header::AUTHORIZAT
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error};
use tracing::{debug, error, warn};

const DEFAULT_EXCHANGE_URL: &str = "http://localhost";

Expand DownExpand Up@@ -102,7 +102,7 @@ pub enum AuthError {
pub struct AuthorizationMetadata {
pub authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
pub registration_endpoint: Option<String>,
pub issuer: Option<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
Expand DownExpand Up@@ -273,7 +273,7 @@ impl AuthorizationManager {
return Ok(metadata);
}

debug!("No valid .well-known endpoint found, falling back to default endpoints");
warn!("No valid .well-known endpoint found, falling back to default endpoints");

// fallback to default endpoints
let mut auth_base = self.base_url.clone();
Expand All@@ -290,7 +290,7 @@ impl AuthorizationManager {
Ok(AuthorizationMetadata {
authorization_endpoint: create_endpoint("authorize"),
token_endpoint: create_endpoint("token"),
registration_endpoint: create_endpoint("register"),
registration_endpoint: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that supporting DCR is a SHOULD in the MCP spec, If we cannot get metadata at all and we construct these defaults, should we continue to populate a default uri here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It aimed to achieve better compatibility with the RFC 8414 protocol, according to the rfc 8414 , it should be optional
image
.
And in the spec file ,the registrat should be alt in the picture, and user can setting the client ID by himself which we provide this interface ,
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub, for example, does not provide a value for registration_endpoint in their response, as they do not support DCR and require pre-registration of clients. (I had to make this same change in a fork, to support GitHub properly, and had not yet had an opportunity to open a PR here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

issuer: None,
jwks_uri: None,
scopes_supported: None,
Expand DownExpand Up@@ -323,12 +323,10 @@ impl AuthorizationManager {
let token_url = TokenUrl::new(metadata.token_endpoint.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?;

// debug!("token url: {:?}", token_url);
let client_id = ClientId::new(config.client_id);
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;

debug!("client_id: {:?}", client_id);
let mut client_builder = BasicClient::new(client_id.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url)
Expand All@@ -349,14 +347,16 @@ impl AuthorizationManager {
redirect_uri: &str,
) -> Result<OAuthClientConfig, AuthError> {
if self.metadata.is_none() {
error!("No authorization support detected");
return Err(AuthError::NoAuthorizationSupport);
}
Comment thread
jokemanfire marked this conversation as resolved.

let metadata = self.metadata.as_ref().unwrap();
let registration_url = metadata.registration_endpoint.clone();
let Some(registration_url) = metadata.registration_endpoint.as_ref() else {
return Err(AuthError::RegistrationFailed(
"Dynamic client registration not supported".to_string(),
));
};

debug!("registration url: {:?}", registration_url);
// prepare registration request
let registration_request = ClientRegistrationRequest {
client_name: name.to_string(),
Expand All@@ -369,8 +369,6 @@ impl AuthorizationManager {
response_types: vec!["code".to_string()],
};

debug!("registration request: {:?}", registration_request);

let response = match self
.http_client
.post(registration_url)
Expand All@@ -380,7 +378,6 @@ impl AuthorizationManager {
{
Ok(response) => response,
Err(e) => {
Comment thread
jokemanfire marked this conversation as resolved.
error!("Registration request failed: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"HTTP request error: {}",
e
Expand All@@ -395,7 +392,6 @@ impl AuthorizationManager {
Err(_) => "cannot get error details".to_string(),
};

error!("Registration failed: HTTP {} - {}", status, error_text);
return Err(AuthError::RegistrationFailed(format!(
"HTTP {}: {}",
status, error_text
Expand All@@ -406,7 +402,6 @@ impl AuthorizationManager {
let reg_response = match response.json::<ClientRegistrationResponse>().await {
Ok(response) => response,
Err(e) => {
error!("Failed to parse registration response: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"analyze response error: {}",
e
Expand DownExpand Up@@ -471,7 +466,6 @@ impl AuthorizationManager {
pkce_verifier,
csrf_token,
});
debug!("set authorization state: {:?}", self.state.read().await);

Ok(auth_url.to_string())
}
Expand DownExpand Up@@ -625,9 +619,9 @@ impl AuthorizationSession {
redirect_uri: &str,
client_name: Option<&str>,
) -> Result<Self, AuthError> {
// set redirect uri
// Default client config
let config = OAuthClientConfig {
client_id: "mcp-client".to_string(), // temporary id, will be updated by dynamic registration
client_id: "mcp-client".to_string(),
client_secret: None,
scopes: scopes.iter().map(|s| s.to_string()).collect(),
redirect_uri: redirect_uri.to_string(),
Expand All@@ -640,7 +634,10 @@ impl AuthorizationSession {
{
Ok(config) => config,
Err(e) => {
eprintln!("Dynamic registration failed: {}", e);
warn!(
"Dynamic registration failed: {}, fallback to default config",
e
);
// fallback to default config
config
}
Expand Down
2 changes: 1 addition & 1 deletion examples/servers/src/complex_auth_sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -525,7 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
issuer: Some(BIND_ADDRESS.to_string()),
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
additional_fields,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions crates/rmcp/src/transport/auth.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ use reqwest::{Client as HttpClient, IntoUrl, StatusCode, Url, header::AUTHORIZAT
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error};
use tracing::{debug, error, warn};

const DEFAULT_EXCHANGE_URL: &str = "http://localhost";

Expand DownExpand Up@@ -102,7 +102,7 @@ pub enum AuthError {
pub struct AuthorizationMetadata {
pub authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
pub registration_endpoint: Option<String>,
pub issuer: Option<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
Expand DownExpand Up@@ -273,7 +273,7 @@ impl AuthorizationManager {
return Ok(metadata);
}

debug!("No valid .well-known endpoint found, falling back to default endpoints");
warn!("No valid .well-known endpoint found, falling back to default endpoints");

// fallback to default endpoints
let mut auth_base = self.base_url.clone();
Expand All@@ -290,7 +290,7 @@ impl AuthorizationManager {
Ok(AuthorizationMetadata {
authorization_endpoint: create_endpoint("authorize"),
token_endpoint: create_endpoint("token"),
registration_endpoint: create_endpoint("register"),
registration_endpoint: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that supporting DCR is a SHOULD in the MCP spec, If we cannot get metadata at all and we construct these defaults, should we continue to populate a default uri here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It aimed to achieve better compatibility with the RFC 8414 protocol, according to the rfc 8414 , it should be optional
image
.
And in the spec file ,the registrat should be alt in the picture, and user can setting the client ID by himself which we provide this interface ,
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub, for example, does not provide a value for registration_endpoint in their response, as they do not support DCR and require pre-registration of clients. (I had to make this same change in a fork, to support GitHub properly, and had not yet had an opportunity to open a PR here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

issuer: None,
jwks_uri: None,
scopes_supported: None,
Expand DownExpand Up@@ -323,12 +323,10 @@ impl AuthorizationManager {
let token_url = TokenUrl::new(metadata.token_endpoint.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?;

// debug!("token url: {:?}", token_url);
let client_id = ClientId::new(config.client_id);
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;

debug!("client_id: {:?}", client_id);
let mut client_builder = BasicClient::new(client_id.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url)
Expand All@@ -349,14 +347,16 @@ impl AuthorizationManager {
redirect_uri: &str,
) -> Result<OAuthClientConfig, AuthError> {
if self.metadata.is_none() {
error!("No authorization support detected");
return Err(AuthError::NoAuthorizationSupport);
}
Comment thread
jokemanfire marked this conversation as resolved.

let metadata = self.metadata.as_ref().unwrap();
let registration_url = metadata.registration_endpoint.clone();
let Some(registration_url) = metadata.registration_endpoint.as_ref() else {
return Err(AuthError::RegistrationFailed(
"Dynamic client registration not supported".to_string(),
));
};

debug!("registration url: {:?}", registration_url);
// prepare registration request
let registration_request = ClientRegistrationRequest {
client_name: name.to_string(),
Expand All@@ -369,8 +369,6 @@ impl AuthorizationManager {
response_types: vec!["code".to_string()],
};

debug!("registration request: {:?}", registration_request);

let response = match self
.http_client
.post(registration_url)
Expand All@@ -380,7 +378,6 @@ impl AuthorizationManager {
{
Ok(response) => response,
Err(e) => {
Comment thread
jokemanfire marked this conversation as resolved.
error!("Registration request failed: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"HTTP request error: {}",
e
Expand All@@ -395,7 +392,6 @@ impl AuthorizationManager {
Err(_) => "cannot get error details".to_string(),
};

error!("Registration failed: HTTP {} - {}", status, error_text);
return Err(AuthError::RegistrationFailed(format!(
"HTTP {}: {}",
status, error_text
Expand All@@ -406,7 +402,6 @@ impl AuthorizationManager {
let reg_response = match response.json::<ClientRegistrationResponse>().await {
Ok(response) => response,
Err(e) => {
error!("Failed to parse registration response: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"analyze response error: {}",
e
Expand DownExpand Up@@ -471,7 +466,6 @@ impl AuthorizationManager {
pkce_verifier,
csrf_token,
});
debug!("set authorization state: {:?}", self.state.read().await);

Ok(auth_url.to_string())
}
Expand DownExpand Up@@ -625,9 +619,9 @@ impl AuthorizationSession {
redirect_uri: &str,
client_name: Option<&str>,
) -> Result<Self, AuthError> {
// set redirect uri
// Default client config
let config = OAuthClientConfig {
client_id: "mcp-client".to_string(), // temporary id, will be updated by dynamic registration
client_id: "mcp-client".to_string(),
client_secret: None,
scopes: scopes.iter().map(|s| s.to_string()).collect(),
redirect_uri: redirect_uri.to_string(),
Expand All@@ -640,7 +634,10 @@ impl AuthorizationSession {
{
Ok(config) => config,
Err(e) => {
eprintln!("Dynamic registration failed: {}", e);
warn!(
"Dynamic registration failed: {}, fallback to default config",
e
);
// fallback to default config
config
}
Expand Down
2 changes: 1 addition & 1 deletion examples/servers/src/complex_auth_sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -525,7 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
issuer: Some(BIND_ADDRESS.to_string()),
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
additional_fields,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions crates/rmcp/src/transport/auth.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ use reqwest::{Client as HttpClient, IntoUrl, StatusCode, Url, header::AUTHORIZAT
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error};
use tracing::{debug, error, warn};

const DEFAULT_EXCHANGE_URL: &str = "http://localhost";

Expand DownExpand Up@@ -102,7 +102,7 @@ pub enum AuthError {
pub struct AuthorizationMetadata {
pub authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
pub registration_endpoint: Option<String>,
pub issuer: Option<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
Expand DownExpand Up@@ -273,7 +273,7 @@ impl AuthorizationManager {
return Ok(metadata);
}

debug!("No valid .well-known endpoint found, falling back to default endpoints");
warn!("No valid .well-known endpoint found, falling back to default endpoints");

// fallback to default endpoints
let mut auth_base = self.base_url.clone();
Expand All@@ -290,7 +290,7 @@ impl AuthorizationManager {
Ok(AuthorizationMetadata {
authorization_endpoint: create_endpoint("authorize"),
token_endpoint: create_endpoint("token"),
registration_endpoint: create_endpoint("register"),
registration_endpoint: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that supporting DCR is a SHOULD in the MCP spec, If we cannot get metadata at all and we construct these defaults, should we continue to populate a default uri here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It aimed to achieve better compatibility with the RFC 8414 protocol, according to the rfc 8414 , it should be optional
image
.
And in the spec file ,the registrat should be alt in the picture, and user can setting the client ID by himself which we provide this interface ,
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub, for example, does not provide a value for registration_endpoint in their response, as they do not support DCR and require pre-registration of clients. (I had to make this same change in a fork, to support GitHub properly, and had not yet had an opportunity to open a PR here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

issuer: None,
jwks_uri: None,
scopes_supported: None,
Expand DownExpand Up@@ -323,12 +323,10 @@ impl AuthorizationManager {
let token_url = TokenUrl::new(metadata.token_endpoint.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?;

// debug!("token url: {:?}", token_url);
let client_id = ClientId::new(config.client_id);
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;

debug!("client_id: {:?}", client_id);
let mut client_builder = BasicClient::new(client_id.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url)
Expand All@@ -349,14 +347,16 @@ impl AuthorizationManager {
redirect_uri: &str,
) -> Result<OAuthClientConfig, AuthError> {
if self.metadata.is_none() {
error!("No authorization support detected");
return Err(AuthError::NoAuthorizationSupport);
}
Comment thread
jokemanfire marked this conversation as resolved.

let metadata = self.metadata.as_ref().unwrap();
let registration_url = metadata.registration_endpoint.clone();
let Some(registration_url) = metadata.registration_endpoint.as_ref() else {
return Err(AuthError::RegistrationFailed(
"Dynamic client registration not supported".to_string(),
));
};

debug!("registration url: {:?}", registration_url);
// prepare registration request
let registration_request = ClientRegistrationRequest {
client_name: name.to_string(),
Expand All@@ -369,8 +369,6 @@ impl AuthorizationManager {
response_types: vec!["code".to_string()],
};

debug!("registration request: {:?}", registration_request);

let response = match self
.http_client
.post(registration_url)
Expand All@@ -380,7 +378,6 @@ impl AuthorizationManager {
{
Ok(response) => response,
Err(e) => {
Comment thread
jokemanfire marked this conversation as resolved.
error!("Registration request failed: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"HTTP request error: {}",
e
Expand All@@ -395,7 +392,6 @@ impl AuthorizationManager {
Err(_) => "cannot get error details".to_string(),
};

error!("Registration failed: HTTP {} - {}", status, error_text);
return Err(AuthError::RegistrationFailed(format!(
"HTTP {}: {}",
status, error_text
Expand All@@ -406,7 +402,6 @@ impl AuthorizationManager {
let reg_response = match response.json::<ClientRegistrationResponse>().await {
Ok(response) => response,
Err(e) => {
error!("Failed to parse registration response: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"analyze response error: {}",
e
Expand DownExpand Up@@ -471,7 +466,6 @@ impl AuthorizationManager {
pkce_verifier,
csrf_token,
});
debug!("set authorization state: {:?}", self.state.read().await);

Ok(auth_url.to_string())
}
Expand DownExpand Up@@ -625,9 +619,9 @@ impl AuthorizationSession {
redirect_uri: &str,
client_name: Option<&str>,
) -> Result<Self, AuthError> {
// set redirect uri
// Default client config
let config = OAuthClientConfig {
client_id: "mcp-client".to_string(), // temporary id, will be updated by dynamic registration
client_id: "mcp-client".to_string(),
client_secret: None,
scopes: scopes.iter().map(|s| s.to_string()).collect(),
redirect_uri: redirect_uri.to_string(),
Expand All@@ -640,7 +634,10 @@ impl AuthorizationSession {
{
Ok(config) => config,
Err(e) => {
eprintln!("Dynamic registration failed: {}", e);
warn!(
"Dynamic registration failed: {}, fallback to default config",
e
);
// fallback to default config
config
}
Expand Down
2 changes: 1 addition & 1 deletion examples/servers/src/complex_auth_sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -525,7 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
issuer: Some(BIND_ADDRESS.to_string()),
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
additional_fields,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions crates/rmcp/src/transport/auth.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ use reqwest::{Client as HttpClient, IntoUrl, StatusCode, Url, header::AUTHORIZAT
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error};
use tracing::{debug, error, warn};

const DEFAULT_EXCHANGE_URL: &str = "http://localhost";

Expand DownExpand Up@@ -102,7 +102,7 @@ pub enum AuthError {
pub struct AuthorizationMetadata {
pub authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
pub registration_endpoint: Option<String>,
pub issuer: Option<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
Expand DownExpand Up@@ -273,7 +273,7 @@ impl AuthorizationManager {
return Ok(metadata);
}

debug!("No valid .well-known endpoint found, falling back to default endpoints");
warn!("No valid .well-known endpoint found, falling back to default endpoints");

// fallback to default endpoints
let mut auth_base = self.base_url.clone();
Expand All@@ -290,7 +290,7 @@ impl AuthorizationManager {
Ok(AuthorizationMetadata {
authorization_endpoint: create_endpoint("authorize"),
token_endpoint: create_endpoint("token"),
registration_endpoint: create_endpoint("register"),
registration_endpoint: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that supporting DCR is a SHOULD in the MCP spec, If we cannot get metadata at all and we construct these defaults, should we continue to populate a default uri here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It aimed to achieve better compatibility with the RFC 8414 protocol, according to the rfc 8414 , it should be optional
image
.
And in the spec file ,the registrat should be alt in the picture, and user can setting the client ID by himself which we provide this interface ,
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub, for example, does not provide a value for registration_endpoint in their response, as they do not support DCR and require pre-registration of clients. (I had to make this same change in a fork, to support GitHub properly, and had not yet had an opportunity to open a PR here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

issuer: None,
jwks_uri: None,
scopes_supported: None,
Expand DownExpand Up@@ -323,12 +323,10 @@ impl AuthorizationManager {
let token_url = TokenUrl::new(metadata.token_endpoint.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?;

// debug!("token url: {:?}", token_url);
let client_id = ClientId::new(config.client_id);
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;

debug!("client_id: {:?}", client_id);
let mut client_builder = BasicClient::new(client_id.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url)
Expand All@@ -349,14 +347,16 @@ impl AuthorizationManager {
redirect_uri: &str,
) -> Result<OAuthClientConfig, AuthError> {
if self.metadata.is_none() {
error!("No authorization support detected");
return Err(AuthError::NoAuthorizationSupport);
}
Comment thread
jokemanfire marked this conversation as resolved.

let metadata = self.metadata.as_ref().unwrap();
let registration_url = metadata.registration_endpoint.clone();
let Some(registration_url) = metadata.registration_endpoint.as_ref() else {
return Err(AuthError::RegistrationFailed(
"Dynamic client registration not supported".to_string(),
));
};

debug!("registration url: {:?}", registration_url);
// prepare registration request
let registration_request = ClientRegistrationRequest {
client_name: name.to_string(),
Expand All@@ -369,8 +369,6 @@ impl AuthorizationManager {
response_types: vec!["code".to_string()],
};

debug!("registration request: {:?}", registration_request);

let response = match self
.http_client
.post(registration_url)
Expand All@@ -380,7 +378,6 @@ impl AuthorizationManager {
{
Ok(response) => response,
Err(e) => {
Comment thread
jokemanfire marked this conversation as resolved.
error!("Registration request failed: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"HTTP request error: {}",
e
Expand All@@ -395,7 +392,6 @@ impl AuthorizationManager {
Err(_) => "cannot get error details".to_string(),
};

error!("Registration failed: HTTP {} - {}", status, error_text);
return Err(AuthError::RegistrationFailed(format!(
"HTTP {}: {}",
status, error_text
Expand All@@ -406,7 +402,6 @@ impl AuthorizationManager {
let reg_response = match response.json::<ClientRegistrationResponse>().await {
Ok(response) => response,
Err(e) => {
error!("Failed to parse registration response: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"analyze response error: {}",
e
Expand DownExpand Up@@ -471,7 +466,6 @@ impl AuthorizationManager {
pkce_verifier,
csrf_token,
});
debug!("set authorization state: {:?}", self.state.read().await);

Ok(auth_url.to_string())
}
Expand DownExpand Up@@ -625,9 +619,9 @@ impl AuthorizationSession {
redirect_uri: &str,
client_name: Option<&str>,
) -> Result<Self, AuthError> {
// set redirect uri
// Default client config
let config = OAuthClientConfig {
client_id: "mcp-client".to_string(), // temporary id, will be updated by dynamic registration
client_id: "mcp-client".to_string(),
client_secret: None,
scopes: scopes.iter().map(|s| s.to_string()).collect(),
redirect_uri: redirect_uri.to_string(),
Expand All@@ -640,7 +634,10 @@ impl AuthorizationSession {
{
Ok(config) => config,
Err(e) => {
eprintln!("Dynamic registration failed: {}", e);
warn!(
"Dynamic registration failed: {}, fallback to default config",
e
);
// fallback to default config
config
}
Expand Down
2 changes: 1 addition & 1 deletion examples/servers/src/complex_auth_sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -525,7 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
issuer: Some(BIND_ADDRESS.to_string()),
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
additional_fields,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions crates/rmcp/src/transport/auth.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ use reqwest::{Client as HttpClient, IntoUrl, StatusCode, Url, header::AUTHORIZAT
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error};
use tracing::{debug, error, warn};

const DEFAULT_EXCHANGE_URL: &str = "http://localhost";

Expand DownExpand Up@@ -102,7 +102,7 @@ pub enum AuthError {
pub struct AuthorizationMetadata {
pub authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
pub registration_endpoint: Option<String>,
pub issuer: Option<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
Expand DownExpand Up@@ -273,7 +273,7 @@ impl AuthorizationManager {
return Ok(metadata);
}

debug!("No valid .well-known endpoint found, falling back to default endpoints");
warn!("No valid .well-known endpoint found, falling back to default endpoints");

// fallback to default endpoints
let mut auth_base = self.base_url.clone();
Expand All@@ -290,7 +290,7 @@ impl AuthorizationManager {
Ok(AuthorizationMetadata {
authorization_endpoint: create_endpoint("authorize"),
token_endpoint: create_endpoint("token"),
registration_endpoint: create_endpoint("register"),
registration_endpoint: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that supporting DCR is a SHOULD in the MCP spec, If we cannot get metadata at all and we construct these defaults, should we continue to populate a default uri here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It aimed to achieve better compatibility with the RFC 8414 protocol, according to the rfc 8414 , it should be optional
image
.
And in the spec file ,the registrat should be alt in the picture, and user can setting the client ID by himself which we provide this interface ,
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub, for example, does not provide a value for registration_endpoint in their response, as they do not support DCR and require pre-registration of clients. (I had to make this same change in a fork, to support GitHub properly, and had not yet had an opportunity to open a PR here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

issuer: None,
jwks_uri: None,
scopes_supported: None,
Expand DownExpand Up@@ -323,12 +323,10 @@ impl AuthorizationManager {
let token_url = TokenUrl::new(metadata.token_endpoint.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?;

// debug!("token url: {:?}", token_url);
let client_id = ClientId::new(config.client_id);
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;

debug!("client_id: {:?}", client_id);
let mut client_builder = BasicClient::new(client_id.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url)
Expand All@@ -349,14 +347,16 @@ impl AuthorizationManager {
redirect_uri: &str,
) -> Result<OAuthClientConfig, AuthError> {
if self.metadata.is_none() {
error!("No authorization support detected");
return Err(AuthError::NoAuthorizationSupport);
}
Comment thread
jokemanfire marked this conversation as resolved.

let metadata = self.metadata.as_ref().unwrap();
let registration_url = metadata.registration_endpoint.clone();
let Some(registration_url) = metadata.registration_endpoint.as_ref() else {
return Err(AuthError::RegistrationFailed(
"Dynamic client registration not supported".to_string(),
));
};

debug!("registration url: {:?}", registration_url);
// prepare registration request
let registration_request = ClientRegistrationRequest {
client_name: name.to_string(),
Expand All@@ -369,8 +369,6 @@ impl AuthorizationManager {
response_types: vec!["code".to_string()],
};

debug!("registration request: {:?}", registration_request);

let response = match self
.http_client
.post(registration_url)
Expand All@@ -380,7 +378,6 @@ impl AuthorizationManager {
{
Ok(response) => response,
Err(e) => {
Comment thread
jokemanfire marked this conversation as resolved.
error!("Registration request failed: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"HTTP request error: {}",
e
Expand All@@ -395,7 +392,6 @@ impl AuthorizationManager {
Err(_) => "cannot get error details".to_string(),
};

error!("Registration failed: HTTP {} - {}", status, error_text);
return Err(AuthError::RegistrationFailed(format!(
"HTTP {}: {}",
status, error_text
Expand All@@ -406,7 +402,6 @@ impl AuthorizationManager {
let reg_response = match response.json::<ClientRegistrationResponse>().await {
Ok(response) => response,
Err(e) => {
error!("Failed to parse registration response: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"analyze response error: {}",
e
Expand DownExpand Up@@ -471,7 +466,6 @@ impl AuthorizationManager {
pkce_verifier,
csrf_token,
});
debug!("set authorization state: {:?}", self.state.read().await);

Ok(auth_url.to_string())
}
Expand DownExpand Up@@ -625,9 +619,9 @@ impl AuthorizationSession {
redirect_uri: &str,
client_name: Option<&str>,
) -> Result<Self, AuthError> {
// set redirect uri
// Default client config
let config = OAuthClientConfig {
client_id: "mcp-client".to_string(), // temporary id, will be updated by dynamic registration
client_id: "mcp-client".to_string(),
client_secret: None,
scopes: scopes.iter().map(|s| s.to_string()).collect(),
redirect_uri: redirect_uri.to_string(),
Expand All@@ -640,7 +634,10 @@ impl AuthorizationSession {
{
Ok(config) => config,
Err(e) => {
eprintln!("Dynamic registration failed: {}", e);
warn!(
"Dynamic registration failed: {}, fallback to default config",
e
);
// fallback to default config
config
}
Expand Down
2 changes: 1 addition & 1 deletion examples/servers/src/complex_auth_sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -525,7 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
issuer: Some(BIND_ADDRESS.to_string()),
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
additional_fields,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions crates/rmcp/src/transport/auth.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ use reqwest::{Client as HttpClient, IntoUrl, StatusCode, Url, header::AUTHORIZAT
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error};
use tracing::{debug, error, warn};

const DEFAULT_EXCHANGE_URL: &str = "http://localhost";

Expand DownExpand Up@@ -102,7 +102,7 @@ pub enum AuthError {
pub struct AuthorizationMetadata {
pub authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
pub registration_endpoint: Option<String>,
pub issuer: Option<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
Expand DownExpand Up@@ -273,7 +273,7 @@ impl AuthorizationManager {
return Ok(metadata);
}

debug!("No valid .well-known endpoint found, falling back to default endpoints");
warn!("No valid .well-known endpoint found, falling back to default endpoints");

// fallback to default endpoints
let mut auth_base = self.base_url.clone();
Expand All@@ -290,7 +290,7 @@ impl AuthorizationManager {
Ok(AuthorizationMetadata {
authorization_endpoint: create_endpoint("authorize"),
token_endpoint: create_endpoint("token"),
registration_endpoint: create_endpoint("register"),
registration_endpoint: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that supporting DCR is a SHOULD in the MCP spec, If we cannot get metadata at all and we construct these defaults, should we continue to populate a default uri here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It aimed to achieve better compatibility with the RFC 8414 protocol, according to the rfc 8414 , it should be optional
image
.
And in the spec file ,the registrat should be alt in the picture, and user can setting the client ID by himself which we provide this interface ,
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub, for example, does not provide a value for registration_endpoint in their response, as they do not support DCR and require pre-registration of clients. (I had to make this same change in a fork, to support GitHub properly, and had not yet had an opportunity to open a PR here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

issuer: None,
jwks_uri: None,
scopes_supported: None,
Expand DownExpand Up@@ -323,12 +323,10 @@ impl AuthorizationManager {
let token_url = TokenUrl::new(metadata.token_endpoint.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?;

// debug!("token url: {:?}", token_url);
let client_id = ClientId::new(config.client_id);
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;

debug!("client_id: {:?}", client_id);
let mut client_builder = BasicClient::new(client_id.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url)
Expand All@@ -349,14 +347,16 @@ impl AuthorizationManager {
redirect_uri: &str,
) -> Result<OAuthClientConfig, AuthError> {
if self.metadata.is_none() {
error!("No authorization support detected");
return Err(AuthError::NoAuthorizationSupport);
}
Comment thread
jokemanfire marked this conversation as resolved.

let metadata = self.metadata.as_ref().unwrap();
let registration_url = metadata.registration_endpoint.clone();
let Some(registration_url) = metadata.registration_endpoint.as_ref() else {
return Err(AuthError::RegistrationFailed(
"Dynamic client registration not supported".to_string(),
));
};

debug!("registration url: {:?}", registration_url);
// prepare registration request
let registration_request = ClientRegistrationRequest {
client_name: name.to_string(),
Expand All@@ -369,8 +369,6 @@ impl AuthorizationManager {
response_types: vec!["code".to_string()],
};

debug!("registration request: {:?}", registration_request);

let response = match self
.http_client
.post(registration_url)
Expand All@@ -380,7 +378,6 @@ impl AuthorizationManager {
{
Ok(response) => response,
Err(e) => {
Comment thread
jokemanfire marked this conversation as resolved.
error!("Registration request failed: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"HTTP request error: {}",
e
Expand All@@ -395,7 +392,6 @@ impl AuthorizationManager {
Err(_) => "cannot get error details".to_string(),
};

error!("Registration failed: HTTP {} - {}", status, error_text);
return Err(AuthError::RegistrationFailed(format!(
"HTTP {}: {}",
status, error_text
Expand All@@ -406,7 +402,6 @@ impl AuthorizationManager {
let reg_response = match response.json::<ClientRegistrationResponse>().await {
Ok(response) => response,
Err(e) => {
error!("Failed to parse registration response: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"analyze response error: {}",
e
Expand DownExpand Up@@ -471,7 +466,6 @@ impl AuthorizationManager {
pkce_verifier,
csrf_token,
});
debug!("set authorization state: {:?}", self.state.read().await);

Ok(auth_url.to_string())
}
Expand DownExpand Up@@ -625,9 +619,9 @@ impl AuthorizationSession {
redirect_uri: &str,
client_name: Option<&str>,
) -> Result<Self, AuthError> {
// set redirect uri
// Default client config
let config = OAuthClientConfig {
client_id: "mcp-client".to_string(), // temporary id, will be updated by dynamic registration
client_id: "mcp-client".to_string(),
client_secret: None,
scopes: scopes.iter().map(|s| s.to_string()).collect(),
redirect_uri: redirect_uri.to_string(),
Expand All@@ -640,7 +634,10 @@ impl AuthorizationSession {
{
Ok(config) => config,
Err(e) => {
eprintln!("Dynamic registration failed: {}", e);
warn!(
"Dynamic registration failed: {}, fallback to default config",
e
);
// fallback to default config
config
}
Expand Down
2 changes: 1 addition & 1 deletion examples/servers/src/complex_auth_sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -525,7 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
issuer: Some(BIND_ADDRESS.to_string()),
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
additional_fields,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions crates/rmcp/src/transport/auth.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ use reqwest::{Client as HttpClient, IntoUrl, StatusCode, Url, header::AUTHORIZAT
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error};
use tracing::{debug, error, warn};

const DEFAULT_EXCHANGE_URL: &str = "http://localhost";

Expand DownExpand Up@@ -102,7 +102,7 @@ pub enum AuthError {
pub struct AuthorizationMetadata {
pub authorization_endpoint: String,
pub token_endpoint: String,
pub registration_endpoint: String,
pub registration_endpoint: Option<String>,
pub issuer: Option<String>,
pub jwks_uri: Option<String>,
pub scopes_supported: Option<Vec<String>>,
Expand DownExpand Up@@ -273,7 +273,7 @@ impl AuthorizationManager {
return Ok(metadata);
}

debug!("No valid .well-known endpoint found, falling back to default endpoints");
warn!("No valid .well-known endpoint found, falling back to default endpoints");

// fallback to default endpoints
let mut auth_base = self.base_url.clone();
Expand All@@ -290,7 +290,7 @@ impl AuthorizationManager {
Ok(AuthorizationMetadata {
authorization_endpoint: create_endpoint("authorize"),
token_endpoint: create_endpoint("token"),
registration_endpoint: create_endpoint("register"),
registration_endpoint: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that supporting DCR is a SHOULD in the MCP spec, If we cannot get metadata at all and we construct these defaults, should we continue to populate a default uri here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It aimed to achieve better compatibility with the RFC 8414 protocol, according to the rfc 8414 , it should be optional
image
.
And in the spec file ,the registrat should be alt in the picture, and user can setting the client ID by himself which we provide this interface ,
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub, for example, does not provide a value for registration_endpoint in their response, as they do not support DCR and require pre-registration of clients. (I had to make this same change in a fork, to support GitHub properly, and had not yet had an opportunity to open a PR here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

issuer: None,
jwks_uri: None,
scopes_supported: None,
Expand DownExpand Up@@ -323,12 +323,10 @@ impl AuthorizationManager {
let token_url = TokenUrl::new(metadata.token_endpoint.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?;

// debug!("token url: {:?}", token_url);
let client_id = ClientId::new(config.client_id);
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;

debug!("client_id: {:?}", client_id);
let mut client_builder = BasicClient::new(client_id.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url)
Expand All@@ -349,14 +347,16 @@ impl AuthorizationManager {
redirect_uri: &str,
) -> Result<OAuthClientConfig, AuthError> {
if self.metadata.is_none() {
error!("No authorization support detected");
return Err(AuthError::NoAuthorizationSupport);
}
Comment thread
jokemanfire marked this conversation as resolved.

let metadata = self.metadata.as_ref().unwrap();
let registration_url = metadata.registration_endpoint.clone();
let Some(registration_url) = metadata.registration_endpoint.as_ref() else {
return Err(AuthError::RegistrationFailed(
"Dynamic client registration not supported".to_string(),
));
};

debug!("registration url: {:?}", registration_url);
// prepare registration request
let registration_request = ClientRegistrationRequest {
client_name: name.to_string(),
Expand All@@ -369,8 +369,6 @@ impl AuthorizationManager {
response_types: vec!["code".to_string()],
};

debug!("registration request: {:?}", registration_request);

let response = match self
.http_client
.post(registration_url)
Expand All@@ -380,7 +378,6 @@ impl AuthorizationManager {
{
Ok(response) => response,
Err(e) => {
Comment thread
jokemanfire marked this conversation as resolved.
error!("Registration request failed: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"HTTP request error: {}",
e
Expand All@@ -395,7 +392,6 @@ impl AuthorizationManager {
Err(_) => "cannot get error details".to_string(),
};

error!("Registration failed: HTTP {} - {}", status, error_text);
return Err(AuthError::RegistrationFailed(format!(
"HTTP {}: {}",
status, error_text
Expand All@@ -406,7 +402,6 @@ impl AuthorizationManager {
let reg_response = match response.json::<ClientRegistrationResponse>().await {
Ok(response) => response,
Err(e) => {
error!("Failed to parse registration response: {}", e);
return Err(AuthError::RegistrationFailed(format!(
"analyze response error: {}",
e
Expand DownExpand Up@@ -471,7 +466,6 @@ impl AuthorizationManager {
pkce_verifier,
csrf_token,
});
debug!("set authorization state: {:?}", self.state.read().await);

Ok(auth_url.to_string())
}
Expand DownExpand Up@@ -625,9 +619,9 @@ impl AuthorizationSession {
redirect_uri: &str,
client_name: Option<&str>,
) -> Result<Self, AuthError> {
// set redirect uri
// Default client config
let config = OAuthClientConfig {
client_id: "mcp-client".to_string(), // temporary id, will be updated by dynamic registration
client_id: "mcp-client".to_string(),
client_secret: None,
scopes: scopes.iter().map(|s| s.to_string()).collect(),
redirect_uri: redirect_uri.to_string(),
Expand All@@ -640,7 +634,10 @@ impl AuthorizationSession {
{
Ok(config) => config,
Err(e) => {
eprintln!("Dynamic registration failed: {}", e);
warn!(
"Dynamic registration failed: {}, fallback to default config",
e
);
// fallback to default config
config
}
Expand Down
2 changes: 1 addition & 1 deletion examples/servers/src/complex_auth_sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -525,7 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
issuer: Some(BIND_ADDRESS.to_string()),
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
additional_fields,
Expand Down
Loading