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; }

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