1use crate::auth::OAuthConfig;
2use crate::handler_types::{RegisteredPrompt, RegisteredResource, RegisteredTool};
3use crate::jsonrpc::JsonRpcResponse;
4use crate::metadata::{PromptMeta, ResourceMeta, ToolMeta};
5use crate::protocol::{Implementation, ServerCapabilities};
6use crate::transport::create_app;
7use actix_web::{middleware::Logger, App, HttpServer};
8use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::broadcast;
11
12pub struct HttpMcpServer {
14 pub(crate) server_info: Implementation,
15 pub(crate) capabilities: ServerCapabilities,
16 pub(crate) tools: HashMap<String, RegisteredTool>,
17 pub(crate) resources: HashMap<String, RegisteredResource>,
18 pub(crate) prompts: HashMap<String, RegisteredPrompt>,
19 pub(crate) oauth_config: Option<OAuthConfig>,
20 pub(crate) enable_cors: bool,
21 pub(crate) response_tx: broadcast::Sender<JsonRpcResponse>,
22}
23
24impl HttpMcpServer {
25 pub fn builder() -> HttpMcpServerBuilder {
27 HttpMcpServerBuilder::new()
28 }
29
30 pub async fn run(self, addr: impl Into<String>) -> std::io::Result<()> {
32 let addr = addr.into();
33 let server = Arc::new(self);
34
35 tracing::info!("Starting MCP server on {}", addr);
36
37 HttpServer::new(move || {
38 App::new()
39 .wrap(Logger::default())
40 .configure(|cfg| create_app(cfg, server.clone()))
41 })
42 .bind(&addr)?
43 .run()
44 .await
45 }
46}
47
48pub struct HttpMcpServerBuilder {
50 name: String,
51 version: String,
52 tools: HashMap<String, RegisteredTool>,
53 resources: HashMap<String, RegisteredResource>,
54 prompts: HashMap<String, RegisteredPrompt>,
55 oauth_config: Option<OAuthConfig>,
56 enable_cors: bool,
57}
58
59impl HttpMcpServerBuilder {
60 pub fn new() -> Self {
61 Self {
62 name: "httpmcp-server".to_string(),
63 version: "1.0.0".to_string(),
64 tools: HashMap::new(),
65 resources: HashMap::new(),
66 prompts: HashMap::new(),
67 oauth_config: None,
68 enable_cors: true,
69 }
70 }
71
72 pub fn name(mut self, name: impl Into<String>) -> Self {
74 self.name = name.into();
75 self
76 }
77
78 pub fn version(mut self, version: impl Into<String>) -> Self {
80 self.version = version.into();
81 self
82 }
83
84 pub fn tool<F, Fut>(mut self, name: impl Into<String>, meta: ToolMeta, handler: F) -> Self
86 where
87 F: Fn(HashMap<String, serde_json::Value>, crate::context::RequestContext) -> Fut
88 + Send
89 + Sync
90 + 'static,
91 Fut: std::future::Future<Output = crate::error::Result<serde_json::Value>> + Send + 'static,
92 {
93 let name_str = name.into();
94 let tool = RegisteredTool {
95 meta: meta.to_tool(name_str.clone()),
96 handler: Box::new(move |args, ctx| Box::pin(handler(args, ctx))),
97 };
98 self.tools.insert(name_str, tool);
99 self
100 }
101
102 pub fn resource<FL, FR, FutL, FutR>(
104 mut self,
105 uri: impl Into<String>,
106 meta: ResourceMeta,
107 list_handler: FL,
108 read_handler: FR,
109 ) -> Self
110 where
111 FL: Fn(Option<String>, crate::context::RequestContext) -> FutL + Send + Sync + 'static,
112 FutL: std::future::Future<
113 Output = crate::error::Result<(Vec<crate::protocol::Resource>, Option<String>)>,
114 > + Send
115 + 'static,
116 FR: Fn(String, crate::context::RequestContext) -> FutR + Send + Sync + 'static,
117 FutR: std::future::Future<
118 Output = crate::error::Result<Vec<crate::protocol::ResourceContents>>,
119 > + Send
120 + 'static,
121 {
122 let uri_str = uri.into();
123 let resource = RegisteredResource {
124 meta: meta.to_resource(uri_str.clone()),
125 list_handler: Box::new(move |cursor, ctx| Box::pin(list_handler(cursor, ctx))),
126 read_handler: Box::new(move |uri, ctx| Box::pin(read_handler(uri, ctx))),
127 };
128 self.resources.insert(uri_str, resource);
129 self
130 }
131
132 pub fn prompt<F, Fut>(mut self, name: impl Into<String>, meta: PromptMeta, handler: F) -> Self
134 where
135 F: Fn(String, Option<HashMap<String, String>>, crate::context::RequestContext) -> Fut
136 + Send
137 + Sync
138 + 'static,
139 Fut: std::future::Future<
140 Output = crate::error::Result<(
141 Option<String>,
142 Vec<crate::protocol::PromptMessage>,
143 )>,
144 > + Send
145 + 'static,
146 {
147 let name_str = name.into();
148 let prompt = RegisteredPrompt {
149 meta: meta.to_prompt(name_str.clone()),
150 handler: Box::new(move |name, args, ctx| Box::pin(handler(name, args, ctx))),
151 };
152 self.prompts.insert(name_str, prompt);
153 self
154 }
155
156 pub fn with_oauth(
158 mut self,
159 client_id: impl Into<String>,
160 client_secret: impl Into<String>,
161 _token_url: impl Into<String>,
162 _auth_url: impl Into<String>,
163 ) -> Self {
164 self.oauth_config = Some(OAuthConfig {
165 client_id: client_id.into(),
166 client_secret: client_secret.into(),
167 });
168 self
169 }
170
171 pub fn enable_cors(mut self, enable: bool) -> Self {
173 self.enable_cors = enable;
174 self
175 }
176
177 pub fn build(self) -> crate::error::Result<HttpMcpServer> {
179 let capabilities = ServerCapabilities {
180 logging: Some(Default::default()),
181 prompts: if self.prompts.is_empty() {
182 None
183 } else {
184 Some(Default::default())
185 },
186 resources: if self.resources.is_empty() {
187 None
188 } else {
189 Some(Default::default())
190 },
191 tools: if self.tools.is_empty() {
192 None
193 } else {
194 Some(Default::default())
195 },
196 };
197
198 let (response_tx, _) = broadcast::channel(100);
200
201 Ok(HttpMcpServer {
202 server_info: Implementation {
203 name: self.name,
204 version: self.version,
205 },
206 capabilities,
207 tools: self.tools,
208 resources: self.resources,
209 prompts: self.prompts,
210 oauth_config: self.oauth_config,
211 enable_cors: self.enable_cors,
212 response_tx,
213 })
214 }
215}
216
217impl Default for HttpMcpServerBuilder {
218 fn default() -> Self {
219 Self::new()
220 }
221}