Skip to content

Commit 76ed940

Browse files
authored
Merge pull request #51 from Shopify/validate_admin_gql
WIP Refactor tools to better detect/correct hallucinations within code blocks
2 parents 1d04311 + 5ddceea commit 76ed940

File tree

13 files changed

+842
-20
lines changed

13 files changed

+842
-20
lines changed

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
22
"editor.defaultFormatter": "esbenp.prettier-vscode",
3-
"editor.formatOnSave": true
3+
"editor.formatOnSave": true,
4+
"editor.codeActionsOnSave": {
5+
"source.organizeImports": "explicit"
6+
}
47
}

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
"description": "A command line tool for setting up Shopify Dev MCP server",
1919
"dependencies": {
2020
"@modelcontextprotocol/sdk": "^1.6.1",
21+
"graphql": "^16.11.0",
2122
"zod": "^3.24.2"
2223
},
2324
"devDependencies": {
2425
"@changesets/changelog-github": "^0.5.1",
2526
"@changesets/cli": "^2.29.4",
2627
"@jest/globals": "^29.7.0",
2728
"@modelcontextprotocol/inspector": "^0.5.1",
29+
"@types/graphql": "^14.2.3",
2830
"@types/node": "^22.13.10",
2931
"@vitest/coverage-v8": "^3.0.9",
3032
"memfs": "^4.17.0",

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5-
import { shopifyTools } from "./tools/index.js";
6-
import { shopifyPrompts } from "./prompts/index.js";
75
import { readFileSync } from "fs";
8-
import { fileURLToPath } from "url";
96
import { dirname, resolve } from "path";
7+
import { fileURLToPath } from "url";
8+
import { shopifyPrompts } from "./prompts/index.js";
9+
import { shopifyTools } from "./tools/index.js";
1010

1111
// Get package.json version
1212
const __filename = fileURLToPath(import.meta.url);

src/prompts/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { z } from "zod";
21
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { z } from "zod";
33

44
export function shopifyPrompts(server: McpServer) {
55
server.prompt(

src/tools/index.test.ts

Lines changed: 260 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import {
2+
afterAll,
3+
afterEach,
4+
beforeEach,
25
describe,
6+
expect,
37
it,
48
test,
5-
expect,
6-
beforeEach,
79
vi,
8-
afterAll,
9-
afterEach,
1010
} from "vitest";
1111

1212
global.fetch = vi.fn();
1313

14-
import { shopifyTools, searchShopifyDocs } from "./index.js";
1514
import {
1615
instrumentationData,
1716
isInstrumentationDisabled,
1817
} from "../instrumentation.js";
19-
import { searchShopifyAdminSchema } from "./shopify-admin-schema.js";
18+
import { ValidationResult } from "../types.js";
19+
import validateGraphQLOperation from "../validations/graphqlSchema.js";
20+
import { searchShopifyDocs, shopifyTools } from "./index.js";
21+
import { searchShopifyAdminSchema } from "./shopifyAdminSchema.js";
2022

2123
const originalConsoleError = console.error;
2224
const originalConsoleWarn = console.warn;
@@ -78,10 +80,15 @@ vi.mock("../instrumentation.js", () => ({
7880
}));
7981

8082
// Mock searchShopifyAdminSchema
81-
vi.mock("./shopify-admin-schema.js", () => ({
83+
vi.mock("./shopifyAdminSchema.js", () => ({
8284
searchShopifyAdminSchema: vi.fn(),
8385
}));
8486

87+
// Mock validateGraphQLOperation
88+
vi.mock("../validations/graphqlSchema.js", () => ({
89+
default: vi.fn(),
90+
}));
91+
8592
vi.mock("../../package.json", () => ({
8693
default: { version: "1.0.0" },
8794
}));
@@ -609,3 +616,249 @@ describe("get_started tool behavior", () => {
609616
expect(result.content[0].text).toContain("Network failure");
610617
});
611618
});
619+
620+
describe("validate_graphql tool", () => {
621+
let mockServer: any;
622+
let validateGraphQLOperationMock: any;
623+
624+
beforeEach(() => {
625+
vi.clearAllMocks();
626+
validateGraphQLOperationMock = vi.mocked(validateGraphQLOperation);
627+
628+
// Mock fetch for getting started APIs
629+
const fetchMock = global.fetch as any;
630+
fetchMock.mockResolvedValue({
631+
ok: true,
632+
status: 200,
633+
text: async () => JSON.stringify(sampleGettingStartedApisResponse),
634+
});
635+
636+
// Create a mock server that captures the registered tools
637+
mockServer = {
638+
tool: vi.fn((name, description, schema, handler) => {
639+
if (name === "validate_graphql") {
640+
mockServer.validateHandler = handler;
641+
}
642+
}),
643+
validateHandler: null,
644+
};
645+
646+
// Mock instrumentation
647+
vi.mocked(instrumentationData).mockReturnValue({
648+
packageVersion: "1.0.0",
649+
timestamp: "2024-01-01T00:00:00.000Z",
650+
});
651+
vi.mocked(isInstrumentationDisabled).mockReturnValue(false);
652+
});
653+
654+
test("validates multiple code snippets successfully", async () => {
655+
// Setup mock responses
656+
validateGraphQLOperationMock
657+
.mockResolvedValueOnce({
658+
result: ValidationResult.SUCCESS,
659+
resultDetail:
660+
"Successfully validated GraphQL query against Shopify Admin API schema.",
661+
})
662+
.mockResolvedValueOnce({
663+
result: ValidationResult.FAILED,
664+
resultDetail:
665+
"No GraphQL operation found in the provided code snippet.",
666+
});
667+
668+
// Register the tools
669+
await shopifyTools(mockServer);
670+
671+
// Ensure the handler was registered
672+
expect(mockServer.validateHandler).not.toBeNull();
673+
674+
const testCodeSnippets = ["query { products { id } }", "const x = 1;"];
675+
676+
// Call the handler
677+
const result = await mockServer.validateHandler({
678+
code: testCodeSnippets,
679+
api: "admin",
680+
});
681+
682+
// Verify validateGraphQLOperation was called correctly
683+
expect(validateGraphQLOperationMock).toHaveBeenCalledTimes(2);
684+
expect(validateGraphQLOperationMock).toHaveBeenNthCalledWith(
685+
1,
686+
testCodeSnippets[0],
687+
"admin",
688+
);
689+
expect(validateGraphQLOperationMock).toHaveBeenNthCalledWith(
690+
2,
691+
testCodeSnippets[1],
692+
"admin",
693+
);
694+
695+
// Verify the response
696+
expect(result.content[0].type).toBe("text");
697+
const responseText = result.content[0].text;
698+
expect(responseText).toContain("❌ INVALID");
699+
expect(responseText).toContain("**Total Code Snippets:** 2");
700+
expect(responseText).toContain("Successfully validated GraphQL query");
701+
expect(responseText).toContain("No GraphQL operation found");
702+
});
703+
704+
test("handles validation failures correctly", async () => {
705+
// Setup mock responses with failures
706+
validateGraphQLOperationMock
707+
.mockResolvedValueOnce({
708+
result: ValidationResult.FAILED,
709+
resultDetail:
710+
"GraphQL validation errors: Cannot query field 'invalidField' on type 'Product'.",
711+
})
712+
.mockResolvedValueOnce({
713+
result: ValidationResult.SUCCESS,
714+
resultDetail:
715+
"Successfully validated GraphQL mutation against Shopify Admin API schema.",
716+
});
717+
718+
// Register the tools
719+
await shopifyTools(mockServer);
720+
721+
const testCodeSnippets = [
722+
"query { products { invalidField } }",
723+
"mutation { productCreate(input: {}) { product { id } } }",
724+
];
725+
726+
// Call the handler
727+
const result = await mockServer.validateHandler({
728+
code: testCodeSnippets,
729+
api: "admin",
730+
});
731+
732+
// Verify the response shows invalid overall status
733+
const responseText = result.content[0].text;
734+
expect(responseText).toContain("❌ INVALID");
735+
expect(responseText).toContain("**Total Code Snippets:** 2");
736+
expect(responseText).toContain("Cannot query field 'invalidField'");
737+
expect(responseText).toContain("Successfully validated GraphQL mutation");
738+
});
739+
740+
test("handles mixed validation results", async () => {
741+
// Setup mixed results
742+
validateGraphQLOperationMock
743+
.mockResolvedValueOnce({
744+
result: ValidationResult.SUCCESS,
745+
resultDetail:
746+
"Successfully validated GraphQL query against Shopify Admin API schema.",
747+
})
748+
.mockResolvedValueOnce({
749+
result: ValidationResult.FAILED,
750+
resultDetail:
751+
"No GraphQL operation found in the provided code snippet.",
752+
})
753+
.mockResolvedValueOnce({
754+
result: ValidationResult.FAILED,
755+
resultDetail:
756+
"GraphQL syntax error: Syntax Error: Expected Name, found }",
757+
});
758+
759+
// Register the tools
760+
await shopifyTools(mockServer);
761+
762+
const testCodeSnippets = [
763+
"query { products { id } }",
764+
"const x = 1;",
765+
"query { products { } }",
766+
];
767+
768+
// Call the handler
769+
const result = await mockServer.validateHandler({
770+
code: testCodeSnippets,
771+
api: "admin",
772+
});
773+
774+
// Verify the response shows invalid overall status due to failure
775+
const responseText = result.content[0].text;
776+
expect(responseText).toContain("❌ INVALID");
777+
expect(responseText).toContain("**Total Code Snippets:** 3");
778+
expect(responseText).toContain("Code Snippet 1\n**Status:** ✅ SUCCESS");
779+
expect(responseText).toContain("Code Snippet 2\n**Status:** ❌ FAILED");
780+
expect(responseText).toContain("Code Snippet 3\n**Status:** ❌ FAILED");
781+
expect(responseText).toContain("Syntax Error: Expected Name, found }");
782+
});
783+
784+
test("handles empty code snippets array", async () => {
785+
// Register the tools
786+
await shopifyTools(mockServer);
787+
788+
// Call the handler with empty array
789+
const result = await mockServer.validateHandler({
790+
code: [],
791+
api: "admin",
792+
});
793+
794+
// Verify validateGraphQLOperation was not called
795+
expect(validateGraphQLOperationMock).not.toHaveBeenCalled();
796+
797+
// Verify the response
798+
const responseText = result.content[0].text;
799+
expect(responseText).toContain("✅ VALID");
800+
expect(responseText).toContain("**Total Code Snippets:** 0");
801+
});
802+
803+
test("handles validation function errors", async () => {
804+
// Setup mock to throw an error
805+
validateGraphQLOperationMock.mockRejectedValueOnce(
806+
new Error("Schema loading failed"),
807+
);
808+
809+
// Register the tools
810+
await shopifyTools(mockServer);
811+
812+
const testCodeSnippets = ["query { products { id } }"];
813+
814+
// Call the handler and expect it to handle the error gracefully
815+
await expect(
816+
mockServer.validateHandler({
817+
code: testCodeSnippets,
818+
api: "admin",
819+
}),
820+
).rejects.toThrow("Schema loading failed");
821+
822+
// Verify validateGraphQLOperation was called
823+
expect(validateGraphQLOperationMock).toHaveBeenCalledTimes(1);
824+
});
825+
826+
test("records usage data correctly", async () => {
827+
// Setup successful validation
828+
validateGraphQLOperationMock.mockResolvedValueOnce({
829+
result: ValidationResult.SUCCESS,
830+
resultDetail:
831+
"Successfully validated GraphQL query against Shopify Admin API schema.",
832+
});
833+
834+
// Mock fetch for usage recording
835+
const fetchMock = global.fetch as any;
836+
fetchMock.mockResolvedValue({
837+
ok: true,
838+
status: 200,
839+
});
840+
841+
// Register the tools
842+
await shopifyTools(mockServer);
843+
844+
const testCodeSnippets = ["query { products { id } }"];
845+
846+
// Call the handler
847+
await mockServer.validateHandler({
848+
code: testCodeSnippets,
849+
api: "admin",
850+
});
851+
852+
// Verify usage was recorded (should be called for API list fetch and usage recording)
853+
const usageCalls = fetchMock.mock.calls.filter((call: [string, any]) =>
854+
call[0].includes("/mcp/usage"),
855+
);
856+
expect(usageCalls.length).toBe(1);
857+
858+
// Verify the usage data
859+
const usageCall = usageCalls[0];
860+
const usageBody = JSON.parse(usageCall[1].body);
861+
expect(usageBody.tool).toBe("validate_graphql");
862+
expect(usageBody.parameters).toBe("1 code snippets");
863+
});
864+
});

0 commit comments

Comments
 (0)