@anton.andrusenko/shopify-mcp-admin 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +102 -11
  2. package/dist/index.js +1456 -769
  3. package/package.json +12 -2
package/dist/index.js CHANGED
@@ -10,8 +10,11 @@ import { z } from "zod";
10
10
  var configSchema = z.object({
11
11
  // Required - store identity
12
12
  SHOPIFY_STORE_URL: z.string().min(1, "SHOPIFY_STORE_URL is required").regex(/\.myshopify\.com$/, "Must be a valid myshopify.com domain"),
13
- // Required - authentication
14
- SHOPIFY_ACCESS_TOKEN: z.string().min(1, "SHOPIFY_ACCESS_TOKEN is required"),
13
+ // Authentication Option 1: Legacy Custom App (static token)
14
+ SHOPIFY_ACCESS_TOKEN: z.string().optional(),
15
+ // Authentication Option 2: Dev Dashboard (OAuth 2.0 client credentials)
16
+ SHOPIFY_CLIENT_ID: z.string().optional(),
17
+ SHOPIFY_CLIENT_SECRET: z.string().optional(),
15
18
  // Optional with defaults
16
19
  SHOPIFY_API_VERSION: z.string().default("2025-10"),
17
20
  DEBUG: z.string().optional(),
@@ -19,8 +22,52 @@ var configSchema = z.object({
19
22
  PORT: z.string().default("3000").transform(Number),
20
23
  // Transport selection (AC-2.2.6, AC-2.2.7)
21
24
  // Default: stdio for Claude Desktop compatibility
22
- TRANSPORT: z.enum(["stdio", "http"]).default("stdio")
23
- });
25
+ TRANSPORT: z.enum(["stdio", "http"]).default("stdio"),
26
+ // Store info cache TTL (milliseconds)
27
+ // Default: 5 minutes (300000ms) - configurable for performance tuning
28
+ STORE_INFO_CACHE_TTL_MS: z.string().optional().default("300000").transform(Number).describe("Cache TTL for store info in milliseconds (default: 5 minutes)")
29
+ }).refine(
30
+ (data) => {
31
+ const hasLegacyAuth = !!data.SHOPIFY_ACCESS_TOKEN;
32
+ const hasClientId = !!data.SHOPIFY_CLIENT_ID;
33
+ const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
34
+ const hasClientCredentials = hasClientId && hasClientSecret;
35
+ return hasLegacyAuth || hasClientCredentials;
36
+ },
37
+ {
38
+ message: "Authentication required: Provide either SHOPIFY_ACCESS_TOKEN (legacy) OR both SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET (Dev Dashboard)"
39
+ }
40
+ ).refine(
41
+ (data) => {
42
+ const hasClientId = !!data.SHOPIFY_CLIENT_ID;
43
+ const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
44
+ if (hasClientId && !hasClientSecret) {
45
+ return false;
46
+ }
47
+ return true;
48
+ },
49
+ {
50
+ message: "Incomplete client credentials: SHOPIFY_CLIENT_SECRET is required when SHOPIFY_CLIENT_ID is provided"
51
+ }
52
+ ).refine(
53
+ (data) => {
54
+ const hasClientId = !!data.SHOPIFY_CLIENT_ID;
55
+ const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
56
+ if (hasClientSecret && !hasClientId) {
57
+ return false;
58
+ }
59
+ return true;
60
+ },
61
+ {
62
+ message: "Incomplete client credentials: SHOPIFY_CLIENT_ID is required when SHOPIFY_CLIENT_SECRET is provided"
63
+ }
64
+ );
65
+ function getAuthMode(config) {
66
+ if (config.SHOPIFY_ACCESS_TOKEN) {
67
+ return "token";
68
+ }
69
+ return "client_credentials";
70
+ }
24
71
  function isDebugEnabled(debugValue) {
25
72
  if (!debugValue) return false;
26
73
  const normalized = debugValue.toLowerCase().trim();
@@ -54,8 +101,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
54
101
  // src/utils/logger.ts
55
102
  var SANITIZATION_PATTERNS = [
56
103
  { pattern: /shpat_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
57
- { pattern: /Bearer\s+[a-zA-Z0-9]+/g, replacement: "Bearer [REDACTED]" },
58
- { pattern: /access_token[=:]\s*[a-zA-Z0-9]+/gi, replacement: "access_token=[REDACTED]" }
104
+ { pattern: /shpua_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
105
+ { pattern: /Bearer\s+[a-zA-Z0-9_-]+/g, replacement: "Bearer [REDACTED]" },
106
+ { pattern: /access_token[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "access_token=[REDACTED]" },
107
+ { pattern: /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "client_secret=[REDACTED]" }
59
108
  ];
60
109
  function sanitizeLogMessage(message) {
61
110
  let result = message;
@@ -313,6 +362,283 @@ async function withRateLimit(operation, context, config = DEFAULT_RATE_LIMIT_CON
313
362
  // src/shopify/client.ts
314
363
  import "@shopify/shopify-api/adapters/node";
315
364
  import { shopifyApi } from "@shopify/shopify-api";
365
+
366
+ // src/utils/errors.ts
367
+ var sanitizeErrorMessage = sanitizeLogMessage;
368
+ var ToolError = class _ToolError extends Error {
369
+ /** AI-friendly suggestion for error recovery */
370
+ suggestion;
371
+ /**
372
+ * Create a new ToolError
373
+ *
374
+ * @param message - The error message describing what went wrong
375
+ * @param suggestion - A helpful suggestion for how to resolve the error
376
+ */
377
+ constructor(message, suggestion) {
378
+ super(message);
379
+ this.name = "ToolError";
380
+ this.suggestion = suggestion;
381
+ if (Error.captureStackTrace) {
382
+ Error.captureStackTrace(this, _ToolError);
383
+ }
384
+ }
385
+ };
386
+ function extractErrorMessage(error) {
387
+ if (error instanceof Error) {
388
+ return error.message;
389
+ }
390
+ if (typeof error === "string") {
391
+ return error;
392
+ }
393
+ return "Unknown error";
394
+ }
395
+ function createToolError(error, suggestion) {
396
+ const message = extractErrorMessage(error);
397
+ const safeMessage = sanitizeErrorMessage(message);
398
+ const safeSuggestion = sanitizeErrorMessage(suggestion);
399
+ return {
400
+ isError: true,
401
+ content: [
402
+ {
403
+ type: "text",
404
+ text: `Error: ${safeMessage}
405
+
406
+ Suggestion: ${safeSuggestion}`
407
+ }
408
+ ]
409
+ };
410
+ }
411
+ function safeStringify2(data) {
412
+ try {
413
+ const seen = /* @__PURE__ */ new WeakSet();
414
+ return JSON.stringify(
415
+ data,
416
+ (_key, value) => {
417
+ if (typeof value === "object" && value !== null) {
418
+ if (seen.has(value)) {
419
+ return "[Circular]";
420
+ }
421
+ seen.add(value);
422
+ }
423
+ return value;
424
+ },
425
+ 2
426
+ );
427
+ } catch {
428
+ return "[Unable to stringify]";
429
+ }
430
+ }
431
+ function createToolSuccess(data) {
432
+ const text = safeStringify2(data);
433
+ return {
434
+ content: [
435
+ {
436
+ type: "text",
437
+ text
438
+ }
439
+ ],
440
+ structuredContent: data
441
+ };
442
+ }
443
+
444
+ // src/shopify/token-manager.ts
445
+ var TokenManager = class _TokenManager {
446
+ config;
447
+ cachedToken = null;
448
+ refreshPromise = null;
449
+ /**
450
+ * Refresh buffer: 5 minutes before expiry
451
+ *
452
+ * Tokens are refreshed when within this window of expiration
453
+ * to ensure requests don't fail due to token expiry mid-request.
454
+ */
455
+ static REFRESH_BUFFER_MS = 5 * 60 * 1e3;
456
+ /**
457
+ * Create a new TokenManager instance
458
+ *
459
+ * @param config - Configuration containing store URL and OAuth credentials
460
+ */
461
+ constructor(config) {
462
+ this.config = config;
463
+ log.debug("TokenManager initialized", { storeUrl: config.storeUrl });
464
+ }
465
+ /**
466
+ * Get a valid access token
467
+ *
468
+ * Returns a cached token if valid and not expiring soon.
469
+ * Automatically fetches a new token if:
470
+ * - No token is cached
471
+ * - Cached token is within 5 minutes of expiration
472
+ *
473
+ * Concurrent calls are deduplicated - only one OAuth request
474
+ * is made even if multiple callers request a token simultaneously.
475
+ *
476
+ * @returns Promise resolving to a valid access token string
477
+ * @throws ToolError if token acquisition fails
478
+ */
479
+ async getAccessToken() {
480
+ if (this.refreshPromise) {
481
+ log.debug("Token refresh in progress, waiting...");
482
+ return this.refreshPromise;
483
+ }
484
+ if (this.cachedToken && !this.needsRefresh()) {
485
+ log.debug("Using cached token");
486
+ return this.cachedToken.accessToken;
487
+ }
488
+ log.debug("Fetching new OAuth token");
489
+ this.refreshPromise = this.fetchNewToken().then((token) => {
490
+ this.cachedToken = token;
491
+ log.debug("Token cached successfully", {
492
+ expiresIn: Math.round((token.expiresAt - Date.now()) / 1e3)
493
+ });
494
+ return token.accessToken;
495
+ }).finally(() => {
496
+ this.refreshPromise = null;
497
+ });
498
+ return this.refreshPromise;
499
+ }
500
+ /**
501
+ * Fetch a new token from Shopify OAuth endpoint
502
+ *
503
+ * Implements OAuth 2.0 client credentials grant flow:
504
+ * - POST to /admin/oauth/access_token
505
+ * - Content-Type: application/x-www-form-urlencoded
506
+ * - Body: grant_type, client_id, client_secret
507
+ *
508
+ * @returns Promise resolving to cached token with expiry
509
+ * @throws ToolError on OAuth failure (401, 400, network error)
510
+ * @private
511
+ */
512
+ async fetchNewToken() {
513
+ const tokenEndpoint = this.buildTokenEndpoint();
514
+ try {
515
+ const response = await fetch(tokenEndpoint, {
516
+ method: "POST",
517
+ headers: {
518
+ "Content-Type": "application/x-www-form-urlencoded"
519
+ },
520
+ body: new URLSearchParams({
521
+ grant_type: "client_credentials",
522
+ client_id: this.config.clientId,
523
+ client_secret: this.config.clientSecret
524
+ })
525
+ });
526
+ if (!response.ok) {
527
+ const errorText = await response.text();
528
+ throw this.handleOAuthError(response.status, errorText);
529
+ }
530
+ const data = await response.json();
531
+ if (!data.access_token) {
532
+ throw new ToolError(
533
+ "Invalid OAuth response: missing access_token",
534
+ "This may be a temporary Shopify API issue. Try again in a few minutes."
535
+ );
536
+ }
537
+ const expiresAt = Date.now() + data.expires_in * 1e3;
538
+ return {
539
+ accessToken: data.access_token,
540
+ expiresAt
541
+ };
542
+ } catch (error) {
543
+ if (error instanceof ToolError) {
544
+ throw error;
545
+ }
546
+ const message = error instanceof Error ? error.message : "Unknown error";
547
+ throw new ToolError(
548
+ `OAuth token request failed: ${message}`,
549
+ "Check your network connection and ensure SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET are correct."
550
+ );
551
+ }
552
+ }
553
+ /**
554
+ * Build the OAuth token endpoint URL
555
+ *
556
+ * @returns Full URL to the Shopify OAuth token endpoint
557
+ * @private
558
+ */
559
+ buildTokenEndpoint() {
560
+ const storeUrl = this.config.storeUrl.includes("://") ? this.config.storeUrl : `https://${this.config.storeUrl}`;
561
+ const baseUrl = storeUrl.replace(/\/$/, "");
562
+ return `${baseUrl}/admin/oauth/access_token`;
563
+ }
564
+ /**
565
+ * Check if the cached token needs to be refreshed
566
+ *
567
+ * Returns true if:
568
+ * - No token is cached
569
+ * - Token expires within REFRESH_BUFFER_MS (5 minutes)
570
+ *
571
+ * @returns true if token should be refreshed
572
+ * @private
573
+ */
574
+ needsRefresh() {
575
+ if (!this.cachedToken) {
576
+ return true;
577
+ }
578
+ const refreshThreshold = Date.now() + _TokenManager.REFRESH_BUFFER_MS;
579
+ return refreshThreshold >= this.cachedToken.expiresAt;
580
+ }
581
+ /**
582
+ * Handle OAuth error responses
583
+ *
584
+ * Creates appropriate ToolError with actionable suggestions
585
+ * based on HTTP status code.
586
+ *
587
+ * @param status - HTTP status code
588
+ * @param responseText - Error response body
589
+ * @returns ToolError with appropriate message and suggestion
590
+ * @private
591
+ */
592
+ handleOAuthError(status, responseText) {
593
+ switch (status) {
594
+ case 401:
595
+ return new ToolError(
596
+ "OAuth authentication failed (HTTP 401)",
597
+ "Verify that SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET are correct. Ensure the app has the required scopes configured in the Dev Dashboard."
598
+ );
599
+ case 400:
600
+ return new ToolError(
601
+ "OAuth bad request (HTTP 400)",
602
+ "The OAuth request parameters are invalid. Ensure SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET are set correctly and the app is properly configured in the Dev Dashboard."
603
+ );
604
+ case 403:
605
+ return new ToolError(
606
+ "OAuth forbidden (HTTP 403)",
607
+ "The app may not have permission to access this store. Ensure the app is installed on the store and has the required access scopes."
608
+ );
609
+ case 404:
610
+ return new ToolError(
611
+ "OAuth endpoint not found (HTTP 404)",
612
+ `Verify SHOPIFY_STORE_URL is correct and includes the .myshopify.com domain. Current value appears incorrect. Received: ${responseText.slice(0, 100)}`
613
+ );
614
+ case 429:
615
+ return new ToolError(
616
+ "OAuth rate limited (HTTP 429)",
617
+ "Too many token requests. Wait a few minutes before trying again."
618
+ );
619
+ default:
620
+ return new ToolError(
621
+ `OAuth request failed (HTTP ${status})`,
622
+ `Unexpected error from Shopify OAuth endpoint. Response: ${responseText.slice(0, 200)}`
623
+ );
624
+ }
625
+ }
626
+ /**
627
+ * Clear the cached token
628
+ *
629
+ * Use this for:
630
+ * - Testing: Reset state between tests
631
+ * - Error recovery: Force new token after auth failures
632
+ *
633
+ * The next call to getAccessToken() will fetch a new token.
634
+ */
635
+ clearCache() {
636
+ this.cachedToken = null;
637
+ log.debug("Token cache cleared");
638
+ }
639
+ };
640
+
641
+ // src/shopify/client.ts
316
642
  var DEFAULT_API_VERSION = "2025-10";
317
643
  var SHOP_QUERY = `
318
644
  query {
@@ -322,6 +648,7 @@ var SHOP_QUERY = `
322
648
  }
323
649
  `;
324
650
  var _defaultClient = null;
651
+ var _tokenManager = null;
325
652
  async function verifyConnectivity(client) {
326
653
  const response = await client.graphql.request(SHOP_QUERY);
327
654
  if (response.errors && response.errors.length > 0) {
@@ -377,18 +704,59 @@ async function createShopifyClient(credentials, options = {}) {
377
704
  }
378
705
  return client;
379
706
  }
707
+ async function createClientWithTokenRefresh(credentials, tokenManager) {
708
+ const baseClient = buildClient(credentials);
709
+ let currentToken = credentials.accessToken;
710
+ return {
711
+ ...baseClient,
712
+ graphql: {
713
+ request: async (query, options) => {
714
+ const token = await tokenManager.getAccessToken();
715
+ if (token !== currentToken) {
716
+ log.debug("Token refreshed, updating client credentials");
717
+ currentToken = token;
718
+ credentials.accessToken = token;
719
+ const refreshedClient = buildClient(credentials);
720
+ return refreshedClient.graphql.request(query, options);
721
+ }
722
+ return baseClient.graphql.request(query, options);
723
+ }
724
+ }
725
+ };
726
+ }
380
727
  async function getShopifyClient() {
381
728
  if (_defaultClient !== null) {
382
729
  return _defaultClient;
383
730
  }
384
731
  const config = getConfig();
385
- const credentials = {
386
- storeUrl: config.SHOPIFY_STORE_URL,
387
- accessToken: config.SHOPIFY_ACCESS_TOKEN,
388
- apiVersion: config.SHOPIFY_API_VERSION
389
- };
732
+ const authMode = getAuthMode(config);
733
+ log.info(`Initializing Shopify client with auth mode: ${authMode}`);
390
734
  try {
391
- const client = await createShopifyClient(credentials);
735
+ if (authMode === "token") {
736
+ const credentials2 = {
737
+ storeUrl: config.SHOPIFY_STORE_URL,
738
+ accessToken: config.SHOPIFY_ACCESS_TOKEN,
739
+ apiVersion: config.SHOPIFY_API_VERSION
740
+ };
741
+ const client2 = await createShopifyClient(credentials2);
742
+ _defaultClient = client2;
743
+ return _defaultClient;
744
+ }
745
+ log.debug("Creating TokenManager for client_credentials auth");
746
+ _tokenManager = new TokenManager({
747
+ storeUrl: config.SHOPIFY_STORE_URL,
748
+ clientId: config.SHOPIFY_CLIENT_ID,
749
+ clientSecret: config.SHOPIFY_CLIENT_SECRET
750
+ });
751
+ const initialToken = await _tokenManager.getAccessToken();
752
+ log.debug("Initial OAuth token acquired successfully");
753
+ const credentials = {
754
+ storeUrl: config.SHOPIFY_STORE_URL,
755
+ accessToken: initialToken,
756
+ apiVersion: config.SHOPIFY_API_VERSION
757
+ };
758
+ const client = await createClientWithTokenRefresh(credentials, _tokenManager);
759
+ await verifyConnectivity(client);
392
760
  _defaultClient = client;
393
761
  return _defaultClient;
394
762
  } catch (error) {
@@ -571,7 +939,7 @@ function transformShopResponse(shop) {
571
939
  async function getStoreInfo() {
572
940
  const now = Date.now();
573
941
  const config = getConfig();
574
- const ttl = "STORE_INFO_CACHE_TTL_MS" in config ? config.STORE_INFO_CACHE_TTL_MS ?? DEFAULT_CACHE_TTL_MS : DEFAULT_CACHE_TTL_MS;
942
+ const ttl = config.STORE_INFO_CACHE_TTL_MS ?? DEFAULT_CACHE_TTL_MS;
575
943
  if (_cachedStoreInfo && now - _cacheTimestamp < ttl) {
576
944
  log.debug("Returning cached store info");
577
945
  return _cachedStoreInfo;
@@ -598,6 +966,42 @@ async function getStoreInfo() {
598
966
  var require2 = createRequire(import.meta.url);
599
967
  var packageJson = require2("../package.json");
600
968
  var SERVER_NAME = "shopify-mcp-admin";
969
+ var SERVER_INSTRUCTIONS = `This MCP server provides access to a Shopify store's Admin API.
970
+
971
+ KEY CONCEPTS:
972
+ - Products contain variants; each variant has its own price, SKU, and inventory
973
+ - Inventory is tracked per variant AND per location (multi-location stores)
974
+ - Metafields store custom data using namespace/key pairs
975
+ - Collections organize products for navigation and SEO
976
+ - All IDs use Shopify GID format: "gid://shopify/Product/123"
977
+
978
+ RESOURCE TYPES & GID FORMATS:
979
+ - Products: gid://shopify/Product/{id}
980
+ - Variants: gid://shopify/ProductVariant/{id}
981
+ - Collections: gid://shopify/Collection/{id}
982
+ - Images: gid://shopify/MediaImage/{id}
983
+ - Pages: gid://shopify/Page/{id}
984
+ - Blogs: gid://shopify/Blog/{id}
985
+ - Articles: gid://shopify/Article/{id}
986
+ - Redirects: gid://shopify/UrlRedirect/{id}
987
+ - InventoryItems: gid://shopify/InventoryItem/{id}
988
+ - Locations: gid://shopify/Location/{id}
989
+
990
+ BEST PRACTICES:
991
+ - Always verify product/variant IDs using get-product before updates
992
+ - Use list-low-inventory to proactively identify stock issues
993
+ - When updating SEO, consider updating URL redirects
994
+ - For bulk operations, prefer bulk tools when available
995
+ - Use metafields to store custom SEO data (JSON-LD, Open Graph)
996
+
997
+ RATE LIMITS:
998
+ - Shopify GraphQL has cost-based rate limiting (~50 points/sec)
999
+ - Large queries may retry automatically with exponential backoff
1000
+ - Rate limit errors include helpful retry suggestions
1001
+
1002
+ WORKFLOW HINTS:
1003
+ - Tool descriptions include **Prerequisites:** and **Follow-ups:** for multi-step operations
1004
+ - Check tool category and relationships for semantic grouping`;
601
1005
  function getServerVersion() {
602
1006
  return packageJson.version;
603
1007
  }
@@ -613,9 +1017,13 @@ function createServer() {
613
1017
  capabilities: {
614
1018
  tools: {},
615
1019
  // Enable tools capability (AC-2.1.4)
616
- resources: {}
1020
+ resources: {},
617
1021
  // Enable resources capability (AC-2.1.4)
618
- }
1022
+ logging: {}
1023
+ // Enable logging capability (AC-9.5.1.2)
1024
+ },
1025
+ instructions: SERVER_INSTRUCTIONS
1026
+ // Provide Shopify context to AI agents (AC-9.5.1.1)
619
1027
  }
620
1028
  );
621
1029
  return server;
@@ -1967,86 +2375,23 @@ function createSingleTenantContext(client, shopDomain) {
1967
2375
  };
1968
2376
  }
1969
2377
 
1970
- // src/utils/errors.ts
1971
- var sanitizeErrorMessage = sanitizeLogMessage;
1972
- var ToolError = class _ToolError extends Error {
1973
- /** AI-friendly suggestion for error recovery */
1974
- suggestion;
1975
- /**
1976
- * Create a new ToolError
1977
- *
1978
- * @param message - The error message describing what went wrong
1979
- * @param suggestion - A helpful suggestion for how to resolve the error
1980
- */
1981
- constructor(message, suggestion) {
1982
- super(message);
1983
- this.name = "ToolError";
1984
- this.suggestion = suggestion;
1985
- if (Error.captureStackTrace) {
1986
- Error.captureStackTrace(this, _ToolError);
1987
- }
1988
- }
1989
- };
1990
- function extractErrorMessage(error) {
1991
- if (error instanceof Error) {
1992
- return error.message;
1993
- }
1994
- if (typeof error === "string") {
1995
- return error;
1996
- }
1997
- return "Unknown error";
1998
- }
1999
- function createToolError(error, suggestion) {
2000
- const message = extractErrorMessage(error);
2001
- const safeMessage = sanitizeErrorMessage(message);
2002
- const safeSuggestion = sanitizeErrorMessage(suggestion);
2003
- return {
2004
- isError: true,
2005
- content: [
2006
- {
2007
- type: "text",
2008
- text: `Error: ${safeMessage}
2009
-
2010
- Suggestion: ${safeSuggestion}`
2011
- }
2012
- ]
2013
- };
2014
- }
2015
- function safeStringify2(data) {
2016
- try {
2017
- const seen = /* @__PURE__ */ new WeakSet();
2018
- return JSON.stringify(
2019
- data,
2020
- (_key, value) => {
2021
- if (typeof value === "object" && value !== null) {
2022
- if (seen.has(value)) {
2023
- return "[Circular]";
2024
- }
2025
- seen.add(value);
2026
- }
2027
- return value;
2028
- },
2029
- 2
2030
- );
2031
- } catch {
2032
- return "[Unable to stringify]";
2033
- }
2034
- }
2035
- function createToolSuccess(data) {
2036
- const text = safeStringify2(data);
2378
+ // src/tools/registration.ts
2379
+ var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2380
+ function deriveDefaultAnnotations(name) {
2381
+ const isReadOnly = name.startsWith("get-") || name.startsWith("list-");
2382
+ const isDestructive = name.startsWith("delete-");
2383
+ const isCreate = name.startsWith("create-");
2037
2384
  return {
2038
- content: [
2039
- {
2040
- type: "text",
2041
- text
2042
- }
2043
- ],
2044
- structuredContent: data
2385
+ readOnlyHint: isReadOnly,
2386
+ destructiveHint: isDestructive,
2387
+ // create-* is NOT idempotent (each call creates new resource)
2388
+ // delete-* is NOT idempotent (second call fails - resource gone)
2389
+ // update-*, set-*, add-*, remove-*, reorder-* ARE idempotent
2390
+ idempotentHint: !isCreate && !isDestructive,
2391
+ // All our tools interact with Shopify API
2392
+ openWorldHint: true
2045
2393
  };
2046
2394
  }
2047
-
2048
- // src/tools/registration.ts
2049
- var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2050
2395
  var registeredTools = /* @__PURE__ */ new Map();
2051
2396
  function validateToolName(name) {
2052
2397
  if (!name || name.trim() === "") {
@@ -2106,7 +2451,7 @@ function wrapToolHandler(toolName, schema, handler) {
2106
2451
  };
2107
2452
  }
2108
2453
  function registerTool(definition, handler, options = {}) {
2109
- const { name, title, description, inputSchema: inputSchema41 } = definition;
2454
+ const { name, title, description, inputSchema: inputSchema41, annotations } = definition;
2110
2455
  try {
2111
2456
  if (!options.skipNameValidation) {
2112
2457
  validateToolName(name);
@@ -2116,13 +2461,20 @@ function registerTool(definition, handler, options = {}) {
2116
2461
  }
2117
2462
  const jsonSchema = convertZodToJsonSchema(inputSchema41);
2118
2463
  const wrappedHandler = wrapToolHandler(name, inputSchema41, handler);
2464
+ const finalAnnotations = {
2465
+ ...deriveDefaultAnnotations(name),
2466
+ ...annotations,
2467
+ // Use definition title as fallback for annotation title
2468
+ title: annotations?.title ?? title
2469
+ };
2119
2470
  const registeredTool = {
2120
2471
  name,
2121
2472
  title,
2122
2473
  description,
2123
2474
  inputSchema: jsonSchema,
2124
2475
  zodSchema: inputSchema41,
2125
- handler: wrappedHandler
2476
+ handler: wrappedHandler,
2477
+ annotations: finalAnnotations
2126
2478
  };
2127
2479
  registeredTools.set(name, registeredTool);
2128
2480
  log.debug(`Registered tool: ${name}`);
@@ -2141,7 +2493,8 @@ function getRegisteredTools() {
2141
2493
  return Array.from(registeredTools.values()).map((tool) => ({
2142
2494
  name: tool.name,
2143
2495
  description: `${tool.title}: ${tool.description}`,
2144
- inputSchema: tool.inputSchema
2496
+ inputSchema: tool.inputSchema,
2497
+ annotations: tool.annotations
2145
2498
  }));
2146
2499
  }
2147
2500
  function getToolByName(name) {
@@ -2161,7 +2514,7 @@ function registerContextAwareTool(definition, handler, options = {}) {
2161
2514
  }
2162
2515
 
2163
2516
  // src/tools/add-product-image.ts
2164
- import { z as z2 } from "zod";
2517
+ import { z as z3 } from "zod";
2165
2518
 
2166
2519
  // src/shopify/mutations.ts
2167
2520
  var PRODUCT_CREATE_MUTATION = `
@@ -2801,22 +3154,41 @@ async function addProductImage(productId, input) {
2801
3154
  };
2802
3155
  }
2803
3156
 
3157
+ // src/tools/validators.ts
3158
+ import { z as z2 } from "zod";
3159
+ var GID_PATTERN = /^gid:\/\/shopify\/[A-Za-z]+\/\d+$/;
3160
+ function gidSchema(resourceType) {
3161
+ const pattern = resourceType ? new RegExp(`^gid://shopify/${resourceType}/\\d+$`) : GID_PATTERN;
3162
+ const example = resourceType ? `gid://shopify/${resourceType}/123456789` : "gid://shopify/Product/123456789";
3163
+ return z2.string().regex(pattern, `Must be a valid Shopify GID (e.g., ${example})`).describe(`Shopify Global ID (format: ${example})`);
3164
+ }
3165
+ var productIdSchema = gidSchema("Product");
3166
+ var variantIdSchema = gidSchema("ProductVariant");
3167
+ var collectionIdSchema = gidSchema("Collection");
3168
+ var imageIdSchema = gidSchema("MediaImage");
3169
+ var pageIdSchema = gidSchema("Page");
3170
+ var blogIdSchema = gidSchema("Blog");
3171
+ var articleIdSchema = gidSchema("Article");
3172
+ var redirectIdSchema = gidSchema("UrlRedirect");
3173
+ var inventoryItemIdSchema = gidSchema("InventoryItem");
3174
+ var locationIdSchema = gidSchema("Location");
3175
+
2804
3176
  // src/tools/add-product-image.ts
2805
- var inputSchema = z2.object({
2806
- productId: z2.string().min(1).describe(
3177
+ var inputSchema = z3.object({
3178
+ productId: productIdSchema.describe(
2807
3179
  'Shopify product GID (e.g., "gid://shopify/Product/123"). Required. Use list-products to find product IDs.'
2808
3180
  ),
2809
- url: z2.string().url().describe(
3181
+ url: z3.string().url().describe(
2810
3182
  "Publicly accessible image URL. Shopify will fetch and host the image. Required. Supported formats: JPEG, PNG, GIF, WEBP."
2811
3183
  ),
2812
- altText: z2.string().optional().describe(
3184
+ altText: z3.string().optional().describe(
2813
3185
  "Alt text for accessibility and SEO. Describes the image for screen readers. Recommended for better accessibility and search rankings."
2814
3186
  )
2815
3187
  });
2816
- var outputSchema = z2.object({
2817
- id: z2.string().describe('Image GID (e.g., "gid://shopify/MediaImage/123")'),
2818
- url: z2.string().describe("Shopify CDN URL for the image"),
2819
- altText: z2.string().nullable().describe("Alt text for the image")
3188
+ var outputSchema = z3.object({
3189
+ id: z3.string().describe('Image GID (e.g., "gid://shopify/MediaImage/123")'),
3190
+ url: z3.string().describe("Shopify CDN URL for the image"),
3191
+ altText: z3.string().nullable().describe("Alt text for the image")
2820
3192
  });
2821
3193
  var handleAddProductImage = async (context, params) => {
2822
3194
  log.debug(`Adding image to product on shop: ${context.shopDomain}`);
@@ -2874,6 +3246,14 @@ function registerAddProductImageTool() {
2874
3246
  relatedTools: ["get-product", "update-product-image"],
2875
3247
  prerequisites: ["create-product"],
2876
3248
  followUps: ["update-product-image", "reorder-product-images"]
3249
+ },
3250
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
3251
+ annotations: {
3252
+ readOnlyHint: false,
3253
+ destructiveHint: false,
3254
+ idempotentHint: false,
3255
+ // Adding same URL creates duplicate images!
3256
+ openWorldHint: true
2877
3257
  }
2878
3258
  },
2879
3259
  handleAddProductImage
@@ -2881,7 +3261,7 @@ function registerAddProductImageTool() {
2881
3261
  }
2882
3262
 
2883
3263
  // src/tools/add-products-to-collection.ts
2884
- import { z as z3 } from "zod";
3264
+ import { z as z4 } from "zod";
2885
3265
 
2886
3266
  // src/shopify/collections.ts
2887
3267
  var GET_COLLECTION_QUERY = `
@@ -3282,18 +3662,18 @@ async function removeProductsFromCollection(collectionId, productIds) {
3282
3662
  }
3283
3663
 
3284
3664
  // src/tools/add-products-to-collection.ts
3285
- var inputSchema2 = z3.object({
3286
- collectionId: z3.string().min(1).describe(
3665
+ var inputSchema2 = z4.object({
3666
+ collectionId: collectionIdSchema.describe(
3287
3667
  'Collection GID (e.g., "gid://shopify/Collection/123"). Use list-collections to find valid collection IDs.'
3288
3668
  ),
3289
- productIds: z3.array(z3.string().min(1)).min(1).max(250).describe(
3669
+ productIds: z4.array(productIdSchema).min(1).max(250).describe(
3290
3670
  'Array of product GIDs to add (e.g., ["gid://shopify/Product/123", "gid://shopify/Product/456"]). Maximum 250 products per call. Use list-products to find valid product IDs.'
3291
3671
  )
3292
3672
  });
3293
- var outputSchema2 = z3.object({
3294
- collectionId: z3.string().describe("Collection GID"),
3295
- productsCount: z3.number().describe("Total number of products now in the collection"),
3296
- addedCount: z3.number().describe("Number of products added in this operation")
3673
+ var outputSchema2 = z4.object({
3674
+ collectionId: z4.string().describe("Collection GID"),
3675
+ productsCount: z4.number().describe("Total number of products now in the collection"),
3676
+ addedCount: z4.number().describe("Number of products added in this operation")
3297
3677
  });
3298
3678
  var handleAddProductsToCollection = async (context, params) => {
3299
3679
  log.debug(
@@ -3337,6 +3717,14 @@ function registerAddProductsToCollectionTool() {
3337
3717
  relatedTools: ["remove-products-from-collection", "list-products"],
3338
3718
  prerequisites: ["create-collection", "list-products"],
3339
3719
  followUps: ["get-collection"]
3720
+ },
3721
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
3722
+ annotations: {
3723
+ readOnlyHint: false,
3724
+ destructiveHint: false,
3725
+ idempotentHint: true,
3726
+ // Adding same products is idempotent (no-op)
3727
+ openWorldHint: true
3340
3728
  }
3341
3729
  },
3342
3730
  handleAddProductsToCollection
@@ -3344,7 +3732,7 @@ function registerAddProductsToCollectionTool() {
3344
3732
  }
3345
3733
 
3346
3734
  // src/tools/create-article.ts
3347
- import { z as z4 } from "zod";
3735
+ import { z as z5 } from "zod";
3348
3736
 
3349
3737
  // src/shopify/blogs.ts
3350
3738
  var LIST_BLOGS_QUERY = `
@@ -3850,51 +4238,51 @@ async function deleteArticle(articleId) {
3850
4238
  }
3851
4239
 
3852
4240
  // src/tools/create-article.ts
3853
- var inputSchema3 = z4.object({
3854
- blogId: z4.string().describe(
4241
+ var inputSchema3 = z5.object({
4242
+ blogId: blogIdSchema.describe(
3855
4243
  'The blog ID to create the article in (required). Must be a valid Shopify GID format. Example: "gid://shopify/Blog/12345". Use list-blogs to find blog IDs.'
3856
4244
  ),
3857
- title: z4.string().min(1).describe(
4245
+ title: z5.string().min(1).describe(
3858
4246
  "The title of the article (required). This will be displayed as the article heading. Example: 'How to Use Our Products', '10 Tips for Beginners'."
3859
4247
  ),
3860
- authorName: z4.string().min(1).describe(
4248
+ authorName: z5.string().min(1).describe(
3861
4249
  "The name of the article author (required). This is displayed on the article. Example: 'John Doe', 'Marketing Team'."
3862
4250
  ),
3863
- body: z4.string().optional().describe(
4251
+ body: z5.string().optional().describe(
3864
4252
  "The HTML body content of the article. Supports HTML markup for formatting. Example: '<h2>Introduction</h2><p>Welcome to our guide...</p>'"
3865
4253
  ),
3866
- summary: z4.string().optional().describe(
4254
+ summary: z5.string().optional().describe(
3867
4255
  "A summary or excerpt of the article. Used for previews on blog listing pages. Can include HTML markup."
3868
4256
  ),
3869
- tags: z4.array(z4.string()).optional().describe(
4257
+ tags: z5.array(z5.string()).optional().describe(
3870
4258
  "Tags for categorization. Helps organize articles and improve discoverability. Example: ['guide', 'tutorial', 'beginner']."
3871
4259
  ),
3872
- image: z4.object({
3873
- url: z4.string().url().describe("The URL of the featured image"),
3874
- altText: z4.string().optional().describe("Alt text for accessibility")
4260
+ image: z5.object({
4261
+ url: z5.string().url().describe("The URL of the featured image"),
4262
+ altText: z5.string().optional().describe("Alt text for accessibility")
3875
4263
  }).optional().describe("Featured image for the article."),
3876
- isPublished: z4.boolean().optional().describe(
4264
+ isPublished: z5.boolean().optional().describe(
3877
4265
  "Whether the article should be visible on the storefront. Defaults to false (unpublished). Set to true to make the article immediately visible."
3878
4266
  ),
3879
- publishDate: z4.string().optional().describe(
4267
+ publishDate: z5.string().optional().describe(
3880
4268
  "The date and time when the article should become visible (ISO 8601 format). Example: '2024-01-15T10:00:00Z'. If not set and isPublished is true, publishes immediately."
3881
4269
  ),
3882
- handle: z4.string().optional().describe(
4270
+ handle: z5.string().optional().describe(
3883
4271
  "The URL handle/slug for the article. If not provided, it will be auto-generated from the title. Example: 'how-to-use-our-products' would create URL /blogs/blog-handle/how-to-use-our-products."
3884
4272
  ),
3885
- templateSuffix: z4.string().optional().describe(
4273
+ templateSuffix: z5.string().optional().describe(
3886
4274
  "The suffix of the Liquid template used to render the article. For example, 'featured' would use the template 'article.featured.liquid'."
3887
4275
  )
3888
4276
  });
3889
- var outputSchema3 = z4.object({
3890
- id: z4.string().describe('Created article GID (e.g., "gid://shopify/Article/123")'),
3891
- title: z4.string().describe("Article title"),
3892
- handle: z4.string().describe("URL handle/slug"),
3893
- blog: z4.object({
3894
- id: z4.string().describe("Parent blog GID"),
3895
- title: z4.string().describe("Parent blog title")
4277
+ var outputSchema3 = z5.object({
4278
+ id: z5.string().describe('Created article GID (e.g., "gid://shopify/Article/123")'),
4279
+ title: z5.string().describe("Article title"),
4280
+ handle: z5.string().describe("URL handle/slug"),
4281
+ blog: z5.object({
4282
+ id: z5.string().describe("Parent blog GID"),
4283
+ title: z5.string().describe("Parent blog title")
3896
4284
  }),
3897
- isPublished: z4.boolean().describe("Whether article is visible on storefront")
4285
+ isPublished: z5.boolean().describe("Whether article is visible on storefront")
3898
4286
  });
3899
4287
  var handleCreateArticle = async (context, params) => {
3900
4288
  log.debug(`Creating article on shop: ${context.shopDomain}`);
@@ -3950,6 +4338,14 @@ function registerCreateArticleTool() {
3950
4338
  relatedTools: ["list-blogs", "list-articles", "update-article"],
3951
4339
  prerequisites: ["list-blogs"],
3952
4340
  followUps: ["list-articles", "update-article"]
4341
+ },
4342
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4343
+ annotations: {
4344
+ readOnlyHint: false,
4345
+ destructiveHint: false,
4346
+ idempotentHint: false,
4347
+ // create-* tools are NOT idempotent
4348
+ openWorldHint: true
3953
4349
  }
3954
4350
  },
3955
4351
  handleCreateArticle
@@ -3957,26 +4353,26 @@ function registerCreateArticleTool() {
3957
4353
  }
3958
4354
 
3959
4355
  // src/tools/create-blog.ts
3960
- import { z as z5 } from "zod";
3961
- var inputSchema4 = z5.object({
3962
- title: z5.string().min(1).describe(
4356
+ import { z as z6 } from "zod";
4357
+ var inputSchema4 = z6.object({
4358
+ title: z6.string().min(1).describe(
3963
4359
  "The title of the blog (required). This will be displayed as the blog heading. Example: 'Company News', 'Product Guides', 'Industry Insights'."
3964
4360
  ),
3965
- handle: z5.string().optional().describe(
4361
+ handle: z6.string().optional().describe(
3966
4362
  "The URL handle/slug for the blog. If not provided, it will be auto-generated from the title. Example: 'news' would create URL /blogs/news."
3967
4363
  ),
3968
- commentPolicy: z5.enum(["CLOSED", "MODERATE", "OPEN"]).optional().describe(
4364
+ commentPolicy: z6.enum(["CLOSED", "MODERATE", "OPEN"]).optional().describe(
3969
4365
  "Comment moderation policy for the blog. CLOSED: Comments disabled. MODERATE: Comments require approval. OPEN: Comments appear immediately. Defaults to MODERATE if not specified."
3970
4366
  ),
3971
- templateSuffix: z5.string().optional().describe(
4367
+ templateSuffix: z6.string().optional().describe(
3972
4368
  "The suffix of the Liquid template used to render the blog. For example, 'news' would use the template 'blog.news.liquid'."
3973
4369
  )
3974
4370
  });
3975
- var outputSchema4 = z5.object({
3976
- id: z5.string().describe('Created blog GID (e.g., "gid://shopify/Blog/123")'),
3977
- title: z5.string().describe("Blog title"),
3978
- handle: z5.string().describe("URL handle/slug"),
3979
- commentPolicy: z5.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy")
4371
+ var outputSchema4 = z6.object({
4372
+ id: z6.string().describe('Created blog GID (e.g., "gid://shopify/Blog/123")'),
4373
+ title: z6.string().describe("Blog title"),
4374
+ handle: z6.string().describe("URL handle/slug"),
4375
+ commentPolicy: z6.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy")
3980
4376
  });
3981
4377
  var handleCreateBlog = async (context, params) => {
3982
4378
  log.debug(`Creating blog on shop: ${context.shopDomain}`);
@@ -4018,6 +4414,14 @@ function registerCreateBlogTool() {
4018
4414
  relationships: {
4019
4415
  relatedTools: ["list-blogs", "update-blog", "create-article"],
4020
4416
  followUps: ["create-article", "list-blogs"]
4417
+ },
4418
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4419
+ annotations: {
4420
+ readOnlyHint: false,
4421
+ destructiveHint: false,
4422
+ idempotentHint: false,
4423
+ // create-* tools are NOT idempotent
4424
+ openWorldHint: true
4021
4425
  }
4022
4426
  },
4023
4427
  handleCreateBlog
@@ -4025,20 +4429,20 @@ function registerCreateBlogTool() {
4025
4429
  }
4026
4430
 
4027
4431
  // src/tools/create-collection.ts
4028
- import { z as z6 } from "zod";
4029
- var inputSchema5 = z6.object({
4030
- title: z6.string().min(1).describe("Collection title (required). This will be displayed to customers."),
4031
- handle: z6.string().optional().describe(
4432
+ import { z as z7 } from "zod";
4433
+ var inputSchema5 = z7.object({
4434
+ title: z7.string().min(1).describe("Collection title (required). This will be displayed to customers."),
4435
+ handle: z7.string().optional().describe(
4032
4436
  'URL handle/slug for the collection (e.g., "summer-sale"). Auto-generated from title if not provided.'
4033
4437
  ),
4034
- descriptionHtml: z6.string().optional().describe(
4438
+ descriptionHtml: z7.string().optional().describe(
4035
4439
  "Collection description with HTML formatting. Displayed on collection pages in the store."
4036
4440
  ),
4037
- seo: z6.object({
4038
- title: z6.string().optional().describe("SEO title for search engines"),
4039
- description: z6.string().optional().describe("SEO meta description for search engines")
4441
+ seo: z7.object({
4442
+ title: z7.string().optional().describe("SEO title for search engines"),
4443
+ description: z7.string().optional().describe("SEO meta description for search engines")
4040
4444
  }).optional().describe("SEO metadata to optimize collection visibility in search results"),
4041
- sortOrder: z6.enum([
4445
+ sortOrder: z7.enum([
4042
4446
  "ALPHA_ASC",
4043
4447
  "ALPHA_DESC",
4044
4448
  "BEST_SELLING",
@@ -4050,18 +4454,18 @@ var inputSchema5 = z6.object({
4050
4454
  ]).optional().describe(
4051
4455
  "How products are sorted within the collection. Options: ALPHA_ASC (A-Z), ALPHA_DESC (Z-A), BEST_SELLING, CREATED (oldest first), CREATED_DESC (newest first), MANUAL, PRICE_ASC (low to high), PRICE_DESC (high to low)"
4052
4456
  ),
4053
- image: z6.object({
4054
- src: z6.string().url().describe("Publicly accessible image URL"),
4055
- altText: z6.string().optional().describe("Alt text for accessibility and SEO")
4457
+ image: z7.object({
4458
+ src: z7.string().url().describe("Publicly accessible image URL"),
4459
+ altText: z7.string().optional().describe("Alt text for accessibility and SEO")
4056
4460
  }).optional().describe("Collection image displayed on collection pages"),
4057
- templateSuffix: z6.string().optional().describe(
4461
+ templateSuffix: z7.string().optional().describe(
4058
4462
  'Liquid template suffix for custom collection templates. For example, "featured" uses collection.featured.liquid'
4059
4463
  )
4060
4464
  });
4061
- var outputSchema5 = z6.object({
4062
- id: z6.string().describe('Created collection GID (e.g., "gid://shopify/Collection/123")'),
4063
- title: z6.string().describe("Collection title"),
4064
- handle: z6.string().describe("Collection URL handle")
4465
+ var outputSchema5 = z7.object({
4466
+ id: z7.string().describe('Created collection GID (e.g., "gid://shopify/Collection/123")'),
4467
+ title: z7.string().describe("Collection title"),
4468
+ handle: z7.string().describe("Collection URL handle")
4065
4469
  });
4066
4470
  var handleCreateCollection = async (context, params) => {
4067
4471
  log.debug(`Creating collection on shop: ${context.shopDomain}`);
@@ -4100,6 +4504,14 @@ function registerCreateCollectionTool() {
4100
4504
  relationships: {
4101
4505
  relatedTools: ["list-collections", "get-collection"],
4102
4506
  followUps: ["add-products-to-collection", "update-collection"]
4507
+ },
4508
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4509
+ annotations: {
4510
+ readOnlyHint: false,
4511
+ destructiveHint: false,
4512
+ idempotentHint: false,
4513
+ // Each call creates a new collection
4514
+ openWorldHint: true
4103
4515
  }
4104
4516
  },
4105
4517
  handleCreateCollection
@@ -4107,7 +4519,7 @@ function registerCreateCollectionTool() {
4107
4519
  }
4108
4520
 
4109
4521
  // src/tools/create-page.ts
4110
- import { z as z7 } from "zod";
4522
+ import { z as z8 } from "zod";
4111
4523
 
4112
4524
  // src/shopify/pages.ts
4113
4525
  var PAGE_QUERY = `
@@ -4389,28 +4801,28 @@ async function deletePage(pageId) {
4389
4801
  }
4390
4802
 
4391
4803
  // src/tools/create-page.ts
4392
- var inputSchema6 = z7.object({
4393
- title: z7.string().min(1).describe(
4804
+ var inputSchema6 = z8.object({
4805
+ title: z8.string().min(1).describe(
4394
4806
  "The title of the page (required). This will be displayed as the page heading. Example: 'About Us', 'Contact', 'Privacy Policy'."
4395
4807
  ),
4396
- body: z7.string().optional().describe(
4808
+ body: z8.string().optional().describe(
4397
4809
  "The HTML body content of the page. Supports HTML markup for formatting. Example: '<h1>About Our Company</h1><p>We are a great company.</p>'"
4398
4810
  ),
4399
- handle: z7.string().optional().describe(
4811
+ handle: z8.string().optional().describe(
4400
4812
  "The URL handle/slug for the page. If not provided, it will be auto-generated from the title. Example: 'about-us' would create URL /pages/about-us."
4401
4813
  ),
4402
- isPublished: z7.boolean().optional().describe(
4814
+ isPublished: z8.boolean().optional().describe(
4403
4815
  "Whether the page should be visible on the storefront. Defaults to false (unpublished). Set to true to make the page immediately visible."
4404
4816
  ),
4405
- templateSuffix: z7.string().optional().describe(
4817
+ templateSuffix: z8.string().optional().describe(
4406
4818
  "The suffix of the Liquid template used to render the page. For example, 'contact' would use the template 'page.contact.liquid'."
4407
4819
  )
4408
4820
  });
4409
- var outputSchema6 = z7.object({
4410
- id: z7.string().describe('Created page GID (e.g., "gid://shopify/Page/123")'),
4411
- title: z7.string().describe("Page title"),
4412
- handle: z7.string().describe("URL handle/slug"),
4413
- isPublished: z7.boolean().describe("Whether page is visible on storefront")
4821
+ var outputSchema6 = z8.object({
4822
+ id: z8.string().describe('Created page GID (e.g., "gid://shopify/Page/123")'),
4823
+ title: z8.string().describe("Page title"),
4824
+ handle: z8.string().describe("URL handle/slug"),
4825
+ isPublished: z8.boolean().describe("Whether page is visible on storefront")
4414
4826
  });
4415
4827
  var handleCreatePage = async (context, params) => {
4416
4828
  log.debug(`Creating page on shop: ${context.shopDomain}`);
@@ -4453,6 +4865,14 @@ function registerCreatePageTool() {
4453
4865
  relationships: {
4454
4866
  relatedTools: ["list-pages", "get-page", "update-page"],
4455
4867
  followUps: ["get-page", "update-page"]
4868
+ },
4869
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4870
+ annotations: {
4871
+ readOnlyHint: false,
4872
+ destructiveHint: false,
4873
+ idempotentHint: false,
4874
+ // create-* tools are NOT idempotent
4875
+ openWorldHint: true
4456
4876
  }
4457
4877
  },
4458
4878
  handleCreatePage
@@ -4460,78 +4880,78 @@ function registerCreatePageTool() {
4460
4880
  }
4461
4881
 
4462
4882
  // src/tools/create-product.ts
4463
- import { z as z8 } from "zod";
4464
- var inputSchema7 = z8.object({
4883
+ import { z as z9 } from "zod";
4884
+ var inputSchema7 = z9.object({
4465
4885
  // Required field
4466
- title: z8.string().min(1, "title is required").describe('Product title (required). Example: "Premium Cotton T-Shirt"'),
4886
+ title: z9.string().min(1, "title is required").describe('Product title (required). Example: "Premium Cotton T-Shirt"'),
4467
4887
  // Optional core fields
4468
- description: z8.string().optional().describe('Product description (HTML supported). Example: "<p>Soft cotton t-shirt.</p>"'),
4469
- vendor: z8.string().optional().describe('Product vendor/brand name. Example: "Acme Corp"'),
4470
- productType: z8.string().optional().describe('Product type for categorization. Example: "T-Shirts"'),
4471
- tags: z8.array(z8.string()).optional().describe('Tags for search and filtering. Example: ["summer", "cotton", "casual"]'),
4472
- status: z8.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().default("DRAFT").describe(
4888
+ description: z9.string().optional().describe('Product description (HTML supported). Example: "<p>Soft cotton t-shirt.</p>"'),
4889
+ vendor: z9.string().optional().describe('Product vendor/brand name. Example: "Acme Corp"'),
4890
+ productType: z9.string().optional().describe('Product type for categorization. Example: "T-Shirts"'),
4891
+ tags: z9.array(z9.string()).optional().describe('Tags for search and filtering. Example: ["summer", "cotton", "casual"]'),
4892
+ status: z9.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().default("DRAFT").describe(
4473
4893
  "Product status: ACTIVE (visible), DRAFT (hidden), ARCHIVED (hidden). Default: DRAFT"
4474
4894
  ),
4475
4895
  // SEO metadata
4476
- seo: z8.object({
4477
- title: z8.string().optional().describe("SEO title for search engine results"),
4478
- description: z8.string().optional().describe("SEO meta description for search engines")
4896
+ seo: z9.object({
4897
+ title: z9.string().optional().describe("SEO title for search engine results"),
4898
+ description: z9.string().optional().describe("SEO meta description for search engines")
4479
4899
  }).optional().describe(
4480
4900
  "SEO metadata for search engine optimization. If not provided, Shopify uses product title/description."
4481
4901
  ),
4482
4902
  // Variants
4483
- variants: z8.array(
4484
- z8.object({
4485
- price: z8.string().describe('Variant price as decimal string. Example: "29.99"'),
4486
- compareAtPrice: z8.string().optional().describe('Original price for sale display. Example: "39.99"'),
4487
- sku: z8.string().optional().describe('Stock keeping unit. Example: "SHIRT-001-RED-M"'),
4488
- barcode: z8.string().optional().describe("Barcode (UPC, EAN, etc.)"),
4489
- inventoryQuantity: z8.number().int().min(0).optional().describe("Initial inventory quantity (note: may require separate inventory API)"),
4490
- options: z8.array(z8.string()).optional().describe('Variant options. Example: ["Red", "Medium"]')
4903
+ variants: z9.array(
4904
+ z9.object({
4905
+ price: z9.string().describe('Variant price as decimal string. Example: "29.99"'),
4906
+ compareAtPrice: z9.string().optional().describe('Original price for sale display. Example: "39.99"'),
4907
+ sku: z9.string().optional().describe('Stock keeping unit. Example: "SHIRT-001-RED-M"'),
4908
+ barcode: z9.string().optional().describe("Barcode (UPC, EAN, etc.)"),
4909
+ inventoryQuantity: z9.number().int().min(0).optional().describe("Initial inventory quantity (note: may require separate inventory API)"),
4910
+ options: z9.array(z9.string()).optional().describe('Variant options. Example: ["Red", "Medium"]')
4491
4911
  })
4492
4912
  ).optional().describe("Product variants. If not provided, a default variant is created."),
4493
4913
  // Images
4494
- images: z8.array(
4495
- z8.object({
4496
- url: z8.string().url("Invalid image URL format").describe("Publicly accessible image URL"),
4497
- altText: z8.string().optional().describe("Alt text for accessibility")
4914
+ images: z9.array(
4915
+ z9.object({
4916
+ url: z9.string().url("Invalid image URL format").describe("Publicly accessible image URL"),
4917
+ altText: z9.string().optional().describe("Alt text for accessibility")
4498
4918
  })
4499
4919
  ).optional().describe("Product images. Shopify fetches images from URLs.")
4500
4920
  });
4501
- var outputSchema7 = z8.object({
4502
- id: z8.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
4503
- title: z8.string().describe("Product title"),
4504
- handle: z8.string().describe("URL handle/slug"),
4505
- description: z8.string().nullable().describe("Product description (HTML)"),
4506
- vendor: z8.string().describe("Vendor/brand name"),
4507
- productType: z8.string().describe("Product type"),
4508
- status: z8.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
4509
- tags: z8.array(z8.string()).describe("Product tags"),
4510
- seo: z8.object({
4511
- title: z8.string().nullable().describe("SEO title for search engines"),
4512
- description: z8.string().nullable().describe("SEO description/meta description")
4921
+ var outputSchema7 = z9.object({
4922
+ id: z9.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
4923
+ title: z9.string().describe("Product title"),
4924
+ handle: z9.string().describe("URL handle/slug"),
4925
+ description: z9.string().nullable().describe("Product description (HTML)"),
4926
+ vendor: z9.string().describe("Vendor/brand name"),
4927
+ productType: z9.string().describe("Product type"),
4928
+ status: z9.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
4929
+ tags: z9.array(z9.string()).describe("Product tags"),
4930
+ seo: z9.object({
4931
+ title: z9.string().nullable().describe("SEO title for search engines"),
4932
+ description: z9.string().nullable().describe("SEO description/meta description")
4513
4933
  }).describe("SEO metadata for search engine optimization"),
4514
- createdAt: z8.string().describe("Creation timestamp (ISO 8601)"),
4515
- updatedAt: z8.string().describe("Last update timestamp (ISO 8601)"),
4516
- variants: z8.array(
4517
- z8.object({
4518
- id: z8.string().describe("Variant GID"),
4519
- title: z8.string().describe("Variant title"),
4520
- price: z8.string().describe("Price"),
4521
- compareAtPrice: z8.string().nullable().describe("Compare at price"),
4522
- sku: z8.string().nullable().describe("SKU"),
4523
- barcode: z8.string().nullable().describe("Barcode"),
4524
- inventoryQuantity: z8.number().nullable().describe("Inventory quantity")
4934
+ createdAt: z9.string().describe("Creation timestamp (ISO 8601)"),
4935
+ updatedAt: z9.string().describe("Last update timestamp (ISO 8601)"),
4936
+ variants: z9.array(
4937
+ z9.object({
4938
+ id: z9.string().describe("Variant GID"),
4939
+ title: z9.string().describe("Variant title"),
4940
+ price: z9.string().describe("Price"),
4941
+ compareAtPrice: z9.string().nullable().describe("Compare at price"),
4942
+ sku: z9.string().nullable().describe("SKU"),
4943
+ barcode: z9.string().nullable().describe("Barcode"),
4944
+ inventoryQuantity: z9.number().nullable().describe("Inventory quantity")
4525
4945
  })
4526
4946
  ).describe("Product variants"),
4527
- images: z8.array(
4528
- z8.object({
4529
- id: z8.string().describe("Image GID"),
4530
- url: z8.string().describe("Image URL"),
4531
- altText: z8.string().nullable().describe("Alt text")
4947
+ images: z9.array(
4948
+ z9.object({
4949
+ id: z9.string().describe("Image GID"),
4950
+ url: z9.string().describe("Image URL"),
4951
+ altText: z9.string().nullable().describe("Alt text")
4532
4952
  })
4533
4953
  ).describe("Product images"),
4534
- totalInventory: z8.number().describe("Total inventory across variants")
4954
+ totalInventory: z9.number().describe("Total inventory across variants")
4535
4955
  });
4536
4956
  var handleCreateProduct = async (context, params) => {
4537
4957
  log.debug(`Creating product on shop: ${context.shopDomain}`);
@@ -4574,6 +4994,14 @@ function registerCreateProductTool() {
4574
4994
  relatedTools: ["get-product", "update-product", "list-products"],
4575
4995
  followUps: ["add-product-image", "set-product-metafields", "add-products-to-collection"],
4576
4996
  alternatives: ["update-product"]
4997
+ },
4998
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4999
+ annotations: {
5000
+ readOnlyHint: false,
5001
+ destructiveHint: false,
5002
+ idempotentHint: false,
5003
+ // Each call creates a new product
5004
+ openWorldHint: true
4577
5005
  }
4578
5006
  },
4579
5007
  handleCreateProduct
@@ -4581,7 +5009,7 @@ function registerCreateProductTool() {
4581
5009
  }
4582
5010
 
4583
5011
  // src/tools/create-redirect.ts
4584
- import { z as z9 } from "zod";
5012
+ import { z as z10 } from "zod";
4585
5013
 
4586
5014
  // src/shopify/redirects.ts
4587
5015
  var URL_REDIRECT_CREATE_MUTATION = `
@@ -4729,20 +5157,20 @@ async function deleteRedirect(redirectId) {
4729
5157
  }
4730
5158
 
4731
5159
  // src/tools/create-redirect.ts
4732
- var inputSchema8 = z9.object({
4733
- path: z9.string().min(1).refine((val) => val.startsWith("/"), {
5160
+ var inputSchema8 = z10.object({
5161
+ path: z10.string().min(1).refine((val) => val.startsWith("/"), {
4734
5162
  message: "Path must start with '/' (e.g., '/old-product-url')"
4735
5163
  }).describe(
4736
5164
  "The source path to redirect FROM. Must start with '/'. Example: '/old-product-url' or '/collections/old-collection'."
4737
5165
  ),
4738
- target: z9.string().min(1).describe(
5166
+ target: z10.string().min(1).describe(
4739
5167
  "The destination URL to redirect TO. Can be relative ('/new-product-url') or absolute ('https://example.com/page')."
4740
5168
  )
4741
5169
  });
4742
- var outputSchema8 = z9.object({
4743
- id: z9.string().describe('Created redirect GID (e.g., "gid://shopify/UrlRedirect/123")'),
4744
- path: z9.string().describe("Source path that will redirect"),
4745
- target: z9.string().describe("Target URL visitors will be redirected to")
5170
+ var outputSchema8 = z10.object({
5171
+ id: z10.string().describe('Created redirect GID (e.g., "gid://shopify/UrlRedirect/123")'),
5172
+ path: z10.string().describe("Source path that will redirect"),
5173
+ target: z10.string().describe("Target URL visitors will be redirected to")
4746
5174
  });
4747
5175
  var handleCreateRedirect = async (context, params) => {
4748
5176
  log.debug(`Creating redirect on shop: ${context.shopDomain}`);
@@ -4783,6 +5211,14 @@ function registerCreateRedirectTool() {
4783
5211
  relationships: {
4784
5212
  relatedTools: ["list-redirects", "delete-redirect"],
4785
5213
  followUps: ["list-redirects"]
5214
+ },
5215
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5216
+ annotations: {
5217
+ readOnlyHint: false,
5218
+ destructiveHint: false,
5219
+ idempotentHint: false,
5220
+ // Each call creates a new redirect (duplicate path fails)
5221
+ openWorldHint: true
4786
5222
  }
4787
5223
  },
4788
5224
  handleCreateRedirect
@@ -4790,15 +5226,15 @@ function registerCreateRedirectTool() {
4790
5226
  }
4791
5227
 
4792
5228
  // src/tools/delete-article.ts
4793
- import { z as z10 } from "zod";
4794
- var inputSchema9 = z10.object({
4795
- id: z10.string().describe(
5229
+ import { z as z11 } from "zod";
5230
+ var inputSchema9 = z11.object({
5231
+ id: articleIdSchema.describe(
4796
5232
  'The article ID to delete (required). Must be a valid Shopify GID format. Example: "gid://shopify/Article/12345". Use list-articles to find article IDs. This action cannot be undone - consider using update-article to unpublish instead.'
4797
5233
  )
4798
5234
  });
4799
- var outputSchema9 = z10.object({
4800
- success: z10.boolean().describe("Whether the deletion was successful"),
4801
- deletedArticleId: z10.string().describe("The ID of the deleted article")
5235
+ var outputSchema9 = z11.object({
5236
+ success: z11.boolean().describe("Whether the deletion was successful"),
5237
+ deletedArticleId: z11.string().describe("The ID of the deleted article")
4802
5238
  });
4803
5239
  var handleDeleteArticle = async (context, params) => {
4804
5240
  log.debug(`Deleting article on shop: ${context.shopDomain}`);
@@ -4839,6 +5275,14 @@ function registerDeleteArticleTool() {
4839
5275
  relatedTools: ["list-articles", "update-article"],
4840
5276
  prerequisites: ["list-articles"],
4841
5277
  alternatives: ["update-article"]
5278
+ },
5279
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5280
+ annotations: {
5281
+ readOnlyHint: false,
5282
+ destructiveHint: true,
5283
+ idempotentHint: false,
5284
+ // delete-* tools are NOT idempotent
5285
+ openWorldHint: true
4842
5286
  }
4843
5287
  },
4844
5288
  handleDeleteArticle
@@ -4846,15 +5290,15 @@ function registerDeleteArticleTool() {
4846
5290
  }
4847
5291
 
4848
5292
  // src/tools/delete-blog.ts
4849
- import { z as z11 } from "zod";
4850
- var inputSchema10 = z11.object({
4851
- id: z11.string().describe(
5293
+ import { z as z12 } from "zod";
5294
+ var inputSchema10 = z12.object({
5295
+ id: blogIdSchema.describe(
4852
5296
  'The blog ID to delete (required). Must be a valid Shopify GID format. Example: "gid://shopify/Blog/12345". Use list-blogs to find blog IDs. WARNING: Deleting a blog also deletes ALL articles within it.'
4853
5297
  )
4854
5298
  });
4855
- var outputSchema10 = z11.object({
4856
- success: z11.boolean().describe("Whether the deletion was successful"),
4857
- deletedBlogId: z11.string().describe("The ID of the deleted blog")
5299
+ var outputSchema10 = z12.object({
5300
+ success: z12.boolean().describe("Whether the deletion was successful"),
5301
+ deletedBlogId: z12.string().describe("The ID of the deleted blog")
4858
5302
  });
4859
5303
  var handleDeleteBlog = async (context, params) => {
4860
5304
  log.debug(`Deleting blog on shop: ${context.shopDomain}`);
@@ -4894,6 +5338,14 @@ function registerDeleteBlogTool() {
4894
5338
  relationships: {
4895
5339
  relatedTools: ["list-blogs", "list-articles"],
4896
5340
  prerequisites: ["list-blogs"]
5341
+ },
5342
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5343
+ annotations: {
5344
+ readOnlyHint: false,
5345
+ destructiveHint: true,
5346
+ idempotentHint: false,
5347
+ // delete-* tools are NOT idempotent
5348
+ openWorldHint: true
4897
5349
  }
4898
5350
  },
4899
5351
  handleDeleteBlog
@@ -4901,14 +5353,14 @@ function registerDeleteBlogTool() {
4901
5353
  }
4902
5354
 
4903
5355
  // src/tools/delete-collection.ts
4904
- import { z as z12 } from "zod";
4905
- var inputSchema11 = z12.object({
4906
- collectionId: z12.string().min(1).describe(
5356
+ import { z as z13 } from "zod";
5357
+ var inputSchema11 = z13.object({
5358
+ collectionId: collectionIdSchema.describe(
4907
5359
  'Collection ID to delete (GID format, e.g., "gid://shopify/Collection/123"). Use list-collections or get-collection to find collection IDs.'
4908
5360
  )
4909
5361
  });
4910
- var outputSchema11 = z12.object({
4911
- deletedCollectionId: z12.string().describe("The GID of the deleted collection")
5362
+ var outputSchema11 = z13.object({
5363
+ deletedCollectionId: z13.string().describe("The GID of the deleted collection")
4912
5364
  });
4913
5365
  var handleDeleteCollection = async (context, params) => {
4914
5366
  log.debug(`Deleting collection on shop: ${context.shopDomain}`);
@@ -4945,6 +5397,15 @@ function registerDeleteCollectionTool() {
4945
5397
  relationships: {
4946
5398
  relatedTools: ["list-collections"],
4947
5399
  prerequisites: ["get-collection"]
5400
+ },
5401
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5402
+ annotations: {
5403
+ readOnlyHint: false,
5404
+ destructiveHint: true,
5405
+ // Permanently deletes collection
5406
+ idempotentHint: false,
5407
+ // Second call fails (already deleted)
5408
+ openWorldHint: true
4948
5409
  }
4949
5410
  },
4950
5411
  handleDeleteCollection
@@ -4952,15 +5413,15 @@ function registerDeleteCollectionTool() {
4952
5413
  }
4953
5414
 
4954
5415
  // src/tools/delete-page.ts
4955
- import { z as z13 } from "zod";
4956
- var inputSchema12 = z13.object({
4957
- id: z13.string().min(1).describe(
5416
+ import { z as z14 } from "zod";
5417
+ var inputSchema12 = z14.object({
5418
+ id: pageIdSchema.describe(
4958
5419
  'The page ID to delete (required). Must be a valid Shopify GID format. Example: "gid://shopify/Page/123456789". Use list-pages to find page IDs. WARNING: This action is permanent and cannot be undone.'
4959
5420
  )
4960
5421
  });
4961
- var outputSchema12 = z13.object({
4962
- success: z13.boolean().describe("Whether the deletion was successful"),
4963
- deletedPageId: z13.string().describe("The ID of the deleted page")
5422
+ var outputSchema12 = z14.object({
5423
+ success: z14.boolean().describe("Whether the deletion was successful"),
5424
+ deletedPageId: z14.string().describe("The ID of the deleted page")
4964
5425
  });
4965
5426
  var handleDeletePage = async (context, params) => {
4966
5427
  log.debug(`Deleting page on shop: ${context.shopDomain}`);
@@ -5001,6 +5462,14 @@ function registerDeletePageTool() {
5001
5462
  relatedTools: ["get-page", "list-pages"],
5002
5463
  prerequisites: ["get-page"],
5003
5464
  alternatives: ["update-page"]
5465
+ },
5466
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5467
+ annotations: {
5468
+ readOnlyHint: false,
5469
+ destructiveHint: true,
5470
+ idempotentHint: false,
5471
+ // delete-* tools are NOT idempotent
5472
+ openWorldHint: true
5004
5473
  }
5005
5474
  },
5006
5475
  handleDeletePage
@@ -5008,7 +5477,7 @@ function registerDeletePageTool() {
5008
5477
  }
5009
5478
 
5010
5479
  // src/tools/delete-product-image.ts
5011
- import { z as z14 } from "zod";
5480
+ import { z as z15 } from "zod";
5012
5481
  var FILE_DELETE_MUTATION = `
5013
5482
  mutation FileDelete($fileIds: [ID!]!) {
5014
5483
  fileDelete(fileIds: $fileIds) {
@@ -5020,14 +5489,14 @@ var FILE_DELETE_MUTATION = `
5020
5489
  }
5021
5490
  }
5022
5491
  `;
5023
- var inputSchema13 = z14.object({
5024
- imageId: z14.string().min(1).describe(
5492
+ var inputSchema13 = z15.object({
5493
+ imageId: imageIdSchema.describe(
5025
5494
  'Shopify image GID to delete (e.g., "gid://shopify/MediaImage/123"). Required. Use get-product to find image IDs in the images array.'
5026
5495
  )
5027
5496
  });
5028
- var outputSchema13 = z14.object({
5029
- success: z14.boolean().describe("Whether the deletion was successful"),
5030
- deletedImageId: z14.string().describe("ID of the deleted image")
5497
+ var outputSchema13 = z15.object({
5498
+ success: z15.boolean().describe("Whether the deletion was successful"),
5499
+ deletedImageId: z15.string().describe("ID of the deleted image")
5031
5500
  });
5032
5501
  var handleDeleteProductImage = async (context, params) => {
5033
5502
  log.debug(`Deleting image on shop: ${context.shopDomain}`);
@@ -5085,6 +5554,15 @@ function registerDeleteProductImageTool() {
5085
5554
  relationships: {
5086
5555
  relatedTools: ["get-product", "add-product-image"],
5087
5556
  prerequisites: ["get-product"]
5557
+ },
5558
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5559
+ annotations: {
5560
+ readOnlyHint: false,
5561
+ destructiveHint: true,
5562
+ // Permanently deletes image from Shopify CDN
5563
+ idempotentHint: false,
5564
+ // Second call fails (already deleted)
5565
+ openWorldHint: true
5088
5566
  }
5089
5567
  },
5090
5568
  handleDeleteProductImage
@@ -5092,7 +5570,7 @@ function registerDeleteProductImageTool() {
5092
5570
  }
5093
5571
 
5094
5572
  // src/tools/delete-product-metafields.ts
5095
- import { z as z15 } from "zod";
5573
+ import { z as z16 } from "zod";
5096
5574
 
5097
5575
  // src/shopify/metafields.ts
5098
5576
  var GET_PRODUCT_METAFIELDS_QUERY = `
@@ -5271,28 +5749,28 @@ async function deleteProductMetafields(productId, identifiers) {
5271
5749
  }
5272
5750
 
5273
5751
  // src/tools/delete-product-metafields.ts
5274
- var metafieldIdentifierSchema = z15.object({
5275
- namespace: z15.string().min(1).describe('Metafield namespace (e.g., "custom", "seo"). Must match existing metafield.'),
5276
- key: z15.string().min(1).describe('Metafield key (e.g., "color_hex"). Must match existing metafield.')
5752
+ var metafieldIdentifierSchema = z16.object({
5753
+ namespace: z16.string().min(1).describe('Metafield namespace (e.g., "custom", "seo"). Must match existing metafield.'),
5754
+ key: z16.string().min(1).describe('Metafield key (e.g., "color_hex"). Must match existing metafield.')
5277
5755
  });
5278
- var inputSchema14 = z15.object({
5279
- productId: z15.string().min(1).describe(
5756
+ var inputSchema14 = z16.object({
5757
+ productId: productIdSchema.describe(
5280
5758
  'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Use get-product or list-products to find product IDs.'
5281
5759
  ),
5282
- identifiers: z15.array(metafieldIdentifierSchema).min(1, "At least one metafield identifier is required").describe(
5760
+ identifiers: z16.array(metafieldIdentifierSchema).min(1, "At least one metafield identifier is required").describe(
5283
5761
  "Array of metafield identifiers to delete. Each identifier must have namespace and key. Use get-product-metafields first to verify which metafields exist."
5284
5762
  )
5285
5763
  });
5286
- var outputSchema14 = z15.object({
5287
- success: z15.boolean().describe("Whether the operation succeeded"),
5288
- deletedMetafields: z15.array(
5289
- z15.object({
5290
- ownerId: z15.string().describe("Product GID that owned the metafield"),
5291
- namespace: z15.string().describe("Namespace of deleted metafield"),
5292
- key: z15.string().describe("Key of deleted metafield")
5764
+ var outputSchema14 = z16.object({
5765
+ success: z16.boolean().describe("Whether the operation succeeded"),
5766
+ deletedMetafields: z16.array(
5767
+ z16.object({
5768
+ ownerId: z16.string().describe("Product GID that owned the metafield"),
5769
+ namespace: z16.string().describe("Namespace of deleted metafield"),
5770
+ key: z16.string().describe("Key of deleted metafield")
5293
5771
  })
5294
5772
  ).describe("Identifiers of successfully deleted metafields"),
5295
- deletedCount: z15.number().describe("Number of metafields deleted")
5773
+ deletedCount: z16.number().describe("Number of metafields deleted")
5296
5774
  });
5297
5775
  var handleDeleteProductMetafields = async (context, params) => {
5298
5776
  log.debug(
@@ -5329,6 +5807,15 @@ function registerDeleteProductMetafieldsTool() {
5329
5807
  relationships: {
5330
5808
  relatedTools: ["get-product-metafields"],
5331
5809
  prerequisites: ["get-product-metafields"]
5810
+ },
5811
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5812
+ annotations: {
5813
+ readOnlyHint: false,
5814
+ destructiveHint: true,
5815
+ // Partial data deletion - cannot be recovered
5816
+ idempotentHint: false,
5817
+ // Second call fails (already deleted)
5818
+ openWorldHint: true
5332
5819
  }
5333
5820
  },
5334
5821
  handleDeleteProductMetafields
@@ -5336,13 +5823,15 @@ function registerDeleteProductMetafieldsTool() {
5336
5823
  }
5337
5824
 
5338
5825
  // src/tools/delete-product.ts
5339
- import { z as z16 } from "zod";
5340
- var inputSchema15 = z16.object({
5341
- id: z16.string().min(1).describe('Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Required.')
5826
+ import { z as z17 } from "zod";
5827
+ var inputSchema15 = z17.object({
5828
+ id: productIdSchema.describe(
5829
+ 'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Required.'
5830
+ )
5342
5831
  });
5343
- var outputSchema15 = z16.object({
5344
- deletedProductId: z16.string().describe("The ID of the deleted product"),
5345
- title: z16.string().describe("The title of the deleted product (for confirmation)")
5832
+ var outputSchema15 = z17.object({
5833
+ deletedProductId: z17.string().describe("The ID of the deleted product"),
5834
+ title: z17.string().describe("The title of the deleted product (for confirmation)")
5346
5835
  });
5347
5836
  var handleDeleteProduct = async (context, params) => {
5348
5837
  log.debug(`Deleting product on shop: ${context.shopDomain}`);
@@ -5391,6 +5880,15 @@ function registerDeleteProductTool() {
5391
5880
  relationships: {
5392
5881
  relatedTools: ["list-products"],
5393
5882
  prerequisites: ["get-product"]
5883
+ },
5884
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5885
+ annotations: {
5886
+ readOnlyHint: false,
5887
+ destructiveHint: true,
5888
+ // Permanently deletes product
5889
+ idempotentHint: false,
5890
+ // Second call fails (already deleted)
5891
+ openWorldHint: true
5394
5892
  }
5395
5893
  },
5396
5894
  handleDeleteProduct
@@ -5398,15 +5896,15 @@ function registerDeleteProductTool() {
5398
5896
  }
5399
5897
 
5400
5898
  // src/tools/delete-redirect.ts
5401
- import { z as z17 } from "zod";
5402
- var inputSchema16 = z17.object({
5403
- id: z17.string().min(1).describe(
5899
+ import { z as z18 } from "zod";
5900
+ var inputSchema16 = z18.object({
5901
+ id: redirectIdSchema.describe(
5404
5902
  'Shopify redirect GID (e.g., "gid://shopify/UrlRedirect/123"). Use list-redirects to find redirect IDs.'
5405
5903
  )
5406
5904
  });
5407
- var outputSchema16 = z17.object({
5408
- success: z17.boolean().describe("Whether the deletion succeeded"),
5409
- deletedRedirectId: z17.string().describe("ID of the deleted redirect")
5905
+ var outputSchema16 = z18.object({
5906
+ success: z18.boolean().describe("Whether the deletion succeeded"),
5907
+ deletedRedirectId: z18.string().describe("ID of the deleted redirect")
5410
5908
  });
5411
5909
  var handleDeleteRedirect = async (context, params) => {
5412
5910
  log.debug(`Deleting redirect on shop: ${context.shopDomain}`);
@@ -5455,6 +5953,15 @@ function registerDeleteRedirectTool() {
5455
5953
  relationships: {
5456
5954
  relatedTools: ["create-redirect"],
5457
5955
  prerequisites: ["list-redirects"]
5956
+ },
5957
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5958
+ annotations: {
5959
+ readOnlyHint: false,
5960
+ destructiveHint: true,
5961
+ // Permanently removes redirect - can hurt SEO
5962
+ idempotentHint: false,
5963
+ // Second call fails (already deleted)
5964
+ openWorldHint: true
5458
5965
  }
5459
5966
  },
5460
5967
  handleDeleteRedirect
@@ -5462,7 +5969,7 @@ function registerDeleteRedirectTool() {
5462
5969
  }
5463
5970
 
5464
5971
  // src/tools/get-bulk-inventory.ts
5465
- import { z as z18 } from "zod";
5972
+ import { z as z19 } from "zod";
5466
5973
 
5467
5974
  // src/shopify/inventory.ts
5468
5975
  var GET_INVENTORY_ITEM_QUERY = `
@@ -6033,46 +6540,46 @@ async function getBulkInventory(options) {
6033
6540
  }
6034
6541
 
6035
6542
  // src/tools/get-bulk-inventory.ts
6036
- var inputSchema17 = z18.object({
6037
- productIds: z18.array(z18.string()).min(1, { message: "productIds array cannot be empty" }).max(50, { message: "Maximum 50 product IDs allowed per request" }).describe(
6543
+ var inputSchema17 = z19.object({
6544
+ productIds: z19.array(productIdSchema).min(1, { message: "productIds array cannot be empty" }).max(50, { message: "Maximum 50 product IDs allowed per request" }).describe(
6038
6545
  'Array of Shopify product IDs (e.g., ["gid://shopify/Product/123", "gid://shopify/Product/456"]). Maximum 50 products per request. Get product IDs from list-products or search.'
6039
6546
  ),
6040
- includeVariants: z18.boolean().default(true).describe(
6547
+ includeVariants: z19.boolean().default(true).describe(
6041
6548
  "Include per-variant inventory breakdown. Default: true. Set to false for faster response with only product totals."
6042
6549
  ),
6043
- locationId: z18.string().optional().describe(
6550
+ locationId: locationIdSchema.optional().describe(
6044
6551
  'Optional location ID to filter inventory (e.g., "gid://shopify/Location/789"). If not provided, returns total inventory across all locations.'
6045
6552
  )
6046
6553
  });
6047
- var outputSchema17 = z18.object({
6048
- products: z18.array(
6049
- z18.object({
6050
- productId: z18.string().describe("Product GID"),
6051
- productTitle: z18.string().describe("Product title"),
6052
- totalInventory: z18.number().describe("Total inventory across all variants"),
6053
- variants: z18.array(
6054
- z18.object({
6055
- variantId: z18.string().describe("Variant GID"),
6056
- variantTitle: z18.string().describe('Variant title (e.g., "Red / Large")'),
6057
- sku: z18.string().nullable().describe("SKU (Stock Keeping Unit)"),
6058
- inventoryItemId: z18.string().describe("Inventory item GID"),
6059
- available: z18.number().describe("Available quantity"),
6060
- locations: z18.array(
6061
- z18.object({
6062
- locationId: z18.string().describe("Location GID"),
6063
- locationName: z18.string().describe("Human-readable location name"),
6064
- available: z18.number().describe("Available quantity at this location")
6554
+ var outputSchema17 = z19.object({
6555
+ products: z19.array(
6556
+ z19.object({
6557
+ productId: z19.string().describe("Product GID"),
6558
+ productTitle: z19.string().describe("Product title"),
6559
+ totalInventory: z19.number().describe("Total inventory across all variants"),
6560
+ variants: z19.array(
6561
+ z19.object({
6562
+ variantId: z19.string().describe("Variant GID"),
6563
+ variantTitle: z19.string().describe('Variant title (e.g., "Red / Large")'),
6564
+ sku: z19.string().nullable().describe("SKU (Stock Keeping Unit)"),
6565
+ inventoryItemId: z19.string().describe("Inventory item GID"),
6566
+ available: z19.number().describe("Available quantity"),
6567
+ locations: z19.array(
6568
+ z19.object({
6569
+ locationId: z19.string().describe("Location GID"),
6570
+ locationName: z19.string().describe("Human-readable location name"),
6571
+ available: z19.number().describe("Available quantity at this location")
6065
6572
  })
6066
6573
  ).optional().describe("Per-location inventory breakdown")
6067
6574
  })
6068
6575
  ).optional().describe("Variant details (if includeVariants=true)")
6069
6576
  })
6070
6577
  ).describe("Products with inventory data"),
6071
- notFound: z18.array(z18.string()).describe("Product IDs that were not found in the store"),
6072
- summary: z18.object({
6073
- productsQueried: z18.number().describe("Total number of product IDs provided"),
6074
- productsFound: z18.number().describe("Number of products found in the store"),
6075
- totalInventory: z18.number().describe("Sum of inventory across all found products")
6578
+ notFound: z19.array(z19.string()).describe("Product IDs that were not found in the store"),
6579
+ summary: z19.object({
6580
+ productsQueried: z19.number().describe("Total number of product IDs provided"),
6581
+ productsFound: z19.number().describe("Number of products found in the store"),
6582
+ totalInventory: z19.number().describe("Sum of inventory across all found products")
6076
6583
  }).describe("Summary statistics")
6077
6584
  });
6078
6585
  var handleGetBulkInventory = async (context, params) => {
@@ -6112,6 +6619,13 @@ function registerGetBulkInventoryTool() {
6112
6619
  prerequisites: ["list-products"],
6113
6620
  followUps: ["update-inventory", "get-inventory"],
6114
6621
  alternatives: ["get-inventory"]
6622
+ },
6623
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
6624
+ annotations: {
6625
+ readOnlyHint: true,
6626
+ destructiveHint: false,
6627
+ idempotentHint: true,
6628
+ openWorldHint: true
6115
6629
  }
6116
6630
  },
6117
6631
  handleGetBulkInventory
@@ -6119,30 +6633,30 @@ function registerGetBulkInventoryTool() {
6119
6633
  }
6120
6634
 
6121
6635
  // src/tools/get-collection.ts
6122
- import { z as z19 } from "zod";
6123
- var inputSchema18 = z19.object({
6124
- collectionId: z19.string().min(1).describe(
6636
+ import { z as z20 } from "zod";
6637
+ var inputSchema18 = z20.object({
6638
+ collectionId: collectionIdSchema.describe(
6125
6639
  'Collection ID (GID format, e.g., "gid://shopify/Collection/123"). Use list-collections to find collection IDs.'
6126
6640
  )
6127
6641
  });
6128
- var outputSchema18 = z19.object({
6129
- id: z19.string().describe("Collection GID"),
6130
- title: z19.string().describe("Collection title"),
6131
- handle: z19.string().describe("URL handle/slug"),
6132
- description: z19.string().describe("Plain text description"),
6133
- descriptionHtml: z19.string().describe("HTML description"),
6134
- seo: z19.object({
6135
- title: z19.string().nullable().describe("SEO title"),
6136
- description: z19.string().nullable().describe("SEO description")
6642
+ var outputSchema18 = z20.object({
6643
+ id: z20.string().describe("Collection GID"),
6644
+ title: z20.string().describe("Collection title"),
6645
+ handle: z20.string().describe("URL handle/slug"),
6646
+ description: z20.string().describe("Plain text description"),
6647
+ descriptionHtml: z20.string().describe("HTML description"),
6648
+ seo: z20.object({
6649
+ title: z20.string().nullable().describe("SEO title"),
6650
+ description: z20.string().nullable().describe("SEO description")
6137
6651
  }),
6138
- image: z19.object({
6139
- id: z19.string().describe("Image GID"),
6140
- url: z19.string().describe("Image URL"),
6141
- altText: z19.string().nullable().describe("Alt text")
6652
+ image: z20.object({
6653
+ id: z20.string().describe("Image GID"),
6654
+ url: z20.string().describe("Image URL"),
6655
+ altText: z20.string().nullable().describe("Alt text")
6142
6656
  }).nullable(),
6143
- sortOrder: z19.string().describe("Product sort order within collection"),
6144
- productsCount: z19.number().describe("Number of products in collection"),
6145
- updatedAt: z19.string().describe("Last update timestamp (ISO 8601)")
6657
+ sortOrder: z20.string().describe("Product sort order within collection"),
6658
+ productsCount: z20.number().describe("Number of products in collection"),
6659
+ updatedAt: z20.string().describe("Last update timestamp (ISO 8601)")
6146
6660
  });
6147
6661
  var handleGetCollection = async (context, params) => {
6148
6662
  log.debug(`Getting collection on shop: ${context.shopDomain}`);
@@ -6169,6 +6683,13 @@ function registerGetCollectionTool() {
6169
6683
  relationships: {
6170
6684
  relatedTools: ["list-collections"],
6171
6685
  followUps: ["update-collection", "delete-collection", "add-products-to-collection"]
6686
+ },
6687
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
6688
+ annotations: {
6689
+ readOnlyHint: true,
6690
+ destructiveHint: false,
6691
+ idempotentHint: true,
6692
+ openWorldHint: true
6172
6693
  }
6173
6694
  },
6174
6695
  handleGetCollection
@@ -6176,37 +6697,37 @@ function registerGetCollectionTool() {
6176
6697
  }
6177
6698
 
6178
6699
  // src/tools/get-inventory.ts
6179
- import { z as z20 } from "zod";
6180
- var inputSchema19 = z20.object({
6181
- variantId: z20.string().optional().describe(
6700
+ import { z as z21 } from "zod";
6701
+ var inputSchema19 = z21.object({
6702
+ variantId: variantIdSchema.optional().describe(
6182
6703
  'The Shopify product variant ID (e.g., "gid://shopify/ProductVariant/123"). Use this when you have a variant ID from a product query (get-product or list-products).'
6183
6704
  ),
6184
- inventoryItemId: z20.string().optional().describe(
6705
+ inventoryItemId: inventoryItemIdSchema.optional().describe(
6185
6706
  'The Shopify inventory item ID (e.g., "gid://shopify/InventoryItem/456"). Use this if you already have the inventory item ID from a previous query.'
6186
6707
  ),
6187
- locationId: z20.string().optional().describe(
6708
+ locationId: locationIdSchema.optional().describe(
6188
6709
  'Optional location ID to filter results (e.g., "gid://shopify/Location/789"). If not provided, returns inventory at all locations.'
6189
6710
  )
6190
6711
  }).refine((data) => data.variantId || data.inventoryItemId, {
6191
6712
  message: "Either variantId or inventoryItemId must be provided"
6192
6713
  });
6193
- var outputSchema19 = z20.object({
6194
- inventoryItemId: z20.string().describe("Inventory item GID"),
6195
- variantId: z20.string().describe("Product variant GID"),
6196
- variantTitle: z20.string().describe('Variant title (e.g., "Red / Large")'),
6197
- productId: z20.string().describe("Product GID"),
6198
- productTitle: z20.string().describe("Product title"),
6199
- sku: z20.string().nullable().describe("SKU (Stock Keeping Unit)"),
6200
- tracked: z20.boolean().describe("Whether inventory tracking is enabled for this item"),
6201
- totalAvailable: z20.number().describe("Total available quantity across all locations"),
6202
- locations: z20.array(
6203
- z20.object({
6204
- locationId: z20.string().describe("Location GID"),
6205
- locationName: z20.string().describe("Human-readable location name"),
6206
- isActive: z20.boolean().describe("Whether the location is active"),
6207
- available: z20.number().describe("Quantity available for sale"),
6208
- onHand: z20.number().describe("Physical quantity on hand"),
6209
- committed: z20.number().describe("Quantity committed to orders")
6714
+ var outputSchema19 = z21.object({
6715
+ inventoryItemId: z21.string().describe("Inventory item GID"),
6716
+ variantId: z21.string().describe("Product variant GID"),
6717
+ variantTitle: z21.string().describe('Variant title (e.g., "Red / Large")'),
6718
+ productId: z21.string().describe("Product GID"),
6719
+ productTitle: z21.string().describe("Product title"),
6720
+ sku: z21.string().nullable().describe("SKU (Stock Keeping Unit)"),
6721
+ tracked: z21.boolean().describe("Whether inventory tracking is enabled for this item"),
6722
+ totalAvailable: z21.number().describe("Total available quantity across all locations"),
6723
+ locations: z21.array(
6724
+ z21.object({
6725
+ locationId: z21.string().describe("Location GID"),
6726
+ locationName: z21.string().describe("Human-readable location name"),
6727
+ isActive: z21.boolean().describe("Whether the location is active"),
6728
+ available: z21.number().describe("Quantity available for sale"),
6729
+ onHand: z21.number().describe("Physical quantity on hand"),
6730
+ committed: z21.number().describe("Quantity committed to orders")
6210
6731
  })
6211
6732
  ).describe("Inventory levels by location")
6212
6733
  });
@@ -6280,6 +6801,13 @@ function registerGetInventoryTool() {
6280
6801
  ],
6281
6802
  prerequisites: ["get-product", "list-products"],
6282
6803
  followUps: ["update-inventory"]
6804
+ },
6805
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
6806
+ annotations: {
6807
+ readOnlyHint: true,
6808
+ destructiveHint: false,
6809
+ idempotentHint: true,
6810
+ openWorldHint: true
6283
6811
  }
6284
6812
  },
6285
6813
  handleGetInventory
@@ -6287,23 +6815,23 @@ function registerGetInventoryTool() {
6287
6815
  }
6288
6816
 
6289
6817
  // src/tools/get-page.ts
6290
- import { z as z21 } from "zod";
6291
- var inputSchema20 = z21.object({
6292
- id: z21.string().min(1).describe(
6818
+ import { z as z22 } from "zod";
6819
+ var inputSchema20 = z22.object({
6820
+ id: pageIdSchema.describe(
6293
6821
  'The page ID to retrieve. Must be a valid Shopify GID format. Example: "gid://shopify/Page/123456789". Use list-pages to find page IDs.'
6294
6822
  )
6295
6823
  });
6296
- var outputSchema20 = z21.object({
6297
- id: z21.string().describe("Page GID"),
6298
- title: z21.string().describe("Page title"),
6299
- handle: z21.string().describe("URL handle/slug"),
6300
- body: z21.string().describe("HTML body content"),
6301
- bodySummary: z21.string().describe("Plain text summary (first 150 chars)"),
6302
- isPublished: z21.boolean().describe("Whether page is visible on storefront"),
6303
- publishedAt: z21.string().nullable().describe("ISO timestamp when published"),
6304
- templateSuffix: z21.string().nullable().describe("Custom template suffix"),
6305
- createdAt: z21.string().describe("ISO timestamp of creation"),
6306
- updatedAt: z21.string().describe("ISO timestamp of last update")
6824
+ var outputSchema20 = z22.object({
6825
+ id: z22.string().describe("Page GID"),
6826
+ title: z22.string().describe("Page title"),
6827
+ handle: z22.string().describe("URL handle/slug"),
6828
+ body: z22.string().describe("HTML body content"),
6829
+ bodySummary: z22.string().describe("Plain text summary (first 150 chars)"),
6830
+ isPublished: z22.boolean().describe("Whether page is visible on storefront"),
6831
+ publishedAt: z22.string().nullable().describe("ISO timestamp when published"),
6832
+ templateSuffix: z22.string().nullable().describe("Custom template suffix"),
6833
+ createdAt: z22.string().describe("ISO timestamp of creation"),
6834
+ updatedAt: z22.string().describe("ISO timestamp of last update")
6307
6835
  });
6308
6836
  var handleGetPage = async (context, params) => {
6309
6837
  log.debug(`Getting page on shop: ${context.shopDomain}`);
@@ -6330,6 +6858,13 @@ function registerGetPageTool() {
6330
6858
  relatedTools: ["list-pages", "update-page", "delete-page"],
6331
6859
  prerequisites: ["list-pages"],
6332
6860
  followUps: ["update-page", "delete-page"]
6861
+ },
6862
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
6863
+ annotations: {
6864
+ readOnlyHint: true,
6865
+ destructiveHint: false,
6866
+ idempotentHint: true,
6867
+ openWorldHint: true
6333
6868
  }
6334
6869
  },
6335
6870
  handleGetPage
@@ -6337,27 +6872,27 @@ function registerGetPageTool() {
6337
6872
  }
6338
6873
 
6339
6874
  // src/tools/get-product-metafields.ts
6340
- import { z as z22 } from "zod";
6341
- var inputSchema21 = z22.object({
6342
- productId: z22.string().min(1).describe(
6875
+ import { z as z23 } from "zod";
6876
+ var inputSchema21 = z23.object({
6877
+ productId: productIdSchema.describe(
6343
6878
  'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Use get-product or list-products to find product IDs.'
6344
6879
  ),
6345
- namespace: z22.string().optional().describe(
6880
+ namespace: z23.string().optional().describe(
6346
6881
  'Filter metafields by namespace (e.g., "custom", "seo", "my_app"). If not provided, returns metafields from all namespaces.'
6347
6882
  ),
6348
- keys: z22.array(z22.string()).optional().describe(
6883
+ keys: z23.array(z23.string()).optional().describe(
6349
6884
  'Filter by specific metafield keys within the namespace (e.g., ["color", "size"]). Requires namespace to be specified for meaningful filtering.'
6350
6885
  )
6351
6886
  });
6352
- var outputSchema21 = z22.array(
6353
- z22.object({
6354
- id: z22.string().describe('Metafield GID (e.g., "gid://shopify/Metafield/123")'),
6355
- namespace: z22.string().describe("Metafield namespace (grouping identifier)"),
6356
- key: z22.string().describe("Metafield key (field name)"),
6357
- value: z22.string().describe("Metafield value (the stored data)"),
6358
- type: z22.string().describe('Metafield type (e.g., "single_line_text_field", "json", "number_integer")'),
6359
- createdAt: z22.string().describe("Creation timestamp (ISO 8601)"),
6360
- updatedAt: z22.string().describe("Last update timestamp (ISO 8601)")
6887
+ var outputSchema21 = z23.array(
6888
+ z23.object({
6889
+ id: z23.string().describe('Metafield GID (e.g., "gid://shopify/Metafield/123")'),
6890
+ namespace: z23.string().describe("Metafield namespace (grouping identifier)"),
6891
+ key: z23.string().describe("Metafield key (field name)"),
6892
+ value: z23.string().describe("Metafield value (the stored data)"),
6893
+ type: z23.string().describe('Metafield type (e.g., "single_line_text_field", "json", "number_integer")'),
6894
+ createdAt: z23.string().describe("Creation timestamp (ISO 8601)"),
6895
+ updatedAt: z23.string().describe("Last update timestamp (ISO 8601)")
6361
6896
  })
6362
6897
  );
6363
6898
  var handleGetProductMetafields = async (context, params) => {
@@ -6390,6 +6925,13 @@ function registerGetProductMetafieldsTool() {
6390
6925
  relatedTools: ["get-product", "set-product-metafields"],
6391
6926
  prerequisites: ["get-product"],
6392
6927
  followUps: ["set-product-metafields", "delete-product-metafields"]
6928
+ },
6929
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
6930
+ annotations: {
6931
+ readOnlyHint: true,
6932
+ destructiveHint: false,
6933
+ idempotentHint: true,
6934
+ openWorldHint: true
6393
6935
  }
6394
6936
  },
6395
6937
  handleGetProductMetafields
@@ -6397,51 +6939,51 @@ function registerGetProductMetafieldsTool() {
6397
6939
  }
6398
6940
 
6399
6941
  // src/tools/get-product.ts
6400
- import { z as z23 } from "zod";
6401
- var inputSchema22 = z23.object({
6402
- id: z23.string().optional().describe(
6942
+ import { z as z24 } from "zod";
6943
+ var inputSchema22 = z24.object({
6944
+ id: productIdSchema.optional().describe(
6403
6945
  'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Use either id or handle.'
6404
6946
  ),
6405
- handle: z23.string().optional().describe(
6947
+ handle: z24.string().optional().describe(
6406
6948
  'Product URL handle/slug (e.g., "premium-cotton-t-shirt"). Use either id or handle.'
6407
6949
  )
6408
6950
  }).refine((data) => data.id || data.handle, {
6409
6951
  message: "Either id or handle must be provided"
6410
6952
  });
6411
- var outputSchema22 = z23.object({
6412
- id: z23.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
6413
- title: z23.string().describe("Product title"),
6414
- handle: z23.string().describe("URL handle/slug"),
6415
- description: z23.string().nullable().describe("Product description (HTML)"),
6416
- vendor: z23.string().describe("Vendor/brand name"),
6417
- productType: z23.string().describe("Product type"),
6418
- status: z23.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
6419
- tags: z23.array(z23.string()).describe("Product tags"),
6420
- seo: z23.object({
6421
- title: z23.string().nullable().describe("SEO title for search engines"),
6422
- description: z23.string().nullable().describe("SEO description/meta description")
6953
+ var outputSchema22 = z24.object({
6954
+ id: z24.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
6955
+ title: z24.string().describe("Product title"),
6956
+ handle: z24.string().describe("URL handle/slug"),
6957
+ description: z24.string().nullable().describe("Product description (HTML)"),
6958
+ vendor: z24.string().describe("Vendor/brand name"),
6959
+ productType: z24.string().describe("Product type"),
6960
+ status: z24.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
6961
+ tags: z24.array(z24.string()).describe("Product tags"),
6962
+ seo: z24.object({
6963
+ title: z24.string().nullable().describe("SEO title for search engines"),
6964
+ description: z24.string().nullable().describe("SEO description/meta description")
6423
6965
  }).describe("SEO metadata for search engine optimization"),
6424
- createdAt: z23.string().describe("Creation timestamp (ISO 8601)"),
6425
- updatedAt: z23.string().describe("Last update timestamp (ISO 8601)"),
6426
- variants: z23.array(
6427
- z23.object({
6428
- id: z23.string().describe("Variant GID"),
6429
- title: z23.string().describe("Variant title"),
6430
- price: z23.string().describe("Price"),
6431
- compareAtPrice: z23.string().nullable().describe("Compare at price"),
6432
- sku: z23.string().nullable().describe("SKU"),
6433
- barcode: z23.string().nullable().describe("Barcode"),
6434
- inventoryQuantity: z23.number().nullable().describe("Inventory quantity")
6966
+ createdAt: z24.string().describe("Creation timestamp (ISO 8601)"),
6967
+ updatedAt: z24.string().describe("Last update timestamp (ISO 8601)"),
6968
+ variants: z24.array(
6969
+ z24.object({
6970
+ id: z24.string().describe("Variant GID"),
6971
+ title: z24.string().describe("Variant title"),
6972
+ price: z24.string().describe("Price"),
6973
+ compareAtPrice: z24.string().nullable().describe("Compare at price"),
6974
+ sku: z24.string().nullable().describe("SKU"),
6975
+ barcode: z24.string().nullable().describe("Barcode"),
6976
+ inventoryQuantity: z24.number().nullable().describe("Inventory quantity")
6435
6977
  })
6436
6978
  ).describe("Product variants"),
6437
- images: z23.array(
6438
- z23.object({
6439
- id: z23.string().describe("Image GID"),
6440
- url: z23.string().describe("Image URL"),
6441
- altText: z23.string().nullable().describe("Alt text")
6979
+ images: z24.array(
6980
+ z24.object({
6981
+ id: z24.string().describe("Image GID"),
6982
+ url: z24.string().describe("Image URL"),
6983
+ altText: z24.string().nullable().describe("Alt text")
6442
6984
  })
6443
6985
  ).describe("Product images"),
6444
- totalInventory: z23.number().describe("Total inventory across variants")
6986
+ totalInventory: z24.number().describe("Total inventory across variants")
6445
6987
  });
6446
6988
  var handleGetProduct = async (context, params) => {
6447
6989
  log.debug(`Getting product on shop: ${context.shopDomain}`);
@@ -6492,6 +7034,13 @@ function registerGetProductTool() {
6492
7034
  "add-product-image",
6493
7035
  "get-product-metafields"
6494
7036
  ]
7037
+ },
7038
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7039
+ annotations: {
7040
+ readOnlyHint: true,
7041
+ destructiveHint: false,
7042
+ idempotentHint: true,
7043
+ openWorldHint: true
6495
7044
  }
6496
7045
  },
6497
7046
  handleGetProduct
@@ -6499,36 +7048,36 @@ function registerGetProductTool() {
6499
7048
  }
6500
7049
 
6501
7050
  // src/tools/list-articles.ts
6502
- import { z as z24 } from "zod";
6503
- var inputSchema23 = z24.object({
6504
- first: z24.number().int().min(1).max(50).optional().default(10).describe("Number of articles to return (1-50). Defaults to 10."),
6505
- cursor: z24.string().optional().describe(
7051
+ import { z as z25 } from "zod";
7052
+ var inputSchema23 = z25.object({
7053
+ first: z25.number().int().min(1).max(50).optional().default(10).describe("Number of articles to return (1-50). Defaults to 10."),
7054
+ cursor: z25.string().optional().describe(
6506
7055
  "Pagination cursor from a previous response. Use the endCursor from the previous response to get the next page of results."
6507
7056
  ),
6508
- query: z24.string().optional().describe(
7057
+ query: z25.string().optional().describe(
6509
7058
  "Search query to filter articles. Supports Shopify search syntax. Examples: 'title:Guide', 'tag:tutorial', 'blog_id:123456789'."
6510
7059
  ),
6511
- blogId: z24.string().optional().describe(
7060
+ blogId: z25.string().optional().describe(
6512
7061
  'Filter articles by blog ID. Provide the numeric ID (not the full GID) to filter. Example: To filter blog "gid://shopify/Blog/123456", pass blogId as "123456".'
6513
7062
  ),
6514
- sortKey: z24.enum(["ID", "TITLE", "UPDATED_AT", "PUBLISHED_AT", "AUTHOR", "BLOG_TITLE"]).optional().describe("Sort order for results. Defaults to ID.")
7063
+ sortKey: z25.enum(["ID", "TITLE", "UPDATED_AT", "PUBLISHED_AT", "AUTHOR", "BLOG_TITLE"]).optional().describe("Sort order for results. Defaults to ID.")
6515
7064
  });
6516
- var outputSchema23 = z24.object({
6517
- articles: z24.array(
6518
- z24.object({
6519
- id: z24.string().describe("Article GID"),
6520
- title: z24.string().describe("Article title"),
6521
- handle: z24.string().describe("URL handle/slug"),
6522
- blog: z24.object({
6523
- id: z24.string().describe("Parent blog GID"),
6524
- title: z24.string().describe("Parent blog title")
7065
+ var outputSchema23 = z25.object({
7066
+ articles: z25.array(
7067
+ z25.object({
7068
+ id: z25.string().describe("Article GID"),
7069
+ title: z25.string().describe("Article title"),
7070
+ handle: z25.string().describe("URL handle/slug"),
7071
+ blog: z25.object({
7072
+ id: z25.string().describe("Parent blog GID"),
7073
+ title: z25.string().describe("Parent blog title")
6525
7074
  }),
6526
- isPublished: z24.boolean().describe("Whether article is visible on storefront"),
6527
- publishedAt: z24.string().nullable().describe("ISO timestamp when published")
7075
+ isPublished: z25.boolean().describe("Whether article is visible on storefront"),
7076
+ publishedAt: z25.string().nullable().describe("ISO timestamp when published")
6528
7077
  })
6529
7078
  ),
6530
- hasNextPage: z24.boolean().describe("Whether more articles are available"),
6531
- endCursor: z24.string().nullable().describe("Cursor for next page pagination")
7079
+ hasNextPage: z25.boolean().describe("Whether more articles are available"),
7080
+ endCursor: z25.string().nullable().describe("Cursor for next page pagination")
6532
7081
  });
6533
7082
  var handleListArticles = async (context, params) => {
6534
7083
  log.debug(`Listing articles on shop: ${context.shopDomain}`);
@@ -6578,6 +7127,13 @@ function registerListArticlesTool() {
6578
7127
  relationships: {
6579
7128
  relatedTools: ["list-blogs", "create-article", "update-article"],
6580
7129
  followUps: ["update-article", "delete-article", "create-article"]
7130
+ },
7131
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7132
+ annotations: {
7133
+ readOnlyHint: true,
7134
+ destructiveHint: false,
7135
+ idempotentHint: true,
7136
+ openWorldHint: true
6581
7137
  }
6582
7138
  },
6583
7139
  handleListArticles
@@ -6585,29 +7141,29 @@ function registerListArticlesTool() {
6585
7141
  }
6586
7142
 
6587
7143
  // src/tools/list-blogs.ts
6588
- import { z as z25 } from "zod";
6589
- var inputSchema24 = z25.object({
6590
- first: z25.number().int().min(1).max(50).optional().default(10).describe("Number of blogs to return (1-50). Defaults to 10."),
6591
- cursor: z25.string().optional().describe(
7144
+ import { z as z26 } from "zod";
7145
+ var inputSchema24 = z26.object({
7146
+ first: z26.number().int().min(1).max(50).optional().default(10).describe("Number of blogs to return (1-50). Defaults to 10."),
7147
+ cursor: z26.string().optional().describe(
6592
7148
  "Pagination cursor from a previous response. Use the endCursor from the previous response to get the next page of results."
6593
7149
  ),
6594
- query: z25.string().optional().describe(
7150
+ query: z26.string().optional().describe(
6595
7151
  "Search query to filter blogs. Supports Shopify search syntax. Examples: 'title:News', 'handle:company-blog'."
6596
7152
  ),
6597
- sortKey: z25.enum(["ID", "TITLE", "UPDATED_AT"]).optional().describe("Sort order for results. Defaults to ID.")
7153
+ sortKey: z26.enum(["ID", "TITLE", "UPDATED_AT"]).optional().describe("Sort order for results. Defaults to ID.")
6598
7154
  });
6599
- var outputSchema24 = z25.object({
6600
- blogs: z25.array(
6601
- z25.object({
6602
- id: z25.string().describe("Blog GID"),
6603
- title: z25.string().describe("Blog title"),
6604
- handle: z25.string().describe("URL handle/slug"),
6605
- commentPolicy: z25.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy"),
6606
- articlesCount: z25.number().describe("Number of articles in the blog")
7155
+ var outputSchema24 = z26.object({
7156
+ blogs: z26.array(
7157
+ z26.object({
7158
+ id: z26.string().describe("Blog GID"),
7159
+ title: z26.string().describe("Blog title"),
7160
+ handle: z26.string().describe("URL handle/slug"),
7161
+ commentPolicy: z26.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy"),
7162
+ articlesCount: z26.number().describe("Number of articles in the blog")
6607
7163
  })
6608
7164
  ),
6609
- hasNextPage: z25.boolean().describe("Whether more blogs are available"),
6610
- endCursor: z25.string().nullable().describe("Cursor for next page pagination")
7165
+ hasNextPage: z26.boolean().describe("Whether more blogs are available"),
7166
+ endCursor: z26.string().nullable().describe("Cursor for next page pagination")
6611
7167
  });
6612
7168
  var handleListBlogs = async (context, params) => {
6613
7169
  log.debug(`Listing blogs on shop: ${context.shopDomain}`);
@@ -6651,6 +7207,13 @@ function registerListBlogsTool() {
6651
7207
  relationships: {
6652
7208
  relatedTools: ["create-blog", "update-blog", "list-articles"],
6653
7209
  followUps: ["create-article", "update-blog", "create-blog"]
7210
+ },
7211
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7212
+ annotations: {
7213
+ readOnlyHint: true,
7214
+ destructiveHint: false,
7215
+ idempotentHint: true,
7216
+ openWorldHint: true
6654
7217
  }
6655
7218
  },
6656
7219
  handleListBlogs
@@ -6658,40 +7221,40 @@ function registerListBlogsTool() {
6658
7221
  }
6659
7222
 
6660
7223
  // src/tools/list-collections.ts
6661
- import { z as z26 } from "zod";
6662
- var inputSchema25 = z26.object({
6663
- first: z26.number().int().min(1).max(50).optional().default(10).describe("Number of collections to return (1-50). Default: 10"),
6664
- after: z26.string().optional().describe(
7224
+ import { z as z27 } from "zod";
7225
+ var inputSchema25 = z27.object({
7226
+ first: z27.number().int().min(1).max(50).optional().default(10).describe("Number of collections to return (1-50). Default: 10"),
7227
+ after: z27.string().optional().describe(
6665
7228
  "Pagination cursor from previous response. Use the endCursor value from the previous page to get the next page of results."
6666
7229
  ),
6667
- query: z26.string().optional().describe(
7230
+ query: z27.string().optional().describe(
6668
7231
  'Search query to filter collections. Searches across collection titles and handles. Example: "summer" finds collections with "summer" in the title or handle.'
6669
7232
  ),
6670
- sortKey: z26.enum(["TITLE", "UPDATED_AT", "ID", "RELEVANCE"]).optional().describe(
7233
+ sortKey: z27.enum(["TITLE", "UPDATED_AT", "ID", "RELEVANCE"]).optional().describe(
6671
7234
  "Sort collections by: TITLE (alphabetical), UPDATED_AT (most recently updated first), ID (by collection ID), or RELEVANCE (best match when using query filter)"
6672
7235
  )
6673
7236
  });
6674
- var outputSchema25 = z26.object({
6675
- collections: z26.array(
6676
- z26.object({
6677
- id: z26.string().describe("Collection GID"),
6678
- title: z26.string().describe("Collection title"),
6679
- handle: z26.string().describe("URL handle"),
6680
- seo: z26.object({
6681
- title: z26.string().nullable().describe("SEO title"),
6682
- description: z26.string().nullable().describe("SEO description")
7237
+ var outputSchema25 = z27.object({
7238
+ collections: z27.array(
7239
+ z27.object({
7240
+ id: z27.string().describe("Collection GID"),
7241
+ title: z27.string().describe("Collection title"),
7242
+ handle: z27.string().describe("URL handle"),
7243
+ seo: z27.object({
7244
+ title: z27.string().nullable().describe("SEO title"),
7245
+ description: z27.string().nullable().describe("SEO description")
6683
7246
  }),
6684
- productsCount: z26.number().describe("Number of products in collection")
7247
+ productsCount: z27.number().describe("Number of products in collection")
6685
7248
  })
6686
7249
  ),
6687
- pageInfo: z26.object({
6688
- hasNextPage: z26.boolean().describe("Whether more pages exist"),
6689
- endCursor: z26.string().nullable().describe("Cursor for next page")
7250
+ pageInfo: z27.object({
7251
+ hasNextPage: z27.boolean().describe("Whether more pages exist"),
7252
+ endCursor: z27.string().nullable().describe("Cursor for next page")
6690
7253
  }),
6691
- summary: z26.object({
6692
- totalReturned: z26.number().describe("Number of collections in this response"),
6693
- hasMore: z26.boolean().describe("Whether more collections are available"),
6694
- hint: z26.string().describe("Suggestion for next action")
7254
+ summary: z27.object({
7255
+ totalReturned: z27.number().describe("Number of collections in this response"),
7256
+ hasMore: z27.boolean().describe("Whether more collections are available"),
7257
+ hint: z27.string().describe("Suggestion for next action")
6695
7258
  }).describe("AI-friendly summary for context management")
6696
7259
  });
6697
7260
  var handleListCollections = async (context, params) => {
@@ -6725,6 +7288,13 @@ function registerListCollectionsTool() {
6725
7288
  relationships: {
6726
7289
  relatedTools: ["get-collection"],
6727
7290
  followUps: ["get-collection", "update-collection", "add-products-to-collection"]
7291
+ },
7292
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7293
+ annotations: {
7294
+ readOnlyHint: true,
7295
+ destructiveHint: false,
7296
+ idempotentHint: true,
7297
+ openWorldHint: true
6728
7298
  }
6729
7299
  },
6730
7300
  handleListCollections
@@ -6732,47 +7302,47 @@ function registerListCollectionsTool() {
6732
7302
  }
6733
7303
 
6734
7304
  // src/tools/list-low-inventory.ts
6735
- import { z as z27 } from "zod";
6736
- var inputSchema26 = z27.object({
6737
- threshold: z27.number().int().min(0).default(10).describe(
7305
+ import { z as z28 } from "zod";
7306
+ var inputSchema26 = z28.object({
7307
+ threshold: z28.number().int().min(0).default(10).describe(
6738
7308
  "Products with total inventory at or below this number will be included. Default: 10. Use 0 to find only out-of-stock products."
6739
7309
  ),
6740
- includeZero: z27.boolean().default(true).describe(
7310
+ includeZero: z28.boolean().default(true).describe(
6741
7311
  "Include products with zero inventory. Default: true. Set to false to see only low-stock (not out-of-stock) products."
6742
7312
  ),
6743
- status: z27.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe(
7313
+ status: z28.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe(
6744
7314
  "Filter by product status. Default: ACTIVE. Use DRAFT to check pre-launch products, ARCHIVED for historical."
6745
7315
  ),
6746
- first: z27.number().int().min(1).max(100).default(50).describe("Number of products to return per page. Default: 50, max: 100."),
6747
- after: z27.string().optional().describe("Pagination cursor from previous response. Use pageInfo.endCursor to get next page.")
7316
+ first: z28.number().int().min(1).max(100).default(50).describe("Number of products to return per page. Default: 50, max: 100."),
7317
+ after: z28.string().optional().describe("Pagination cursor from previous response. Use pageInfo.endCursor to get next page.")
6748
7318
  });
6749
- var outputSchema26 = z27.object({
6750
- products: z27.array(
6751
- z27.object({
6752
- productId: z27.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
6753
- productTitle: z27.string().describe("Product title"),
6754
- status: z27.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
6755
- totalInventory: z27.number().describe("Total inventory across all variants"),
6756
- isOutOfStock: z27.boolean().describe("True if total inventory is zero"),
6757
- variants: z27.array(
6758
- z27.object({
6759
- variantId: z27.string().describe("Variant GID"),
6760
- variantTitle: z27.string().describe('Variant title (e.g., "Red / Large")'),
6761
- sku: z27.string().nullable().describe("SKU (Stock Keeping Unit)"),
6762
- inventoryItemId: z27.string().describe("Inventory item GID"),
6763
- available: z27.number().describe("Available quantity for sale")
7319
+ var outputSchema26 = z28.object({
7320
+ products: z28.array(
7321
+ z28.object({
7322
+ productId: z28.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
7323
+ productTitle: z28.string().describe("Product title"),
7324
+ status: z28.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
7325
+ totalInventory: z28.number().describe("Total inventory across all variants"),
7326
+ isOutOfStock: z28.boolean().describe("True if total inventory is zero"),
7327
+ variants: z28.array(
7328
+ z28.object({
7329
+ variantId: z28.string().describe("Variant GID"),
7330
+ variantTitle: z28.string().describe('Variant title (e.g., "Red / Large")'),
7331
+ sku: z28.string().nullable().describe("SKU (Stock Keeping Unit)"),
7332
+ inventoryItemId: z28.string().describe("Inventory item GID"),
7333
+ available: z28.number().describe("Available quantity for sale")
6764
7334
  })
6765
7335
  ).describe("Variant breakdown with inventory")
6766
7336
  })
6767
7337
  ).describe("Products with low inventory, sorted by total inventory ascending"),
6768
- pageInfo: z27.object({
6769
- hasNextPage: z27.boolean().describe("Whether more products exist after this page"),
6770
- endCursor: z27.string().nullable().describe('Cursor for the last item - use as "after" to get next page')
7338
+ pageInfo: z28.object({
7339
+ hasNextPage: z28.boolean().describe("Whether more products exist after this page"),
7340
+ endCursor: z28.string().nullable().describe('Cursor for the last item - use as "after" to get next page')
6771
7341
  }).describe("Pagination information"),
6772
- summary: z27.object({
6773
- totalProducts: z27.number().describe("Number of products in this response"),
6774
- outOfStockCount: z27.number().describe("Number of products with zero inventory"),
6775
- lowStockCount: z27.number().describe("Number of products with inventory > 0 but below threshold")
7342
+ summary: z28.object({
7343
+ totalProducts: z28.number().describe("Number of products in this response"),
7344
+ outOfStockCount: z28.number().describe("Number of products with zero inventory"),
7345
+ lowStockCount: z28.number().describe("Number of products with inventory > 0 but below threshold")
6776
7346
  }).describe("Summary statistics")
6777
7347
  });
6778
7348
  var handleListLowInventory = async (context, params) => {
@@ -6802,6 +7372,13 @@ function registerListLowInventoryTool() {
6802
7372
  relationships: {
6803
7373
  relatedTools: ["get-inventory", "update-inventory", "get-bulk-inventory"],
6804
7374
  followUps: ["get-inventory", "update-inventory"]
7375
+ },
7376
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7377
+ annotations: {
7378
+ readOnlyHint: true,
7379
+ destructiveHint: false,
7380
+ idempotentHint: true,
7381
+ openWorldHint: true
6805
7382
  }
6806
7383
  },
6807
7384
  handleListLowInventory
@@ -6809,31 +7386,31 @@ function registerListLowInventoryTool() {
6809
7386
  }
6810
7387
 
6811
7388
  // src/tools/list-pages.ts
6812
- import { z as z28 } from "zod";
6813
- var inputSchema27 = z28.object({
6814
- first: z28.number().int().min(1).max(50).optional().describe(
7389
+ import { z as z29 } from "zod";
7390
+ var inputSchema27 = z29.object({
7391
+ first: z29.number().int().min(1).max(50).optional().describe(
6815
7392
  "Number of pages to return per request. Default: 10, Maximum: 50. Use pagination (cursor) to retrieve more results."
6816
7393
  ),
6817
- cursor: z28.string().optional().describe(
7394
+ cursor: z29.string().optional().describe(
6818
7395
  "Pagination cursor from a previous list-pages response (endCursor). Use this to get the next page of results."
6819
7396
  ),
6820
- query: z28.string().optional().describe(
7397
+ query: z29.string().optional().describe(
6821
7398
  "Search query to filter pages. Supports Shopify search syntax. Examples: 'title:About', 'handle:contact', 'created_at:>2024-01-01'."
6822
7399
  ),
6823
- sortKey: z28.enum(["ID", "TITLE", "UPDATED_AT", "PUBLISHED_AT"]).optional().describe("Field to sort pages by. Options: ID (default), TITLE, UPDATED_AT, PUBLISHED_AT.")
7400
+ sortKey: z29.enum(["ID", "TITLE", "UPDATED_AT", "PUBLISHED_AT"]).optional().describe("Field to sort pages by. Options: ID (default), TITLE, UPDATED_AT, PUBLISHED_AT.")
6824
7401
  });
6825
- var outputSchema27 = z28.object({
6826
- pages: z28.array(
6827
- z28.object({
6828
- id: z28.string().describe("Page GID"),
6829
- title: z28.string().describe("Page title"),
6830
- handle: z28.string().describe("URL handle/slug"),
6831
- isPublished: z28.boolean().describe("Whether page is visible"),
6832
- updatedAt: z28.string().describe("ISO timestamp of last update")
7402
+ var outputSchema27 = z29.object({
7403
+ pages: z29.array(
7404
+ z29.object({
7405
+ id: z29.string().describe("Page GID"),
7406
+ title: z29.string().describe("Page title"),
7407
+ handle: z29.string().describe("URL handle/slug"),
7408
+ isPublished: z29.boolean().describe("Whether page is visible"),
7409
+ updatedAt: z29.string().describe("ISO timestamp of last update")
6833
7410
  })
6834
7411
  ),
6835
- hasNextPage: z28.boolean().describe("Whether more pages exist"),
6836
- endCursor: z28.string().nullable().describe("Cursor for next page of results")
7412
+ hasNextPage: z29.boolean().describe("Whether more pages exist"),
7413
+ endCursor: z29.string().nullable().describe("Cursor for next page of results")
6837
7414
  });
6838
7415
  var handleListPages = async (context, params) => {
6839
7416
  log.debug(`Listing pages on shop: ${context.shopDomain}`);
@@ -6865,6 +7442,13 @@ function registerListPagesTool() {
6865
7442
  relationships: {
6866
7443
  relatedTools: ["get-page", "create-page"],
6867
7444
  followUps: ["get-page", "update-page", "create-page"]
7445
+ },
7446
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7447
+ annotations: {
7448
+ readOnlyHint: true,
7449
+ destructiveHint: false,
7450
+ idempotentHint: true,
7451
+ openWorldHint: true
6868
7452
  }
6869
7453
  },
6870
7454
  handleListPages
@@ -6872,46 +7456,46 @@ function registerListPagesTool() {
6872
7456
  }
6873
7457
 
6874
7458
  // src/tools/list-products.ts
6875
- import { z as z29 } from "zod";
6876
- var inputSchema28 = z29.object({
6877
- first: z29.number().int().min(1).max(50).optional().describe("Number of products to return (1-50). Default: 10"),
6878
- after: z29.string().optional().describe("Pagination cursor from previous response's endCursor. Omit for first page."),
6879
- query: z29.string().optional().describe(
7459
+ import { z as z30 } from "zod";
7460
+ var inputSchema28 = z30.object({
7461
+ first: z30.number().int().min(1).max(50).optional().describe("Number of products to return (1-50). Default: 10"),
7462
+ after: z30.string().optional().describe("Pagination cursor from previous response's endCursor. Omit for first page."),
7463
+ query: z30.string().optional().describe(
6880
7464
  'Search query for title, description, tags. Example: "summer dress", "organic cotton"'
6881
7465
  ),
6882
- status: z29.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe(
7466
+ status: z30.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe(
6883
7467
  "Filter by product status. ACTIVE = published, DRAFT = not published, ARCHIVED = hidden"
6884
7468
  ),
6885
- vendor: z29.string().optional().describe('Filter by exact vendor name. Example: "Acme Corp", "Nike"'),
6886
- productType: z29.string().optional().describe('Filter by exact product type. Example: "T-Shirts", "Shoes"'),
6887
- sortBy: z29.enum(["TITLE", "CREATED_AT", "UPDATED_AT", "INVENTORY_TOTAL"]).optional().describe("Sort field. TITLE, CREATED_AT (default), UPDATED_AT, or INVENTORY_TOTAL"),
6888
- sortOrder: z29.enum(["ASC", "DESC"]).optional().describe("Sort direction. ASC (default) for ascending, DESC for descending")
7469
+ vendor: z30.string().optional().describe('Filter by exact vendor name. Example: "Acme Corp", "Nike"'),
7470
+ productType: z30.string().optional().describe('Filter by exact product type. Example: "T-Shirts", "Shoes"'),
7471
+ sortBy: z30.enum(["TITLE", "CREATED_AT", "UPDATED_AT", "INVENTORY_TOTAL"]).optional().describe("Sort field. TITLE, CREATED_AT (default), UPDATED_AT, or INVENTORY_TOTAL"),
7472
+ sortOrder: z30.enum(["ASC", "DESC"]).optional().describe("Sort direction. ASC (default) for ascending, DESC for descending")
6889
7473
  });
6890
- var outputSchema28 = z29.object({
6891
- products: z29.array(
6892
- z29.object({
6893
- id: z29.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
6894
- title: z29.string().describe("Product title"),
6895
- handle: z29.string().describe("URL handle/slug"),
6896
- status: z29.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
6897
- vendor: z29.string().describe("Vendor/brand name"),
6898
- productType: z29.string().describe("Product type"),
6899
- totalInventory: z29.number().describe("Total inventory across all variants"),
6900
- primaryVariantPrice: z29.string().describe('Price of the first variant (e.g., "29.99")'),
6901
- imageUrl: z29.string().nullable().describe("First image URL or null if no images")
7474
+ var outputSchema28 = z30.object({
7475
+ products: z30.array(
7476
+ z30.object({
7477
+ id: z30.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
7478
+ title: z30.string().describe("Product title"),
7479
+ handle: z30.string().describe("URL handle/slug"),
7480
+ status: z30.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
7481
+ vendor: z30.string().describe("Vendor/brand name"),
7482
+ productType: z30.string().describe("Product type"),
7483
+ totalInventory: z30.number().describe("Total inventory across all variants"),
7484
+ primaryVariantPrice: z30.string().describe('Price of the first variant (e.g., "29.99")'),
7485
+ imageUrl: z30.string().nullable().describe("First image URL or null if no images")
6902
7486
  })
6903
7487
  ).describe("Array of product summaries"),
6904
- pageInfo: z29.object({
6905
- hasNextPage: z29.boolean().describe("Whether more products exist after this page"),
6906
- hasPreviousPage: z29.boolean().describe("Whether products exist before this page"),
6907
- startCursor: z29.string().nullable().describe("Cursor for the first item in this page"),
6908
- endCursor: z29.string().nullable().describe('Cursor for the last item - use as "after" to get next page')
7488
+ pageInfo: z30.object({
7489
+ hasNextPage: z30.boolean().describe("Whether more products exist after this page"),
7490
+ hasPreviousPage: z30.boolean().describe("Whether products exist before this page"),
7491
+ startCursor: z30.string().nullable().describe("Cursor for the first item in this page"),
7492
+ endCursor: z30.string().nullable().describe('Cursor for the last item - use as "after" to get next page')
6909
7493
  }).describe("Pagination information"),
6910
- totalCount: z29.number().describe("Number of products returned in this page"),
6911
- summary: z29.object({
6912
- totalReturned: z29.number().describe("Number of products in this response"),
6913
- hasMore: z29.boolean().describe("Whether more products are available"),
6914
- hint: z29.string().describe("Suggestion for next action")
7494
+ totalCount: z30.number().describe("Number of products returned in this page"),
7495
+ summary: z30.object({
7496
+ totalReturned: z30.number().describe("Number of products in this response"),
7497
+ hasMore: z30.boolean().describe("Whether more products are available"),
7498
+ hint: z30.string().describe("Suggestion for next action")
6915
7499
  }).describe("AI-friendly summary for context management")
6916
7500
  });
6917
7501
  var handleListProducts = async (context, params) => {
@@ -6952,6 +7536,13 @@ function registerListProductsTool() {
6952
7536
  relationships: {
6953
7537
  relatedTools: ["get-product"],
6954
7538
  followUps: ["get-product", "update-product", "delete-product"]
7539
+ },
7540
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7541
+ annotations: {
7542
+ readOnlyHint: true,
7543
+ destructiveHint: false,
7544
+ idempotentHint: true,
7545
+ openWorldHint: true
6955
7546
  }
6956
7547
  },
6957
7548
  handleListProducts
@@ -6959,30 +7550,30 @@ function registerListProductsTool() {
6959
7550
  }
6960
7551
 
6961
7552
  // src/tools/list-redirects.ts
6962
- import { z as z30 } from "zod";
6963
- var inputSchema29 = z30.object({
6964
- first: z30.number().int().min(1).max(50).optional().default(10).describe("Number of redirects to return (1-50, default: 10)."),
6965
- cursor: z30.string().optional().describe(
7553
+ import { z as z31 } from "zod";
7554
+ var inputSchema29 = z31.object({
7555
+ first: z31.number().int().min(1).max(50).optional().default(10).describe("Number of redirects to return (1-50, default: 10)."),
7556
+ cursor: z31.string().optional().describe(
6966
7557
  "Pagination cursor from previous response (endCursor). Use this to fetch the next page of results."
6967
7558
  ),
6968
- query: z30.string().optional().describe(
7559
+ query: z31.string().optional().describe(
6969
7560
  "Search filter. Supports 'path:' and 'target:' prefixes. Examples: 'path:/old-' to find redirects starting with /old-, 'target:/products/' to find redirects pointing to /products/."
6970
7561
  )
6971
7562
  });
6972
- var outputSchema29 = z30.object({
6973
- redirects: z30.array(
6974
- z30.object({
6975
- id: z30.string().describe("Redirect GID"),
6976
- path: z30.string().describe("Source path"),
6977
- target: z30.string().describe("Target URL")
7563
+ var outputSchema29 = z31.object({
7564
+ redirects: z31.array(
7565
+ z31.object({
7566
+ id: z31.string().describe("Redirect GID"),
7567
+ path: z31.string().describe("Source path"),
7568
+ target: z31.string().describe("Target URL")
6978
7569
  })
6979
7570
  ),
6980
- hasNextPage: z30.boolean().describe("Whether more results exist"),
6981
- endCursor: z30.string().nullable().describe("Cursor for fetching next page"),
6982
- summary: z30.object({
6983
- totalReturned: z30.number().describe("Number of redirects in this response"),
6984
- hasMore: z30.boolean().describe("Whether more redirects are available"),
6985
- hint: z30.string().describe("Suggestion for next action")
7571
+ hasNextPage: z31.boolean().describe("Whether more results exist"),
7572
+ endCursor: z31.string().nullable().describe("Cursor for fetching next page"),
7573
+ summary: z31.object({
7574
+ totalReturned: z31.number().describe("Number of redirects in this response"),
7575
+ hasMore: z31.boolean().describe("Whether more redirects are available"),
7576
+ hint: z31.string().describe("Suggestion for next action")
6986
7577
  }).describe("AI-friendly summary for context management")
6987
7578
  });
6988
7579
  var handleListRedirects = async (context, params) => {
@@ -7027,6 +7618,13 @@ function registerListRedirectsTool() {
7027
7618
  relationships: {
7028
7619
  relatedTools: ["create-redirect"],
7029
7620
  followUps: ["delete-redirect"]
7621
+ },
7622
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7623
+ annotations: {
7624
+ readOnlyHint: true,
7625
+ destructiveHint: false,
7626
+ idempotentHint: true,
7627
+ openWorldHint: true
7030
7628
  }
7031
7629
  },
7032
7630
  handleListRedirects
@@ -7034,18 +7632,18 @@ function registerListRedirectsTool() {
7034
7632
  }
7035
7633
 
7036
7634
  // src/tools/remove-products-from-collection.ts
7037
- import { z as z31 } from "zod";
7038
- var inputSchema30 = z31.object({
7039
- collectionId: z31.string().min(1).describe(
7635
+ import { z as z32 } from "zod";
7636
+ var inputSchema30 = z32.object({
7637
+ collectionId: collectionIdSchema.describe(
7040
7638
  'Collection GID (e.g., "gid://shopify/Collection/123"). Use list-collections to find valid collection IDs.'
7041
7639
  ),
7042
- productIds: z31.array(z31.string().min(1)).min(1).max(250).describe(
7640
+ productIds: z32.array(productIdSchema).min(1).max(250).describe(
7043
7641
  'Array of product GIDs to remove (e.g., ["gid://shopify/Product/123", "gid://shopify/Product/456"]). Maximum 250 products per call. Use list-products to find valid product IDs.'
7044
7642
  )
7045
7643
  });
7046
- var outputSchema30 = z31.object({
7047
- success: z31.boolean().describe("Whether the operation succeeded"),
7048
- removedCount: z31.number().describe("Number of products removed from the collection")
7644
+ var outputSchema30 = z32.object({
7645
+ success: z32.boolean().describe("Whether the operation succeeded"),
7646
+ removedCount: z32.number().describe("Number of products removed from the collection")
7049
7647
  });
7050
7648
  var handleRemoveProductsFromCollection = async (context, params) => {
7051
7649
  log.debug(
@@ -7082,6 +7680,15 @@ function registerRemoveProductsFromCollectionTool() {
7082
7680
  relationships: {
7083
7681
  relatedTools: ["add-products-to-collection", "get-collection"],
7084
7682
  prerequisites: ["get-collection"]
7683
+ },
7684
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7685
+ annotations: {
7686
+ readOnlyHint: false,
7687
+ destructiveHint: false,
7688
+ // Does not delete products, just removes from collection
7689
+ idempotentHint: true,
7690
+ // Removing already-removed products is idempotent
7691
+ openWorldHint: true
7085
7692
  }
7086
7693
  },
7087
7694
  handleRemoveProductsFromCollection
@@ -7089,7 +7696,7 @@ function registerRemoveProductsFromCollectionTool() {
7089
7696
  }
7090
7697
 
7091
7698
  // src/tools/reorder-product-images.ts
7092
- import { z as z32 } from "zod";
7699
+ import { z as z33 } from "zod";
7093
7700
  var PRODUCT_REORDER_MEDIA_MUTATION = `
7094
7701
  mutation ProductReorderMedia($id: ID!, $moves: [MoveInput!]!) {
7095
7702
  productReorderMedia(id: $id, moves: $moves) {
@@ -7104,23 +7711,23 @@ var PRODUCT_REORDER_MEDIA_MUTATION = `
7104
7711
  }
7105
7712
  }
7106
7713
  `;
7107
- var moveSchema = z32.object({
7108
- id: z32.string().min(1).describe('Image GID to move (e.g., "gid://shopify/MediaImage/123")'),
7109
- newPosition: z32.number().int().min(0).describe("Zero-based target position. Position 0 is the featured image.")
7714
+ var moveSchema = z33.object({
7715
+ id: imageIdSchema.describe('Image GID to move (e.g., "gid://shopify/MediaImage/123")'),
7716
+ newPosition: z33.number().int().min(0).describe("Zero-based target position. Position 0 is the featured image.")
7110
7717
  });
7111
- var inputSchema31 = z32.object({
7112
- productId: z32.string().min(1).describe(
7718
+ var inputSchema31 = z33.object({
7719
+ productId: productIdSchema.describe(
7113
7720
  'Shopify product GID (e.g., "gid://shopify/Product/123"). Required. Use list-products to find product IDs.'
7114
7721
  ),
7115
- moves: z32.array(moveSchema).min(1).describe(
7722
+ moves: z33.array(moveSchema).min(1).describe(
7116
7723
  "Array of move operations. Each move specifies an image ID and its new position. Only include images whose position is actually changing. Positions are 0-indexed (position 0 is the featured/hero image)."
7117
7724
  )
7118
7725
  });
7119
- var outputSchema31 = z32.object({
7120
- success: z32.boolean().describe("Whether the reorder operation was initiated successfully"),
7121
- jobId: z32.string().nullable().describe("Background job ID for tracking (may be null if processed immediately)"),
7122
- jobDone: z32.boolean().describe("Whether the job completed immediately"),
7123
- message: z32.string().describe("Human-readable message describing the result")
7726
+ var outputSchema31 = z33.object({
7727
+ success: z33.boolean().describe("Whether the reorder operation was initiated successfully"),
7728
+ jobId: z33.string().nullable().describe("Background job ID for tracking (may be null if processed immediately)"),
7729
+ jobDone: z33.boolean().describe("Whether the job completed immediately"),
7730
+ message: z33.string().describe("Human-readable message describing the result")
7124
7731
  });
7125
7732
  var handleReorderProductImages = async (context, params) => {
7126
7733
  log.debug(`Reordering images on shop: ${context.shopDomain}`);
@@ -7195,6 +7802,14 @@ function registerReorderProductImagesTool() {
7195
7802
  relationships: {
7196
7803
  relatedTools: ["add-product-image", "update-product-image"],
7197
7804
  prerequisites: ["get-product"]
7805
+ },
7806
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7807
+ annotations: {
7808
+ readOnlyHint: false,
7809
+ destructiveHint: false,
7810
+ idempotentHint: true,
7811
+ // Same order produces same result
7812
+ openWorldHint: true
7198
7813
  }
7199
7814
  },
7200
7815
  handleReorderProductImages
@@ -7202,45 +7817,45 @@ function registerReorderProductImagesTool() {
7202
7817
  }
7203
7818
 
7204
7819
  // src/tools/set-product-metafields.ts
7205
- import { z as z33 } from "zod";
7820
+ import { z as z34 } from "zod";
7206
7821
  var MAX_METAFIELDS_PER_CALL = 25;
7207
- var metafieldInputSchema = z33.object({
7208
- namespace: z33.string().min(1).describe(
7822
+ var metafieldInputSchema = z34.object({
7823
+ namespace: z34.string().min(1).describe(
7209
7824
  'Metafield namespace (grouping identifier, e.g., "custom", "seo", "my_app"). Use consistent namespaces to organize related metafields.'
7210
7825
  ),
7211
- key: z33.string().min(1).describe(
7826
+ key: z34.string().min(1).describe(
7212
7827
  'Metafield key (field name, e.g., "color_hex", "schema_markup"). Keys should be lowercase with underscores, unique within a namespace.'
7213
7828
  ),
7214
- value: z33.string().describe("The value to store. All values are stored as strings; JSON should be stringified."),
7215
- type: z33.string().min(1).describe(
7829
+ value: z34.string().describe("The value to store. All values are stored as strings; JSON should be stringified."),
7830
+ type: z34.string().min(1).describe(
7216
7831
  'Metafield type. Required for creating new metafields. Common types: "single_line_text_field" (short text), "multi_line_text_field" (long text), "number_integer", "number_decimal", "boolean", "json", "url", "date", "date_time".'
7217
7832
  )
7218
7833
  });
7219
- var inputSchema32 = z33.object({
7220
- productId: z33.string().min(1).describe(
7834
+ var inputSchema32 = z34.object({
7835
+ productId: productIdSchema.describe(
7221
7836
  'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Use get-product or list-products to find product IDs.'
7222
7837
  ),
7223
- metafields: z33.array(metafieldInputSchema).min(1, "At least one metafield is required").max(
7838
+ metafields: z34.array(metafieldInputSchema).min(1, "At least one metafield is required").max(
7224
7839
  MAX_METAFIELDS_PER_CALL,
7225
7840
  `Maximum ${MAX_METAFIELDS_PER_CALL} metafields per call. Split into multiple calls for larger batches.`
7226
7841
  ).describe(
7227
7842
  `Array of metafields to create or update (max ${MAX_METAFIELDS_PER_CALL} per call). Existing metafields with matching namespace+key are updated; new ones are created.`
7228
7843
  )
7229
7844
  });
7230
- var outputSchema32 = z33.object({
7231
- success: z33.boolean().describe("Whether the operation succeeded"),
7232
- metafields: z33.array(
7233
- z33.object({
7234
- id: z33.string().describe("Metafield GID"),
7235
- namespace: z33.string().describe("Metafield namespace"),
7236
- key: z33.string().describe("Metafield key"),
7237
- value: z33.string().describe("Metafield value"),
7238
- type: z33.string().describe("Metafield type"),
7239
- createdAt: z33.string().describe("Creation timestamp (ISO 8601)"),
7240
- updatedAt: z33.string().describe("Last update timestamp (ISO 8601)")
7845
+ var outputSchema32 = z34.object({
7846
+ success: z34.boolean().describe("Whether the operation succeeded"),
7847
+ metafields: z34.array(
7848
+ z34.object({
7849
+ id: z34.string().describe("Metafield GID"),
7850
+ namespace: z34.string().describe("Metafield namespace"),
7851
+ key: z34.string().describe("Metafield key"),
7852
+ value: z34.string().describe("Metafield value"),
7853
+ type: z34.string().describe("Metafield type"),
7854
+ createdAt: z34.string().describe("Creation timestamp (ISO 8601)"),
7855
+ updatedAt: z34.string().describe("Last update timestamp (ISO 8601)")
7241
7856
  })
7242
7857
  ).describe("Created or updated metafields"),
7243
- count: z33.number().describe("Number of metafields processed")
7858
+ count: z34.number().describe("Number of metafields processed")
7244
7859
  });
7245
7860
  var handleSetProductMetafields = async (context, params) => {
7246
7861
  log.debug(
@@ -7284,6 +7899,14 @@ function registerSetProductMetafieldsTool() {
7284
7899
  relatedTools: ["get-product", "get-product-metafields"],
7285
7900
  prerequisites: ["get-product"],
7286
7901
  followUps: ["get-product-metafields"]
7902
+ },
7903
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7904
+ annotations: {
7905
+ readOnlyHint: false,
7906
+ destructiveHint: false,
7907
+ idempotentHint: true,
7908
+ // Same values produce same result
7909
+ openWorldHint: true
7287
7910
  }
7288
7911
  },
7289
7912
  handleSetProductMetafields
@@ -7291,47 +7914,47 @@ function registerSetProductMetafieldsTool() {
7291
7914
  }
7292
7915
 
7293
7916
  // src/tools/update-article.ts
7294
- import { z as z34 } from "zod";
7295
- var inputSchema33 = z34.object({
7296
- id: z34.string().describe(
7917
+ import { z as z35 } from "zod";
7918
+ var inputSchema33 = z35.object({
7919
+ id: articleIdSchema.describe(
7297
7920
  'The article ID to update (required). Must be a valid Shopify GID format. Example: "gid://shopify/Article/12345". Use list-articles to find article IDs.'
7298
7921
  ),
7299
- title: z34.string().optional().describe("The new title for the article. Only provided if changing the title."),
7300
- authorName: z34.string().optional().describe("The new author name for the article."),
7301
- body: z34.string().optional().describe("The new HTML body content of the article. Supports HTML markup for formatting."),
7302
- summary: z34.string().optional().describe("A summary or excerpt of the article."),
7303
- tags: z34.array(z34.string()).optional().describe(
7922
+ title: z35.string().optional().describe("The new title for the article. Only provided if changing the title."),
7923
+ authorName: z35.string().optional().describe("The new author name for the article."),
7924
+ body: z35.string().optional().describe("The new HTML body content of the article. Supports HTML markup for formatting."),
7925
+ summary: z35.string().optional().describe("A summary or excerpt of the article."),
7926
+ tags: z35.array(z35.string()).optional().describe(
7304
7927
  "New tags for categorization. This replaces all existing tags. Example: ['guide', 'tutorial', 'beginner']."
7305
7928
  ),
7306
- image: z34.object({
7307
- url: z34.string().url().describe("The URL of the featured image"),
7308
- altText: z34.string().optional().describe("Alt text for accessibility")
7929
+ image: z35.object({
7930
+ url: z35.string().url().describe("The URL of the featured image"),
7931
+ altText: z35.string().optional().describe("Alt text for accessibility")
7309
7932
  }).optional().describe("Featured image for the article."),
7310
- isPublished: z34.boolean().optional().describe(
7933
+ isPublished: z35.boolean().optional().describe(
7311
7934
  "Whether the article should be visible on the storefront. Set to true to publish, false to unpublish."
7312
7935
  ),
7313
- publishDate: z34.string().optional().describe(
7936
+ publishDate: z35.string().optional().describe(
7314
7937
  "The date and time when the article should become visible (ISO 8601 format). Example: '2024-01-15T10:00:00Z'."
7315
7938
  ),
7316
- handle: z34.string().optional().describe(
7939
+ handle: z35.string().optional().describe(
7317
7940
  "The new URL handle/slug for the article. Set redirectNewHandle to true to automatically redirect the old URL to the new one."
7318
7941
  ),
7319
- templateSuffix: z34.string().optional().describe(
7942
+ templateSuffix: z35.string().optional().describe(
7320
7943
  "The suffix of the Liquid template used to render the article. For example, 'featured' would use the template 'article.featured.liquid'."
7321
7944
  ),
7322
- redirectNewHandle: z34.boolean().optional().describe(
7945
+ redirectNewHandle: z35.boolean().optional().describe(
7323
7946
  "Whether to automatically redirect the old URL to the new handle. Set to true when changing the handle to preserve SEO and existing links."
7324
7947
  )
7325
7948
  });
7326
- var outputSchema33 = z34.object({
7327
- id: z34.string().describe("Updated article GID"),
7328
- title: z34.string().describe("Article title"),
7329
- handle: z34.string().describe("URL handle/slug"),
7330
- blog: z34.object({
7331
- id: z34.string().describe("Parent blog GID"),
7332
- title: z34.string().describe("Parent blog title")
7949
+ var outputSchema33 = z35.object({
7950
+ id: z35.string().describe("Updated article GID"),
7951
+ title: z35.string().describe("Article title"),
7952
+ handle: z35.string().describe("URL handle/slug"),
7953
+ blog: z35.object({
7954
+ id: z35.string().describe("Parent blog GID"),
7955
+ title: z35.string().describe("Parent blog title")
7333
7956
  }),
7334
- isPublished: z34.boolean().describe("Whether article is visible on storefront")
7957
+ isPublished: z35.boolean().describe("Whether article is visible on storefront")
7335
7958
  });
7336
7959
  var handleUpdateArticle = async (context, params) => {
7337
7960
  log.debug(`Updating article on shop: ${context.shopDomain}`);
@@ -7387,6 +8010,13 @@ function registerUpdateArticleTool() {
7387
8010
  relatedTools: ["list-articles", "create-article", "delete-article"],
7388
8011
  prerequisites: ["list-articles"],
7389
8012
  followUps: ["list-articles"]
8013
+ },
8014
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8015
+ annotations: {
8016
+ readOnlyHint: false,
8017
+ destructiveHint: false,
8018
+ idempotentHint: true,
8019
+ openWorldHint: true
7390
8020
  }
7391
8021
  },
7392
8022
  handleUpdateArticle
@@ -7394,30 +8024,30 @@ function registerUpdateArticleTool() {
7394
8024
  }
7395
8025
 
7396
8026
  // src/tools/update-blog.ts
7397
- import { z as z35 } from "zod";
7398
- var inputSchema34 = z35.object({
7399
- id: z35.string().describe(
8027
+ import { z as z36 } from "zod";
8028
+ var inputSchema34 = z36.object({
8029
+ id: blogIdSchema.describe(
7400
8030
  'The blog ID to update (required). Must be a valid Shopify GID format. Example: "gid://shopify/Blog/12345". Use list-blogs to find blog IDs.'
7401
8031
  ),
7402
- title: z35.string().optional().describe("The new title for the blog. Only provided if changing the title."),
7403
- handle: z35.string().optional().describe(
8032
+ title: z36.string().optional().describe("The new title for the blog. Only provided if changing the title."),
8033
+ handle: z36.string().optional().describe(
7404
8034
  "The new URL handle/slug for the blog. Set redirectNewHandle to true to automatically redirect the old URL to the new one."
7405
8035
  ),
7406
- commentPolicy: z35.enum(["CLOSED", "MODERATE", "OPEN"]).optional().describe(
8036
+ commentPolicy: z36.enum(["CLOSED", "MODERATE", "OPEN"]).optional().describe(
7407
8037
  "Comment moderation policy for the blog. CLOSED: Comments disabled. MODERATE: Comments require approval. OPEN: Comments appear immediately."
7408
8038
  ),
7409
- templateSuffix: z35.string().optional().describe(
8039
+ templateSuffix: z36.string().optional().describe(
7410
8040
  "The suffix of the Liquid template used to render the blog. For example, 'news' would use the template 'blog.news.liquid'."
7411
8041
  ),
7412
- redirectNewHandle: z35.boolean().optional().describe(
8042
+ redirectNewHandle: z36.boolean().optional().describe(
7413
8043
  "Whether to automatically redirect the old URL to the new handle. Set to true when changing the handle to preserve SEO and existing links."
7414
8044
  )
7415
8045
  });
7416
- var outputSchema34 = z35.object({
7417
- id: z35.string().describe("Updated blog GID"),
7418
- title: z35.string().describe("Blog title"),
7419
- handle: z35.string().describe("URL handle/slug"),
7420
- commentPolicy: z35.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy")
8046
+ var outputSchema34 = z36.object({
8047
+ id: z36.string().describe("Updated blog GID"),
8048
+ title: z36.string().describe("Blog title"),
8049
+ handle: z36.string().describe("URL handle/slug"),
8050
+ commentPolicy: z36.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy")
7421
8051
  });
7422
8052
  var handleUpdateBlog = async (context, params) => {
7423
8053
  log.debug(`Updating blog on shop: ${context.shopDomain}`);
@@ -7467,6 +8097,13 @@ function registerUpdateBlogTool() {
7467
8097
  relatedTools: ["list-blogs", "create-blog", "delete-blog"],
7468
8098
  prerequisites: ["list-blogs"],
7469
8099
  followUps: ["list-blogs"]
8100
+ },
8101
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8102
+ annotations: {
8103
+ readOnlyHint: false,
8104
+ destructiveHint: false,
8105
+ idempotentHint: true,
8106
+ openWorldHint: true
7470
8107
  }
7471
8108
  },
7472
8109
  handleUpdateBlog
@@ -7474,21 +8111,21 @@ function registerUpdateBlogTool() {
7474
8111
  }
7475
8112
 
7476
8113
  // src/tools/update-collection.ts
7477
- import { z as z36 } from "zod";
7478
- var inputSchema35 = z36.object({
7479
- collectionId: z36.string().min(1).describe(
8114
+ import { z as z37 } from "zod";
8115
+ var inputSchema35 = z37.object({
8116
+ collectionId: collectionIdSchema.describe(
7480
8117
  'Collection ID to update (GID format, e.g., "gid://shopify/Collection/123"). Use list-collections or get-collection to find collection IDs.'
7481
8118
  ),
7482
- title: z36.string().min(1).optional().describe("New collection title"),
7483
- handle: z36.string().optional().describe(
8119
+ title: z37.string().min(1).optional().describe("New collection title"),
8120
+ handle: z37.string().optional().describe(
7484
8121
  "New URL handle/slug. When changing, set redirectNewHandle: true to create automatic redirect from the old URL."
7485
8122
  ),
7486
- descriptionHtml: z36.string().optional().describe("New HTML description"),
7487
- seo: z36.object({
7488
- title: z36.string().optional().describe("New SEO title"),
7489
- description: z36.string().optional().describe("New SEO meta description")
8123
+ descriptionHtml: z37.string().optional().describe("New HTML description"),
8124
+ seo: z37.object({
8125
+ title: z37.string().optional().describe("New SEO title"),
8126
+ description: z37.string().optional().describe("New SEO meta description")
7490
8127
  }).optional().describe("Updated SEO metadata"),
7491
- sortOrder: z36.enum([
8128
+ sortOrder: z37.enum([
7492
8129
  "ALPHA_ASC",
7493
8130
  "ALPHA_DESC",
7494
8131
  "BEST_SELLING",
@@ -7498,19 +8135,19 @@ var inputSchema35 = z36.object({
7498
8135
  "PRICE_ASC",
7499
8136
  "PRICE_DESC"
7500
8137
  ]).optional().describe("New product sort order within collection"),
7501
- image: z36.object({
7502
- src: z36.string().url().describe("New image URL (publicly accessible)"),
7503
- altText: z36.string().optional().describe("New alt text for accessibility")
8138
+ image: z37.object({
8139
+ src: z37.string().url().describe("New image URL (publicly accessible)"),
8140
+ altText: z37.string().optional().describe("New alt text for accessibility")
7504
8141
  }).optional().describe("New collection image"),
7505
- templateSuffix: z36.string().optional().describe("New Liquid template suffix"),
7506
- redirectNewHandle: z36.boolean().optional().describe(
8142
+ templateSuffix: z37.string().optional().describe("New Liquid template suffix"),
8143
+ redirectNewHandle: z37.boolean().optional().describe(
7507
8144
  "When changing the handle, set to true to create an automatic URL redirect from the old handle to the new one. Important for SEO to preserve link equity."
7508
8145
  )
7509
8146
  });
7510
- var outputSchema35 = z36.object({
7511
- id: z36.string().describe("Updated collection GID"),
7512
- title: z36.string().describe("Collection title"),
7513
- handle: z36.string().describe("Collection URL handle")
8147
+ var outputSchema35 = z37.object({
8148
+ id: z37.string().describe("Updated collection GID"),
8149
+ title: z37.string().describe("Collection title"),
8150
+ handle: z37.string().describe("Collection URL handle")
7514
8151
  });
7515
8152
  var handleUpdateCollection = async (context, params) => {
7516
8153
  log.debug(`Updating collection on shop: ${context.shopDomain}`);
@@ -7557,6 +8194,14 @@ function registerUpdateCollectionTool() {
7557
8194
  relatedTools: ["get-collection", "list-collections"],
7558
8195
  prerequisites: ["get-collection"],
7559
8196
  followUps: ["add-products-to-collection", "remove-products-from-collection"]
8197
+ },
8198
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8199
+ annotations: {
8200
+ readOnlyHint: false,
8201
+ destructiveHint: false,
8202
+ idempotentHint: true,
8203
+ // Same update produces same result
8204
+ openWorldHint: true
7560
8205
  }
7561
8206
  },
7562
8207
  handleUpdateCollection
@@ -7564,8 +8209,8 @@ function registerUpdateCollectionTool() {
7564
8209
  }
7565
8210
 
7566
8211
  // src/tools/update-inventory.ts
7567
- import { z as z37 } from "zod";
7568
- var inventoryReasonEnum = z37.enum([
8212
+ import { z as z38 } from "zod";
8213
+ var inventoryReasonEnum = z38.enum([
7569
8214
  "correction",
7570
8215
  "cycle_count_available",
7571
8216
  "damaged",
@@ -7584,20 +8229,20 @@ var inventoryReasonEnum = z37.enum([
7584
8229
  "safety_stock",
7585
8230
  "shrinkage"
7586
8231
  ]);
7587
- var inputSchema36 = z37.object({
7588
- inventoryItemId: z37.string().min(1).describe(
8232
+ var inputSchema36 = z38.object({
8233
+ inventoryItemId: inventoryItemIdSchema.describe(
7589
8234
  'The Shopify inventory item ID (e.g., "gid://shopify/InventoryItem/456"). Get this from get-inventory tool or from get-product response.'
7590
8235
  ),
7591
- locationId: z37.string().min(1).describe(
8236
+ locationId: locationIdSchema.describe(
7592
8237
  'The location ID where inventory should be updated (e.g., "gid://shopify/Location/789"). Get this from get-inventory tool.'
7593
8238
  ),
7594
- setQuantity: z37.number().int().min(0).optional().describe(
8239
+ setQuantity: z38.number().int().min(0).optional().describe(
7595
8240
  "Set inventory to this exact quantity. Example: 100. Use for absolute stock counts after physical inventory. Cannot be used together with adjustQuantity."
7596
8241
  ),
7597
- adjustQuantity: z37.number().int().optional().describe(
8242
+ adjustQuantity: z38.number().int().optional().describe(
7598
8243
  "Adjust inventory by this amount. Positive to add, negative to subtract. Example: -5 to reduce by 5, +10 to add 10. Use for incremental changes. Cannot be used together with setQuantity."
7599
8244
  ),
7600
- name: z37.enum(["available", "on_hand"]).optional().default("available").describe(
8245
+ name: z38.enum(["available", "on_hand"]).optional().default("available").describe(
7601
8246
  'Which quantity to modify. "available" (default) for sellable stock, "on_hand" for physical count.'
7602
8247
  ),
7603
8248
  reason: inventoryReasonEnum.optional().default("correction").describe(
@@ -7608,16 +8253,16 @@ var inputSchema36 = z37.object({
7608
8253
  }).refine((data) => !(data.setQuantity !== void 0 && data.adjustQuantity !== void 0), {
7609
8254
  message: "Cannot provide both setQuantity and adjustQuantity. Choose one."
7610
8255
  });
7611
- var outputSchema36 = z37.object({
7612
- success: z37.boolean().describe("Whether the operation succeeded"),
7613
- inventoryItemId: z37.string().describe("Inventory item GID"),
7614
- locationId: z37.string().describe("Location GID where inventory was updated"),
7615
- locationName: z37.string().describe("Human-readable location name"),
7616
- previousQuantity: z37.number().describe("Quantity before the change"),
7617
- newQuantity: z37.number().describe("Quantity after the change"),
7618
- delta: z37.number().describe("The actual change applied (positive or negative)"),
7619
- name: z37.string().describe("Which quantity was modified (available or on_hand)"),
7620
- reason: z37.string().describe("Reason for the adjustment")
8256
+ var outputSchema36 = z38.object({
8257
+ success: z38.boolean().describe("Whether the operation succeeded"),
8258
+ inventoryItemId: z38.string().describe("Inventory item GID"),
8259
+ locationId: z38.string().describe("Location GID where inventory was updated"),
8260
+ locationName: z38.string().describe("Human-readable location name"),
8261
+ previousQuantity: z38.number().describe("Quantity before the change"),
8262
+ newQuantity: z38.number().describe("Quantity after the change"),
8263
+ delta: z38.number().describe("The actual change applied (positive or negative)"),
8264
+ name: z38.string().describe("Which quantity was modified (available or on_hand)"),
8265
+ reason: z38.string().describe("Reason for the adjustment")
7621
8266
  });
7622
8267
  var handleUpdateInventory = async (context, params) => {
7623
8268
  log.debug(`Updating inventory on shop: ${context.shopDomain}`);
@@ -7698,6 +8343,13 @@ function registerUpdateInventoryTool() {
7698
8343
  relatedTools: ["get-inventory", "get-bulk-inventory", "list-low-inventory"],
7699
8344
  prerequisites: ["get-inventory"],
7700
8345
  followUps: ["get-inventory"]
8346
+ },
8347
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8348
+ annotations: {
8349
+ readOnlyHint: false,
8350
+ destructiveHint: false,
8351
+ idempotentHint: true,
8352
+ openWorldHint: true
7701
8353
  }
7702
8354
  },
7703
8355
  handleUpdateInventory
@@ -7705,33 +8357,33 @@ function registerUpdateInventoryTool() {
7705
8357
  }
7706
8358
 
7707
8359
  // src/tools/update-page.ts
7708
- import { z as z38 } from "zod";
7709
- var inputSchema37 = z38.object({
7710
- id: z38.string().min(1).describe(
8360
+ import { z as z39 } from "zod";
8361
+ var inputSchema37 = z39.object({
8362
+ id: pageIdSchema.describe(
7711
8363
  'The page ID to update (required). Must be a valid Shopify GID format. Example: "gid://shopify/Page/123456789". Use list-pages to find page IDs.'
7712
8364
  ),
7713
- title: z38.string().optional().describe("The new title for the page. Only provide if you want to change it."),
7714
- body: z38.string().optional().describe(
8365
+ title: z39.string().optional().describe("The new title for the page. Only provide if you want to change it."),
8366
+ body: z39.string().optional().describe(
7715
8367
  "The new HTML body content for the page. Supports HTML markup. Only provide if you want to change it."
7716
8368
  ),
7717
- handle: z38.string().optional().describe(
8369
+ handle: z39.string().optional().describe(
7718
8370
  "The new URL handle/slug for the page. Only provide if you want to change the URL. Consider setting redirectNewHandle to true when changing handles."
7719
8371
  ),
7720
- isPublished: z38.boolean().optional().describe(
8372
+ isPublished: z39.boolean().optional().describe(
7721
8373
  "Whether the page should be visible on the storefront. Set to true to publish, false to unpublish."
7722
8374
  ),
7723
- templateSuffix: z38.string().optional().describe(
8375
+ templateSuffix: z39.string().optional().describe(
7724
8376
  "The suffix of the Liquid template used to render the page. Set to empty string to use the default template."
7725
8377
  ),
7726
- redirectNewHandle: z38.boolean().optional().describe(
8378
+ redirectNewHandle: z39.boolean().optional().describe(
7727
8379
  "Whether to create a redirect from the old handle to the new handle when changing handles. Recommended to preserve SEO value when changing page URLs."
7728
8380
  )
7729
8381
  });
7730
- var outputSchema37 = z38.object({
7731
- id: z38.string().describe("Updated page GID"),
7732
- title: z38.string().describe("Page title"),
7733
- handle: z38.string().describe("URL handle/slug"),
7734
- isPublished: z38.boolean().describe("Whether page is visible on storefront")
8382
+ var outputSchema37 = z39.object({
8383
+ id: z39.string().describe("Updated page GID"),
8384
+ title: z39.string().describe("Page title"),
8385
+ handle: z39.string().describe("URL handle/slug"),
8386
+ isPublished: z39.boolean().describe("Whether page is visible on storefront")
7735
8387
  });
7736
8388
  var handleUpdatePage = async (context, params) => {
7737
8389
  log.debug(`Updating page on shop: ${context.shopDomain}`);
@@ -7782,6 +8434,13 @@ function registerUpdatePageTool() {
7782
8434
  relatedTools: ["get-page", "list-pages"],
7783
8435
  prerequisites: ["get-page"],
7784
8436
  followUps: ["get-page"]
8437
+ },
8438
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8439
+ annotations: {
8440
+ readOnlyHint: false,
8441
+ destructiveHint: false,
8442
+ idempotentHint: true,
8443
+ openWorldHint: true
7785
8444
  }
7786
8445
  },
7787
8446
  handleUpdatePage
@@ -7789,7 +8448,7 @@ function registerUpdatePageTool() {
7789
8448
  }
7790
8449
 
7791
8450
  // src/tools/update-product-image.ts
7792
- import { z as z39 } from "zod";
8451
+ import { z as z40 } from "zod";
7793
8452
  var FILE_UPDATE_MUTATION = `
7794
8453
  mutation FileUpdate($files: [FileUpdateInput!]!) {
7795
8454
  fileUpdate(files: $files) {
@@ -7809,18 +8468,18 @@ var FILE_UPDATE_MUTATION = `
7809
8468
  }
7810
8469
  }
7811
8470
  `;
7812
- var inputSchema38 = z39.object({
7813
- imageId: z39.string().min(1).describe(
8471
+ var inputSchema38 = z40.object({
8472
+ imageId: imageIdSchema.describe(
7814
8473
  'Shopify image GID (e.g., "gid://shopify/MediaImage/123"). Required. Use get-product to find image IDs in the media field.'
7815
8474
  ),
7816
- altText: z39.string().describe(
8475
+ altText: z40.string().describe(
7817
8476
  "New alt text for the image. Pass an empty string to clear the alt text. Alt text describes the image for screen readers and helps search engines understand image content."
7818
8477
  )
7819
8478
  });
7820
- var outputSchema38 = z39.object({
7821
- id: z39.string().describe('Image GID (e.g., "gid://shopify/MediaImage/123")'),
7822
- url: z39.string().nullable().describe("Shopify CDN URL for the image"),
7823
- altText: z39.string().nullable().describe("Updated alt text for the image")
8479
+ var outputSchema38 = z40.object({
8480
+ id: z40.string().describe('Image GID (e.g., "gid://shopify/MediaImage/123")'),
8481
+ url: z40.string().nullable().describe("Shopify CDN URL for the image"),
8482
+ altText: z40.string().nullable().describe("Updated alt text for the image")
7824
8483
  });
7825
8484
  var handleUpdateProductImage = async (context, params) => {
7826
8485
  log.debug(`Updating image alt text on shop: ${context.shopDomain}`);
@@ -7887,6 +8546,14 @@ function registerUpdateProductImageTool() {
7887
8546
  relatedTools: ["reorder-product-images", "add-product-image"],
7888
8547
  prerequisites: ["add-product-image"],
7889
8548
  followUps: ["reorder-product-images"]
8549
+ },
8550
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8551
+ annotations: {
8552
+ readOnlyHint: false,
8553
+ destructiveHint: false,
8554
+ idempotentHint: true,
8555
+ // Same alt text update produces same result
8556
+ openWorldHint: true
7890
8557
  }
7891
8558
  },
7892
8559
  handleUpdateProductImage
@@ -7894,19 +8561,19 @@ function registerUpdateProductImageTool() {
7894
8561
  }
7895
8562
 
7896
8563
  // src/tools/update-product-variant.ts
7897
- import { z as z40 } from "zod";
7898
- var inputSchema39 = z40.object({
7899
- productId: z40.string().min(1).describe(
8564
+ import { z as z41 } from "zod";
8565
+ var inputSchema39 = z41.object({
8566
+ productId: productIdSchema.describe(
7900
8567
  'Shopify product GID (e.g., "gid://shopify/Product/123"). Required. Use get-product to find the product ID containing the variant.'
7901
8568
  ),
7902
- id: z40.string().min(1).describe(
8569
+ id: variantIdSchema.describe(
7903
8570
  'Shopify variant GID (e.g., "gid://shopify/ProductVariant/456"). Required. Use get-product to find variant IDs for a product.'
7904
8571
  ),
7905
- price: z40.string().optional().describe('New variant price as decimal string (e.g., "29.99")'),
7906
- compareAtPrice: z40.string().nullable().optional().describe(
8572
+ price: z41.string().optional().describe('New variant price as decimal string (e.g., "29.99")'),
8573
+ compareAtPrice: z41.string().nullable().optional().describe(
7907
8574
  'Original price for sale display (e.g., "39.99"). When set higher than price, Shopify shows strikethrough pricing. Set to null to remove the compare at price.'
7908
8575
  ),
7909
- barcode: z40.string().nullable().optional().describe("Barcode (UPC, EAN, ISBN, etc.). Set to null to remove.")
8576
+ barcode: z41.string().nullable().optional().describe("Barcode (UPC, EAN, ISBN, etc.). Set to null to remove.")
7910
8577
  }).refine(
7911
8578
  (data) => {
7912
8579
  const { productId: _productId, id: _id, ...updateFields } = data;
@@ -7914,14 +8581,14 @@ var inputSchema39 = z40.object({
7914
8581
  },
7915
8582
  { message: "At least one field to update must be provided (price, compareAtPrice, or barcode)" }
7916
8583
  );
7917
- var outputSchema39 = z40.object({
7918
- id: z40.string().describe('Variant GID (e.g., "gid://shopify/ProductVariant/123")'),
7919
- title: z40.string().describe('Variant title (e.g., "Red / Medium")'),
7920
- price: z40.string().describe("Current price as decimal string"),
7921
- compareAtPrice: z40.string().nullable().describe("Compare at price for sale display"),
7922
- sku: z40.string().nullable().describe("Stock keeping unit"),
7923
- barcode: z40.string().nullable().describe("Barcode (UPC, EAN, etc.)"),
7924
- inventoryQuantity: z40.number().nullable().describe("Available inventory quantity")
8584
+ var outputSchema39 = z41.object({
8585
+ id: z41.string().describe('Variant GID (e.g., "gid://shopify/ProductVariant/123")'),
8586
+ title: z41.string().describe('Variant title (e.g., "Red / Medium")'),
8587
+ price: z41.string().describe("Current price as decimal string"),
8588
+ compareAtPrice: z41.string().nullable().describe("Compare at price for sale display"),
8589
+ sku: z41.string().nullable().describe("Stock keeping unit"),
8590
+ barcode: z41.string().nullable().describe("Barcode (UPC, EAN, etc.)"),
8591
+ inventoryQuantity: z41.number().nullable().describe("Available inventory quantity")
7925
8592
  });
7926
8593
  var handleUpdateProductVariant = async (context, params) => {
7927
8594
  log.debug(`Updating product variant on shop: ${context.shopDomain}`);
@@ -7957,6 +8624,14 @@ function registerUpdateProductVariantTool() {
7957
8624
  relationships: {
7958
8625
  relatedTools: ["get-product", "get-inventory"],
7959
8626
  prerequisites: ["get-product"]
8627
+ },
8628
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8629
+ annotations: {
8630
+ readOnlyHint: false,
8631
+ destructiveHint: false,
8632
+ idempotentHint: true,
8633
+ // Same update produces same result
8634
+ openWorldHint: true
7960
8635
  }
7961
8636
  },
7962
8637
  handleUpdateProductVariant
@@ -7964,23 +8639,25 @@ function registerUpdateProductVariantTool() {
7964
8639
  }
7965
8640
 
7966
8641
  // src/tools/update-product.ts
7967
- import { z as z41 } from "zod";
7968
- var inputSchema40 = z41.object({
7969
- id: z41.string().min(1).describe('Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Required.'),
7970
- title: z41.string().min(1).optional().describe("New product title"),
7971
- description: z41.string().optional().describe("New product description (HTML supported)"),
7972
- vendor: z41.string().optional().describe("New product vendor/brand name"),
7973
- productType: z41.string().optional().describe("New product type for categorization"),
7974
- tags: z41.array(z41.string()).optional().describe("New tags (replaces existing tags)"),
7975
- status: z41.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe("New product status: ACTIVE (visible), DRAFT (hidden), ARCHIVED (hidden)"),
7976
- seo: z41.object({
7977
- title: z41.string().optional().describe("SEO title for search engine results"),
7978
- description: z41.string().optional().describe("SEO meta description for search engines")
8642
+ import { z as z42 } from "zod";
8643
+ var inputSchema40 = z42.object({
8644
+ id: productIdSchema.describe(
8645
+ 'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Required.'
8646
+ ),
8647
+ title: z42.string().min(1).optional().describe("New product title"),
8648
+ description: z42.string().optional().describe("New product description (HTML supported)"),
8649
+ vendor: z42.string().optional().describe("New product vendor/brand name"),
8650
+ productType: z42.string().optional().describe("New product type for categorization"),
8651
+ tags: z42.array(z42.string()).optional().describe("New tags (replaces existing tags)"),
8652
+ status: z42.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe("New product status: ACTIVE (visible), DRAFT (hidden), ARCHIVED (hidden)"),
8653
+ seo: z42.object({
8654
+ title: z42.string().optional().describe("SEO title for search engine results"),
8655
+ description: z42.string().optional().describe("SEO meta description for search engines")
7979
8656
  }).optional().describe("SEO metadata for search engine optimization"),
7980
- handle: z41.string().optional().describe(
8657
+ handle: z42.string().optional().describe(
7981
8658
  'New URL handle/slug (e.g., "my-product"). Use with redirectNewHandle for URL changes.'
7982
8659
  ),
7983
- redirectNewHandle: z41.boolean().optional().describe(
8660
+ redirectNewHandle: z42.boolean().optional().describe(
7984
8661
  "If true, creates automatic redirect from old handle to new handle. Use when changing handle."
7985
8662
  )
7986
8663
  }).refine(
@@ -7990,40 +8667,40 @@ var inputSchema40 = z41.object({
7990
8667
  },
7991
8668
  { message: "At least one field to update must be provided" }
7992
8669
  );
7993
- var outputSchema40 = z41.object({
7994
- id: z41.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
7995
- title: z41.string().describe("Product title"),
7996
- handle: z41.string().describe("URL handle/slug"),
7997
- description: z41.string().nullable().describe("Product description (HTML)"),
7998
- vendor: z41.string().describe("Vendor/brand name"),
7999
- productType: z41.string().describe("Product type"),
8000
- status: z41.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
8001
- tags: z41.array(z41.string()).describe("Product tags"),
8002
- seo: z41.object({
8003
- title: z41.string().nullable().describe("SEO title for search engines"),
8004
- description: z41.string().nullable().describe("SEO description/meta description")
8670
+ var outputSchema40 = z42.object({
8671
+ id: z42.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
8672
+ title: z42.string().describe("Product title"),
8673
+ handle: z42.string().describe("URL handle/slug"),
8674
+ description: z42.string().nullable().describe("Product description (HTML)"),
8675
+ vendor: z42.string().describe("Vendor/brand name"),
8676
+ productType: z42.string().describe("Product type"),
8677
+ status: z42.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
8678
+ tags: z42.array(z42.string()).describe("Product tags"),
8679
+ seo: z42.object({
8680
+ title: z42.string().nullable().describe("SEO title for search engines"),
8681
+ description: z42.string().nullable().describe("SEO description/meta description")
8005
8682
  }).describe("SEO metadata for search engine optimization"),
8006
- createdAt: z41.string().describe("Creation timestamp (ISO 8601)"),
8007
- updatedAt: z41.string().describe("Last update timestamp (ISO 8601)"),
8008
- variants: z41.array(
8009
- z41.object({
8010
- id: z41.string().describe("Variant GID"),
8011
- title: z41.string().describe("Variant title"),
8012
- price: z41.string().describe("Price"),
8013
- compareAtPrice: z41.string().nullable().describe("Compare at price"),
8014
- sku: z41.string().nullable().describe("SKU"),
8015
- barcode: z41.string().nullable().describe("Barcode"),
8016
- inventoryQuantity: z41.number().nullable().describe("Inventory quantity")
8683
+ createdAt: z42.string().describe("Creation timestamp (ISO 8601)"),
8684
+ updatedAt: z42.string().describe("Last update timestamp (ISO 8601)"),
8685
+ variants: z42.array(
8686
+ z42.object({
8687
+ id: z42.string().describe("Variant GID"),
8688
+ title: z42.string().describe("Variant title"),
8689
+ price: z42.string().describe("Price"),
8690
+ compareAtPrice: z42.string().nullable().describe("Compare at price"),
8691
+ sku: z42.string().nullable().describe("SKU"),
8692
+ barcode: z42.string().nullable().describe("Barcode"),
8693
+ inventoryQuantity: z42.number().nullable().describe("Inventory quantity")
8017
8694
  })
8018
8695
  ).describe("Product variants"),
8019
- images: z41.array(
8020
- z41.object({
8021
- id: z41.string().describe("Image GID"),
8022
- url: z41.string().describe("Image URL"),
8023
- altText: z41.string().nullable().describe("Alt text")
8696
+ images: z42.array(
8697
+ z42.object({
8698
+ id: z42.string().describe("Image GID"),
8699
+ url: z42.string().describe("Image URL"),
8700
+ altText: z42.string().nullable().describe("Alt text")
8024
8701
  })
8025
8702
  ).describe("Product images"),
8026
- totalInventory: z41.number().describe("Total inventory across variants")
8703
+ totalInventory: z42.number().describe("Total inventory across variants")
8027
8704
  });
8028
8705
  var handleUpdateProduct = async (context, params) => {
8029
8706
  log.debug(`Updating product on shop: ${context.shopDomain}`);
@@ -8060,6 +8737,14 @@ function registerUpdateProductTool() {
8060
8737
  relatedTools: ["list-products", "get-product"],
8061
8738
  prerequisites: ["get-product"],
8062
8739
  followUps: ["add-product-image", "set-product-metafields"]
8740
+ },
8741
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8742
+ annotations: {
8743
+ readOnlyHint: false,
8744
+ destructiveHint: false,
8745
+ idempotentHint: true,
8746
+ // Same update produces same result
8747
+ openWorldHint: true
8063
8748
  }
8064
8749
  },
8065
8750
  handleUpdateProduct
@@ -8075,7 +8760,9 @@ function setupToolHandlers(server) {
8075
8760
  tools: tools.map((tool) => ({
8076
8761
  name: tool.name,
8077
8762
  description: tool.description,
8078
- inputSchema: tool.inputSchema
8763
+ inputSchema: tool.inputSchema,
8764
+ // Include MCP tool annotations (Epic 9.5 - MCP Best Practices)
8765
+ annotations: tool.annotations
8079
8766
  }))
8080
8767
  };
8081
8768
  });