From 8e4a657faf7dddd84a05c4dfafdc73c70bfb3aaf Mon Sep 17 00:00:00 2001 From: Michal Pietrusinski Date: Fri, 12 Sep 2025 17:37:14 +0200 Subject: [PATCH] feat: Enhance Execute tool with customizable hints support --- .../src/introspection/tools/execute.rs | 35 +++++++++++++------ crates/apollo-mcp-server/src/main.rs | 8 ++++- crates/apollo-mcp-server/src/runtime.rs | 2 ++ .../src/runtime/introspection.rs | 35 +++++++++++++++++++ crates/apollo-mcp-server/src/server.rs | 3 ++ crates/apollo-mcp-server/src/server/states.rs | 2 ++ .../src/server/states/starting.rs | 2 +- graphql/TheSpaceDevs/config.yaml | 2 ++ graphql/TheSpaceDevs/execute_hints.txt | 23 ++++++++++++ 9 files changed, 99 insertions(+), 13 deletions(-) create mode 100644 graphql/TheSpaceDevs/execute_hints.txt diff --git a/crates/apollo-mcp-server/src/introspection/tools/execute.rs b/crates/apollo-mcp-server/src/introspection/tools/execute.rs index 2f8702b3..a9248bb3 100644 --- a/crates/apollo-mcp-server/src/introspection/tools/execute.rs +++ b/crates/apollo-mcp-server/src/introspection/tools/execute.rs @@ -32,12 +32,25 @@ pub struct Input { } impl Execute { - pub fn new(mutation_mode: MutationMode) -> Self { + pub fn new(mutation_mode: MutationMode, hints: Option) -> Self { + let base_description = "Execute a GraphQL operation. Use the `introspect` tool to get information about the GraphQL schema. Always use the schema to create operations - do not try arbitrary operations. If available, first use the `validate` tool to validate operations. DO NOT try to execute introspection queries."; + + let description = match hints.as_ref() { + Some(hint_text) => { + tracing::info!("Execute tool created with custom hints: {}", hint_text); + format!("{}\n\n{}", base_description, hint_text) + }, + None => { + tracing::info!("Execute tool created without custom hints"); + base_description.to_string() + }, + }; + Self { mutation_mode, tool: Tool::new( EXECUTE_TOOL_NAME, - "Execute a GraphQL operation. Use the `introspect` tool to get information about the GraphQL schema. Always use the schema to create operations - do not try arbitrary operations. If available, first use the `validate` tool to validate operations. DO NOT try to execute introspection queries.", + description, schema_from_type!(Input), ), } @@ -106,7 +119,7 @@ mod tests { #[test] fn execute_query_with_variables_as_string() { - let execute = Execute::new(MutationMode::None); + let execute = Execute::new(MutationMode::None, None); let query = "query GetUser($id: ID!) { user(id: $id) { id name } }"; let variables = json!({ "id": "123" }); @@ -128,7 +141,7 @@ mod tests { #[test] fn execute_query_with_variables_as_json() { - let execute = Execute::new(MutationMode::None); + let execute = Execute::new(MutationMode::None, None); let query = "query GetUser($id: ID!) { user(id: $id) { id name } }"; let variables = json!({ "id": "123" }); @@ -150,7 +163,7 @@ mod tests { #[test] fn execute_query_without_variables() { - let execute = Execute::new(MutationMode::None); + let execute = Execute::new(MutationMode::None, None); let query = "query GetUser($id: ID!) { user(id: $id) { id name } }"; @@ -170,7 +183,7 @@ mod tests { #[test] fn execute_query_anonymous_operation() { - let execute = Execute::new(MutationMode::None); + let execute = Execute::new(MutationMode::None, None); let query = "{ user(id: \"123\") { id name } }"; let input = json!({ @@ -188,7 +201,7 @@ mod tests { #[test] fn execute_query_err_with_mutation_when_mutation_mode_is_none() { - let execute = Execute::new(MutationMode::None); + let execute = Execute::new(MutationMode::None, None); let query = "mutation MutationName { id }".to_string(); let input = json!({ @@ -207,7 +220,7 @@ mod tests { #[test] fn execute_query_ok_with_mutation_when_mutation_mode_is_all() { - let execute = Execute::new(MutationMode::All); + let execute = Execute::new(MutationMode::All, None); let query = "mutation MutationName { id }".to_string(); let input = json!({ @@ -230,7 +243,7 @@ mod tests { MutationMode::Explicit, MutationMode::All, ] { - let execute = Execute::new(mutation_mode); + let execute = Execute::new(mutation_mode, None); let input = json!({ "query": "subscription SubscriptionName { id }", @@ -249,7 +262,7 @@ mod tests { #[test] fn execute_query_invalid_input() { - let execute = Execute::new(MutationMode::None); + let execute = Execute::new(MutationMode::None, None); let input = json!({ "nonsense": "whatever", @@ -275,7 +288,7 @@ mod tests { #[test] fn execute_query_invalid_variables() { - let execute = Execute::new(MutationMode::None); + let execute = Execute::new(MutationMode::None, None); let input = json!({ "query": "query GetUser($id: ID!) { user(id: $id) { id name } }", diff --git a/crates/apollo-mcp-server/src/main.rs b/crates/apollo-mcp-server/src/main.rs index 3f3d8738..592a5990 100644 --- a/crates/apollo-mcp-server/src/main.rs +++ b/crates/apollo-mcp-server/src/main.rs @@ -37,7 +37,8 @@ struct Args { #[tokio::main] async fn main() -> anyhow::Result<()> { - let config: runtime::Config = match Args::parse().config { + let args = Args::parse(); + let config: runtime::Config = match &args.config { Some(config_path) => runtime::read_config(config_path)?, None => runtime::read_config_from_env().unwrap_or_default(), }; @@ -109,6 +110,10 @@ async fn main() -> anyhow::Result<()> { .then(|| config.graphos.graph_ref()) .transpose()?; + // Resolve execute hints from either inline hints or hints_file + let config_dir = args.config.as_ref().and_then(|p| p.parent()); + let resolved_execute_hints = config.introspection.execute.resolve_hints(config_dir)?; + let transport = config.transport.clone(); Ok(Server::builder() @@ -119,6 +124,7 @@ async fn main() -> anyhow::Result<()> { .maybe_explorer_graph_ref(explorer_graph_ref) .headers(config.headers) .execute_introspection(config.introspection.execute.enabled) + .maybe_execute_hints(resolved_execute_hints) .validate_introspection(config.introspection.validate.enabled) .introspect_introspection(config.introspection.introspect.enabled) .introspect_minify(config.introspection.introspect.minify) diff --git a/crates/apollo-mcp-server/src/runtime.rs b/crates/apollo-mcp-server/src/runtime.rs index bc63f7af..03b181e6 100644 --- a/crates/apollo-mcp-server/src/runtime.rs +++ b/crates/apollo-mcp-server/src/runtime.rs @@ -220,6 +220,8 @@ mod test { introspection: Introspection { execute: ExecuteConfig { enabled: false, + hints: None, + hints_file: None, }, introspect: IntrospectConfig { enabled: false, diff --git a/crates/apollo-mcp-server/src/runtime/introspection.rs b/crates/apollo-mcp-server/src/runtime/introspection.rs index 61451049..f9366d8a 100644 --- a/crates/apollo-mcp-server/src/runtime/introspection.rs +++ b/crates/apollo-mcp-server/src/runtime/introspection.rs @@ -1,5 +1,6 @@ use schemars::JsonSchema; use serde::Deserialize; +use std::path::Path; /// Introspection configuration #[derive(Debug, Default, Deserialize, JsonSchema)] @@ -24,6 +25,40 @@ pub struct Introspection { pub struct ExecuteConfig { /// Enable introspection for execution pub enabled: bool, + + /// Additional hints to append to the execute tool description + pub hints: Option, + + /// Path to a file containing additional hints to append to the execute tool description. + /// If both hints and hints_file are provided, hints_file takes precedence. + /// Path is relative to the configuration file location. + pub hints_file: Option, +} + +impl ExecuteConfig { + /// Resolve hints from either inline hints or hints_file + /// If hints_file is provided, it takes precedence over inline hints + /// The file path is resolved relative to the config_dir if provided + pub fn resolve_hints(&self, config_dir: Option<&Path>) -> Result, std::io::Error> { + if let Some(hints_file) = &self.hints_file { + let hints_path = if let Some(config_dir) = config_dir { + config_dir.join(hints_file) + } else { + hints_file.into() + }; + + let hints_content = std::fs::read_to_string(&hints_path) + .map_err(|e| { + tracing::warn!("Failed to read hints file '{}': {}", hints_path.display(), e); + e + })?; + + tracing::info!("Loaded hints from file: {}", hints_path.display()); + Ok(Some(hints_content)) + } else { + Ok(self.hints.clone()) + } + } } /// Introspect-specific introspection configuration diff --git a/crates/apollo-mcp-server/src/server.rs b/crates/apollo-mcp-server/src/server.rs index 6ef0153c..9edeb32f 100644 --- a/crates/apollo-mcp-server/src/server.rs +++ b/crates/apollo-mcp-server/src/server.rs @@ -26,6 +26,7 @@ pub struct Server { endpoint: Url, headers: HeaderMap, execute_introspection: bool, + execute_hints: Option, validate_introspection: bool, introspect_introspection: bool, introspect_minify: bool, @@ -103,6 +104,7 @@ impl Server { endpoint: Url, headers: HeaderMap, execute_introspection: bool, + execute_hints: Option, validate_introspection: bool, introspect_introspection: bool, search_introspection: bool, @@ -130,6 +132,7 @@ impl Server { endpoint, headers, execute_introspection, + execute_hints, validate_introspection, introspect_introspection, search_introspection, diff --git a/crates/apollo-mcp-server/src/server/states.rs b/crates/apollo-mcp-server/src/server/states.rs index 2bf71147..4a61b1ee 100644 --- a/crates/apollo-mcp-server/src/server/states.rs +++ b/crates/apollo-mcp-server/src/server/states.rs @@ -34,6 +34,7 @@ struct Config { endpoint: Url, headers: HeaderMap, execute_introspection: bool, + execute_hints: Option, validate_introspection: bool, introspect_introspection: bool, search_introspection: bool, @@ -67,6 +68,7 @@ impl StateMachine { endpoint: server.endpoint, headers: server.headers, execute_introspection: server.execute_introspection, + execute_hints: server.execute_hints, validate_introspection: server.validate_introspection, introspect_introspection: server.introspect_introspection, search_introspection: server.search_introspection, diff --git a/crates/apollo-mcp-server/src/server/states/starting.rs b/crates/apollo-mcp-server/src/server/states/starting.rs index 4109faca..03b3818b 100644 --- a/crates/apollo-mcp-server/src/server/states/starting.rs +++ b/crates/apollo-mcp-server/src/server/states/starting.rs @@ -64,7 +64,7 @@ impl Starting { let execute_tool = self .config .execute_introspection - .then(|| Execute::new(self.config.mutation_mode)); + .then(|| Execute::new(self.config.mutation_mode, self.config.execute_hints.clone())); let root_query_type = self .config diff --git a/graphql/TheSpaceDevs/config.yaml b/graphql/TheSpaceDevs/config.yaml index 5e32861b..802455ae 100644 --- a/graphql/TheSpaceDevs/config.yaml +++ b/graphql/TheSpaceDevs/config.yaml @@ -13,6 +13,8 @@ overrides: introspection: execute: enabled: true + hints_file: execute_hints.txt + # hints: Some inline hints that could be used instead of hints_file introspect: enabled: true search: diff --git a/graphql/TheSpaceDevs/execute_hints.txt b/graphql/TheSpaceDevs/execute_hints.txt new file mode 100644 index 00000000..ce124d6c --- /dev/null +++ b/graphql/TheSpaceDevs/execute_hints.txt @@ -0,0 +1,23 @@ +🚀 TheSpaceDevs API Execution Hints: + +• Popular queries to explore: + - Launch data: Use 'launches' field with filters like upcoming, status, or date ranges + - Spacecraft info: Query 'spacecraft' for technical details and mission history + - Astronaut data: Access 'astronauts' for crew information and flight records + - Agency details: Use 'agencies' to get space organization information + +• Common filter patterns: + - Date filtering: Use 'net__gte' and 'net__lte' for launch time windows + - Status filtering: 'status__ids' accepts arrays like [1,3,4] for launch statuses + - Limit results: Always include 'limit' parameter to control response size + +• Best practices: + - Start with small limit values (10-20) to avoid large responses + - Use 'ordering' parameter to sort results (e.g., 'net' for launch date) + - Check 'total_count' in responses to understand dataset size + - Combine multiple fields for richer data (launches + spacecraft + agencies) + +• Example useful queries: + - Next SpaceX launches: filter by agency name and upcoming status + - ISS crew rotations: query astronauts with ISS mission filters + - Recent successful launches: filter by status and recent dates \ No newline at end of file