GraphQL MCP Server

by ctkadvisors
Verified
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const readline_1 = __importDefault(require("readline")); const graphql_request_1 = require("graphql-request"); const graphql_1 = require("graphql"); // Configuration const GRAPHQL_API_ENDPOINT = process.env.GRAPHQL_API_ENDPOINT || "https://countries.trevorblades.com/graphql"; const GRAPHQL_API_KEY = process.env.GRAPHQL_API_KEY || ""; const DEBUG = process.env.DEBUG === "true"; const CACHE_TTL = 3600000; // 1 hour in milliseconds const ENABLE_MUTATIONS = process.env.ENABLE_MUTATIONS === "true"; // Mutations are OFF by default // Parse whitelisted queries from environment variable let WHITELISTED_QUERIES = null; if (process.env.WHITELISTED_QUERIES) { try { if (typeof process.env.WHITELISTED_QUERIES === "string") { // Try parsing as JSON array first if (process.env.WHITELISTED_QUERIES.startsWith("[")) { WHITELISTED_QUERIES = JSON.parse(process.env.WHITELISTED_QUERIES); } else { // Otherwise treat as comma-separated list WHITELISTED_QUERIES = process.env.WHITELISTED_QUERIES.split(",") .map((q) => q.trim()) .filter(Boolean); } } log("info", `Loaded whitelist with ${(WHITELISTED_QUERIES && WHITELISTED_QUERIES.length) || 0} queries`, { whitelist: WHITELISTED_QUERIES }); } catch (error) { log("error", `Failed to parse WHITELISTED_QUERIES: ${error instanceof Error ? error.message : String(error)}`); // If parsing fails, don't use a whitelist WHITELISTED_QUERIES = null; } } // Parse whitelisted mutations from environment variable let WHITELISTED_MUTATIONS = null; if (process.env.WHITELISTED_MUTATIONS) { try { if (typeof process.env.WHITELISTED_MUTATIONS === "string") { // Try parsing as JSON array first if (process.env.WHITELISTED_MUTATIONS.startsWith("[")) { WHITELISTED_MUTATIONS = JSON.parse(process.env.WHITELISTED_MUTATIONS); } else { // Otherwise treat as comma-separated list WHITELISTED_MUTATIONS = process.env.WHITELISTED_MUTATIONS.split(",") .map((q) => q.trim()) .filter(Boolean); } } log("info", `Loaded mutation whitelist with ${(WHITELISTED_MUTATIONS && WHITELISTED_MUTATIONS.length) || 0} mutations`, { whitelist: WHITELISTED_MUTATIONS }); } catch (error) { log("error", `Failed to parse WHITELISTED_MUTATIONS: ${error instanceof Error ? error.message : String(error)}`); // If parsing fails, don't use a whitelist WHITELISTED_MUTATIONS = null; } } // Debug logging to stderr function log(level, message, data = {}) { const timestamp = new Date().toISOString(); console.error(JSON.stringify({ timestamp, level, message, ...data })); } // Global state let schemaCache = null; let schemaFetchInProgress = false; let schemaCacheExpiry = null; // Map of truncated tool names to original field names let toolNameMappings = {}; // Execute GraphQL query async function executeQuery(args) { try { const endpoint = args.endpoint || GRAPHQL_API_ENDPOINT; if (!endpoint) { throw new Error("No GraphQL endpoint specified"); } const headers = { ...(args.headers || {}) }; if (GRAPHQL_API_KEY && !headers.Authorization) { headers.Authorization = `Bearer ${GRAPHQL_API_KEY}`; } // Validate query try { (0, graphql_1.parse)(args.query); } catch (error) { throw new Error(`Invalid GraphQL query: ${error instanceof Error ? error.message : String(error)}`); } // Create client and execute query const client = new graphql_request_1.GraphQLClient(endpoint, { headers }); const startTime = Date.now(); const result = await client.request(args.query, args.variables || {}); const duration = Date.now() - startTime; log("info", `GraphQL query executed successfully in ${duration}ms`); // Return the result directly, it will be formatted in the tool handler return result; } catch (error) { log("error", `Error executing GraphQL query: ${error instanceof Error ? error.message : String(error)}`); throw new Error(`GraphQL query error: ${error instanceof Error ? error.message : String(error)}`); } } // Fetch schema and generate tools async function fetchSchema() { // 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 = {}; if (GRAPHQL_API_KEY) { headers.Authorization = `Bearer ${GRAPHQL_API_KEY}`; } // Create client and execute introspection query const client = new graphql_request_1.GraphQLClient(GRAPHQL_API_ENDPOINT, { headers }); const startTime = Date.now(); const data = await client.request((0, graphql_1.getIntrospectionQuery)()); const schema = (0, graphql_1.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; } } // Get the actual type from a GraphQL type (removing List and NonNull wrappers) function getNamedType(type) { if ((0, graphql_1.isNonNullType)(type)) { return getNamedType(type.ofType); } if ((0, graphql_1.isListType)(type)) { return getNamedType(type.ofType); } return type; } // Get tools based on schema - TRULY DYNAMIC TOOLS GENERATION function getToolsFromSchema(schema) { if (!schema) { return []; // Return empty tools list if no schema available } const tools = []; // 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 = {}; const required = []; // 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 ((0, graphql_1.isNonNullType)(argType)) { isNonNull = true; argType = argType.ofType; } let isList = false; if ((0, graphql_1.isListType)(argType)) { isList = true; argType = argType.ofType; // Unwrap non-null inside list if needed if ((0, graphql_1.isNonNullType)(argType)) { argType = argType.ofType; } } const baseType = getNamedType(argType); // Create property definition if ((0, graphql_1.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 ((0, graphql_1.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 ((0, graphql_1.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 = { 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; } // Get tools based on mutation schema function getToolsFromMutationType(schema) { if (!schema) { return []; // Return empty tools list if no schema available } const tools = []; // Get the mutation type from the schema const mutationType = schema.getMutationType(); if (!mutationType) { log("info", "Schema has no mutation type"); return tools; } // Get all available mutation fields const fields = mutationType.getFields(); // Process each mutation 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_MUTATIONS && !WHITELISTED_MUTATIONS.includes(fieldName)) { if (DEBUG) { log("debug", `Skipping mutation field ${fieldName} - not in whitelist`); } continue; } try { // Analyze arguments for the field const properties = {}; const required = []; // Process arguments (same logic as query arguments) field.args.forEach((arg) => { // Get the actual GraphQL type (unwrap non-null and list types) let argType = arg.type; let isNonNull = false; if ((0, graphql_1.isNonNullType)(argType)) { isNonNull = true; argType = argType.ofType; } let isList = false; if ((0, graphql_1.isListType)(argType)) { isList = true; argType = argType.ofType; // Unwrap non-null inside list if needed if ((0, graphql_1.isNonNullType)(argType)) { argType = argType.ofType; } } const baseType = getNamedType(argType); // Create property definition if ((0, graphql_1.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 ((0, graphql_1.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 ((0, graphql_1.isInputObjectType)(baseType)) { // For input object types (complex types), describe it as object properties[arg.name] = { type: "object", description: `${arg.name} - Input type: ${baseType.name}`, }; } 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 mutation field - Truncate name to 64 characters if needed let fullName = `mutation_${fieldName}`; let truncatedName = fullName; if (fullName.length > 64) { // Truncate the fieldName part to make the full name fit in 64 chars // Keep the mutation_ prefix for clarity const prefixLength = "mutation_".length; const maxFieldNameLength = 64 - prefixLength; truncatedName = `mutation_${fieldName.substring(0, maxFieldNameLength)}`; // Store mapping from truncated to original full name toolNameMappings[truncatedName] = fullName; } const tool = { name: truncatedName, description: field.description || `GraphQL ${fieldName} mutation`, inputSchema: { type: "object", properties: properties, required: required, }, }; tools.push(tool); } catch (error) { log("error", `Error processing mutation field ${fieldName}: ${error instanceof Error ? error.message : String(error)}`); } } return tools; } // Helper function to convert GraphQL scalar types to JSON Schema types function getJsonSchemaType(graphqlType) { const typeMap = { Int: "integer", Float: "number", String: "string", Boolean: "boolean", ID: "string", }; return typeMap[graphqlType] || "string"; } // Analyze object type fields to efficiently build query function analyzeFields(type, schema, visitedTypes = new Set(), depth = 0) { // Prevent infinite recursion and limit depth if (visitedTypes.has(type.name) || depth > 2) { return []; } // For non-object types, just return empty array if (!(0, graphql_1.isObjectType)(type)) { return []; } visitedTypes.add(type.name); const fields = type.getFields(); const result = []; // 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 ((0, graphql_1.isScalarType)(fieldType) || (0, graphql_1.isEnumType)(fieldType)) { // Include all scalar and enum fields directly result.push(fieldName); } else if ((0, graphql_1.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; } // Build selection set string for GraphQL query function buildSelectionSet(fields, indent = " ") { if (!fields || fields.length === 0) return ""; let result = ""; for (const field of fields) { if (typeof field === "string") { // It's a scalar field result += `${indent}${field}\n`; } else if (typeof field === "object" && field.name && field.fields) { // It's an object field with nested fields result += `${indent}${field.name} {\n${buildSelectionSet(field.fields, indent + " ")}\n${indent}}\n`; } } return result; } // Process and validate input arguments based on schema function processArguments(args, fieldArgs, schema) { if (!args || !fieldArgs) return {}; const processedArgs = {}; // Process each argument for (const [argName, argValue] of Object.entries(args)) { // Find the argument definition in the schema const argDef = fieldArgs.find((a) => a.name === argName); if (!argDef) continue; const baseType = getNamedType(argDef.type); // Handle based on the type if ((0, graphql_1.isInputObjectType)(baseType)) { // For complex input types if (argValue === "") { // If it's an empty string, convert to null or empty object // depending on if it's required if ((0, graphql_1.isNonNullType)(argDef.type)) { processedArgs[argName] = {}; // Empty object for required inputs } else { processedArgs[argName] = null; // null for optional inputs } } else if (typeof argValue === "string" && (argValue.startsWith("{") || argValue === "")) { // Try to parse as JSON if it looks like an object try { processedArgs[argName] = JSON.parse(argValue); } catch (e) { // If it can't be parsed, use null or empty object processedArgs[argName] = (0, graphql_1.isNonNullType)(argDef.type) ? {} : null; } } else { // Use as is if it's already an object processedArgs[argName] = argValue; } } else if ((0, graphql_1.isEnumType)(baseType)) { // For enum types, ensure the value is a string processedArgs[argName] = String(argValue); } else if ((0, graphql_1.isScalarType)(baseType)) { // Handle scalar types if (baseType.name === "Int") { processedArgs[argName] = parseInt(argValue, 10); } else if (baseType.name === "Float") { processedArgs[argName] = parseFloat(argValue); } else if (baseType.name === "Boolean") { processedArgs[argName] = Boolean(argValue); } else { processedArgs[argName] = argValue; } } else { // Default: use as-is processedArgs[argName] = argValue; } // Skip null or undefined values for non-required fields if ((processedArgs[argName] === null || processedArgs[argName] === undefined) && !(0, graphql_1.isNonNullType)(argDef.type)) { delete processedArgs[argName]; } } return processedArgs; } // Handle tool execution async function executeGraphQLTool(name, args) { 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 = []; 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 ((0, graphql_1.isObjectType)(returnType)) { // For objects or lists of objects, analyze fields const fields = analyzeFields(returnType, schema); selectionSet = buildSelectionSet(fields); } else if ((0, graphql_1.isListType)(returnType)) { // For lists, unwrap and analyze the inner type const innerType = getNamedType(returnType.ofType); if ((0, graphql_1.isObjectType)(innerType)) { const fields = analyzeFields(innerType, 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; } } // Handle mutation execution async function executeGraphQLMutation(name, args) { try { // If this name is in our mapping, use the original field name const fullName = toolNameMappings[name] || name; // Extract the actual mutation name (remove 'mutation_' prefix) const mutationName = fullName.replace(/^mutation_/, ""); // Check if the mutation is in the whitelist (if whitelist is enabled) if (WHITELISTED_MUTATIONS && !WHITELISTED_MUTATIONS.includes(mutationName)) { throw new Error(`Mutation '${mutationName}' is not in the whitelist`); } // Get the schema const schema = await fetchSchema(); if (!schema) { throw new Error("Schema not available"); } // Get the mutation type const mutationType = schema.getMutationType(); if (!mutationType) { throw new Error("Schema has no mutation type"); } // Get the field for this mutation const fields = mutationType.getFields(); const field = fields[mutationName]; if (!field) { throw new Error(`Unknown mutation: ${mutationName}`); } 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 = []; 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 ((0, graphql_1.isObjectType)(returnType)) { // For objects or lists of objects, analyze fields const fields = analyzeFields(returnType, schema); selectionSet = buildSelectionSet(fields); } else if ((0, graphql_1.isListType)(returnType)) { // For lists, unwrap and analyze the inner type const innerType = getNamedType(returnType.ofType); if ((0, graphql_1.isObjectType)(innerType)) { const fields = analyzeFields(innerType, schema); selectionSet = buildSelectionSet(fields); } } // Build the final mutation query const shouldIncludeVarDefs = fieldArgs && fieldArgs.length > 0; const mutation = ` mutation ${mutationName}Mutation${shouldIncludeVarDefs ? `(${varDefs})` : ""} { ${mutationName}${fieldArgs && fieldArgs.length > 0 ? `(${fieldArgs})` : ""} ${selectionSet ? `{\n${selectionSet} }` : ""} } `; log("debug", `Generated mutation for ${mutationName}:`, { mutation, variables: processedArgs, }); // Execute the mutation return await executeQuery({ query: mutation, variables: processedArgs }); } catch (error) { throw new Error(`Error executing mutation for ${mutationName}: ${error instanceof Error ? error.message : String(error)}`); } } catch (error) { log("error", `Error executing mutation ${name}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // Provide resources list (empty but handled to be more compatible) function getResourcesList() { return { resources: [] }; } // Provide prompts list (empty but handled to be more compatible) function getPromptsList() { return { prompts: [] }; } async function main() { try { log("info", "GraphQL MCP Server starting..."); log("info", `GraphQL API Endpoint: ${GRAPHQL_API_ENDPOINT}`); log("info", `API Key: ${GRAPHQL_API_KEY ? "Configured" : "Not configured"}`); log("info", `Query Whitelist: ${WHITELISTED_QUERIES ? `Enabled (${WHITELISTED_QUERIES.length} queries)` : "Disabled (all queries allowed)"}`); log("info", `Mutation Whitelist: ${WHITELISTED_MUTATIONS ? `Enabled (${WHITELISTED_MUTATIONS.length} mutations)` : "Disabled (all mutations allowed)"}`); // Set up readline interface for stdin/stdout const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout, terminal: false, }); // Handle incoming JSON-RPC messages rl.on("line", async (line) => { try { if (DEBUG) { log("debug", `Received message: ${line.substring(0, 100)}...`); } const request = JSON.parse(line); const { method, id } = request; // Handle initialize if (method === "initialize") { log("info", "Handling initialize request"); const response = { jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {}, resources: {}, prompts: {}, }, serverInfo: { name: "graphql-mcp-server", version: "1.0.0", }, }, }; console.log(JSON.stringify(response)); return; } // Handle initialized notification if (method === "notifications/initialized") { // No response needed for notifications return; } // 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 = { jsonrpc: "2.0", id, result: { tools: allTools, }, }; console.log(JSON.stringify(response)); return; } // Handle tools/call if (method === "tools/call" || method === "tool.call") { const { name, arguments: args } = request.params; log("info", `Tool call: ${name}`, { args }); // Determine if this is a mutation or a query const isMutation = name.startsWith("mutation_"); // Check if mutations are enabled if (isMutation && !ENABLE_MUTATIONS) { log("error", `Mutation ${name} called but mutations are disabled`); const response = { jsonrpc: "2.0", id, error: { code: -32000, message: "Mutations are disabled. Set ENABLE_MUTATIONS=true to enable them.", }, }; console.log(JSON.stringify(response)); return; } // Get result from GraphQL tool execution try { let result; if (isMutation) { result = await executeGraphQLMutation(name, args); } else { result = await executeGraphQLTool(name, args); } // Log the result structure log("debug", `Result structure for ${name}:`, { resultStructure: JSON.stringify(result), }); // Handle undefined and null values to prevent Zod validation errors const sanitizeValue = (obj) => { if (obj === undefined || obj === null) { return null; } if (typeof obj === "object" && obj !== null) { if (Array.isArray(obj)) { return obj.map((item) => sanitizeValue(item)); } else { const newObj = {}; for (const [key, value] of Object.entries(obj)) { newObj[key] = sanitizeValue(value); } return newObj; } } return obj; }; // Sanitize the result to avoid Zod validation errors const sanitizedResult = JSON.stringify(sanitizeValue(result), null, 2); const response = { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: sanitizedResult || "{}" }], }, }; console.log(JSON.stringify(response)); } catch (error) { log("error", `Error in tool ${name}: ${error instanceof Error ? error.message : String(error)}`); // Format error response const response = { jsonrpc: "2.0", id, error: { code: -32000, message: error instanceof Error ? error.message : String(error), }, }; console.log(JSON.stringify(response)); } return; } // Handle resources/list if (method === "resources/list") { const response = { jsonrpc: "2.0", id, result: getResourcesList(), }; console.log(JSON.stringify(response)); return; } // Handle prompts/list if (method === "prompts/list") { const response = { jsonrpc: "2.0", id, result: getPromptsList(), }; console.log(JSON.stringify(response)); return; } // Method not found log("warning", `Method not found: ${method}`); const response = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method '${method}' not found`, }, }; console.log(JSON.stringify(response)); } catch (error) { log("error", `Error processing message: ${error instanceof Error ? error.message : String(error)}`, { stack: error instanceof Error ? error.stack : undefined, }); // Try to get the ID from the original message let id = null; try { id = JSON.parse(line).id; } catch { // Ignore parse errors } // Return error response const response = { jsonrpc: "2.0", id, error: { code: -32000, message: `Error processing request: ${error instanceof Error ? error.message : String(error)}`, }, }; console.log(JSON.stringify(response)); } }); // Handle process termination process.on("uncaughtException", (error) => { log("error", `Uncaught exception: ${error.message}`, { stack: error.stack, }); }); process.on("unhandledRejection", (reason) => { log("error", `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`); }); // Initialize schema in the background fetchSchema().catch((error) => { log("error", `Initial schema fetch failed: ${error instanceof Error ? error.message : String(error)}`); }); log("info", "GraphQL MCP Server started and ready"); } catch (error) { log("error", "Fatal error during initialization:", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); process.exit(1); } } // Start the server main().catch((error) => { console.error(`Fatal error: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); }); //# sourceMappingURL=graphql-mcp-server.js.map