Skip to content

Support storefront api #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
dist/
data/**/*.json
!data/schemas.json
.DS_Store
Binary file added data/admin_schema_2024-04.json.gz
Binary file not shown.
Binary file added data/admin_schema_2024-07.json.gz
Binary file not shown.
Binary file added data/admin_schema_2024-10.json.gz
Binary file not shown.
42 changes: 42 additions & 0 deletions data/schemas.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
Binary file added data/storefront_schema_2024-04.json.gz
Binary file not shown.
Binary file added data/storefront_schema_2024-07.json.gz
Binary file not shown.
Binary file added data/storefront_schema_2024-10.json.gz
Binary file not shown.
Binary file added data/storefront_schema_2025-01.json.gz
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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;
Expand Down Expand Up @@ -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:");
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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"],
});

Expand All @@ -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"],
});

Expand All @@ -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"],
});

Expand All @@ -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"],
});

Expand 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
Expand All @@ -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"],
});

Expand Down
80 changes: 64 additions & 16 deletions src/shopify-admin-schema.ts → src/introspect-graphql-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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<string> {
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
Expand Down Expand Up @@ -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);
Expand Down
28 changes: 23 additions & 5 deletions src/shopify.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand Down