diff --git a/.gitignore b/.gitignore index 1773796..2775f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ dist/ data/**/*.json +!data/schemas.json .DS_Store diff --git a/data/admin_schema_2024-04.json.gz b/data/admin_schema_2024-04.json.gz new file mode 100644 index 0000000..5d41c5f Binary files /dev/null and b/data/admin_schema_2024-04.json.gz differ diff --git a/data/admin_schema_2024-07.json.gz b/data/admin_schema_2024-07.json.gz new file mode 100644 index 0000000..dbe8101 Binary files /dev/null and b/data/admin_schema_2024-07.json.gz differ diff --git a/data/admin_schema_2024-10.json.gz b/data/admin_schema_2024-10.json.gz new file mode 100644 index 0000000..8b7b3d8 Binary files /dev/null and b/data/admin_schema_2024-10.json.gz differ diff --git a/data/schemas.json b/data/schemas.json new file mode 100644 index 0000000..70f8bb2 --- /dev/null +++ b/data/schemas.json @@ -0,0 +1,42 @@ +[ + { + "api": "admin", + "file": "admin_schema_2024-04.json.gz", + "version": "2024-04" + }, + { + "api": "admin", + "file": "admin_schema_2024-07.json.gz", + "version": "2024-07" + }, + { + "api": "admin", + "file": "admin_schema_2024-10.json.gz", + "version": "2024-10" + }, + { + "api": "admin", + "file": "admin_schema_2025-01.json.gz", + "version": "2025-01" + }, + { + "api": "storefront", + "file": "storefront_schema_2024-04.json.gz", + "version": "2024-04" + }, + { + "api": "storefront", + "file": "storefront_schema_2024-07.json.gz", + "version": "2024-07" + }, + { + "api": "storefront", + "file": "storefront_schema_2024-10.json.gz", + "version": "2024-10" + }, + { + "api": "storefront", + "file": "storefront_schema_2025-01.json.gz", + "version": "2025-01" + } +] diff --git a/data/storefront_schema_2024-04.json.gz b/data/storefront_schema_2024-04.json.gz new file mode 100644 index 0000000..87ae3e5 Binary files /dev/null and b/data/storefront_schema_2024-04.json.gz differ diff --git a/data/storefront_schema_2024-07.json.gz b/data/storefront_schema_2024-07.json.gz new file mode 100644 index 0000000..725f939 Binary files /dev/null and b/data/storefront_schema_2024-07.json.gz differ diff --git a/data/storefront_schema_2024-10.json.gz b/data/storefront_schema_2024-10.json.gz new file mode 100644 index 0000000..fdbf5d6 Binary files /dev/null and b/data/storefront_schema_2024-10.json.gz differ diff --git a/data/storefront_schema_2025-01.json.gz b/data/storefront_schema_2025-01.json.gz new file mode 100644 index 0000000..a477da2 Binary files /dev/null and b/data/storefront_schema_2025-01.json.gz differ diff --git a/src/shopify-admin-schema.test.ts b/src/introspect-graphql-schema.test.ts similarity index 95% rename from src/shopify-admin-schema.test.ts rename to src/introspect-graphql-schema.test.ts index 859c304..1430fce 100644 --- a/src/shopify-admin-schema.test.ts +++ b/src/introspect-graphql-schema.test.ts @@ -2,8 +2,10 @@ import { describe, test, expect, beforeEach, vi, afterAll } from "vitest"; // Mock the module -vi.mock("./shopify-admin-schema.js", async () => { - const actual = (await vi.importActual("./shopify-admin-schema.js")) as any; +vi.mock("./introspect-graphql-schema.js", async () => { + const actual = (await vi.importActual( + "./introspect-graphql-schema.js" + )) as any; return { ...actual, loadSchemaContent: vi.fn(), @@ -17,11 +19,11 @@ import { formatField, formatSchemaType, formatGraphqlOperation, - searchShopifyAdminSchema, + introspectGraphqlSchema, filterAndSortItems, MAX_FIELDS_TO_SHOW, loadSchemaContent, -} from "./shopify-admin-schema.js"; +} from "./introspect-graphql-schema.js"; // Mock console.error const originalConsoleError = console.error; @@ -491,7 +493,7 @@ describe("searchShopifyAdminSchema", () => { }); test("returns formatted results for a search query", async () => { - const result = await searchShopifyAdminSchema("product"); + const result = await introspectGraphqlSchema("product"); expect(result.success).toBe(true); expect(result.responseText).toContain("## Matching GraphQL Types:"); @@ -504,7 +506,7 @@ describe("searchShopifyAdminSchema", () => { }); test("normalizes query by removing trailing s", async () => { - await searchShopifyAdminSchema("products"); + await introspectGraphqlSchema("products"); // Check that console.error was called with the normalized search term const logCalls = (console.error as any).mock.calls.map( @@ -520,7 +522,7 @@ describe("searchShopifyAdminSchema", () => { }); test("normalizes query by removing spaces", async () => { - await searchShopifyAdminSchema("product input"); + await introspectGraphqlSchema("product input"); // Check that console.error was called with the normalized search term const logCalls = (console.error as any).mock.calls.map( @@ -536,7 +538,7 @@ describe("searchShopifyAdminSchema", () => { }); test("handles empty query", async () => { - const result = await searchShopifyAdminSchema(""); + const result = await introspectGraphqlSchema(""); expect(result.success).toBe(true); // Should not filter the schema @@ -545,7 +547,7 @@ describe("searchShopifyAdminSchema", () => { }); test("filters results to show only types", async () => { - const result = await searchShopifyAdminSchema("product", { + const result = await introspectGraphqlSchema("product", { filter: ["types"], }); @@ -560,7 +562,7 @@ describe("searchShopifyAdminSchema", () => { }); test("filters results to show only queries", async () => { - const result = await searchShopifyAdminSchema("product", { + const result = await introspectGraphqlSchema("product", { filter: ["queries"], }); @@ -576,7 +578,7 @@ describe("searchShopifyAdminSchema", () => { }); test("filters results to show only mutations", async () => { - const result = await searchShopifyAdminSchema("product", { + const result = await introspectGraphqlSchema("product", { filter: ["mutations"], }); @@ -591,7 +593,7 @@ describe("searchShopifyAdminSchema", () => { }); test("shows all sections when operationType is 'all'", async () => { - const result = await searchShopifyAdminSchema("product", { + const result = await introspectGraphqlSchema("product", { filter: ["all"], }); @@ -607,7 +609,7 @@ describe("searchShopifyAdminSchema", () => { }); test("defaults to showing all sections when filter is not provided", async () => { - const result = await searchShopifyAdminSchema("product"); + const result = await introspectGraphqlSchema("product"); expect(result.success).toBe(true); // Should include all sections @@ -621,7 +623,7 @@ describe("searchShopifyAdminSchema", () => { }); test("can show multiple sections with array of filters", async () => { - const result = await searchShopifyAdminSchema("product", { + const result = await introspectGraphqlSchema("product", { filter: ["queries", "mutations"], }); diff --git a/src/shopify-admin-schema.ts b/src/introspect-graphql-schema.ts similarity index 82% rename from src/shopify-admin-schema.ts rename to src/introspect-graphql-schema.ts index 38fb6f4..972dee5 100644 --- a/src/shopify-admin-schema.ts +++ b/src/introspect-graphql-schema.ts @@ -8,38 +8,76 @@ import { existsSync } from "fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Path to the schema file in the data folder -export const SCHEMA_FILE_PATH = path.join( +// Path to the schemas configuration file +export const SCHEMAS_CONFIG_PATH = path.join( __dirname, "..", "data", - "admin_schema_2025-01.json" + "schemas.json" ); +// Function to get the schema path for a specific API +export async function getSchemaPath( + api: string, + version?: string +): Promise { + try { + const schemasConfigContent = await fs.readFile(SCHEMAS_CONFIG_PATH, "utf8"); + const schemasConfig = JSON.parse(schemasConfigContent); + + const schemaConfig = schemasConfig.find( + (config: any) => + config.api === api && (!version || config.version === version) + ); + if (!schemaConfig) { + throw new Error( + `Schema configuration for API "${api}"${ + version ? ` version "${version}"` : "" + } not found` + ); + } + + return path.join(__dirname, "..", "data", schemaConfig.file); + } catch (error) { + console.error( + `[shopify-admin-schema-tool] Error reading schema configuration: ${error}` + ); + throw error; + } +} + // Function to load schema content, handling decompression if needed export async function loadSchemaContent(schemaPath: string): Promise { - const gzippedSchemaPath = `${schemaPath}.gz`; + // Strip .gz extension if present for the uncompressed path check + const uncompressedPath = schemaPath.endsWith(".gz") + ? schemaPath.slice(0, -3) + : schemaPath; // If uncompressed file doesn't exist but gzipped does, decompress it - if (!existsSync(schemaPath) && existsSync(gzippedSchemaPath)) { + if (!existsSync(uncompressedPath) && existsSync(schemaPath)) { console.error( - `[shopify-admin-schema-tool] Decompressing GraphQL schema from ${gzippedSchemaPath}` + `[shopify-admin-schema-tool] Decompressing GraphQL schema from ${schemaPath}` ); - const compressedData = await fs.readFile(gzippedSchemaPath); + const compressedData = await fs.readFile(schemaPath); const schemaContent = zlib.gunzipSync(compressedData).toString("utf-8"); // Save the uncompressed content to disk - await fs.writeFile(schemaPath, schemaContent, "utf-8"); + await fs.writeFile(uncompressedPath, schemaContent, "utf-8"); console.error( - `[shopify-admin-schema-tool] Saved uncompressed schema to ${schemaPath}` + `[shopify-admin-schema-tool] Saved uncompressed schema to ${uncompressedPath}` ); return schemaContent; } - console.error( - `[shopify-admin-schema-tool] Reading GraphQL schema from ${schemaPath}` - ); - return fs.readFile(schemaPath, "utf8"); + // If uncompressed file exists, read it directly + if (existsSync(uncompressedPath)) { + console.error( + `[shopify-admin-schema-tool] Reading GraphQL schema from ${uncompressedPath}` + ); + return fs.readFile(uncompressedPath, "utf8"); + } + + throw new Error(`Schema file not found: ${schemaPath}`); } // Maximum number of fields to extract from an object @@ -196,14 +234,24 @@ export const formatGraphqlOperation = (query: any): string => { }; // Function to search and format schema data -export async function searchShopifyAdminSchema( +export async function introspectGraphqlSchema( query: string, { + api = "admin", + version = "2025-01", filter = ["all"], - }: { filter?: Array<"all" | "types" | "queries" | "mutations"> } = {} + }: { + api?: "admin" | "storefront"; + version?: "2024-04" | "2024-07" | "2024-10" | "2025-01"; + filter?: Array<"all" | "types" | "queries" | "mutations">; + } = {} ) { try { - const schemaContent = await loadSchemaContent(SCHEMA_FILE_PATH); + // Get the appropriate schema path based on the API and version + const schemaPath = await getSchemaPath(api, version); + + // Load the schema content + const schemaContent = await loadSchemaContent(schemaPath); // Parse the schema content const schemaJson = JSON.parse(schemaContent); diff --git a/src/shopify.ts b/src/shopify.ts index cee30c3..87c9f6d 100644 --- a/src/shopify.ts +++ b/src/shopify.ts @@ -1,6 +1,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { searchShopifyAdminSchema } from "./shopify-admin-schema.js"; +import { introspectGraphqlSchema } from "./introspect-graphql-schema.js"; const SHOPIFY_BASE_URL = "https://shopify.dev"; @@ -88,14 +88,28 @@ export async function searchShopifyDocs(prompt: string) { export function shopifyTools(server: McpServer) { server.tool( - "introspect-admin-schema", - "Introspect the Shopify Admin GraphQL schema. Only use this for the Shopify Admin API, not for other APIs like Storefront API or Functions API.", + "introspect-graphql-schema", + "Introspect the Shopify GraphQL schema. Only use this for the Shopify Admin API, not for other APIs like Storefront API or Functions API.", { query: z .string() .describe( "Search term to filter schema elements by name. Only pass simple terms like 'product', 'discountProduct', etc." ), + api: z + .enum(["admin", "storefront"]) + .optional() + .default("admin") + .describe( + "The API to introspect. Can be 'admin' or 'storefront'. Default is 'admin'." + ), + version: z + .enum(["2024-04", "2024-07", "2024-10", "2025-01"]) + .optional() + .default("2025-01") + .describe( + "The version of the API to introspect. Default is '2025-01'." + ), filter: z .array(z.enum(["all", "types", "queries", "mutations"])) .optional() @@ -104,8 +118,12 @@ export function shopifyTools(server: McpServer) { "Filter results to show specific sections. Can include 'types', 'queries', 'mutations', or 'all' (default)" ), }, - async ({ query, filter }, extra) => { - const result = await searchShopifyAdminSchema(query, { filter }); + async ({ query, filter, api, version }, extra) => { + const result = await introspectGraphqlSchema(query, { + filter, + api, + version, + }); if (result.success) { return {