Skip to main content
Glama
ctkadvisors

GraphQL MCP Server

by ctkadvisors

continents

Query global continents data efficiently using a GraphQL API with customizable filters to retrieve precise geographic information.

Instructions

GraphQL continents query

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
filterNofilter - Input type: ContinentFilterInput

Implementation Reference

  • The tools/list JSON-RPC handler that dynamically generates and registers the 'continents' tool (along with others) by fetching the schema and calling getToolsFromSchema to build the MCPTool list.
    // Handle tools/list
    if (method === "tools/list") {
      // Get schema (will use cache if available)
      const schema = await fetchSchema();
      const queryTools = getToolsFromSchema(schema);
      
      // Only include mutations if they are enabled
      const mutationTools = ENABLE_MUTATIONS ? getToolsFromMutationType(schema) : [];
      const allTools = [...queryTools, ...mutationTools];
    
      log(
        "info",
        `Returning ${allTools.length} tools (${queryTools.length} queries, ${mutationTools.length} mutations${!ENABLE_MUTATIONS ? ' - mutations disabled' : ''})`
      );
    
      const response: JSONRPCResponse = {
        jsonrpc: "2.0",
        id,
        result: {
          tools: allTools,
        },
      };
      console.log(JSON.stringify(response));
      return;
  • Dynamically generates the MCPTool definition including name, description, and inputSchema for the 'continents' query field from the GraphQL schema.
    function getToolsFromSchema(schema: GraphQLSchema | null): MCPTool[] {
      if (!schema) {
        return []; // Return empty tools list if no schema available
      }
    
      const tools: MCPTool[] = [];
    
      // Get the query type from the schema
      const queryType = schema.getQueryType();
      if (!queryType) {
        log("warning", "Schema has no query type");
        return tools;
      }
    
      // Get all available query fields
      const fields = queryType.getFields();
    
      // Process each query field as a potential tool
      for (const [fieldName, field] of Object.entries(fields)) {
        // Skip fields that aren't in the whitelist (if a whitelist is provided)
        if (WHITELISTED_QUERIES && !WHITELISTED_QUERIES.includes(fieldName)) {
          if (DEBUG) {
            log("debug", `Skipping field ${fieldName} - not in whitelist`);
          }
          continue;
        }
        try {
          // Analyze arguments for the field
          const properties: Record<string, any> = {};
          const required: string[] = [];
    
          // Process arguments
          field.args.forEach((arg) => {
            // Get the actual GraphQL type (unwrap non-null and list types)
            let argType = arg.type;
            let isNonNull = false;
    
            if (isNonNullType(argType)) {
              isNonNull = true;
              argType = argType.ofType;
            }
    
            let isList = false;
            if (isListType(argType)) {
              isList = true;
              argType = argType.ofType;
              // Unwrap non-null inside list if needed
              if (isNonNullType(argType)) {
                argType = argType.ofType;
              }
            }
    
            const baseType = getNamedType(argType);
    
            // Create property definition
            if (isScalarType(baseType)) {
              // For scalar types like Int, String, etc.
              properties[arg.name] = {
                type: getJsonSchemaType(baseType.name),
                description:
                  arg.description || `${arg.name} parameter (${baseType.name})`,
              };
            } else if (isEnumType(baseType)) {
              // For enum types, specify allowed values
              const enumValues = baseType.getValues().map((val) => val.name);
              properties[arg.name] = {
                type: "string",
                enum: enumValues,
                description:
                  arg.description || `${arg.name} parameter (${baseType.name})`,
              };
            } else if (isInputObjectType(baseType)) {
              // For input object types (complex types), describe it as object
              properties[arg.name] = {
                type: "object",
                description: `${arg.name} - Input type: ${baseType.name}`,
              };
    
              // If we wanted to go deeper, we could recursively define the object structure:
              // properties[arg.name].properties = getInputObjectProperties(baseType);
            } else {
              // Default catch-all
              properties[arg.name] = {
                type: "string",
                description: arg.description || `${arg.name} parameter`,
              };
            }
    
            // If arg is non-null, add to required list
            if (isNonNull) {
              required.push(arg.name);
            }
          });
    
          // Create a tool for this field - Truncate name to 64 characters if needed
          const truncatedName =
            fieldName.length > 64 ? fieldName.substring(0, 64) : fieldName;
    
          // Store mapping from truncated to original name if truncation occurred
          if (truncatedName !== fieldName) {
            toolNameMappings[truncatedName] = fieldName;
          }
    
          const tool: MCPTool = {
            name: truncatedName,
            description: field.description || `GraphQL ${fieldName} query`,
            inputSchema: {
              type: "object",
              properties: properties,
              required: required,
            },
          };
    
          tools.push(tool);
        } catch (error) {
          log(
            "error",
            `Error processing field ${fieldName}: ${
              error instanceof Error ? error.message : String(error)
            }`
          );
        }
      }
    
      return tools;
    }
  • The core handler function that implements the execution logic for the 'continents' tool: resolves the GraphQL field, processes arguments, dynamically builds an optimized GraphQL query, and executes it against the countries API.
    async function executeGraphQLTool(
      name: string,
      args: Record<string, any> | undefined
    ): Promise<any> {
      try {
        // If this name is in our mapping, use the original field name
        const actualFieldName = toolNameMappings[name] || name;
    
        // Check if the tool is in the whitelist (if whitelist is enabled)
        if (WHITELISTED_QUERIES && !WHITELISTED_QUERIES.includes(actualFieldName)) {
          throw new Error(`Tool '${actualFieldName}' is not in the whitelist`);
        }
    
        // Get the schema
        const schema = await fetchSchema();
        if (!schema) {
          throw new Error("Schema not available");
        }
    
        // Get the query type
        const queryType = schema.getQueryType();
        if (!queryType) {
          throw new Error("Schema has no query type");
        }
    
        // Get the field for this tool using the resolved field name
        const fields = queryType.getFields();
        const field = fields[actualFieldName];
    
        if (!field) {
          throw new Error(`Unknown field: ${actualFieldName}`);
        }
    
        try {
          // Process input arguments
          const processedArgs = processArguments(args, field.args, schema);
    
          // Get return type and analyze it
          const returnType = getNamedType(field.type);
    
          // First determine which arguments are actually being used
          const usedArgNames: string[] = [];
          field.args.forEach((arg) => {
            if (processedArgs && processedArgs[arg.name] !== undefined) {
              usedArgNames.push(arg.name);
            }
          });
    
          // Build variables definition - only for arguments that are actually used
          const varDefs = field.args
            .filter((arg) => usedArgNames.includes(arg.name))
            .map((arg) => {
              // Need to preserve non-null and list wrappers in variable definitions
              const typeStr = arg.type.toString();
              return `$${arg.name}: ${typeStr}`;
            })
            .filter(Boolean)
            .join(", ");
    
          // Build field arguments - only for arguments that are actually used
          const fieldArgs = usedArgNames
            .map((argName) => {
              return `${argName}: $${argName}`;
            })
            .join(", ");
    
          // Build selection set based on return type
          let selectionSet = "";
    
          if (isObjectType(returnType)) {
            // For objects or lists of objects, analyze fields
            const fields = analyzeFields(returnType, schema);
            selectionSet = buildSelectionSet(fields);
          } else if (isListType(returnType)) {
            // For lists, unwrap and analyze the inner type
            const innerType = getNamedType(returnType.ofType);
            if (isObjectType(innerType)) {
              const fields = analyzeFields(innerType as GraphQLObjectType, schema);
              selectionSet = buildSelectionSet(fields);
            }
          }
    
          // Build the final query
          // Only include variable definitions if fieldArgs is used
          const shouldIncludeVarDefs = fieldArgs && fieldArgs.length > 0;
          const query = `
            query ${name}Query${shouldIncludeVarDefs ? `(${varDefs})` : ""} {
              ${name}${fieldArgs && fieldArgs.length > 0 ? `(${fieldArgs})` : ""} ${
            selectionSet ? `{\n${selectionSet}  }` : ""
          }
            }
          `;
    
          log("debug", `Generated query for ${name}:`, {
            query,
            variables: processedArgs,
          });
    
          // Execute the query
          return await executeQuery({ query, variables: processedArgs });
        } catch (error) {
          throw new Error(
            `Error executing query for ${name}: ${
              error instanceof Error ? error.message : String(error)
            }`
          );
        }
      } catch (error) {
        log(
          "error",
          `Error executing tool ${name}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        throw error;
      }
    }
  • Fetches and caches the GraphQL schema from https://countries.trevorblades.com/graphql, which contains the definition of the 'continents' query field used for dynamic tool generation.
    async function fetchSchema(): Promise<GraphQLSchema | null> {
      // Prevent concurrent schema fetches
      if (schemaFetchInProgress) {
        log("info", "Schema fetch already in progress, waiting for it to complete");
        while (schemaFetchInProgress) {
          await new Promise((resolve) => setTimeout(resolve, 50));
        }
        return schemaCache;
      }
    
      // Check if we have a valid cached schema
      const now = Date.now();
      if (schemaCache && schemaCacheExpiry && now < schemaCacheExpiry) {
        log("info", "Using cached schema");
        return schemaCache;
      }
    
      try {
        schemaFetchInProgress = true;
        log("info", `Fetching GraphQL schema from ${GRAPHQL_API_ENDPOINT}`);
    
        // Build headers
        const headers: Record<string, string> = {};
        if (GRAPHQL_API_KEY) {
          headers.Authorization = `Bearer ${GRAPHQL_API_KEY}`;
        }
    
        // Create client and execute introspection query
        const client = new GraphQLClient(GRAPHQL_API_ENDPOINT, { headers });
        const startTime = Date.now();
        const data = await client.request<IntrospectionQuery>(
          getIntrospectionQuery()
        );
        const schema = buildClientSchema(data);
        const duration = Date.now() - startTime;
    
        log("info", `Schema fetched successfully in ${duration}ms`);
    
        // Cache the schema and reset the name mappings
        schemaCache = schema;
        schemaCacheExpiry = now + CACHE_TTL;
        toolNameMappings = {}; // Reset mappings as schema might have changed
    
        return schema;
      } catch (error) {
        log(
          "error",
          `Error fetching schema: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        return null;
      } finally {
        schemaFetchInProgress = false;
      }
    }
  • Helper that analyzes the return type of the continents query to automatically generate an efficient GraphQL selection set, preventing over-fetching and N+1 issues.
    function analyzeFields(
      type: GraphQLObjectType,
      schema: GraphQLSchema,
      visitedTypes: Set<string> = new Set(),
      depth: number = 0
    ): FieldSelection[] {
      // Prevent infinite recursion and limit depth
      if (visitedTypes.has(type.name) || depth > 2) {
        return [];
      }
    
      // For non-object types, just return empty array
      if (!isObjectType(type)) {
        return [];
      }
    
      visitedTypes.add(type.name);
    
      const fields = type.getFields();
      const result: FieldSelection[] = [];
    
      // Add fields to query (being careful of nested objects to avoid N+1 problems)
      for (const [fieldName, field] of Object.entries(fields)) {
        // Skip fields that need arguments - they might cause issues
        if (field.args.length > 0) {
          continue;
        }
    
        const fieldType = getNamedType(field.type);
    
        if (isScalarType(fieldType) || isEnumType(fieldType)) {
          // Include all scalar and enum fields directly
          result.push(fieldName);
        } else if (isObjectType(fieldType) && depth < 2) {
          // For object types, recurse one level to include key identifying fields
          // This is where we're careful about N+1 issues - limited depth
          const nestedFields = analyzeFields(
            fieldType,
            schema,
            visitedTypes,
            depth + 1
          );
    
          if (nestedFields.length > 0) {
            result.push({
              name: fieldName,
              fields: nestedFields.slice(0, 3), // Limit to first few fields to avoid overfetching
            });
          }
        }
      }
    
      return result;
    }
Install Server

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ctkadvisors/graphql-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server