A Rust-based MCP server that aggregates multiple upstream MCP servers, provides a GraphQL interface for tool discovery and execution, and includes an admin UI for management and monitoring.
vmcp exposes a single tool `query_graphql` that dynamically wraps upstream MCP servers via GraphQL introspection. The tool definition is visible in source (crates/vmcp-server/src/) with a clear description and three input parameters (query, variables, operationName). However, the schema lacks critical detail: no type constraints on the 'query' parameter (is it a raw GraphQL string or a validated document?), no documentation of what GraphQL operations are available or how to discover them, no output schema documented, and no error recovery guidance. The 'Risk: WRITE' annotation signals the tool can mutate state, but there is no description of how to safely use it or what preconditions must be met. The tool's purpose, to act as a universal gateway to arbitrary upstream servers, makes it inherently risky and powerful, but the definition does not guide an LLM on safe composition. Average params per tool: 3 (within baseline 4). Description length: ~80 chars (below production baseline 194 chars, violating pattern:tool-description length guidance). No documented output schema, no pagination support, no error classification.
Tools (1)
query_graphqlwriteauth42/100
Execute a GraphQL query against the aggregated upstream servers to discover and invoke tools
Naming does not follow verb_noun pattern. 'query_graphql' is generic and does not signal what the tool actually does (e.g., 'invoke_upstream_tool', 'discover_and_execute_upstream_tool', or 'execute_aggregated_tool'). An LLM cannot infer from the name alone what action this tool performs or when to prefer it over direct upstream calls.
Tool description is too brief (~80 chars) and lacks critical detail. Does not explain: (1) What GraphQL operations are available? (2) How does an LLM discover upstream tools? (3) What does the return value contain? (4) When should the LLM call this vs. individual upstream tools? (5) What happens on GraphQL parse errors or upstream failures? Production baseline is 194 chars; this is 41% of that.
No documented output schema. LLMs cannot infer the structure of the GraphQL response. Will the result be raw JSON? Wrapped in a 'data' field? What fields are safe to extract? Without a schema, the LLM may attempt to navigate the response incorrectly, leading to failed downstream tool invocations.
Recommendations
Rename tool to reflect its actual purpose: 'execute_aggregated_tool' or 'invoke_upstream_tool' (not 'query_graphql'). Help LLMs understand this is a gateway to upstream MCP servers, not a generic GraphQL client.
Expand description to 150-250 chars covering: (1) 'This tool invokes tools from aggregated upstream MCP servers by sending a GraphQL query. Use it to discover and call tools from the upstream pool without manually routing to individual servers.' (2) Include discovery guidance: 'First, query the GraphQL introspection schema to see available tools, fields, and types.' (3) Warn about mutations: 'This tool can modify state in upstream servers. For write operations, review the GraphQL query before executing.'
Add parameter descriptions: (1) 'query', 'GraphQL query document (string). Must be syntactically valid GraphQL. Use introspection queries (e.g., `{ __schema { types { name } } }`) to discover available tools. Supports variables for parameterization.' (2) 'variables', 'Optional object mapping variable names to values. Use to parameterize the GraphQL query (e.g., { "tool_name": "send_email", "user_id": "123" }).' (3) 'operationName', 'Optional. If the query document contains multiple operations, specify which to execute by name.' (4) Add a constraint on 'query' length (e.g., max 10KB to prevent DOS).
Document output schema: 'Returns a JSON object with two possible top-level fields: (1) "data", the successful query result, containing nested tool names, parameters, and return values from upstream servers. (2) "errors", array of error objects with message, path, and extensions if the query failed or partially failed. Example: { "data": { "tools": [ { "name": "send_email", "input": { "to": {...}, "subject": {...} } } ] } }.' Include examples of successful and error responses.
The 'query' parameter lacks a description and type constraint. Is it a raw GraphQL query string? Must it be syntactically valid? Can it include arbitrary variables? No guidance on format, length limits, or examples. LLMs will guess and may pass invalid or injection-prone payloads.
No error handling guidance. What happens if a GraphQL query is malformed? If an upstream server is unreachable? If a field is not found? The tool definition does not categorize errors as retryable, user-fixable, or fatal, preventing the LLM from recovering intelligently.
Tool is marked 'Risk: WRITE' but no dry-run or confirmation mechanism is documented. If an LLM issues a mutation (e.g., creating a resource in an upstream server), there is no way to preview the effect or ask for confirmation before execution. Agents make mistakes, this pattern is missing.
No documentation of how to discover available upstream tools or GraphQL schema. An LLM calling this tool must already know what queries to write. There is no 'introspect schema' or 'list available tools' guidance, forcing the LLM to infer or hallucinate GraphQL operations.
query_graphql
Add error classification: 'GraphQL parse errors (invalid syntax) are user-fixable, the LLM should re-read the error message and correct the query. Upstream service errors (e.g., tool not found) are retryable, try another query or introspect the schema. Network timeouts are retryable with exponential backoff. If an error includes suggestions (e.g., "Did you mean: send_email_to_user?"), the LLM should retry with the suggestion.'
Add discovery mechanism: 'To see what tools are available, first call query_graphql with: { query: "query { tools { name description input { name type } } }" }. This returns all upstream tools, their input parameters, and descriptions. Use this to understand what you can invoke before calling mutations.'
Add a confirmation or dry-run pattern: 'For write operations (mutations), first call with a dry-run flag or validation query to preview the effect. E.g., call with query: "mutation { dryRun: createTicket(title: \"...\") { id, title } }" to simulate without persisting. Then execute the real mutation only after confirmation.'
Add rate-limit and timeout guidance: 'This tool may timeout if the upstream GraphQL server is slow or overloaded. Timeout is 30 seconds; if exceeded, the query is cancelled. Set reasonable result limits in your GraphQL query (e.g., `limit: 20`) to avoid returning thousands of items that exhaust context.'
Document the relationship between GraphQL field names and actual upstream tool parameter names. E.g., 'The GraphQL introspection schema exposes upstream tool parameters as fields. If a tool accepts "user_id", the GraphQL field is also "user_id". Match names exactly; typos will cause "field not found" errors.'
For the optional parameters (variables, operationName), clarify when they are needed: 'Use variables to parameterize queries so the LLM does not have to concatenate strings. Use operationName only if your query document contains multiple operations (uncommon).' Provide an example: { "query": "query GetUser($id: String!) { user(id: $id) { name email } }", "variables": { "id": "user_123" } }