github_mcp/core/
mcp_server.rs1use std::sync::Arc;
4
5use rmcp::handler::server::router::tool::ToolRouter;
6use rmcp::handler::server::wrapper::Parameters;
7use rmcp::model::{
8 CallToolResult, ContentBlock, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo,
9};
10use rmcp::service::RequestContext;
11use rmcp::transport::stdio;
12use rmcp::{
13 ErrorData as McpError, RoleServer, ServerHandler, ServiceExt, schemars, tool, tool_handler,
14 tool_router,
15};
16use serde::Deserialize;
17use tokio::sync::Mutex;
18
19use crate::auth::auth_manager::{AuthManager, header_location_for};
20use crate::core::config_schema::Config;
21use crate::core::errors::McpifyError;
22use crate::data::store::{cached_store_connection, get_endpoint};
23use crate::http::auth_extractor::extract_request_credentials;
24use crate::tools::call_tool::call_operation;
25use crate::tools::get_tool::get_operation;
26use crate::tools::search_tool::search_operations;
27
28fn default_search_limit() -> usize {
29 5
30}
31
32#[derive(Debug, Deserialize, schemars::JsonSchema)]
33pub struct SearchArgs {
34 pub query: String,
36 #[serde(default = "default_search_limit")]
38 pub limit: usize,
39}
40
41#[derive(Debug, Deserialize, schemars::JsonSchema)]
42pub struct GetArgs {
43 pub operation_id: String,
45}
46
47#[derive(Debug, Deserialize, schemars::JsonSchema)]
48pub struct CallArgs {
49 pub operation_id: String,
51 #[serde(default)]
53 pub arguments: serde_json::Value,
54}
55
56#[derive(Clone)]
61pub struct McpifyServer {
62 api_version: String,
63 config: Config,
64 auth_manager: Arc<Mutex<AuthManager>>,
65 tool_router: ToolRouter<McpifyServer>,
66}
67
68#[tool_router]
69impl McpifyServer {
70 pub fn new(api_version: String, config: Config, auth_manager: Arc<Mutex<AuthManager>>) -> Self {
78 Self {
79 api_version,
80 config,
81 auth_manager,
82 tool_router: Self::tool_router(),
83 }
84 }
85
86 #[tool(
87 description = "Semantic search for GitHub v3 REST API operations using a natural-language query."
88 )]
89 async fn search(
90 &self,
91 Parameters(args): Parameters<SearchArgs>,
92 ) -> Result<CallToolResult, McpError> {
93 let api_version = self.api_version.clone();
94 self.run_tool("search", async move {
95 let conn = cached_store_connection(&api_version)?.lock().unwrap();
96 search_operations(&conn, &args.query, args.limit)
97 })
98 .await
99 }
100
101 #[tool(
102 description = "Return the schema, path, method, and documentation for a specific GitHub v3 REST API operationId."
103 )]
104 async fn get(&self, Parameters(args): Parameters<GetArgs>) -> Result<CallToolResult, McpError> {
105 let api_version = self.api_version.clone();
106 self.run_tool("get", async move {
107 let conn = cached_store_connection(&api_version)?.lock().unwrap();
108 get_operation(&conn, &args.operation_id)
109 })
110 .await
111 }
112
113 #[tool(
114 description = "Validate arguments, invoke a live GitHub v3 REST API API operation, and validate the response."
115 )]
116 async fn call(
117 &self,
118 Parameters(args): Parameters<CallArgs>,
119 context: RequestContext<RoleServer>,
120 ) -> Result<CallToolResult, McpError> {
121 let api_version = self.api_version.clone();
122 let config = self.config.clone();
123 let auth_manager = self.auth_manager.clone();
124
125 let request_credentials = context
133 .extensions
134 .get::<axum::http::request::Parts>()
135 .and_then(|parts| {
136 let (header_location, header_name) = header_location_for(config.auth_method);
137 extract_request_credentials(&parts.headers, header_location, header_name).ok()
138 });
139
140 self.run_tool("call", async move {
141 let endpoint = {
146 let conn = cached_store_connection(&api_version)?.lock().unwrap();
147 get_endpoint(&conn, &args.operation_id)?.ok_or_else(|| {
148 McpifyError::NotFound(format!("unknown operationId '{}'", args.operation_id))
149 })?
150 };
151
152 let mut auth_manager = auth_manager.lock().await;
153 call_operation(
154 &endpoint,
155 &config,
156 &mut auth_manager,
157 &args.operation_id,
158 args.arguments,
159 request_credentials.as_ref(),
160 )
161 .await
162 })
163 .await
164 }
165}
166
167impl McpifyServer {
168 async fn run_tool<F>(&self, tool_name: &str, fut: F) -> Result<CallToolResult, McpError>
173 where
174 F: std::future::Future<Output = anyhow::Result<serde_json::Value>>,
175 {
176 match fut.await {
177 Ok(value) => {
178 let text =
179 serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string());
180 Ok(CallToolResult::success(vec![ContentBlock::text(text)]))
181 }
182 Err(err) => {
183 tracing::error!(tool = tool_name, error = %err, "tool execution failed");
184 Ok(CallToolResult::error(vec![ContentBlock::text(
185 err.to_string(),
186 )]))
187 }
188 }
189 }
190}
191
192#[tool_handler(router = self.tool_router.clone())]
197impl ServerHandler for McpifyServer {
198 fn get_info(&self) -> ServerInfo {
199 ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
200 .with_server_info(Implementation::from_build_env())
201 .with_protocol_version(ProtocolVersion::V_2024_11_05)
202 .with_instructions(
203 "Exposes exactly 3 tools -- search, get, call -- backed by an embedded \
204 semantic database, so you never need the full API surface in context."
205 .to_string(),
206 )
207 }
208}
209
210pub async fn connect_stdio<S>(server: S) -> anyhow::Result<()>
214where
215 S: rmcp::ServerHandler,
216{
217 let running = server.serve(stdio()).await?;
218 tracing::info!("MCP server connected over stdio");
219 running.waiting().await?;
220 Ok(())
221}