diff --git a/package-lock.json b/package-lock.json
index 3f6aebf..c17d490 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",
+ "graphql": "^16.11.0",
"zod": "^3.24.2"
},
"bin": {
@@ -5037,6 +5038,15 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/graphql": {
+ "version": "16.11.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
+ "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
diff --git a/package.json b/package.json
index 47b9f81..80fa8d1 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"description": "A command line tool for setting up Shopify Dev MCP server",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",
+ "graphql": "^16.11.0",
"zod": "^3.24.2"
},
"devDependencies": {
diff --git a/src/tools/index.test.ts b/src/tools/index.test.ts
index ffd53a6..8014f83 100644
--- a/src/tools/index.test.ts
+++ b/src/tools/index.test.ts
@@ -8,16 +8,39 @@ import {
afterAll,
afterEach,
} from "vitest";
+import { GraphQLError } from "graphql";
global.fetch = vi.fn();
-import { shopifyTools, searchShopifyDocs } from "./index.js";
import {
instrumentationData,
isInstrumentationDisabled,
} from "../instrumentation.js";
import { searchShopifyAdminSchema } from "./shopify-admin-schema.js";
+// Mock the graphql validation
+vi.mock("./shopify-graphql-validation.js", () => {
+ return {
+ validateGraphQL: vi.fn(),
+ formatValidationErrors: vi.fn((errors: Array<{ message: string }>) => {
+ return errors
+ .map((error, index: number) => `${index + 1}. ${error.message}`)
+ .join("\n");
+ }),
+ };
+});
+
+// Now import the modules to test
+import {
+ searchShopifyDocs,
+ validateShopifyGraphQL,
+ shopifyTools,
+} from "./index.js";
+import {
+ validateGraphQL,
+ formatValidationErrors,
+} from "./shopify-graphql-validation.js";
+
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
console.error = vi.fn();
@@ -268,6 +291,32 @@ describe("recordUsage", () => {
});
});
+// Prepare mock validation results
+const validResult = {
+ isValid: true,
+ errors: undefined,
+};
+
+// Create actual GraphQLError instances for the invalid result
+const graphqlError1 = new GraphQLError(
+ "Cannot query field 'invalid' on type 'Product'",
+);
+const graphqlError2 = new GraphQLError("Unknown type 'NonExistentType'");
+
+// Add locations manually
+Object.defineProperty(graphqlError1, "locations", {
+ value: [{ line: 3, column: 5 }],
+});
+
+Object.defineProperty(graphqlError2, "locations", {
+ value: [{ line: 7, column: 10 }],
+});
+
+const invalidResult = {
+ isValid: false,
+ errors: [graphqlError1, graphqlError2],
+};
+
describe("searchShopifyDocs", () => {
let fetchMock: any;
@@ -415,6 +464,110 @@ describe("searchShopifyDocs", () => {
});
});
+describe("validateShopifyGraphQL", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ // Default mock for validateGraphQL
+ vi.mocked(validateGraphQL).mockResolvedValue(validResult);
+ vi.mocked(console.error).mockClear();
+ });
+
+ test("calls local validateGraphQL function with correct code", async () => {
+ const graphqlCode = `
+ query GetProduct {
+ product(id: "gid://shopify/Product/123") {
+ title
+ description
+ }
+ }
+ `;
+
+ await validateShopifyGraphQL(graphqlCode);
+
+ // Verify validateGraphQL was called with the code
+ expect(validateGraphQL).toHaveBeenCalledTimes(1);
+ expect(validateGraphQL).toHaveBeenCalledWith(graphqlCode);
+ });
+
+ test("returns properly formatted result for valid GraphQL", async () => {
+ const graphqlCode = `query { product(id: "123") { title } }`;
+
+ const result = await validateShopifyGraphQL(graphqlCode);
+
+ expect(result.success).toBe(true);
+ expect(result.isValid).toBe(true);
+ expect(result.formattedText).toContain("## GraphQL Validation Results");
+ expect(result.formattedText).toContain("**Valid:** ✅ Yes");
+ expect(result.rawResponse).toEqual({ is_valid: true, errors: undefined });
+ });
+
+ test("returns properly formatted result for invalid GraphQL with errors", async () => {
+ // Mock an invalid validation result
+ vi.mocked(validateGraphQL).mockResolvedValue(invalidResult);
+
+ // Mock formatValidationErrors to return formatted errors
+ vi.mocked(formatValidationErrors).mockReturnValue(
+ "1. Cannot query field 'invalid' on type 'Product' (Line 3, Column 5)\n" +
+ "2. Unknown type 'NonExistentType' (Line 7, Column 10)",
+ );
+
+ const graphqlCode = `
+ query GetProduct {
+ product(id: "gid://shopify/Product/123") {
+ invalid
+ title
+ someType {
+ field: NonExistentType
+ }
+ }
+ }
+ `;
+
+ const result = await validateShopifyGraphQL(graphqlCode);
+
+ expect(result.success).toBe(true);
+ expect(result.isValid).toBe(false);
+ expect(result.formattedText).toContain("## GraphQL Validation Results");
+ expect(result.formattedText).toContain("**Valid:** ❌ No");
+ expect(result.formattedText).toContain("**Errors:**");
+ expect(result.formattedText).toContain(
+ "Cannot query field 'invalid' on type 'Product'",
+ );
+ expect(result.formattedText).toContain("Unknown type 'NonExistentType'");
+
+ // Verify formatValidationErrors was called with the errors
+ expect(formatValidationErrors).toHaveBeenCalledTimes(1);
+ expect(formatValidationErrors).toHaveBeenCalledWith(invalidResult.errors);
+ });
+
+ test("handles validation errors gracefully", async () => {
+ // Clear mocks before this specific test
+ vi.clearAllMocks();
+
+ // Mock a validation function error
+ vi.mocked(validateGraphQL).mockRejectedValue(
+ new Error("Validation function failed"),
+ );
+
+ const graphqlCode = "query { invalid syntax }";
+
+ const result = await validateShopifyGraphQL(graphqlCode);
+
+ expect(result.success).toBe(false);
+ expect(result.isValid).toBe(false);
+ expect(result.formattedText).toContain(
+ "Error validating GraphQL: Validation function failed",
+ );
+
+ // Verify error was logged once
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(vi.mocked(console.error).mock.calls[0][0]).toContain(
+ "Error validating GraphQL",
+ );
+ });
+});
+
describe("fetchGettingStartedApis", () => {
let fetchMock: any;
@@ -609,3 +762,78 @@ describe("get_started tool behavior", () => {
expect(result.content[0].text).toContain("Network failure");
});
});
+
+describe("shopifyTools tool registration", () => {
+ let mockServer: any;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ // Setup fetch mock for getting_started_apis endpoint
+ const fetchMock = global.fetch as any;
+ fetchMock.mockResolvedValue({
+ ok: true,
+ status: 200,
+ statusText: "OK",
+ headers: {
+ forEach: (callback: (value: string, key: string) => void) => {
+ callback("application/json", "content-type");
+ },
+ },
+ text: async () => JSON.stringify(sampleGettingStartedApisResponse),
+ });
+
+ // Create a mock server that captures registered tools
+ mockServer = {
+ registeredTools: new Map(),
+ tool: vi.fn((name, description, schema, handler) => {
+ mockServer.registeredTools.set(name, { description, schema, handler });
+ }),
+ };
+ });
+
+ test("registers validate_graphql tool correctly", async () => {
+ // Import and register tools
+ const { shopifyTools } = await import("./index.js");
+ await shopifyTools(mockServer);
+
+ // Verify the validate_graphql tool was registered
+ expect(mockServer.registeredTools.has("validate_graphql")).toBe(true);
+
+ const tool = mockServer.registeredTools.get("validate_graphql");
+ expect(tool).toBeDefined();
+ expect(tool.description).toContain("validates GraphQL code");
+ expect(tool.description).toContain(
+ "ALWAYS MAKE SURE THAT THE GRAPHQL CODE YOU GENERATE IS VALID",
+ );
+
+ // Verify the handler function exists
+ expect(tool.handler).toBeDefined();
+ expect(typeof tool.handler).toBe("function");
+ });
+
+ test("validate_graphql tool executes correctly", async () => {
+ // Mock the validation functions
+ vi.mocked(validateGraphQL).mockResolvedValue({
+ isValid: true,
+ errors: undefined,
+ });
+
+ // Import and register tools
+ const { shopifyTools } = await import("./index.js");
+ await shopifyTools(mockServer);
+
+ // Get the handler
+ const tool = mockServer.registeredTools.get("validate_graphql");
+ const handler = tool.handler;
+
+ // Execute the handler
+ const result = await handler({ code: "query { shop { name } }" });
+
+ // Verify the result
+ expect(result.content).toBeDefined();
+ expect(result.content[0].type).toBe("text");
+ expect(result.content[0].text).toContain("## GraphQL Validation Results");
+ expect(result.content[0].text).toContain("✅ Yes");
+ });
+});
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 4bcb896..348d282 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -6,6 +6,11 @@ import {
isInstrumentationDisabled,
} from "../instrumentation.js";
+import {
+ validateGraphQL,
+ formatValidationErrors,
+} from "./shopify-graphql-validation.js";
+
const SHOPIFY_BASE_URL = process.env.DEV
? "https://shopify-dev.myshopify.io/"
: "https://shopify.dev/";
@@ -136,6 +141,54 @@ export async function searchShopifyDocs(prompt: string) {
}
}
+/**
+ * Validates GraphQL code against the Shopify Admin API schema
+ * @param code The GraphQL code to validate
+ * @returns Validation results including whether the code is valid and any errors
+ */
+export async function validateShopifyGraphQL(code: string) {
+ try {
+ // Use the local validation function
+ const validationResult = await validateGraphQL(code);
+
+ // Format the response
+ let formattedResponse = `## GraphQL Validation Results\n\n`;
+ formattedResponse += `**Valid:** ${validationResult.isValid ? "✅ Yes" : "❌ No"}\n\n`;
+
+ // Add errors if any
+ if (
+ !validationResult.isValid &&
+ validationResult.errors &&
+ validationResult.errors.length > 0
+ ) {
+ formattedResponse += `**Errors:**\n\n`;
+ formattedResponse += formatValidationErrors(validationResult.errors);
+ }
+
+ return {
+ success: true,
+ isValid: validationResult.isValid,
+ formattedText: formattedResponse,
+ rawResponse: {
+ is_valid: validationResult.isValid,
+ errors: validationResult.errors,
+ },
+ };
+ } catch (error) {
+ // Log the error once
+ console.error(`[validate-graphql] Error validating GraphQL: ${error}`);
+
+ return {
+ success: false,
+ isValid: false,
+ formattedText: `Error validating GraphQL: ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
+
export async function shopifyTools(server: McpServer): Promise {
server.tool(
"introspect_admin_schema",
@@ -176,10 +229,40 @@ export async function shopifyTools(server: McpServer): Promise {
},
);
+ server.tool(
+ "validate_graphql",
+ `This tool validates GraphQL code against the Shopify Admin API GraphQL schema and returns validation results including any errors.
+ ALWAYS MAKE SURE THAT THE GRAPHQL CODE YOU GENERATE IS VALID WITH THIS TOOL.
+
+ It takes one argument:
+ - code: The GraphQL code to validate`,
+ {
+ code: z.string().describe("The GraphQL code to validate"),
+ },
+ async ({ code }) => {
+ const result = await validateShopifyGraphQL(code);
+
+ recordUsage("validate_graphql", code, result.formattedText).catch(
+ () => {},
+ );
+
+ return {
+ content: [
+ {
+ type: "text" as const,
+ text: result.formattedText,
+ },
+ ],
+ };
+ },
+ );
+
server.tool(
"search_dev_docs",
`This tool will take in the user prompt, search shopify.dev, and return relevant documentation and code examples that will help answer the user's question.
+ YOU MUST CALL THIS TOOL AT LEAST ONCE FOR A SHOPIFY RELATED QUESTION TO CHECK IF THE DOCUMENTATION HAS ANY EXAMPLES AVAILABLE.
+
It takes one argument: prompt, which is the search query for Shopify documentation.`,
{
prompt: z.string().describe("The search query for Shopify documentation"),
@@ -263,7 +346,7 @@ export async function shopifyTools(server: McpServer): Promise {
server.tool(
"get_started",
`
- YOU MUST CALL THIS TOOL FIRST WHENEVER YOU ARE IN A SHOPIFY APP AND THE USER WANTS TO LEARN OR INTERACT WITH ANY OF THESE APIS:
+ YOU MUST CALL THIS TOOL FIRST WHENEVER YOU ARE IN A SHOPIFY APP AND/OR THE USER WANTS TO LEARN OR INTERACT WITH ANY OF THESE APIS:
Valid arguments for \`api\` are:
${gettingStartedApis.map((api) => ` - ${api.name}: ${api.description}`).join("\n")}
@@ -282,13 +365,16 @@ ${gettingStartedApis.map((api) => ` - ${api.name}: ${api.description}`).join(
const text = `Please specify which Shopify API you are building for. Valid options are: ${options}.`;
return {
- content: [{ type: "text", text }],
+ content: [{ type: "text" as const, text }],
};
}
try {
const response = await fetch(
- `${SHOPIFY_BASE_URL}/mcp/getting_started?api=${api}`,
+ new URL(
+ `/mcp/getting_started?api=${api}`,
+ SHOPIFY_BASE_URL,
+ ).toString(),
);
if (!response.ok) {
diff --git a/src/tools/shopify-admin-schema.ts b/src/tools/shopify-admin-schema.ts
index fd1e02b..c1b4455 100644
--- a/src/tools/shopify-admin-schema.ts
+++ b/src/tools/shopify-admin-schema.ts
@@ -141,6 +141,23 @@ export const formatSchemaType = (item: any): string => {
} more input fields`;
}
}
+ // For ENUM types, list enum values
+ else if (
+ item.kind === "ENUM" &&
+ item.enumValues &&
+ item.enumValues.length > 0
+ ) {
+ result += "\n Enum Values:";
+ for (const value of item.enumValues) {
+ result += `\n ${value.name}`;
+ if (value.isDeprecated) {
+ result += ` @deprecated`;
+ if (value.deprecationReason) {
+ result += ` (${value.deprecationReason})`;
+ }
+ }
+ }
+ }
// For regular object types, use fields
else if (item.fields && item.fields.length > 0) {
result += "\n Fields:";
diff --git a/src/tools/shopify-graphql-validation.test.ts b/src/tools/shopify-graphql-validation.test.ts
new file mode 100644
index 0000000..074b5f2
--- /dev/null
+++ b/src/tools/shopify-graphql-validation.test.ts
@@ -0,0 +1,275 @@
+import { describe, test, expect, vi, beforeEach } from "vitest";
+import {
+ validateGraphQL,
+ formatValidationErrors,
+} from "./shopify-graphql-validation.js";
+import { GraphQLError } from "graphql";
+
+describe("GraphQL validation", () => {
+ test("validates a correct GraphQL query", async () => {
+ const validQuery = `
+ query GetProduct {
+ product(id: "gid://shopify/Product/123") {
+ id
+ title
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(validQuery);
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors).toBeUndefined();
+ });
+
+ test("reports errors for invalid fields", async () => {
+ const invalidQuery = `
+ query GetProduct {
+ product(id: "gid://shopify/Product/123") {
+ id
+ title
+ nonExistentField
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(invalidQuery);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toBeDefined();
+ expect(result.errors?.length).toBeGreaterThan(0);
+ expect(result.errors?.[0].message).toMatch(/Cannot query field/);
+ });
+
+ test("reports errors for missing required arguments", async () => {
+ const invalidQuery = `
+ query GetProduct {
+ product {
+ id
+ title
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(invalidQuery);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toBeDefined();
+ expect(result.errors?.length).toBeGreaterThan(0);
+ expect(result.errors?.[0].message).toMatch(/argument.*is required/);
+ });
+
+ test("handles syntax errors in GraphQL", async () => {
+ const invalidSyntax = `
+ query GetProduct {
+ product(id: "gid://shopify/Product/123") {
+ id
+ title
+ // Missing closing brace
+ `;
+
+ const result = await validateGraphQL(invalidSyntax);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toBeDefined();
+ expect(result.errors?.length).toBe(1);
+ expect(result.errors?.[0].message).toMatch(
+ /Syntax Error|GraphQL validation error/,
+ );
+ });
+
+ test("validates mutations correctly", async () => {
+ const mutation = `
+ mutation CreateProduct($input: ProductCreateInput!) {
+ productCreate(product: $input) {
+ product {
+ id
+ title
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(mutation);
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors).toBeUndefined();
+ });
+
+ test("handles multiple operations in one document", async () => {
+ const multipleOps = `
+ query GetProduct($id: ID!) {
+ product(id: $id) {
+ title
+ }
+ }
+
+ query GetShop {
+ shop {
+ name
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(multipleOps);
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors).toBeUndefined();
+ });
+
+ test("validates fragments correctly", async () => {
+ const queryWithFragment = `
+ fragment ProductFields on Product {
+ id
+ title
+ description
+ }
+
+ query GetProduct($id: ID!) {
+ product(id: $id) {
+ ...ProductFields
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(queryWithFragment);
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors).toBeUndefined();
+ });
+
+ test("handles empty or whitespace-only input", async () => {
+ const emptyQuery = " \n \n ";
+
+ const result = await validateGraphQL(emptyQuery);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors?.[0].message).toMatch(/Syntax Error|Expected/);
+ });
+
+ test("reports multiple validation errors", async () => {
+ const queryWithMultipleErrors = `
+ query BadQuery {
+ product(wrongArg: "123") {
+ nonExistentField1
+ nonExistentField2
+ }
+ nonExistentQuery {
+ field
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(queryWithMultipleErrors);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toBeDefined();
+ expect(result.errors!.length).toBeGreaterThan(1);
+ });
+
+ test("validates complex nested queries", async () => {
+ const complexQuery = `
+ query GetProductWithVariants($id: ID!) {
+ product(id: $id) {
+ id
+ title
+ variants(first: 10) {
+ edges {
+ node {
+ id
+ title
+ price
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(complexQuery);
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors).toBeUndefined();
+ });
+
+ test("validates queries with directives", async () => {
+ const queryWithDirectives = `
+ query GetProductConditional($id: ID!, $includeVariants: Boolean!) {
+ product(id: $id) {
+ id
+ title
+ variants(first: 10) @include(if: $includeVariants) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(queryWithDirectives);
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors).toBeUndefined();
+ });
+
+ test("reports errors for incorrect variable types", async () => {
+ const queryWithWrongVarType = `
+ query GetProduct($id: String!) {
+ product(id: $id) {
+ id
+ title
+ }
+ }
+ `;
+
+ const result = await validateGraphQL(queryWithWrongVarType);
+
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toBeDefined();
+ expect(result.errors?.[0].message).toMatch(
+ /Variable.*used in position expecting type/,
+ );
+ });
+});
+
+describe("formatValidationErrors", () => {
+ test("formats errors with locations", () => {
+ // Create errors with locations manually
+ const error1 = new GraphQLError("Field does not exist");
+ const error2 = new GraphQLError("Invalid argument");
+
+ // Add locations manually to match the interface
+ Object.defineProperty(error1, "locations", {
+ value: [{ line: 3, column: 5 }],
+ });
+
+ Object.defineProperty(error2, "locations", {
+ value: [{ line: 10, column: 12 }],
+ });
+
+ const formatted = formatValidationErrors([error1, error2]);
+
+ expect(formatted).toContain("1. Field does not exist (Line 3, Column 5)");
+ expect(formatted).toContain("2. Invalid argument (Line 10, Column 12)");
+ });
+
+ test("formats errors without locations", () => {
+ const errors = [
+ new GraphQLError("Parser error"),
+ new GraphQLError("Validation error"),
+ ];
+
+ const formatted = formatValidationErrors(errors);
+
+ expect(formatted).toContain("1. Parser error");
+ expect(formatted).toContain("2. Validation error");
+ });
+});
diff --git a/src/tools/shopify-graphql-validation.ts b/src/tools/shopify-graphql-validation.ts
new file mode 100644
index 0000000..153f22f
--- /dev/null
+++ b/src/tools/shopify-graphql-validation.ts
@@ -0,0 +1,78 @@
+import { buildClientSchema, validate, parse, GraphQLError } from "graphql";
+import { loadSchemaContent } from "./shopify-admin-schema.js";
+import { SCHEMA_FILE_PATH } from "./shopify-admin-schema.js";
+
+/**
+ * Interface for GraphQL validation result
+ */
+export interface ValidationResult {
+ isValid: boolean;
+ errors?: readonly GraphQLError[];
+}
+
+/**
+ * Validates GraphQL code against the local Shopify Admin GraphQL schema
+ * @param code The GraphQL code to validate
+ * @returns Promise resolving to a ValidationResult object containing validation status and any errors
+ */
+export async function validateGraphQL(code: string): Promise {
+ try {
+ // Load schema content from the local schema file
+ const schemaContent = await loadSchemaContent(SCHEMA_FILE_PATH);
+
+ // Parse the schema content
+ const schemaJson = JSON.parse(schemaContent);
+
+ // Build a GraphQL schema from the JSON
+ const schema = buildClientSchema(schemaJson.data);
+
+ // Parse the GraphQL code into an AST
+ const documentAST = parse(code);
+
+ // Validate the document against the schema
+ const validationErrors = validate(schema, documentAST);
+
+ // Return validation result
+ return {
+ isValid: validationErrors.length === 0,
+ errors: validationErrors.length > 0 ? validationErrors : undefined,
+ };
+ } catch (error) {
+ // Handle parsing or other errors
+ const errorMessage = error instanceof Error ? error.message : String(error);
+
+ // Create a GraphQL error to maintain consistent error format
+ const graphqlError = new GraphQLError(
+ `GraphQL validation error: ${errorMessage}`,
+ { originalError: error instanceof Error ? error : undefined },
+ );
+
+ return {
+ isValid: false,
+ errors: [graphqlError],
+ };
+ }
+}
+
+/**
+ * Format validation errors into a human-readable string
+ * @param errors Array of GraphQL errors
+ * @returns Formatted error message string
+ */
+export function formatValidationErrors(
+ errors: readonly GraphQLError[],
+): string {
+ return errors
+ .map((error, index) => {
+ let message = `${index + 1}. ${error.message}`;
+
+ // Add location information if available
+ if (error.locations && error.locations.length > 0) {
+ const location = error.locations[0];
+ message += ` (Line ${location.line}, Column ${location.column})`;
+ }
+
+ return message;
+ })
+ .join("\n");
+}