fix(examples): add clients in examples's readme (#225)
Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
This commit is contained in:
parent
4e8686ff47
commit
0d52b7d24f
4 changed files with 109 additions and 17 deletions
93
examples/clients/src/auth/callback.html
Normal file
93
examples/clients/src/auth/callback.html
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OAuth Success</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--background-color: #f8f9fa;
|
||||
--text-color: #333;
|
||||
--border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 1rem;
|
||||
animation: scaleIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--primary-color);
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">✓</div>
|
||||
<h1>Authorization Success</h1>
|
||||
<p>You have successfully authorized the MCP client. You can now close this window and return to the application.</p>
|
||||
<button class="close-button" onclick="window.close()">Close Window</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
229
examples/clients/src/auth/oauth_client.rs
Normal file
229
examples/clients/src/auth/oauth_client.rs
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Query, State},
|
||||
response::Html,
|
||||
routing::get,
|
||||
};
|
||||
use rmcp::{
|
||||
ServiceExt,
|
||||
model::ClientInfo,
|
||||
transport::{
|
||||
SseClientTransport,
|
||||
auth::{AuthClient, OAuthState},
|
||||
sse_client::SseClientConfig,
|
||||
},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
|
||||
sync::{Mutex, oneshot},
|
||||
};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
const MCP_SERVER_URL: &str = "http://localhost:3000/mcp";
|
||||
const MCP_REDIRECT_URI: &str = "http://localhost:8080/callback";
|
||||
const MCP_SSE_URL: &str = "http://localhost:3000/mcp/sse";
|
||||
const CALLBACK_PORT: u16 = 8080;
|
||||
const CALLBACK_HTML: &str = include_str!("callback.html");
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
code_receiver: Arc<Mutex<Option<oneshot::Sender<String>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CallbackParams {
|
||||
code: String,
|
||||
#[allow(dead_code)]
|
||||
state: Option<String>,
|
||||
}
|
||||
|
||||
async fn callback_handler(
|
||||
Query(params): Query<CallbackParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> Html<String> {
|
||||
tracing::info!("Received callback with code: {}", params.code);
|
||||
|
||||
// Send the code to the main thread
|
||||
if let Some(sender) = state.code_receiver.lock().await.take() {
|
||||
let _ = sender.send(params.code);
|
||||
}
|
||||
// Return success page
|
||||
Html(CALLBACK_HTML.to_string())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "debug".to_string().into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
// it is a http server for handling callback
|
||||
// Create channel for receiving authorization code
|
||||
let (code_sender, code_receiver) = oneshot::channel::<String>();
|
||||
|
||||
// Create app state
|
||||
let app_state = AppState {
|
||||
code_receiver: Arc::new(Mutex::new(Some(code_sender))),
|
||||
};
|
||||
|
||||
// Start HTTP server for handling callbacks
|
||||
let app = Router::new()
|
||||
.route("/callback", get(callback_handler))
|
||||
.with_state(app_state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], CALLBACK_PORT));
|
||||
tracing::info!("Starting callback server at: http://{}", addr);
|
||||
|
||||
// Start server in a separate task
|
||||
tokio::spawn(async move {
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
let result = axum::serve(listener, app).await;
|
||||
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Callback server error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Get server URL
|
||||
let server_url = MCP_SERVER_URL.to_string();
|
||||
tracing::info!("Using MCP server URL: {}", server_url);
|
||||
|
||||
// Initialize oauth state machine
|
||||
let mut oauth_state = OAuthState::new(&server_url, None)
|
||||
.await
|
||||
.context("Failed to initialize oauth state machine")?;
|
||||
oauth_state
|
||||
.start_authorization(&["mcp", "profile", "email"], MCP_REDIRECT_URI)
|
||||
.await
|
||||
.context("Failed to start authorization")?;
|
||||
|
||||
// Output authorization URL to user
|
||||
let mut output = BufWriter::new(tokio::io::stdout());
|
||||
output.write_all(b"\n=== MCP OAuth Client ===\n\n").await?;
|
||||
output
|
||||
.write_all(b"Please open the following URL in your browser to authorize:\n\n")
|
||||
.await?;
|
||||
output
|
||||
.write_all(oauth_state.get_authorization_url().await?.as_bytes())
|
||||
.await?;
|
||||
output
|
||||
.write_all(b"\n\nWaiting for browser callback, please do not close this window...\n")
|
||||
.await?;
|
||||
output.flush().await?;
|
||||
|
||||
// Wait for authorization code
|
||||
tracing::info!("Waiting for authorization code...");
|
||||
let auth_code = code_receiver
|
||||
.await
|
||||
.context("Failed to get authorization code")?;
|
||||
tracing::info!("Received authorization code: {}", auth_code);
|
||||
// Exchange code for access token
|
||||
tracing::info!("Exchanging authorization code for access token...");
|
||||
oauth_state
|
||||
.handle_callback(&auth_code)
|
||||
.await
|
||||
.context("Failed to handle callback")?;
|
||||
tracing::info!("Successfully obtained access token");
|
||||
|
||||
output
|
||||
.write_all(b"\nAuthorization successful! Access token obtained.\n\n")
|
||||
.await?;
|
||||
output.flush().await?;
|
||||
|
||||
// Create authorized transport, this transport is authorized by the oauth state machine
|
||||
tracing::info!("Establishing authorized connection to MCP server...");
|
||||
let am = oauth_state
|
||||
.into_authorization_manager()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
|
||||
let client = AuthClient::new(reqwest::Client::default(), am);
|
||||
let transport = SseClientTransport::start_with_client(
|
||||
client,
|
||||
SseClientConfig {
|
||||
sse_endpoint: MCP_SSE_URL.into(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create client and connect to MCP server
|
||||
let client_service = ClientInfo::default();
|
||||
let client = client_service.serve(transport).await?;
|
||||
tracing::info!("Successfully connected to MCP server");
|
||||
|
||||
// Test API requests
|
||||
output
|
||||
.write_all(b"Fetching available tools from server...\n")
|
||||
.await?;
|
||||
output.flush().await?;
|
||||
|
||||
match client.peer().list_all_tools().await {
|
||||
Ok(tools) => {
|
||||
output
|
||||
.write_all(format!("Available tools: {}\n\n", tools.len()).as_bytes())
|
||||
.await?;
|
||||
for tool in tools {
|
||||
output
|
||||
.write_all(
|
||||
format!(
|
||||
"- {} ({})\n",
|
||||
tool.name,
|
||||
tool.description.unwrap_or_default()
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
output
|
||||
.write_all(format!("Error fetching tools: {}\n", e).as_bytes())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
.write_all(b"\nFetching available prompts from server...\n")
|
||||
.await?;
|
||||
output.flush().await?;
|
||||
|
||||
match client.peer().list_all_prompts().await {
|
||||
Ok(prompts) => {
|
||||
output
|
||||
.write_all(format!("Available prompts: {}\n\n", prompts.len()).as_bytes())
|
||||
.await?;
|
||||
for prompt in prompts {
|
||||
output
|
||||
.write_all(format!("- {}\n", prompt.name).as_bytes())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
output
|
||||
.write_all(format!("Error fetching prompts: {}\n", e).as_bytes())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
.write_all(b"\nConnection established successfully. You are now authenticated with the MCP server.\n")
|
||||
.await?;
|
||||
output.flush().await?;
|
||||
|
||||
// Keep the program running, wait for user input to exit
|
||||
output.write_all(b"\nPress Enter to exit...\n").await?;
|
||||
output.flush().await?;
|
||||
|
||||
let mut input = String::new();
|
||||
let mut reader = BufReader::new(tokio::io::stdin());
|
||||
reader.read_line(&mut input).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
/// This example show how to store multiple clients in a map and call tools on them.
|
||||
/// into_dyn() is used to convert the service to a dynamic service.
|
||||
/// For example, you can use this to call tools on a service that is running in a different process.
|
||||
/// or a service that is running in a different machine.
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
@ -20,7 +24,7 @@ async fn main() -> Result<()> {
|
|||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let mut client_list = HashMap::new();
|
||||
let mut clients_map = HashMap::new();
|
||||
for idx in 0..10 {
|
||||
let service = ()
|
||||
.into_dyn()
|
||||
|
|
@ -30,10 +34,10 @@ async fn main() -> Result<()> {
|
|||
},
|
||||
))?)
|
||||
.await?;
|
||||
client_list.insert(idx, service);
|
||||
clients_map.insert(idx, service);
|
||||
}
|
||||
|
||||
for (_, service) in client_list.iter() {
|
||||
for (_, service) in clients_map.iter() {
|
||||
// Initialize
|
||||
let _server_info = service.peer_info();
|
||||
|
||||
|
|
@ -48,7 +52,7 @@ async fn main() -> Result<()> {
|
|||
})
|
||||
.await?;
|
||||
}
|
||||
for (_, service) in client_list {
|
||||
for (_, service) in clients_map {
|
||||
service.cancel().await?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
48
examples/clients/src/git_stdio.rs
Normal file
48
examples/clients/src/git_stdio.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use anyhow::Result;
|
||||
use rmcp::{
|
||||
model::CallToolRequestParam,
|
||||
service::ServiceExt,
|
||||
transport::{ConfigureCommandExt, TokioChildProcess},
|
||||
};
|
||||
use tokio::process::Command;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
let service = ()
|
||||
.serve(TokioChildProcess::new(Command::new("uvx").configure(
|
||||
|cmd| {
|
||||
cmd.arg("mcp-server-git");
|
||||
},
|
||||
))?)
|
||||
.await?;
|
||||
|
||||
// or serve_client((), TokioChildProcess::new(cmd)?).await?;
|
||||
|
||||
// Initialize
|
||||
let server_info = service.peer_info();
|
||||
tracing::info!("Connected to server: {server_info:#?}");
|
||||
|
||||
// List tools
|
||||
let tools = service.list_tools(Default::default()).await?;
|
||||
tracing::info!("Available tools: {tools:#?}");
|
||||
|
||||
// Call tool 'git_status' with arguments = {"repo_path": "."}
|
||||
let tool_result = service
|
||||
.call_tool(CallToolRequestParam {
|
||||
name: "git_status".into(),
|
||||
arguments: serde_json::json!({ "repo_path": "." }).as_object().cloned(),
|
||||
})
|
||||
.await?;
|
||||
tracing::info!("Tool result: {tool_result:#?}");
|
||||
service.cancel().await?;
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in a new issue