@anton.andrusenko/shopify-mcp-admin 0.2.0 → 0.4.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 +31 -0
  2. package/dist/index.js +1755 -754
  3. package/package.json +12 -2
package/dist/index.js CHANGED
@@ -22,7 +22,10 @@ var configSchema = z.object({
22
22
  PORT: z.string().default("3000").transform(Number),
23
23
  // Transport selection (AC-2.2.6, AC-2.2.7)
24
24
  // Default: stdio for Claude Desktop compatibility
25
- TRANSPORT: z.enum(["stdio", "http"]).default("stdio")
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)")
26
29
  }).refine(
27
30
  (data) => {
28
31
  const hasLegacyAuth = !!data.SHOPIFY_ACCESS_TOKEN;
@@ -936,7 +939,7 @@ function transformShopResponse(shop) {
936
939
  async function getStoreInfo() {
937
940
  const now = Date.now();
938
941
  const config = getConfig();
939
- 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;
940
943
  if (_cachedStoreInfo && now - _cacheTimestamp < ttl) {
941
944
  log.debug("Returning cached store info");
942
945
  return _cachedStoreInfo;
@@ -963,6 +966,42 @@ async function getStoreInfo() {
963
966
  var require2 = createRequire(import.meta.url);
964
967
  var packageJson = require2("../package.json");
965
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`;
966
1005
  function getServerVersion() {
967
1006
  return packageJson.version;
968
1007
  }
@@ -978,9 +1017,13 @@ function createServer() {
978
1017
  capabilities: {
979
1018
  tools: {},
980
1019
  // Enable tools capability (AC-2.1.4)
981
- resources: {}
1020
+ resources: {},
982
1021
  // Enable resources capability (AC-2.1.4)
983
- }
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)
984
1027
  }
985
1028
  );
986
1029
  return server;
@@ -2334,6 +2377,21 @@ function createSingleTenantContext(client, shopDomain) {
2334
2377
 
2335
2378
  // src/tools/registration.ts
2336
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-");
2384
+ return {
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
2393
+ };
2394
+ }
2337
2395
  var registeredTools = /* @__PURE__ */ new Map();
2338
2396
  function validateToolName(name) {
2339
2397
  if (!name || name.trim() === "") {
@@ -2393,7 +2451,7 @@ function wrapToolHandler(toolName, schema, handler) {
2393
2451
  };
2394
2452
  }
2395
2453
  function registerTool(definition, handler, options = {}) {
2396
- const { name, title, description, inputSchema: inputSchema41 } = definition;
2454
+ const { name, title, description, inputSchema: inputSchema46, annotations } = definition;
2397
2455
  try {
2398
2456
  if (!options.skipNameValidation) {
2399
2457
  validateToolName(name);
@@ -2401,15 +2459,22 @@ function registerTool(definition, handler, options = {}) {
2401
2459
  if (registeredTools.has(name)) {
2402
2460
  throw new Error(`Tool "${name}" is already registered. Tool names must be unique.`);
2403
2461
  }
2404
- const jsonSchema = convertZodToJsonSchema(inputSchema41);
2405
- const wrappedHandler = wrapToolHandler(name, inputSchema41, handler);
2462
+ const jsonSchema = convertZodToJsonSchema(inputSchema46);
2463
+ const wrappedHandler = wrapToolHandler(name, inputSchema46, handler);
2464
+ const finalAnnotations = {
2465
+ ...deriveDefaultAnnotations(name),
2466
+ ...annotations,
2467
+ // Use definition title as fallback for annotation title
2468
+ title: annotations?.title ?? title
2469
+ };
2406
2470
  const registeredTool = {
2407
2471
  name,
2408
2472
  title,
2409
2473
  description,
2410
2474
  inputSchema: jsonSchema,
2411
- zodSchema: inputSchema41,
2412
- handler: wrappedHandler
2475
+ zodSchema: inputSchema46,
2476
+ handler: wrappedHandler,
2477
+ annotations: finalAnnotations
2413
2478
  };
2414
2479
  registeredTools.set(name, registeredTool);
2415
2480
  log.debug(`Registered tool: ${name}`);
@@ -2428,7 +2493,8 @@ function getRegisteredTools() {
2428
2493
  return Array.from(registeredTools.values()).map((tool) => ({
2429
2494
  name: tool.name,
2430
2495
  description: `${tool.title}: ${tool.description}`,
2431
- inputSchema: tool.inputSchema
2496
+ inputSchema: tool.inputSchema,
2497
+ annotations: tool.annotations
2432
2498
  }));
2433
2499
  }
2434
2500
  function getToolByName(name) {
@@ -2448,7 +2514,7 @@ function registerContextAwareTool(definition, handler, options = {}) {
2448
2514
  }
2449
2515
 
2450
2516
  // src/tools/add-product-image.ts
2451
- import { z as z2 } from "zod";
2517
+ import { z as z3 } from "zod";
2452
2518
 
2453
2519
  // src/shopify/mutations.ts
2454
2520
  var PRODUCT_CREATE_MUTATION = `
@@ -3088,22 +3154,42 @@ async function addProductImage(productId, input) {
3088
3154
  };
3089
3155
  }
3090
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
+ var marketIdSchema = gidSchema("Market");
3176
+
3091
3177
  // src/tools/add-product-image.ts
3092
- var inputSchema = z2.object({
3093
- productId: z2.string().min(1).describe(
3178
+ var inputSchema = z3.object({
3179
+ productId: productIdSchema.describe(
3094
3180
  'Shopify product GID (e.g., "gid://shopify/Product/123"). Required. Use list-products to find product IDs.'
3095
3181
  ),
3096
- url: z2.string().url().describe(
3182
+ url: z3.string().url().describe(
3097
3183
  "Publicly accessible image URL. Shopify will fetch and host the image. Required. Supported formats: JPEG, PNG, GIF, WEBP."
3098
3184
  ),
3099
- altText: z2.string().optional().describe(
3185
+ altText: z3.string().optional().describe(
3100
3186
  "Alt text for accessibility and SEO. Describes the image for screen readers. Recommended for better accessibility and search rankings."
3101
3187
  )
3102
3188
  });
3103
- var outputSchema = z2.object({
3104
- id: z2.string().describe('Image GID (e.g., "gid://shopify/MediaImage/123")'),
3105
- url: z2.string().describe("Shopify CDN URL for the image"),
3106
- altText: z2.string().nullable().describe("Alt text for the image")
3189
+ var outputSchema = z3.object({
3190
+ id: z3.string().describe('Image GID (e.g., "gid://shopify/MediaImage/123")'),
3191
+ url: z3.string().describe("Shopify CDN URL for the image"),
3192
+ altText: z3.string().nullable().describe("Alt text for the image")
3107
3193
  });
3108
3194
  var handleAddProductImage = async (context, params) => {
3109
3195
  log.debug(`Adding image to product on shop: ${context.shopDomain}`);
@@ -3161,6 +3247,14 @@ function registerAddProductImageTool() {
3161
3247
  relatedTools: ["get-product", "update-product-image"],
3162
3248
  prerequisites: ["create-product"],
3163
3249
  followUps: ["update-product-image", "reorder-product-images"]
3250
+ },
3251
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
3252
+ annotations: {
3253
+ readOnlyHint: false,
3254
+ destructiveHint: false,
3255
+ idempotentHint: false,
3256
+ // Adding same URL creates duplicate images!
3257
+ openWorldHint: true
3164
3258
  }
3165
3259
  },
3166
3260
  handleAddProductImage
@@ -3168,7 +3262,7 @@ function registerAddProductImageTool() {
3168
3262
  }
3169
3263
 
3170
3264
  // src/tools/add-products-to-collection.ts
3171
- import { z as z3 } from "zod";
3265
+ import { z as z4 } from "zod";
3172
3266
 
3173
3267
  // src/shopify/collections.ts
3174
3268
  var GET_COLLECTION_QUERY = `
@@ -3569,18 +3663,18 @@ async function removeProductsFromCollection(collectionId, productIds) {
3569
3663
  }
3570
3664
 
3571
3665
  // src/tools/add-products-to-collection.ts
3572
- var inputSchema2 = z3.object({
3573
- collectionId: z3.string().min(1).describe(
3666
+ var inputSchema2 = z4.object({
3667
+ collectionId: collectionIdSchema.describe(
3574
3668
  'Collection GID (e.g., "gid://shopify/Collection/123"). Use list-collections to find valid collection IDs.'
3575
3669
  ),
3576
- productIds: z3.array(z3.string().min(1)).min(1).max(250).describe(
3670
+ productIds: z4.array(productIdSchema).min(1).max(250).describe(
3577
3671
  '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.'
3578
3672
  )
3579
3673
  });
3580
- var outputSchema2 = z3.object({
3581
- collectionId: z3.string().describe("Collection GID"),
3582
- productsCount: z3.number().describe("Total number of products now in the collection"),
3583
- addedCount: z3.number().describe("Number of products added in this operation")
3674
+ var outputSchema2 = z4.object({
3675
+ collectionId: z4.string().describe("Collection GID"),
3676
+ productsCount: z4.number().describe("Total number of products now in the collection"),
3677
+ addedCount: z4.number().describe("Number of products added in this operation")
3584
3678
  });
3585
3679
  var handleAddProductsToCollection = async (context, params) => {
3586
3680
  log.debug(
@@ -3624,6 +3718,14 @@ function registerAddProductsToCollectionTool() {
3624
3718
  relatedTools: ["remove-products-from-collection", "list-products"],
3625
3719
  prerequisites: ["create-collection", "list-products"],
3626
3720
  followUps: ["get-collection"]
3721
+ },
3722
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
3723
+ annotations: {
3724
+ readOnlyHint: false,
3725
+ destructiveHint: false,
3726
+ idempotentHint: true,
3727
+ // Adding same products is idempotent (no-op)
3728
+ openWorldHint: true
3627
3729
  }
3628
3730
  },
3629
3731
  handleAddProductsToCollection
@@ -3631,7 +3733,7 @@ function registerAddProductsToCollectionTool() {
3631
3733
  }
3632
3734
 
3633
3735
  // src/tools/create-article.ts
3634
- import { z as z4 } from "zod";
3736
+ import { z as z5 } from "zod";
3635
3737
 
3636
3738
  // src/shopify/blogs.ts
3637
3739
  var LIST_BLOGS_QUERY = `
@@ -4137,51 +4239,51 @@ async function deleteArticle(articleId) {
4137
4239
  }
4138
4240
 
4139
4241
  // src/tools/create-article.ts
4140
- var inputSchema3 = z4.object({
4141
- blogId: z4.string().describe(
4242
+ var inputSchema3 = z5.object({
4243
+ blogId: blogIdSchema.describe(
4142
4244
  '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.'
4143
4245
  ),
4144
- title: z4.string().min(1).describe(
4246
+ title: z5.string().min(1).describe(
4145
4247
  "The title of the article (required). This will be displayed as the article heading. Example: 'How to Use Our Products', '10 Tips for Beginners'."
4146
4248
  ),
4147
- authorName: z4.string().min(1).describe(
4249
+ authorName: z5.string().min(1).describe(
4148
4250
  "The name of the article author (required). This is displayed on the article. Example: 'John Doe', 'Marketing Team'."
4149
4251
  ),
4150
- body: z4.string().optional().describe(
4252
+ body: z5.string().optional().describe(
4151
4253
  "The HTML body content of the article. Supports HTML markup for formatting. Example: '<h2>Introduction</h2><p>Welcome to our guide...</p>'"
4152
4254
  ),
4153
- summary: z4.string().optional().describe(
4255
+ summary: z5.string().optional().describe(
4154
4256
  "A summary or excerpt of the article. Used for previews on blog listing pages. Can include HTML markup."
4155
4257
  ),
4156
- tags: z4.array(z4.string()).optional().describe(
4258
+ tags: z5.array(z5.string()).optional().describe(
4157
4259
  "Tags for categorization. Helps organize articles and improve discoverability. Example: ['guide', 'tutorial', 'beginner']."
4158
4260
  ),
4159
- image: z4.object({
4160
- url: z4.string().url().describe("The URL of the featured image"),
4161
- altText: z4.string().optional().describe("Alt text for accessibility")
4261
+ image: z5.object({
4262
+ url: z5.string().url().describe("The URL of the featured image"),
4263
+ altText: z5.string().optional().describe("Alt text for accessibility")
4162
4264
  }).optional().describe("Featured image for the article."),
4163
- isPublished: z4.boolean().optional().describe(
4265
+ isPublished: z5.boolean().optional().describe(
4164
4266
  "Whether the article should be visible on the storefront. Defaults to false (unpublished). Set to true to make the article immediately visible."
4165
4267
  ),
4166
- publishDate: z4.string().optional().describe(
4268
+ publishDate: z5.string().optional().describe(
4167
4269
  "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."
4168
4270
  ),
4169
- handle: z4.string().optional().describe(
4271
+ handle: z5.string().optional().describe(
4170
4272
  "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."
4171
4273
  ),
4172
- templateSuffix: z4.string().optional().describe(
4274
+ templateSuffix: z5.string().optional().describe(
4173
4275
  "The suffix of the Liquid template used to render the article. For example, 'featured' would use the template 'article.featured.liquid'."
4174
4276
  )
4175
4277
  });
4176
- var outputSchema3 = z4.object({
4177
- id: z4.string().describe('Created article GID (e.g., "gid://shopify/Article/123")'),
4178
- title: z4.string().describe("Article title"),
4179
- handle: z4.string().describe("URL handle/slug"),
4180
- blog: z4.object({
4181
- id: z4.string().describe("Parent blog GID"),
4182
- title: z4.string().describe("Parent blog title")
4278
+ var outputSchema3 = z5.object({
4279
+ id: z5.string().describe('Created article GID (e.g., "gid://shopify/Article/123")'),
4280
+ title: z5.string().describe("Article title"),
4281
+ handle: z5.string().describe("URL handle/slug"),
4282
+ blog: z5.object({
4283
+ id: z5.string().describe("Parent blog GID"),
4284
+ title: z5.string().describe("Parent blog title")
4183
4285
  }),
4184
- isPublished: z4.boolean().describe("Whether article is visible on storefront")
4286
+ isPublished: z5.boolean().describe("Whether article is visible on storefront")
4185
4287
  });
4186
4288
  var handleCreateArticle = async (context, params) => {
4187
4289
  log.debug(`Creating article on shop: ${context.shopDomain}`);
@@ -4237,6 +4339,14 @@ function registerCreateArticleTool() {
4237
4339
  relatedTools: ["list-blogs", "list-articles", "update-article"],
4238
4340
  prerequisites: ["list-blogs"],
4239
4341
  followUps: ["list-articles", "update-article"]
4342
+ },
4343
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4344
+ annotations: {
4345
+ readOnlyHint: false,
4346
+ destructiveHint: false,
4347
+ idempotentHint: false,
4348
+ // create-* tools are NOT idempotent
4349
+ openWorldHint: true
4240
4350
  }
4241
4351
  },
4242
4352
  handleCreateArticle
@@ -4244,26 +4354,26 @@ function registerCreateArticleTool() {
4244
4354
  }
4245
4355
 
4246
4356
  // src/tools/create-blog.ts
4247
- import { z as z5 } from "zod";
4248
- var inputSchema4 = z5.object({
4249
- title: z5.string().min(1).describe(
4357
+ import { z as z6 } from "zod";
4358
+ var inputSchema4 = z6.object({
4359
+ title: z6.string().min(1).describe(
4250
4360
  "The title of the blog (required). This will be displayed as the blog heading. Example: 'Company News', 'Product Guides', 'Industry Insights'."
4251
4361
  ),
4252
- handle: z5.string().optional().describe(
4362
+ handle: z6.string().optional().describe(
4253
4363
  "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."
4254
4364
  ),
4255
- commentPolicy: z5.enum(["CLOSED", "MODERATE", "OPEN"]).optional().describe(
4365
+ commentPolicy: z6.enum(["CLOSED", "MODERATE", "OPEN"]).optional().describe(
4256
4366
  "Comment moderation policy for the blog. CLOSED: Comments disabled. MODERATE: Comments require approval. OPEN: Comments appear immediately. Defaults to MODERATE if not specified."
4257
4367
  ),
4258
- templateSuffix: z5.string().optional().describe(
4368
+ templateSuffix: z6.string().optional().describe(
4259
4369
  "The suffix of the Liquid template used to render the blog. For example, 'news' would use the template 'blog.news.liquid'."
4260
4370
  )
4261
4371
  });
4262
- var outputSchema4 = z5.object({
4263
- id: z5.string().describe('Created blog GID (e.g., "gid://shopify/Blog/123")'),
4264
- title: z5.string().describe("Blog title"),
4265
- handle: z5.string().describe("URL handle/slug"),
4266
- commentPolicy: z5.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy")
4372
+ var outputSchema4 = z6.object({
4373
+ id: z6.string().describe('Created blog GID (e.g., "gid://shopify/Blog/123")'),
4374
+ title: z6.string().describe("Blog title"),
4375
+ handle: z6.string().describe("URL handle/slug"),
4376
+ commentPolicy: z6.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy")
4267
4377
  });
4268
4378
  var handleCreateBlog = async (context, params) => {
4269
4379
  log.debug(`Creating blog on shop: ${context.shopDomain}`);
@@ -4305,6 +4415,14 @@ function registerCreateBlogTool() {
4305
4415
  relationships: {
4306
4416
  relatedTools: ["list-blogs", "update-blog", "create-article"],
4307
4417
  followUps: ["create-article", "list-blogs"]
4418
+ },
4419
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4420
+ annotations: {
4421
+ readOnlyHint: false,
4422
+ destructiveHint: false,
4423
+ idempotentHint: false,
4424
+ // create-* tools are NOT idempotent
4425
+ openWorldHint: true
4308
4426
  }
4309
4427
  },
4310
4428
  handleCreateBlog
@@ -4312,20 +4430,20 @@ function registerCreateBlogTool() {
4312
4430
  }
4313
4431
 
4314
4432
  // src/tools/create-collection.ts
4315
- import { z as z6 } from "zod";
4316
- var inputSchema5 = z6.object({
4317
- title: z6.string().min(1).describe("Collection title (required). This will be displayed to customers."),
4318
- handle: z6.string().optional().describe(
4433
+ import { z as z7 } from "zod";
4434
+ var inputSchema5 = z7.object({
4435
+ title: z7.string().min(1).describe("Collection title (required). This will be displayed to customers."),
4436
+ handle: z7.string().optional().describe(
4319
4437
  'URL handle/slug for the collection (e.g., "summer-sale"). Auto-generated from title if not provided.'
4320
4438
  ),
4321
- descriptionHtml: z6.string().optional().describe(
4439
+ descriptionHtml: z7.string().optional().describe(
4322
4440
  "Collection description with HTML formatting. Displayed on collection pages in the store."
4323
4441
  ),
4324
- seo: z6.object({
4325
- title: z6.string().optional().describe("SEO title for search engines"),
4326
- description: z6.string().optional().describe("SEO meta description for search engines")
4442
+ seo: z7.object({
4443
+ title: z7.string().optional().describe("SEO title for search engines"),
4444
+ description: z7.string().optional().describe("SEO meta description for search engines")
4327
4445
  }).optional().describe("SEO metadata to optimize collection visibility in search results"),
4328
- sortOrder: z6.enum([
4446
+ sortOrder: z7.enum([
4329
4447
  "ALPHA_ASC",
4330
4448
  "ALPHA_DESC",
4331
4449
  "BEST_SELLING",
@@ -4337,18 +4455,18 @@ var inputSchema5 = z6.object({
4337
4455
  ]).optional().describe(
4338
4456
  "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)"
4339
4457
  ),
4340
- image: z6.object({
4341
- src: z6.string().url().describe("Publicly accessible image URL"),
4342
- altText: z6.string().optional().describe("Alt text for accessibility and SEO")
4458
+ image: z7.object({
4459
+ src: z7.string().url().describe("Publicly accessible image URL"),
4460
+ altText: z7.string().optional().describe("Alt text for accessibility and SEO")
4343
4461
  }).optional().describe("Collection image displayed on collection pages"),
4344
- templateSuffix: z6.string().optional().describe(
4462
+ templateSuffix: z7.string().optional().describe(
4345
4463
  'Liquid template suffix for custom collection templates. For example, "featured" uses collection.featured.liquid'
4346
4464
  )
4347
4465
  });
4348
- var outputSchema5 = z6.object({
4349
- id: z6.string().describe('Created collection GID (e.g., "gid://shopify/Collection/123")'),
4350
- title: z6.string().describe("Collection title"),
4351
- handle: z6.string().describe("Collection URL handle")
4466
+ var outputSchema5 = z7.object({
4467
+ id: z7.string().describe('Created collection GID (e.g., "gid://shopify/Collection/123")'),
4468
+ title: z7.string().describe("Collection title"),
4469
+ handle: z7.string().describe("Collection URL handle")
4352
4470
  });
4353
4471
  var handleCreateCollection = async (context, params) => {
4354
4472
  log.debug(`Creating collection on shop: ${context.shopDomain}`);
@@ -4387,14 +4505,352 @@ function registerCreateCollectionTool() {
4387
4505
  relationships: {
4388
4506
  relatedTools: ["list-collections", "get-collection"],
4389
4507
  followUps: ["add-products-to-collection", "update-collection"]
4508
+ },
4509
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4510
+ annotations: {
4511
+ readOnlyHint: false,
4512
+ destructiveHint: false,
4513
+ idempotentHint: false,
4514
+ // Each call creates a new collection
4515
+ openWorldHint: true
4390
4516
  }
4391
4517
  },
4392
4518
  handleCreateCollection
4393
4519
  );
4394
4520
  }
4395
4521
 
4522
+ // src/tools/create-market.ts
4523
+ import { z as z8 } from "zod";
4524
+
4525
+ // src/shopify/markets.ts
4526
+ var LIST_MARKETS_QUERY = `
4527
+ query Markets($first: Int!, $after: String) {
4528
+ markets(first: $first, after: $after) {
4529
+ nodes {
4530
+ id
4531
+ name
4532
+ handle
4533
+ enabled
4534
+ status
4535
+ type
4536
+ currencySettings {
4537
+ baseCurrency { currencyCode }
4538
+ localCurrencies
4539
+ }
4540
+ }
4541
+ pageInfo {
4542
+ hasNextPage
4543
+ endCursor
4544
+ }
4545
+ }
4546
+ }
4547
+ `;
4548
+ var GET_MARKET_QUERY = `
4549
+ query Market($id: ID!) {
4550
+ market(id: $id) {
4551
+ id
4552
+ name
4553
+ handle
4554
+ enabled
4555
+ status
4556
+ type
4557
+ currencySettings {
4558
+ baseCurrency { currencyCode currencyName }
4559
+ localCurrencies
4560
+ }
4561
+ webPresences(first: 10) {
4562
+ nodes {
4563
+ id
4564
+ defaultLocale { locale name }
4565
+ alternateLocales { locale name }
4566
+ subfolderSuffix
4567
+ domain { host }
4568
+ rootUrls { locale url }
4569
+ }
4570
+ }
4571
+ }
4572
+ }
4573
+ `;
4574
+ var MARKET_CREATE_MUTATION = `
4575
+ mutation MarketCreate($input: MarketCreateInput!) {
4576
+ marketCreate(input: $input) {
4577
+ market { id name handle enabled }
4578
+ userErrors { field message }
4579
+ }
4580
+ }
4581
+ `;
4582
+ var MARKET_UPDATE_MUTATION = `
4583
+ mutation MarketUpdate($id: ID!, $input: MarketUpdateInput!) {
4584
+ marketUpdate(id: $id, input: $input) {
4585
+ market { id name handle enabled }
4586
+ userErrors { field message }
4587
+ }
4588
+ }
4589
+ `;
4590
+ var MARKET_DELETE_MUTATION = `
4591
+ mutation MarketDelete($id: ID!) {
4592
+ marketDelete(id: $id) {
4593
+ deletedId
4594
+ userErrors { field message }
4595
+ }
4596
+ }
4597
+ `;
4598
+ async function listMarkets(first = 50, after) {
4599
+ const client = await getShopifyClient();
4600
+ log.debug(`Listing markets: first=${first}, after=${after}`);
4601
+ const response = await withRateLimit(
4602
+ () => client.graphql.request(LIST_MARKETS_QUERY, {
4603
+ variables: { first, after }
4604
+ }),
4605
+ "list-markets"
4606
+ );
4607
+ if (response.errors && response.errors.length > 0) {
4608
+ const errorMessage = response.errors.map((e) => e.message).join(", ");
4609
+ throw new Error(`GraphQL error: ${errorMessage}`);
4610
+ }
4611
+ const marketsData = response.data?.markets;
4612
+ if (!marketsData) {
4613
+ throw new Error("No response data from markets query");
4614
+ }
4615
+ const markets = marketsData.nodes.map((node) => ({
4616
+ id: node.id,
4617
+ name: node.name,
4618
+ handle: node.handle,
4619
+ enabled: node.enabled,
4620
+ status: node.status,
4621
+ type: node.type,
4622
+ currencySettings: node.currencySettings ? {
4623
+ baseCurrency: {
4624
+ currencyCode: node.currencySettings.baseCurrency.currencyCode
4625
+ },
4626
+ localCurrencies: node.currencySettings.localCurrencies
4627
+ } : void 0
4628
+ }));
4629
+ log.debug(`Listed ${markets.length} markets`);
4630
+ return {
4631
+ markets,
4632
+ pageInfo: {
4633
+ hasNextPage: marketsData.pageInfo.hasNextPage,
4634
+ endCursor: marketsData.pageInfo.endCursor
4635
+ }
4636
+ };
4637
+ }
4638
+ async function getMarket(marketId) {
4639
+ const client = await getShopifyClient();
4640
+ log.debug(`Fetching market: ${marketId}`);
4641
+ const response = await withRateLimit(
4642
+ () => client.graphql.request(GET_MARKET_QUERY, {
4643
+ variables: { id: marketId }
4644
+ }),
4645
+ "get-market"
4646
+ );
4647
+ if (response.errors && response.errors.length > 0) {
4648
+ const errorMessage = response.errors.map((e) => e.message).join(", ");
4649
+ throw new Error(`GraphQL error: ${errorMessage}`);
4650
+ }
4651
+ const market = response.data?.market;
4652
+ if (!market) {
4653
+ log.debug(`Market not found: ${marketId}`);
4654
+ return null;
4655
+ }
4656
+ return {
4657
+ id: market.id,
4658
+ name: market.name,
4659
+ handle: market.handle,
4660
+ enabled: market.enabled,
4661
+ status: market.status,
4662
+ type: market.type,
4663
+ currencySettings: market.currencySettings ? {
4664
+ baseCurrency: {
4665
+ currencyCode: market.currencySettings.baseCurrency.currencyCode,
4666
+ currencyName: market.currencySettings.baseCurrency.currencyName
4667
+ },
4668
+ localCurrencies: market.currencySettings.localCurrencies
4669
+ } : void 0,
4670
+ webPresences: market.webPresences?.nodes.map((wp) => ({
4671
+ id: wp.id,
4672
+ defaultLocale: {
4673
+ locale: wp.defaultLocale.locale,
4674
+ name: wp.defaultLocale.name
4675
+ },
4676
+ alternateLocales: wp.alternateLocales?.map((al) => ({
4677
+ locale: al.locale,
4678
+ name: al.name
4679
+ })),
4680
+ subfolderSuffix: wp.subfolderSuffix ?? void 0,
4681
+ domain: wp.domain ? { host: wp.domain.host } : void 0,
4682
+ rootUrls: wp.rootUrls?.map((ru) => ({
4683
+ locale: ru.locale,
4684
+ url: ru.url
4685
+ }))
4686
+ }))
4687
+ };
4688
+ }
4689
+ async function createMarket(input) {
4690
+ const client = await getShopifyClient();
4691
+ log.debug(`Creating market: ${input.name}`);
4692
+ const graphqlInput = {
4693
+ name: input.name
4694
+ };
4695
+ if (input.handle !== void 0) {
4696
+ graphqlInput.handle = input.handle;
4697
+ }
4698
+ if (input.regions !== void 0) {
4699
+ graphqlInput.regions = input.regions;
4700
+ }
4701
+ if (input.enabled !== void 0) {
4702
+ graphqlInput.enabled = input.enabled;
4703
+ }
4704
+ const response = await withRateLimit(
4705
+ () => client.graphql.request(MARKET_CREATE_MUTATION, {
4706
+ variables: { input: graphqlInput }
4707
+ }),
4708
+ "create-market"
4709
+ );
4710
+ if (response.errors && response.errors.length > 0) {
4711
+ const errorMessage = response.errors.map((e) => e.message).join(", ");
4712
+ throw new Error(`GraphQL error: ${errorMessage}`);
4713
+ }
4714
+ const result = response.data?.marketCreate;
4715
+ if (!result) {
4716
+ throw new Error("No response data from marketCreate mutation");
4717
+ }
4718
+ if (result.userErrors && result.userErrors.length > 0) {
4719
+ throw new ShopifyUserErrorException(result.userErrors);
4720
+ }
4721
+ if (!result.market) {
4722
+ throw new Error("Market creation succeeded but no market was returned");
4723
+ }
4724
+ log.debug(`Created market: ${result.market.id}`);
4725
+ return result.market;
4726
+ }
4727
+ async function updateMarket(marketId, input) {
4728
+ const client = await getShopifyClient();
4729
+ log.debug(`Updating market: ${marketId}`);
4730
+ const graphqlInput = {};
4731
+ if (input.name !== void 0) {
4732
+ graphqlInput.name = input.name;
4733
+ }
4734
+ if (input.handle !== void 0) {
4735
+ graphqlInput.handle = input.handle;
4736
+ }
4737
+ if (input.enabled !== void 0) {
4738
+ graphqlInput.enabled = input.enabled;
4739
+ }
4740
+ const response = await withRateLimit(
4741
+ () => client.graphql.request(MARKET_UPDATE_MUTATION, {
4742
+ variables: { id: marketId, input: graphqlInput }
4743
+ }),
4744
+ "update-market"
4745
+ );
4746
+ if (response.errors && response.errors.length > 0) {
4747
+ const errorMessage = response.errors.map((e) => e.message).join(", ");
4748
+ throw new Error(`GraphQL error: ${errorMessage}`);
4749
+ }
4750
+ const result = response.data?.marketUpdate;
4751
+ if (!result) {
4752
+ throw new Error("No response data from marketUpdate mutation");
4753
+ }
4754
+ if (result.userErrors && result.userErrors.length > 0) {
4755
+ throw new ShopifyUserErrorException(result.userErrors);
4756
+ }
4757
+ if (!result.market) {
4758
+ throw new Error("Market update succeeded but no market was returned");
4759
+ }
4760
+ log.debug(`Updated market: ${result.market.id}`);
4761
+ return result.market;
4762
+ }
4763
+ async function deleteMarket(marketId) {
4764
+ const client = await getShopifyClient();
4765
+ log.debug(`Deleting market: ${marketId}`);
4766
+ const response = await withRateLimit(
4767
+ () => client.graphql.request(MARKET_DELETE_MUTATION, {
4768
+ variables: { id: marketId }
4769
+ }),
4770
+ "delete-market"
4771
+ );
4772
+ if (response.errors && response.errors.length > 0) {
4773
+ const errorMessage = response.errors.map((e) => e.message).join(", ");
4774
+ throw new Error(`GraphQL error: ${errorMessage}`);
4775
+ }
4776
+ const result = response.data?.marketDelete;
4777
+ if (!result) {
4778
+ throw new Error("No response data from marketDelete mutation");
4779
+ }
4780
+ if (result.userErrors && result.userErrors.length > 0) {
4781
+ throw new ShopifyUserErrorException(result.userErrors);
4782
+ }
4783
+ if (!result.deletedId) {
4784
+ throw new Error("Market deletion succeeded but no deletedId was returned");
4785
+ }
4786
+ log.debug(`Deleted market: ${result.deletedId}`);
4787
+ return result.deletedId;
4788
+ }
4789
+
4790
+ // src/tools/create-market.ts
4791
+ var inputSchema6 = z8.object({
4792
+ name: z8.string().min(1).describe('Market name (required). Not shown to customers. Example: "Canada", "Europe"'),
4793
+ handle: z8.string().optional().describe(
4794
+ 'URL handle/slug for the market. Auto-generated from name if not provided. Example: "ca" for Canada, "eu" for Europe'
4795
+ ),
4796
+ countryCodes: z8.array(z8.string().length(2)).optional().describe(
4797
+ 'Array of ISO 3166-1 alpha-2 country codes to include in this market. Example: ["CA"] for Canada, ["FR", "DE", "IT"] for multiple European countries'
4798
+ ),
4799
+ enabled: z8.boolean().optional().default(true).describe("Whether the market should be enabled. Default: true")
4800
+ });
4801
+ var outputSchema6 = z8.object({
4802
+ id: z8.string().describe("Created market GID"),
4803
+ name: z8.string().describe("Market name"),
4804
+ handle: z8.string().describe("URL handle"),
4805
+ enabled: z8.boolean().describe("Whether the market is enabled")
4806
+ });
4807
+ var handleCreateMarket = async (context, params) => {
4808
+ log.debug(`Creating market "${params.name}" on shop: ${context.shopDomain}`);
4809
+ const input = {
4810
+ name: params.name
4811
+ };
4812
+ if (params.handle !== void 0) {
4813
+ input.handle = params.handle;
4814
+ }
4815
+ if (params.countryCodes !== void 0 && params.countryCodes.length > 0) {
4816
+ input.regions = { countryCodes: params.countryCodes };
4817
+ }
4818
+ if (params.enabled !== void 0) {
4819
+ input.enabled = params.enabled;
4820
+ }
4821
+ const market = await createMarket(input);
4822
+ log.debug(`Created market: ${market.id}`);
4823
+ return market;
4824
+ };
4825
+ function registerCreateMarketTool() {
4826
+ registerContextAwareTool(
4827
+ {
4828
+ name: "create-market",
4829
+ title: "Create Market",
4830
+ description: "Create a new market for regional targeting. Markets group one or more regions (countries) for localized shopping experiences. Provide a name (required) and optionally: handle, country codes, and enabled status. **Typical workflow:** create-market \u2192 create-web-presence (configure SEO URLs) \u2192 enable-shop-locale (add languages). **Prerequisites:** None. Store must have Markets feature enabled on their plan. **Follow-ups:** get-market, create-web-presence, update-market.",
4831
+ inputSchema: inputSchema6,
4832
+ outputSchema: outputSchema6,
4833
+ // AI Agent Optimization
4834
+ category: "market",
4835
+ relationships: {
4836
+ relatedTools: ["list-markets", "get-market"],
4837
+ followUps: ["get-market", "update-market"]
4838
+ },
4839
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
4840
+ annotations: {
4841
+ readOnlyHint: false,
4842
+ destructiveHint: false,
4843
+ idempotentHint: false,
4844
+ // Creating markets is not idempotent
4845
+ openWorldHint: true
4846
+ }
4847
+ },
4848
+ handleCreateMarket
4849
+ );
4850
+ }
4851
+
4396
4852
  // src/tools/create-page.ts
4397
- import { z as z7 } from "zod";
4853
+ import { z as z9 } from "zod";
4398
4854
 
4399
4855
  // src/shopify/pages.ts
4400
4856
  var PAGE_QUERY = `
@@ -4676,28 +5132,28 @@ async function deletePage(pageId) {
4676
5132
  }
4677
5133
 
4678
5134
  // src/tools/create-page.ts
4679
- var inputSchema6 = z7.object({
4680
- title: z7.string().min(1).describe(
5135
+ var inputSchema7 = z9.object({
5136
+ title: z9.string().min(1).describe(
4681
5137
  "The title of the page (required). This will be displayed as the page heading. Example: 'About Us', 'Contact', 'Privacy Policy'."
4682
5138
  ),
4683
- body: z7.string().optional().describe(
5139
+ body: z9.string().optional().describe(
4684
5140
  "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>'"
4685
5141
  ),
4686
- handle: z7.string().optional().describe(
5142
+ handle: z9.string().optional().describe(
4687
5143
  "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."
4688
5144
  ),
4689
- isPublished: z7.boolean().optional().describe(
5145
+ isPublished: z9.boolean().optional().describe(
4690
5146
  "Whether the page should be visible on the storefront. Defaults to false (unpublished). Set to true to make the page immediately visible."
4691
5147
  ),
4692
- templateSuffix: z7.string().optional().describe(
5148
+ templateSuffix: z9.string().optional().describe(
4693
5149
  "The suffix of the Liquid template used to render the page. For example, 'contact' would use the template 'page.contact.liquid'."
4694
5150
  )
4695
5151
  });
4696
- var outputSchema6 = z7.object({
4697
- id: z7.string().describe('Created page GID (e.g., "gid://shopify/Page/123")'),
4698
- title: z7.string().describe("Page title"),
4699
- handle: z7.string().describe("URL handle/slug"),
4700
- isPublished: z7.boolean().describe("Whether page is visible on storefront")
5152
+ var outputSchema7 = z9.object({
5153
+ id: z9.string().describe('Created page GID (e.g., "gid://shopify/Page/123")'),
5154
+ title: z9.string().describe("Page title"),
5155
+ handle: z9.string().describe("URL handle/slug"),
5156
+ isPublished: z9.boolean().describe("Whether page is visible on storefront")
4701
5157
  });
4702
5158
  var handleCreatePage = async (context, params) => {
4703
5159
  log.debug(`Creating page on shop: ${context.shopDomain}`);
@@ -4734,12 +5190,20 @@ function registerCreatePageTool() {
4734
5190
  name: "create-page",
4735
5191
  title: "Create Page",
4736
5192
  description: "Create a new online store page. Pages are static content like 'About Us', 'Contact', 'FAQ', or policy pages. Provide a title (required) and optional body content (HTML supported), handle (URL slug), and publish status. New pages are unpublished by default. Useful for: creating informational pages, legal pages, landing pages. **Typical workflow:** create-page \u2192 update-page (to publish). **Prerequisites:** None. **Follow-ups:** get-page, update-page. Returns created page ID, title, handle, and publish status.",
4737
- inputSchema: inputSchema6,
4738
- outputSchema: outputSchema6,
5193
+ inputSchema: inputSchema7,
5194
+ outputSchema: outputSchema7,
4739
5195
  category: "content",
4740
5196
  relationships: {
4741
5197
  relatedTools: ["list-pages", "get-page", "update-page"],
4742
5198
  followUps: ["get-page", "update-page"]
5199
+ },
5200
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5201
+ annotations: {
5202
+ readOnlyHint: false,
5203
+ destructiveHint: false,
5204
+ idempotentHint: false,
5205
+ // create-* tools are NOT idempotent
5206
+ openWorldHint: true
4743
5207
  }
4744
5208
  },
4745
5209
  handleCreatePage
@@ -4747,78 +5211,78 @@ function registerCreatePageTool() {
4747
5211
  }
4748
5212
 
4749
5213
  // src/tools/create-product.ts
4750
- import { z as z8 } from "zod";
4751
- var inputSchema7 = z8.object({
5214
+ import { z as z10 } from "zod";
5215
+ var inputSchema8 = z10.object({
4752
5216
  // Required field
4753
- title: z8.string().min(1, "title is required").describe('Product title (required). Example: "Premium Cotton T-Shirt"'),
5217
+ title: z10.string().min(1, "title is required").describe('Product title (required). Example: "Premium Cotton T-Shirt"'),
4754
5218
  // Optional core fields
4755
- description: z8.string().optional().describe('Product description (HTML supported). Example: "<p>Soft cotton t-shirt.</p>"'),
4756
- vendor: z8.string().optional().describe('Product vendor/brand name. Example: "Acme Corp"'),
4757
- productType: z8.string().optional().describe('Product type for categorization. Example: "T-Shirts"'),
4758
- tags: z8.array(z8.string()).optional().describe('Tags for search and filtering. Example: ["summer", "cotton", "casual"]'),
4759
- status: z8.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().default("DRAFT").describe(
5219
+ description: z10.string().optional().describe('Product description (HTML supported). Example: "<p>Soft cotton t-shirt.</p>"'),
5220
+ vendor: z10.string().optional().describe('Product vendor/brand name. Example: "Acme Corp"'),
5221
+ productType: z10.string().optional().describe('Product type for categorization. Example: "T-Shirts"'),
5222
+ tags: z10.array(z10.string()).optional().describe('Tags for search and filtering. Example: ["summer", "cotton", "casual"]'),
5223
+ status: z10.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().default("DRAFT").describe(
4760
5224
  "Product status: ACTIVE (visible), DRAFT (hidden), ARCHIVED (hidden). Default: DRAFT"
4761
5225
  ),
4762
5226
  // SEO metadata
4763
- seo: z8.object({
4764
- title: z8.string().optional().describe("SEO title for search engine results"),
4765
- description: z8.string().optional().describe("SEO meta description for search engines")
5227
+ seo: z10.object({
5228
+ title: z10.string().optional().describe("SEO title for search engine results"),
5229
+ description: z10.string().optional().describe("SEO meta description for search engines")
4766
5230
  }).optional().describe(
4767
5231
  "SEO metadata for search engine optimization. If not provided, Shopify uses product title/description."
4768
5232
  ),
4769
5233
  // Variants
4770
- variants: z8.array(
4771
- z8.object({
4772
- price: z8.string().describe('Variant price as decimal string. Example: "29.99"'),
4773
- compareAtPrice: z8.string().optional().describe('Original price for sale display. Example: "39.99"'),
4774
- sku: z8.string().optional().describe('Stock keeping unit. Example: "SHIRT-001-RED-M"'),
4775
- barcode: z8.string().optional().describe("Barcode (UPC, EAN, etc.)"),
4776
- inventoryQuantity: z8.number().int().min(0).optional().describe("Initial inventory quantity (note: may require separate inventory API)"),
4777
- options: z8.array(z8.string()).optional().describe('Variant options. Example: ["Red", "Medium"]')
5234
+ variants: z10.array(
5235
+ z10.object({
5236
+ price: z10.string().describe('Variant price as decimal string. Example: "29.99"'),
5237
+ compareAtPrice: z10.string().optional().describe('Original price for sale display. Example: "39.99"'),
5238
+ sku: z10.string().optional().describe('Stock keeping unit. Example: "SHIRT-001-RED-M"'),
5239
+ barcode: z10.string().optional().describe("Barcode (UPC, EAN, etc.)"),
5240
+ inventoryQuantity: z10.number().int().min(0).optional().describe("Initial inventory quantity (note: may require separate inventory API)"),
5241
+ options: z10.array(z10.string()).optional().describe('Variant options. Example: ["Red", "Medium"]')
4778
5242
  })
4779
5243
  ).optional().describe("Product variants. If not provided, a default variant is created."),
4780
5244
  // Images
4781
- images: z8.array(
4782
- z8.object({
4783
- url: z8.string().url("Invalid image URL format").describe("Publicly accessible image URL"),
4784
- altText: z8.string().optional().describe("Alt text for accessibility")
5245
+ images: z10.array(
5246
+ z10.object({
5247
+ url: z10.string().url("Invalid image URL format").describe("Publicly accessible image URL"),
5248
+ altText: z10.string().optional().describe("Alt text for accessibility")
4785
5249
  })
4786
5250
  ).optional().describe("Product images. Shopify fetches images from URLs.")
4787
5251
  });
4788
- var outputSchema7 = z8.object({
4789
- id: z8.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
4790
- title: z8.string().describe("Product title"),
4791
- handle: z8.string().describe("URL handle/slug"),
4792
- description: z8.string().nullable().describe("Product description (HTML)"),
4793
- vendor: z8.string().describe("Vendor/brand name"),
4794
- productType: z8.string().describe("Product type"),
4795
- status: z8.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
4796
- tags: z8.array(z8.string()).describe("Product tags"),
4797
- seo: z8.object({
4798
- title: z8.string().nullable().describe("SEO title for search engines"),
4799
- description: z8.string().nullable().describe("SEO description/meta description")
5252
+ var outputSchema8 = z10.object({
5253
+ id: z10.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
5254
+ title: z10.string().describe("Product title"),
5255
+ handle: z10.string().describe("URL handle/slug"),
5256
+ description: z10.string().nullable().describe("Product description (HTML)"),
5257
+ vendor: z10.string().describe("Vendor/brand name"),
5258
+ productType: z10.string().describe("Product type"),
5259
+ status: z10.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
5260
+ tags: z10.array(z10.string()).describe("Product tags"),
5261
+ seo: z10.object({
5262
+ title: z10.string().nullable().describe("SEO title for search engines"),
5263
+ description: z10.string().nullable().describe("SEO description/meta description")
4800
5264
  }).describe("SEO metadata for search engine optimization"),
4801
- createdAt: z8.string().describe("Creation timestamp (ISO 8601)"),
4802
- updatedAt: z8.string().describe("Last update timestamp (ISO 8601)"),
4803
- variants: z8.array(
4804
- z8.object({
4805
- id: z8.string().describe("Variant GID"),
4806
- title: z8.string().describe("Variant title"),
4807
- price: z8.string().describe("Price"),
4808
- compareAtPrice: z8.string().nullable().describe("Compare at price"),
4809
- sku: z8.string().nullable().describe("SKU"),
4810
- barcode: z8.string().nullable().describe("Barcode"),
4811
- inventoryQuantity: z8.number().nullable().describe("Inventory quantity")
5265
+ createdAt: z10.string().describe("Creation timestamp (ISO 8601)"),
5266
+ updatedAt: z10.string().describe("Last update timestamp (ISO 8601)"),
5267
+ variants: z10.array(
5268
+ z10.object({
5269
+ id: z10.string().describe("Variant GID"),
5270
+ title: z10.string().describe("Variant title"),
5271
+ price: z10.string().describe("Price"),
5272
+ compareAtPrice: z10.string().nullable().describe("Compare at price"),
5273
+ sku: z10.string().nullable().describe("SKU"),
5274
+ barcode: z10.string().nullable().describe("Barcode"),
5275
+ inventoryQuantity: z10.number().nullable().describe("Inventory quantity")
4812
5276
  })
4813
5277
  ).describe("Product variants"),
4814
- images: z8.array(
4815
- z8.object({
4816
- id: z8.string().describe("Image GID"),
4817
- url: z8.string().describe("Image URL"),
4818
- altText: z8.string().nullable().describe("Alt text")
5278
+ images: z10.array(
5279
+ z10.object({
5280
+ id: z10.string().describe("Image GID"),
5281
+ url: z10.string().describe("Image URL"),
5282
+ altText: z10.string().nullable().describe("Alt text")
4819
5283
  })
4820
5284
  ).describe("Product images"),
4821
- totalInventory: z8.number().describe("Total inventory across variants")
5285
+ totalInventory: z10.number().describe("Total inventory across variants")
4822
5286
  });
4823
5287
  var handleCreateProduct = async (context, params) => {
4824
5288
  log.debug(`Creating product on shop: ${context.shopDomain}`);
@@ -4853,14 +5317,22 @@ function registerCreateProductTool() {
4853
5317
  name: "create-product",
4854
5318
  title: "Create Product",
4855
5319
  description: "Create a new product in the Shopify store. Supports setting title, description, vendor, type, tags, status, and SEO metadata. Optionally include variants with pricing/SKU and images with URLs. SEO (seo.title, seo.description) is optional - defaults to product title/description if not set. Returns the created product with ID, handle, SEO, variants, and images. Status defaults to DRAFT if not specified. **Typical workflow:** create-product \u2192 add-product-image \u2192 set-product-metafields \u2192 add-products-to-collection. **Prerequisites:** None. **Follow-ups:** add-product-image, set-product-metafields, add-products-to-collection.",
4856
- inputSchema: inputSchema7,
4857
- outputSchema: outputSchema7,
5320
+ inputSchema: inputSchema8,
5321
+ outputSchema: outputSchema8,
4858
5322
  // AI Agent Optimization (Story 5.5)
4859
5323
  category: "product",
4860
5324
  relationships: {
4861
5325
  relatedTools: ["get-product", "update-product", "list-products"],
4862
5326
  followUps: ["add-product-image", "set-product-metafields", "add-products-to-collection"],
4863
5327
  alternatives: ["update-product"]
5328
+ },
5329
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5330
+ annotations: {
5331
+ readOnlyHint: false,
5332
+ destructiveHint: false,
5333
+ idempotentHint: false,
5334
+ // Each call creates a new product
5335
+ openWorldHint: true
4864
5336
  }
4865
5337
  },
4866
5338
  handleCreateProduct
@@ -4868,7 +5340,7 @@ function registerCreateProductTool() {
4868
5340
  }
4869
5341
 
4870
5342
  // src/tools/create-redirect.ts
4871
- import { z as z9 } from "zod";
5343
+ import { z as z11 } from "zod";
4872
5344
 
4873
5345
  // src/shopify/redirects.ts
4874
5346
  var URL_REDIRECT_CREATE_MUTATION = `
@@ -5016,20 +5488,20 @@ async function deleteRedirect(redirectId) {
5016
5488
  }
5017
5489
 
5018
5490
  // src/tools/create-redirect.ts
5019
- var inputSchema8 = z9.object({
5020
- path: z9.string().min(1).refine((val) => val.startsWith("/"), {
5491
+ var inputSchema9 = z11.object({
5492
+ path: z11.string().min(1).refine((val) => val.startsWith("/"), {
5021
5493
  message: "Path must start with '/' (e.g., '/old-product-url')"
5022
5494
  }).describe(
5023
5495
  "The source path to redirect FROM. Must start with '/'. Example: '/old-product-url' or '/collections/old-collection'."
5024
5496
  ),
5025
- target: z9.string().min(1).describe(
5497
+ target: z11.string().min(1).describe(
5026
5498
  "The destination URL to redirect TO. Can be relative ('/new-product-url') or absolute ('https://example.com/page')."
5027
5499
  )
5028
5500
  });
5029
- var outputSchema8 = z9.object({
5030
- id: z9.string().describe('Created redirect GID (e.g., "gid://shopify/UrlRedirect/123")'),
5031
- path: z9.string().describe("Source path that will redirect"),
5032
- target: z9.string().describe("Target URL visitors will be redirected to")
5501
+ var outputSchema9 = z11.object({
5502
+ id: z11.string().describe('Created redirect GID (e.g., "gid://shopify/UrlRedirect/123")'),
5503
+ path: z11.string().describe("Source path that will redirect"),
5504
+ target: z11.string().describe("Target URL visitors will be redirected to")
5033
5505
  });
5034
5506
  var handleCreateRedirect = async (context, params) => {
5035
5507
  log.debug(`Creating redirect on shop: ${context.shopDomain}`);
@@ -5063,13 +5535,21 @@ function registerCreateRedirectTool() {
5063
5535
  name: "create-redirect",
5064
5536
  title: "Create URL Redirect",
5065
5537
  description: "Create a URL redirect to preserve SEO value when URLs change. Redirects automatically forward visitors and search engines from an old path to a new target URL. Path must start with '/'. Useful for: product URL changes, collection restructuring, page migrations, fixing broken links. **Prerequisites:** None. **Follow-ups:** list-redirects to verify creation.",
5066
- inputSchema: inputSchema8,
5067
- outputSchema: outputSchema8,
5538
+ inputSchema: inputSchema9,
5539
+ outputSchema: outputSchema9,
5068
5540
  // AI Agent Optimization (Story 5.5)
5069
5541
  category: "seo",
5070
5542
  relationships: {
5071
5543
  relatedTools: ["list-redirects", "delete-redirect"],
5072
5544
  followUps: ["list-redirects"]
5545
+ },
5546
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5547
+ annotations: {
5548
+ readOnlyHint: false,
5549
+ destructiveHint: false,
5550
+ idempotentHint: false,
5551
+ // Each call creates a new redirect (duplicate path fails)
5552
+ openWorldHint: true
5073
5553
  }
5074
5554
  },
5075
5555
  handleCreateRedirect
@@ -5077,15 +5557,15 @@ function registerCreateRedirectTool() {
5077
5557
  }
5078
5558
 
5079
5559
  // src/tools/delete-article.ts
5080
- import { z as z10 } from "zod";
5081
- var inputSchema9 = z10.object({
5082
- id: z10.string().describe(
5560
+ import { z as z12 } from "zod";
5561
+ var inputSchema10 = z12.object({
5562
+ id: articleIdSchema.describe(
5083
5563
  '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.'
5084
5564
  )
5085
5565
  });
5086
- var outputSchema9 = z10.object({
5087
- success: z10.boolean().describe("Whether the deletion was successful"),
5088
- deletedArticleId: z10.string().describe("The ID of the deleted article")
5566
+ var outputSchema10 = z12.object({
5567
+ success: z12.boolean().describe("Whether the deletion was successful"),
5568
+ deletedArticleId: z12.string().describe("The ID of the deleted article")
5089
5569
  });
5090
5570
  var handleDeleteArticle = async (context, params) => {
5091
5571
  log.debug(`Deleting article on shop: ${context.shopDomain}`);
@@ -5119,13 +5599,21 @@ function registerDeleteArticleTool() {
5119
5599
  name: "delete-article",
5120
5600
  title: "Delete Article",
5121
5601
  description: "Permanently delete an article by ID. This action cannot be undone. The article will no longer be accessible on the storefront. Use list-articles to find the article ID first. Consider unpublishing instead of deleting if you may need the content later. **Typical workflow:** list-articles \u2192 delete-article. **Prerequisites:** Article ID. **Follow-ups:** None. Returns success confirmation and deleted article ID.",
5122
- inputSchema: inputSchema9,
5123
- outputSchema: outputSchema9,
5602
+ inputSchema: inputSchema10,
5603
+ outputSchema: outputSchema10,
5124
5604
  category: "content",
5125
5605
  relationships: {
5126
5606
  relatedTools: ["list-articles", "update-article"],
5127
5607
  prerequisites: ["list-articles"],
5128
5608
  alternatives: ["update-article"]
5609
+ },
5610
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5611
+ annotations: {
5612
+ readOnlyHint: false,
5613
+ destructiveHint: true,
5614
+ idempotentHint: false,
5615
+ // delete-* tools are NOT idempotent
5616
+ openWorldHint: true
5129
5617
  }
5130
5618
  },
5131
5619
  handleDeleteArticle
@@ -5133,15 +5621,15 @@ function registerDeleteArticleTool() {
5133
5621
  }
5134
5622
 
5135
5623
  // src/tools/delete-blog.ts
5136
- import { z as z11 } from "zod";
5137
- var inputSchema10 = z11.object({
5138
- id: z11.string().describe(
5624
+ import { z as z13 } from "zod";
5625
+ var inputSchema11 = z13.object({
5626
+ id: blogIdSchema.describe(
5139
5627
  '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.'
5140
5628
  )
5141
5629
  });
5142
- var outputSchema10 = z11.object({
5143
- success: z11.boolean().describe("Whether the deletion was successful"),
5144
- deletedBlogId: z11.string().describe("The ID of the deleted blog")
5630
+ var outputSchema11 = z13.object({
5631
+ success: z13.boolean().describe("Whether the deletion was successful"),
5632
+ deletedBlogId: z13.string().describe("The ID of the deleted blog")
5145
5633
  });
5146
5634
  var handleDeleteBlog = async (context, params) => {
5147
5635
  log.debug(`Deleting blog on shop: ${context.shopDomain}`);
@@ -5175,12 +5663,20 @@ function registerDeleteBlogTool() {
5175
5663
  name: "delete-blog",
5176
5664
  title: "Delete Blog",
5177
5665
  description: "Permanently delete a blog by ID. This will also delete ALL articles within the blog. This action cannot be undone. Use list-blogs to find the blog ID first. Consider archiving articles before deletion if content may be needed later. **Typical workflow:** list-blogs \u2192 list-articles \u2192 delete-blog. **Prerequisites:** Blog ID. **Follow-ups:** None. Returns success confirmation and deleted blog ID.",
5178
- inputSchema: inputSchema10,
5179
- outputSchema: outputSchema10,
5666
+ inputSchema: inputSchema11,
5667
+ outputSchema: outputSchema11,
5180
5668
  category: "content",
5181
5669
  relationships: {
5182
5670
  relatedTools: ["list-blogs", "list-articles"],
5183
5671
  prerequisites: ["list-blogs"]
5672
+ },
5673
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5674
+ annotations: {
5675
+ readOnlyHint: false,
5676
+ destructiveHint: true,
5677
+ idempotentHint: false,
5678
+ // delete-* tools are NOT idempotent
5679
+ openWorldHint: true
5184
5680
  }
5185
5681
  },
5186
5682
  handleDeleteBlog
@@ -5188,14 +5684,14 @@ function registerDeleteBlogTool() {
5188
5684
  }
5189
5685
 
5190
5686
  // src/tools/delete-collection.ts
5191
- import { z as z12 } from "zod";
5192
- var inputSchema11 = z12.object({
5193
- collectionId: z12.string().min(1).describe(
5687
+ import { z as z14 } from "zod";
5688
+ var inputSchema12 = z14.object({
5689
+ collectionId: collectionIdSchema.describe(
5194
5690
  'Collection ID to delete (GID format, e.g., "gid://shopify/Collection/123"). Use list-collections or get-collection to find collection IDs.'
5195
5691
  )
5196
5692
  });
5197
- var outputSchema11 = z12.object({
5198
- deletedCollectionId: z12.string().describe("The GID of the deleted collection")
5693
+ var outputSchema12 = z14.object({
5694
+ deletedCollectionId: z14.string().describe("The GID of the deleted collection")
5199
5695
  });
5200
5696
  var handleDeleteCollection = async (context, params) => {
5201
5697
  log.debug(`Deleting collection on shop: ${context.shopDomain}`);
@@ -5225,29 +5721,88 @@ function registerDeleteCollectionTool() {
5225
5721
  name: "delete-collection",
5226
5722
  title: "Delete Collection",
5227
5723
  description: "Permanently delete a collection from the store. This removes the collection but does NOT delete the products within it - they remain in the catalog. **Warning:** This action cannot be undone. **Prerequisites:** get-collection to verify the correct collection before deleting.",
5228
- inputSchema: inputSchema11,
5229
- outputSchema: outputSchema11,
5724
+ inputSchema: inputSchema12,
5725
+ outputSchema: outputSchema12,
5230
5726
  // AI Agent Optimization (Story 5.5)
5231
5727
  category: "collection",
5232
5728
  relationships: {
5233
5729
  relatedTools: ["list-collections"],
5234
5730
  prerequisites: ["get-collection"]
5731
+ },
5732
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5733
+ annotations: {
5734
+ readOnlyHint: false,
5735
+ destructiveHint: true,
5736
+ // Permanently deletes collection
5737
+ idempotentHint: false,
5738
+ // Second call fails (already deleted)
5739
+ openWorldHint: true
5235
5740
  }
5236
5741
  },
5237
5742
  handleDeleteCollection
5238
5743
  );
5239
5744
  }
5240
5745
 
5746
+ // src/tools/delete-market.ts
5747
+ import { z as z15 } from "zod";
5748
+ var inputSchema13 = z15.object({
5749
+ id: marketIdSchema.describe(
5750
+ 'Shopify Market GID to delete (e.g., "gid://shopify/Market/123"). Use list-markets or get-market to find market IDs. WARNING: This action cannot be undone.'
5751
+ )
5752
+ });
5753
+ var outputSchema13 = z15.object({
5754
+ deletedId: z15.string().describe("Deleted market GID"),
5755
+ success: z15.boolean().describe("Whether the deletion was successful"),
5756
+ message: z15.string().describe("Confirmation message")
5757
+ });
5758
+ var handleDeleteMarket = async (context, params) => {
5759
+ log.debug(`Deleting market ${params.id} on shop: ${context.shopDomain}`);
5760
+ const deletedId = await deleteMarket(params.id);
5761
+ log.debug(`Deleted market: ${deletedId}`);
5762
+ return {
5763
+ deletedId,
5764
+ success: true,
5765
+ message: `Market ${deletedId} has been permanently deleted. All associated web presences, currency settings, and region configurations have been removed.`
5766
+ };
5767
+ };
5768
+ function registerDeleteMarketTool() {
5769
+ registerContextAwareTool(
5770
+ {
5771
+ name: "delete-market",
5772
+ title: "Delete Market",
5773
+ description: "Permanently delete a market from the store. **WARNING:** This action cannot be undone. Deleting a market removes all its configurations including web presences, currency settings, and region assignments. Products assigned to this market will no longer be available in those regions. **Prerequisites:** get-market to verify the correct market before deleting. **Note:** The primary market cannot be deleted.",
5774
+ inputSchema: inputSchema13,
5775
+ outputSchema: outputSchema13,
5776
+ // AI Agent Optimization
5777
+ category: "market",
5778
+ relationships: {
5779
+ relatedTools: ["get-market", "list-markets"],
5780
+ followUps: ["list-markets"]
5781
+ },
5782
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5783
+ annotations: {
5784
+ readOnlyHint: false,
5785
+ destructiveHint: true,
5786
+ // This is a destructive operation
5787
+ idempotentHint: false,
5788
+ // Deleting twice will fail the second time
5789
+ openWorldHint: true
5790
+ }
5791
+ },
5792
+ handleDeleteMarket
5793
+ );
5794
+ }
5795
+
5241
5796
  // src/tools/delete-page.ts
5242
- import { z as z13 } from "zod";
5243
- var inputSchema12 = z13.object({
5244
- id: z13.string().min(1).describe(
5797
+ import { z as z16 } from "zod";
5798
+ var inputSchema14 = z16.object({
5799
+ id: pageIdSchema.describe(
5245
5800
  '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.'
5246
5801
  )
5247
5802
  });
5248
- var outputSchema12 = z13.object({
5249
- success: z13.boolean().describe("Whether the deletion was successful"),
5250
- deletedPageId: z13.string().describe("The ID of the deleted page")
5803
+ var outputSchema14 = z16.object({
5804
+ success: z16.boolean().describe("Whether the deletion was successful"),
5805
+ deletedPageId: z16.string().describe("The ID of the deleted page")
5251
5806
  });
5252
5807
  var handleDeletePage = async (context, params) => {
5253
5808
  log.debug(`Deleting page on shop: ${context.shopDomain}`);
@@ -5281,13 +5836,21 @@ function registerDeletePageTool() {
5281
5836
  name: "delete-page",
5282
5837
  title: "Delete Page",
5283
5838
  description: "Permanently delete a page by ID. This action cannot be undone. The page will no longer be accessible on the storefront. Use list-pages to find the page ID first. Consider unpublishing instead of deleting if you may need the content later. **Typical workflow:** get-page \u2192 delete-page. **Prerequisites:** Page ID. **Follow-ups:** None. Returns success confirmation and deleted page ID.",
5284
- inputSchema: inputSchema12,
5285
- outputSchema: outputSchema12,
5839
+ inputSchema: inputSchema14,
5840
+ outputSchema: outputSchema14,
5286
5841
  category: "content",
5287
5842
  relationships: {
5288
5843
  relatedTools: ["get-page", "list-pages"],
5289
5844
  prerequisites: ["get-page"],
5290
5845
  alternatives: ["update-page"]
5846
+ },
5847
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5848
+ annotations: {
5849
+ readOnlyHint: false,
5850
+ destructiveHint: true,
5851
+ idempotentHint: false,
5852
+ // delete-* tools are NOT idempotent
5853
+ openWorldHint: true
5291
5854
  }
5292
5855
  },
5293
5856
  handleDeletePage
@@ -5295,7 +5858,7 @@ function registerDeletePageTool() {
5295
5858
  }
5296
5859
 
5297
5860
  // src/tools/delete-product-image.ts
5298
- import { z as z14 } from "zod";
5861
+ import { z as z17 } from "zod";
5299
5862
  var FILE_DELETE_MUTATION = `
5300
5863
  mutation FileDelete($fileIds: [ID!]!) {
5301
5864
  fileDelete(fileIds: $fileIds) {
@@ -5307,14 +5870,14 @@ var FILE_DELETE_MUTATION = `
5307
5870
  }
5308
5871
  }
5309
5872
  `;
5310
- var inputSchema13 = z14.object({
5311
- imageId: z14.string().min(1).describe(
5873
+ var inputSchema15 = z17.object({
5874
+ imageId: imageIdSchema.describe(
5312
5875
  'Shopify image GID to delete (e.g., "gid://shopify/MediaImage/123"). Required. Use get-product to find image IDs in the images array.'
5313
5876
  )
5314
5877
  });
5315
- var outputSchema13 = z14.object({
5316
- success: z14.boolean().describe("Whether the deletion was successful"),
5317
- deletedImageId: z14.string().describe("ID of the deleted image")
5878
+ var outputSchema15 = z17.object({
5879
+ success: z17.boolean().describe("Whether the deletion was successful"),
5880
+ deletedImageId: z17.string().describe("ID of the deleted image")
5318
5881
  });
5319
5882
  var handleDeleteProductImage = async (context, params) => {
5320
5883
  log.debug(`Deleting image on shop: ${context.shopDomain}`);
@@ -5365,13 +5928,22 @@ function registerDeleteProductImageTool() {
5365
5928
  name: "delete-product-image",
5366
5929
  title: "Delete Product Image",
5367
5930
  description: "Remove an image from a product. This permanently deletes the image from Shopify's CDN. **Warning:** Cannot be undone - the image will need to be re-uploaded using add-product-image if needed again. **Prerequisites:** get-product to find image IDs in the images array.",
5368
- inputSchema: inputSchema13,
5369
- outputSchema: outputSchema13,
5931
+ inputSchema: inputSchema15,
5932
+ outputSchema: outputSchema15,
5370
5933
  // AI Agent Optimization (Story 5.5)
5371
5934
  category: "media",
5372
5935
  relationships: {
5373
5936
  relatedTools: ["get-product", "add-product-image"],
5374
5937
  prerequisites: ["get-product"]
5938
+ },
5939
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
5940
+ annotations: {
5941
+ readOnlyHint: false,
5942
+ destructiveHint: true,
5943
+ // Permanently deletes image from Shopify CDN
5944
+ idempotentHint: false,
5945
+ // Second call fails (already deleted)
5946
+ openWorldHint: true
5375
5947
  }
5376
5948
  },
5377
5949
  handleDeleteProductImage
@@ -5379,7 +5951,7 @@ function registerDeleteProductImageTool() {
5379
5951
  }
5380
5952
 
5381
5953
  // src/tools/delete-product-metafields.ts
5382
- import { z as z15 } from "zod";
5954
+ import { z as z18 } from "zod";
5383
5955
 
5384
5956
  // src/shopify/metafields.ts
5385
5957
  var GET_PRODUCT_METAFIELDS_QUERY = `
@@ -5558,28 +6130,28 @@ async function deleteProductMetafields(productId, identifiers) {
5558
6130
  }
5559
6131
 
5560
6132
  // src/tools/delete-product-metafields.ts
5561
- var metafieldIdentifierSchema = z15.object({
5562
- namespace: z15.string().min(1).describe('Metafield namespace (e.g., "custom", "seo"). Must match existing metafield.'),
5563
- key: z15.string().min(1).describe('Metafield key (e.g., "color_hex"). Must match existing metafield.')
6133
+ var metafieldIdentifierSchema = z18.object({
6134
+ namespace: z18.string().min(1).describe('Metafield namespace (e.g., "custom", "seo"). Must match existing metafield.'),
6135
+ key: z18.string().min(1).describe('Metafield key (e.g., "color_hex"). Must match existing metafield.')
5564
6136
  });
5565
- var inputSchema14 = z15.object({
5566
- productId: z15.string().min(1).describe(
6137
+ var inputSchema16 = z18.object({
6138
+ productId: productIdSchema.describe(
5567
6139
  'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Use get-product or list-products to find product IDs.'
5568
6140
  ),
5569
- identifiers: z15.array(metafieldIdentifierSchema).min(1, "At least one metafield identifier is required").describe(
6141
+ identifiers: z18.array(metafieldIdentifierSchema).min(1, "At least one metafield identifier is required").describe(
5570
6142
  "Array of metafield identifiers to delete. Each identifier must have namespace and key. Use get-product-metafields first to verify which metafields exist."
5571
6143
  )
5572
6144
  });
5573
- var outputSchema14 = z15.object({
5574
- success: z15.boolean().describe("Whether the operation succeeded"),
5575
- deletedMetafields: z15.array(
5576
- z15.object({
5577
- ownerId: z15.string().describe("Product GID that owned the metafield"),
5578
- namespace: z15.string().describe("Namespace of deleted metafield"),
5579
- key: z15.string().describe("Key of deleted metafield")
6145
+ var outputSchema16 = z18.object({
6146
+ success: z18.boolean().describe("Whether the operation succeeded"),
6147
+ deletedMetafields: z18.array(
6148
+ z18.object({
6149
+ ownerId: z18.string().describe("Product GID that owned the metafield"),
6150
+ namespace: z18.string().describe("Namespace of deleted metafield"),
6151
+ key: z18.string().describe("Key of deleted metafield")
5580
6152
  })
5581
6153
  ).describe("Identifiers of successfully deleted metafields"),
5582
- deletedCount: z15.number().describe("Number of metafields deleted")
6154
+ deletedCount: z18.number().describe("Number of metafields deleted")
5583
6155
  });
5584
6156
  var handleDeleteProductMetafields = async (context, params) => {
5585
6157
  log.debug(
@@ -5609,13 +6181,22 @@ function registerDeleteProductMetafieldsTool() {
5609
6181
  name: "delete-product-metafields",
5610
6182
  title: "Delete Product Metafields",
5611
6183
  description: "Remove metafields from a product. Identify metafields to delete by their namespace and key combination. Non-existent metafields are silently ignored (no error). **Warning:** This operation is irreversible - deleted metafield data cannot be recovered. **Prerequisites:** get-product-metafields to verify which metafields exist before deletion.",
5612
- inputSchema: inputSchema14,
5613
- outputSchema: outputSchema14,
6184
+ inputSchema: inputSchema16,
6185
+ outputSchema: outputSchema16,
5614
6186
  // AI Agent Optimization (Story 5.5)
5615
6187
  category: "seo",
5616
6188
  relationships: {
5617
6189
  relatedTools: ["get-product-metafields"],
5618
6190
  prerequisites: ["get-product-metafields"]
6191
+ },
6192
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
6193
+ annotations: {
6194
+ readOnlyHint: false,
6195
+ destructiveHint: true,
6196
+ // Partial data deletion - cannot be recovered
6197
+ idempotentHint: false,
6198
+ // Second call fails (already deleted)
6199
+ openWorldHint: true
5619
6200
  }
5620
6201
  },
5621
6202
  handleDeleteProductMetafields
@@ -5623,13 +6204,15 @@ function registerDeleteProductMetafieldsTool() {
5623
6204
  }
5624
6205
 
5625
6206
  // src/tools/delete-product.ts
5626
- import { z as z16 } from "zod";
5627
- var inputSchema15 = z16.object({
5628
- id: z16.string().min(1).describe('Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Required.')
6207
+ import { z as z19 } from "zod";
6208
+ var inputSchema17 = z19.object({
6209
+ id: productIdSchema.describe(
6210
+ 'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Required.'
6211
+ )
5629
6212
  });
5630
- var outputSchema15 = z16.object({
5631
- deletedProductId: z16.string().describe("The ID of the deleted product"),
5632
- title: z16.string().describe("The title of the deleted product (for confirmation)")
6213
+ var outputSchema17 = z19.object({
6214
+ deletedProductId: z19.string().describe("The ID of the deleted product"),
6215
+ title: z19.string().describe("The title of the deleted product (for confirmation)")
5633
6216
  });
5634
6217
  var handleDeleteProduct = async (context, params) => {
5635
6218
  log.debug(`Deleting product on shop: ${context.shopDomain}`);
@@ -5671,13 +6254,22 @@ function registerDeleteProductTool() {
5671
6254
  name: "delete-product",
5672
6255
  title: "Delete Product",
5673
6256
  description: "Delete a product from the Shopify store permanently. Requires the product ID (GID format). Returns confirmation with the deleted product ID and title. **Prerequisites:** get-product or list-products to find the correct product ID. **Warning:** This action cannot be undone.",
5674
- inputSchema: inputSchema15,
5675
- outputSchema: outputSchema15,
6257
+ inputSchema: inputSchema17,
6258
+ outputSchema: outputSchema17,
5676
6259
  // AI Agent Optimization (Story 5.5)
5677
6260
  category: "product",
5678
6261
  relationships: {
5679
6262
  relatedTools: ["list-products"],
5680
6263
  prerequisites: ["get-product"]
6264
+ },
6265
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
6266
+ annotations: {
6267
+ readOnlyHint: false,
6268
+ destructiveHint: true,
6269
+ // Permanently deletes product
6270
+ idempotentHint: false,
6271
+ // Second call fails (already deleted)
6272
+ openWorldHint: true
5681
6273
  }
5682
6274
  },
5683
6275
  handleDeleteProduct
@@ -5685,15 +6277,15 @@ function registerDeleteProductTool() {
5685
6277
  }
5686
6278
 
5687
6279
  // src/tools/delete-redirect.ts
5688
- import { z as z17 } from "zod";
5689
- var inputSchema16 = z17.object({
5690
- id: z17.string().min(1).describe(
6280
+ import { z as z20 } from "zod";
6281
+ var inputSchema18 = z20.object({
6282
+ id: redirectIdSchema.describe(
5691
6283
  'Shopify redirect GID (e.g., "gid://shopify/UrlRedirect/123"). Use list-redirects to find redirect IDs.'
5692
6284
  )
5693
6285
  });
5694
- var outputSchema16 = z17.object({
5695
- success: z17.boolean().describe("Whether the deletion succeeded"),
5696
- deletedRedirectId: z17.string().describe("ID of the deleted redirect")
6286
+ var outputSchema18 = z20.object({
6287
+ success: z20.boolean().describe("Whether the deletion succeeded"),
6288
+ deletedRedirectId: z20.string().describe("ID of the deleted redirect")
5697
6289
  });
5698
6290
  var handleDeleteRedirect = async (context, params) => {
5699
6291
  log.debug(`Deleting redirect on shop: ${context.shopDomain}`);
@@ -5735,13 +6327,22 @@ function registerDeleteRedirectTool() {
5735
6327
  name: "delete-redirect",
5736
6328
  title: "Delete URL Redirect",
5737
6329
  description: "Remove a URL redirect by ID. **Warning:** Deleting means visitors to that path will see 404 error - can hurt SEO if old URL still receives traffic. **Prerequisites:** list-redirects to find redirect IDs.",
5738
- inputSchema: inputSchema16,
5739
- outputSchema: outputSchema16,
6330
+ inputSchema: inputSchema18,
6331
+ outputSchema: outputSchema18,
5740
6332
  // AI Agent Optimization (Story 5.5)
5741
6333
  category: "seo",
5742
6334
  relationships: {
5743
6335
  relatedTools: ["create-redirect"],
5744
6336
  prerequisites: ["list-redirects"]
6337
+ },
6338
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
6339
+ annotations: {
6340
+ readOnlyHint: false,
6341
+ destructiveHint: true,
6342
+ // Permanently removes redirect - can hurt SEO
6343
+ idempotentHint: false,
6344
+ // Second call fails (already deleted)
6345
+ openWorldHint: true
5745
6346
  }
5746
6347
  },
5747
6348
  handleDeleteRedirect
@@ -5749,7 +6350,7 @@ function registerDeleteRedirectTool() {
5749
6350
  }
5750
6351
 
5751
6352
  // src/tools/get-bulk-inventory.ts
5752
- import { z as z18 } from "zod";
6353
+ import { z as z21 } from "zod";
5753
6354
 
5754
6355
  // src/shopify/inventory.ts
5755
6356
  var GET_INVENTORY_ITEM_QUERY = `
@@ -6320,46 +6921,46 @@ async function getBulkInventory(options) {
6320
6921
  }
6321
6922
 
6322
6923
  // src/tools/get-bulk-inventory.ts
6323
- var inputSchema17 = z18.object({
6324
- productIds: z18.array(z18.string()).min(1, { message: "productIds array cannot be empty" }).max(50, { message: "Maximum 50 product IDs allowed per request" }).describe(
6924
+ var inputSchema19 = z21.object({
6925
+ productIds: z21.array(productIdSchema).min(1, { message: "productIds array cannot be empty" }).max(50, { message: "Maximum 50 product IDs allowed per request" }).describe(
6325
6926
  '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.'
6326
6927
  ),
6327
- includeVariants: z18.boolean().default(true).describe(
6928
+ includeVariants: z21.boolean().default(true).describe(
6328
6929
  "Include per-variant inventory breakdown. Default: true. Set to false for faster response with only product totals."
6329
6930
  ),
6330
- locationId: z18.string().optional().describe(
6931
+ locationId: locationIdSchema.optional().describe(
6331
6932
  'Optional location ID to filter inventory (e.g., "gid://shopify/Location/789"). If not provided, returns total inventory across all locations.'
6332
6933
  )
6333
6934
  });
6334
- var outputSchema17 = z18.object({
6335
- products: z18.array(
6336
- z18.object({
6337
- productId: z18.string().describe("Product GID"),
6338
- productTitle: z18.string().describe("Product title"),
6339
- totalInventory: z18.number().describe("Total inventory across all variants"),
6340
- variants: z18.array(
6341
- z18.object({
6342
- variantId: z18.string().describe("Variant GID"),
6343
- variantTitle: z18.string().describe('Variant title (e.g., "Red / Large")'),
6344
- sku: z18.string().nullable().describe("SKU (Stock Keeping Unit)"),
6345
- inventoryItemId: z18.string().describe("Inventory item GID"),
6346
- available: z18.number().describe("Available quantity"),
6347
- locations: z18.array(
6348
- z18.object({
6349
- locationId: z18.string().describe("Location GID"),
6350
- locationName: z18.string().describe("Human-readable location name"),
6351
- available: z18.number().describe("Available quantity at this location")
6935
+ var outputSchema19 = z21.object({
6936
+ products: z21.array(
6937
+ z21.object({
6938
+ productId: z21.string().describe("Product GID"),
6939
+ productTitle: z21.string().describe("Product title"),
6940
+ totalInventory: z21.number().describe("Total inventory across all variants"),
6941
+ variants: z21.array(
6942
+ z21.object({
6943
+ variantId: z21.string().describe("Variant GID"),
6944
+ variantTitle: z21.string().describe('Variant title (e.g., "Red / Large")'),
6945
+ sku: z21.string().nullable().describe("SKU (Stock Keeping Unit)"),
6946
+ inventoryItemId: z21.string().describe("Inventory item GID"),
6947
+ available: z21.number().describe("Available quantity"),
6948
+ locations: z21.array(
6949
+ z21.object({
6950
+ locationId: z21.string().describe("Location GID"),
6951
+ locationName: z21.string().describe("Human-readable location name"),
6952
+ available: z21.number().describe("Available quantity at this location")
6352
6953
  })
6353
6954
  ).optional().describe("Per-location inventory breakdown")
6354
6955
  })
6355
6956
  ).optional().describe("Variant details (if includeVariants=true)")
6356
6957
  })
6357
6958
  ).describe("Products with inventory data"),
6358
- notFound: z18.array(z18.string()).describe("Product IDs that were not found in the store"),
6359
- summary: z18.object({
6360
- productsQueried: z18.number().describe("Total number of product IDs provided"),
6361
- productsFound: z18.number().describe("Number of products found in the store"),
6362
- totalInventory: z18.number().describe("Sum of inventory across all found products")
6959
+ notFound: z21.array(z21.string()).describe("Product IDs that were not found in the store"),
6960
+ summary: z21.object({
6961
+ productsQueried: z21.number().describe("Total number of product IDs provided"),
6962
+ productsFound: z21.number().describe("Number of products found in the store"),
6963
+ totalInventory: z21.number().describe("Sum of inventory across all found products")
6363
6964
  }).describe("Summary statistics")
6364
6965
  });
6365
6966
  var handleGetBulkInventory = async (context, params) => {
@@ -6391,14 +6992,21 @@ function registerGetBulkInventoryTool() {
6391
6992
  name: "get-bulk-inventory",
6392
6993
  title: "Get Bulk Inventory",
6393
6994
  description: "Efficiently query inventory levels for multiple products in a single request. Accepts up to 50 product IDs and returns total inventory plus optional per-variant breakdown. Use this instead of multiple get-inventory calls when checking stock across several products. Returns a summary with counts of products queried vs found, plus total inventory across all products. Products not found in the store are listed in the notFound array rather than causing an error. Perfect for cart verification, collection audits, and batch inventory checks. **Typical workflow:** list-products \u2192 get-bulk-inventory. **Prerequisites:** Product IDs. **Follow-ups:** update-inventory, get-inventory. Returns products with inventory data and summary statistics.",
6394
- inputSchema: inputSchema17,
6395
- outputSchema: outputSchema17,
6995
+ inputSchema: inputSchema19,
6996
+ outputSchema: outputSchema19,
6396
6997
  category: "inventory",
6397
6998
  relationships: {
6398
6999
  relatedTools: ["list-products", "get-inventory", "update-inventory"],
6399
7000
  prerequisites: ["list-products"],
6400
7001
  followUps: ["update-inventory", "get-inventory"],
6401
7002
  alternatives: ["get-inventory"]
7003
+ },
7004
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7005
+ annotations: {
7006
+ readOnlyHint: true,
7007
+ destructiveHint: false,
7008
+ idempotentHint: true,
7009
+ openWorldHint: true
6402
7010
  }
6403
7011
  },
6404
7012
  handleGetBulkInventory
@@ -6406,30 +7014,30 @@ function registerGetBulkInventoryTool() {
6406
7014
  }
6407
7015
 
6408
7016
  // src/tools/get-collection.ts
6409
- import { z as z19 } from "zod";
6410
- var inputSchema18 = z19.object({
6411
- collectionId: z19.string().min(1).describe(
7017
+ import { z as z22 } from "zod";
7018
+ var inputSchema20 = z22.object({
7019
+ collectionId: collectionIdSchema.describe(
6412
7020
  'Collection ID (GID format, e.g., "gid://shopify/Collection/123"). Use list-collections to find collection IDs.'
6413
7021
  )
6414
7022
  });
6415
- var outputSchema18 = z19.object({
6416
- id: z19.string().describe("Collection GID"),
6417
- title: z19.string().describe("Collection title"),
6418
- handle: z19.string().describe("URL handle/slug"),
6419
- description: z19.string().describe("Plain text description"),
6420
- descriptionHtml: z19.string().describe("HTML description"),
6421
- seo: z19.object({
6422
- title: z19.string().nullable().describe("SEO title"),
6423
- description: z19.string().nullable().describe("SEO description")
7023
+ var outputSchema20 = z22.object({
7024
+ id: z22.string().describe("Collection GID"),
7025
+ title: z22.string().describe("Collection title"),
7026
+ handle: z22.string().describe("URL handle/slug"),
7027
+ description: z22.string().describe("Plain text description"),
7028
+ descriptionHtml: z22.string().describe("HTML description"),
7029
+ seo: z22.object({
7030
+ title: z22.string().nullable().describe("SEO title"),
7031
+ description: z22.string().nullable().describe("SEO description")
6424
7032
  }),
6425
- image: z19.object({
6426
- id: z19.string().describe("Image GID"),
6427
- url: z19.string().describe("Image URL"),
6428
- altText: z19.string().nullable().describe("Alt text")
7033
+ image: z22.object({
7034
+ id: z22.string().describe("Image GID"),
7035
+ url: z22.string().describe("Image URL"),
7036
+ altText: z22.string().nullable().describe("Alt text")
6429
7037
  }).nullable(),
6430
- sortOrder: z19.string().describe("Product sort order within collection"),
6431
- productsCount: z19.number().describe("Number of products in collection"),
6432
- updatedAt: z19.string().describe("Last update timestamp (ISO 8601)")
7038
+ sortOrder: z22.string().describe("Product sort order within collection"),
7039
+ productsCount: z22.number().describe("Number of products in collection"),
7040
+ updatedAt: z22.string().describe("Last update timestamp (ISO 8601)")
6433
7041
  });
6434
7042
  var handleGetCollection = async (context, params) => {
6435
7043
  log.debug(`Getting collection on shop: ${context.shopDomain}`);
@@ -6449,13 +7057,20 @@ function registerGetCollectionTool() {
6449
7057
  name: "get-collection",
6450
7058
  title: "Get Collection",
6451
7059
  description: "Retrieve complete details of a collection by ID. Returns all collection attributes including SEO metadata, product count, sort order, and timestamps. **Prerequisites:** list-collections to find collection IDs. **Follow-ups:** update-collection, delete-collection, add-products-to-collection.",
6452
- inputSchema: inputSchema18,
6453
- outputSchema: outputSchema18,
7060
+ inputSchema: inputSchema20,
7061
+ outputSchema: outputSchema20,
6454
7062
  // AI Agent Optimization (Story 5.5)
6455
7063
  category: "collection",
6456
7064
  relationships: {
6457
7065
  relatedTools: ["list-collections"],
6458
7066
  followUps: ["update-collection", "delete-collection", "add-products-to-collection"]
7067
+ },
7068
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7069
+ annotations: {
7070
+ readOnlyHint: true,
7071
+ destructiveHint: false,
7072
+ idempotentHint: true,
7073
+ openWorldHint: true
6459
7074
  }
6460
7075
  },
6461
7076
  handleGetCollection
@@ -6463,37 +7078,37 @@ function registerGetCollectionTool() {
6463
7078
  }
6464
7079
 
6465
7080
  // src/tools/get-inventory.ts
6466
- import { z as z20 } from "zod";
6467
- var inputSchema19 = z20.object({
6468
- variantId: z20.string().optional().describe(
7081
+ import { z as z23 } from "zod";
7082
+ var inputSchema21 = z23.object({
7083
+ variantId: variantIdSchema.optional().describe(
6469
7084
  '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).'
6470
7085
  ),
6471
- inventoryItemId: z20.string().optional().describe(
7086
+ inventoryItemId: inventoryItemIdSchema.optional().describe(
6472
7087
  '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.'
6473
7088
  ),
6474
- locationId: z20.string().optional().describe(
7089
+ locationId: locationIdSchema.optional().describe(
6475
7090
  'Optional location ID to filter results (e.g., "gid://shopify/Location/789"). If not provided, returns inventory at all locations.'
6476
7091
  )
6477
7092
  }).refine((data) => data.variantId || data.inventoryItemId, {
6478
7093
  message: "Either variantId or inventoryItemId must be provided"
6479
7094
  });
6480
- var outputSchema19 = z20.object({
6481
- inventoryItemId: z20.string().describe("Inventory item GID"),
6482
- variantId: z20.string().describe("Product variant GID"),
6483
- variantTitle: z20.string().describe('Variant title (e.g., "Red / Large")'),
6484
- productId: z20.string().describe("Product GID"),
6485
- productTitle: z20.string().describe("Product title"),
6486
- sku: z20.string().nullable().describe("SKU (Stock Keeping Unit)"),
6487
- tracked: z20.boolean().describe("Whether inventory tracking is enabled for this item"),
6488
- totalAvailable: z20.number().describe("Total available quantity across all locations"),
6489
- locations: z20.array(
6490
- z20.object({
6491
- locationId: z20.string().describe("Location GID"),
6492
- locationName: z20.string().describe("Human-readable location name"),
6493
- isActive: z20.boolean().describe("Whether the location is active"),
6494
- available: z20.number().describe("Quantity available for sale"),
6495
- onHand: z20.number().describe("Physical quantity on hand"),
6496
- committed: z20.number().describe("Quantity committed to orders")
7095
+ var outputSchema21 = z23.object({
7096
+ inventoryItemId: z23.string().describe("Inventory item GID"),
7097
+ variantId: z23.string().describe("Product variant GID"),
7098
+ variantTitle: z23.string().describe('Variant title (e.g., "Red / Large")'),
7099
+ productId: z23.string().describe("Product GID"),
7100
+ productTitle: z23.string().describe("Product title"),
7101
+ sku: z23.string().nullable().describe("SKU (Stock Keeping Unit)"),
7102
+ tracked: z23.boolean().describe("Whether inventory tracking is enabled for this item"),
7103
+ totalAvailable: z23.number().describe("Total available quantity across all locations"),
7104
+ locations: z23.array(
7105
+ z23.object({
7106
+ locationId: z23.string().describe("Location GID"),
7107
+ locationName: z23.string().describe("Human-readable location name"),
7108
+ isActive: z23.boolean().describe("Whether the location is active"),
7109
+ available: z23.number().describe("Quantity available for sale"),
7110
+ onHand: z23.number().describe("Physical quantity on hand"),
7111
+ committed: z23.number().describe("Quantity committed to orders")
6497
7112
  })
6498
7113
  ).describe("Inventory levels by location")
6499
7114
  });
@@ -6555,8 +7170,8 @@ function registerGetInventoryTool() {
6555
7170
  name: "get-inventory",
6556
7171
  title: "Get Inventory",
6557
7172
  description: "Retrieve inventory levels for a product variant across all store locations. Provide either a variantId (from get-product or list-products) or inventoryItemId (if you have it directly). Returns available, on-hand, and committed quantities per location, plus totals. Useful for: checking stock before making availability promises, identifying which locations have stock, understanding inventory distribution across warehouse locations. Optionally filter by locationId to get inventory at a specific location only. **Typical workflow:** get-product \u2192 get-inventory \u2192 update-inventory. **Prerequisites:** Variant ID or Inventory Item ID. **Follow-ups:** update-inventory. Returns inventory levels per location with totals.",
6558
- inputSchema: inputSchema19,
6559
- outputSchema: outputSchema19,
7173
+ inputSchema: inputSchema21,
7174
+ outputSchema: outputSchema21,
6560
7175
  category: "inventory",
6561
7176
  relationships: {
6562
7177
  relatedTools: [
@@ -6567,30 +7182,119 @@ function registerGetInventoryTool() {
6567
7182
  ],
6568
7183
  prerequisites: ["get-product", "list-products"],
6569
7184
  followUps: ["update-inventory"]
7185
+ },
7186
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7187
+ annotations: {
7188
+ readOnlyHint: true,
7189
+ destructiveHint: false,
7190
+ idempotentHint: true,
7191
+ openWorldHint: true
6570
7192
  }
6571
7193
  },
6572
7194
  handleGetInventory
6573
7195
  );
6574
7196
  }
6575
7197
 
7198
+ // src/tools/get-market.ts
7199
+ import { z as z24 } from "zod";
7200
+ var inputSchema22 = z24.object({
7201
+ id: marketIdSchema.describe(
7202
+ 'Shopify Market GID (e.g., "gid://shopify/Market/123"). Use list-markets to find market IDs.'
7203
+ )
7204
+ });
7205
+ var outputSchema22 = z24.object({
7206
+ id: z24.string().describe("Market GID"),
7207
+ name: z24.string().describe("Market name (not shown to customers)"),
7208
+ handle: z24.string().describe("URL handle"),
7209
+ enabled: z24.boolean().describe("Whether the market is enabled"),
7210
+ status: z24.enum(["ACTIVE", "INACTIVE"]).describe("Market status"),
7211
+ type: z24.string().describe("Market type (PRIMARY, SINGLE_COUNTRY, MULTI_COUNTRY, CONTINENT)"),
7212
+ currencySettings: z24.object({
7213
+ baseCurrency: z24.object({
7214
+ currencyCode: z24.string().describe("Base currency code (e.g., USD, EUR)"),
7215
+ currencyName: z24.string().optional().describe("Base currency name")
7216
+ }),
7217
+ localCurrencies: z24.boolean().describe("Whether local currencies are enabled")
7218
+ }).optional(),
7219
+ webPresences: z24.array(
7220
+ z24.object({
7221
+ id: z24.string().describe("Web presence GID"),
7222
+ defaultLocale: z24.object({
7223
+ locale: z24.string().describe("Locale code (e.g., en-CA)"),
7224
+ name: z24.string().describe("Locale display name")
7225
+ }),
7226
+ alternateLocales: z24.array(
7227
+ z24.object({
7228
+ locale: z24.string(),
7229
+ name: z24.string()
7230
+ })
7231
+ ).optional(),
7232
+ subfolderSuffix: z24.string().optional().describe("Subfolder suffix (e.g., /en-ca)"),
7233
+ domain: z24.object({
7234
+ host: z24.string().describe("Domain host")
7235
+ }).optional(),
7236
+ rootUrls: z24.array(
7237
+ z24.object({
7238
+ locale: z24.string(),
7239
+ url: z24.string()
7240
+ })
7241
+ ).optional()
7242
+ })
7243
+ ).optional().describe("Web presences (SEO URL configurations)")
7244
+ });
7245
+ var handleGetMarket = async (context, params) => {
7246
+ log.debug(`Getting market ${params.id} on shop: ${context.shopDomain}`);
7247
+ const market = await getMarket(params.id);
7248
+ if (!market) {
7249
+ throw new Error(`Market not found: ${params.id}. Use list-markets to see available markets.`);
7250
+ }
7251
+ log.debug(`Retrieved market: ${market.name}`);
7252
+ return market;
7253
+ };
7254
+ function registerGetMarketTool() {
7255
+ registerContextAwareTool(
7256
+ {
7257
+ name: "get-market",
7258
+ title: "Get Market",
7259
+ description: "Retrieve full details of a specific market by ID. Returns market name, handle, status, type, currency settings, and web presences. Web presences define SEO URL structures (domains, subfolders, locales). **Prerequisites:** Use list-markets to find market IDs. **Follow-ups:** update-market, delete-market, create-web-presence.",
7260
+ inputSchema: inputSchema22,
7261
+ outputSchema: outputSchema22,
7262
+ // AI Agent Optimization
7263
+ category: "market",
7264
+ relationships: {
7265
+ relatedTools: ["list-markets"],
7266
+ followUps: ["update-market", "delete-market"]
7267
+ },
7268
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7269
+ annotations: {
7270
+ readOnlyHint: true,
7271
+ destructiveHint: false,
7272
+ idempotentHint: true,
7273
+ openWorldHint: true
7274
+ }
7275
+ },
7276
+ handleGetMarket
7277
+ );
7278
+ }
7279
+
6576
7280
  // src/tools/get-page.ts
6577
- import { z as z21 } from "zod";
6578
- var inputSchema20 = z21.object({
6579
- id: z21.string().min(1).describe(
7281
+ import { z as z25 } from "zod";
7282
+ var inputSchema23 = z25.object({
7283
+ id: pageIdSchema.describe(
6580
7284
  'The page ID to retrieve. Must be a valid Shopify GID format. Example: "gid://shopify/Page/123456789". Use list-pages to find page IDs.'
6581
7285
  )
6582
7286
  });
6583
- var outputSchema20 = z21.object({
6584
- id: z21.string().describe("Page GID"),
6585
- title: z21.string().describe("Page title"),
6586
- handle: z21.string().describe("URL handle/slug"),
6587
- body: z21.string().describe("HTML body content"),
6588
- bodySummary: z21.string().describe("Plain text summary (first 150 chars)"),
6589
- isPublished: z21.boolean().describe("Whether page is visible on storefront"),
6590
- publishedAt: z21.string().nullable().describe("ISO timestamp when published"),
6591
- templateSuffix: z21.string().nullable().describe("Custom template suffix"),
6592
- createdAt: z21.string().describe("ISO timestamp of creation"),
6593
- updatedAt: z21.string().describe("ISO timestamp of last update")
7287
+ var outputSchema23 = z25.object({
7288
+ id: z25.string().describe("Page GID"),
7289
+ title: z25.string().describe("Page title"),
7290
+ handle: z25.string().describe("URL handle/slug"),
7291
+ body: z25.string().describe("HTML body content"),
7292
+ bodySummary: z25.string().describe("Plain text summary (first 150 chars)"),
7293
+ isPublished: z25.boolean().describe("Whether page is visible on storefront"),
7294
+ publishedAt: z25.string().nullable().describe("ISO timestamp when published"),
7295
+ templateSuffix: z25.string().nullable().describe("Custom template suffix"),
7296
+ createdAt: z25.string().describe("ISO timestamp of creation"),
7297
+ updatedAt: z25.string().describe("ISO timestamp of last update")
6594
7298
  });
6595
7299
  var handleGetPage = async (context, params) => {
6596
7300
  log.debug(`Getting page on shop: ${context.shopDomain}`);
@@ -6610,13 +7314,20 @@ function registerGetPageTool() {
6610
7314
  name: "get-page",
6611
7315
  title: "Get Page",
6612
7316
  description: "Retrieve details of a specific page by ID. Returns the full page including title, body content, handle, publish status, and timestamps. Use list-pages first to find page IDs. Useful for: inspecting page content before editing, verifying page details. **Typical workflow:** list-pages \u2192 get-page \u2192 update-page. **Prerequisites:** Page ID. **Follow-ups:** update-page, delete-page. Returns a single page object with full content.",
6613
- inputSchema: inputSchema20,
6614
- outputSchema: outputSchema20,
7317
+ inputSchema: inputSchema23,
7318
+ outputSchema: outputSchema23,
6615
7319
  category: "content",
6616
7320
  relationships: {
6617
7321
  relatedTools: ["list-pages", "update-page", "delete-page"],
6618
7322
  prerequisites: ["list-pages"],
6619
7323
  followUps: ["update-page", "delete-page"]
7324
+ },
7325
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7326
+ annotations: {
7327
+ readOnlyHint: true,
7328
+ destructiveHint: false,
7329
+ idempotentHint: true,
7330
+ openWorldHint: true
6620
7331
  }
6621
7332
  },
6622
7333
  handleGetPage
@@ -6624,27 +7335,27 @@ function registerGetPageTool() {
6624
7335
  }
6625
7336
 
6626
7337
  // src/tools/get-product-metafields.ts
6627
- import { z as z22 } from "zod";
6628
- var inputSchema21 = z22.object({
6629
- productId: z22.string().min(1).describe(
7338
+ import { z as z26 } from "zod";
7339
+ var inputSchema24 = z26.object({
7340
+ productId: productIdSchema.describe(
6630
7341
  'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Use get-product or list-products to find product IDs.'
6631
7342
  ),
6632
- namespace: z22.string().optional().describe(
7343
+ namespace: z26.string().optional().describe(
6633
7344
  'Filter metafields by namespace (e.g., "custom", "seo", "my_app"). If not provided, returns metafields from all namespaces.'
6634
7345
  ),
6635
- keys: z22.array(z22.string()).optional().describe(
7346
+ keys: z26.array(z26.string()).optional().describe(
6636
7347
  'Filter by specific metafield keys within the namespace (e.g., ["color", "size"]). Requires namespace to be specified for meaningful filtering.'
6637
7348
  )
6638
7349
  });
6639
- var outputSchema21 = z22.array(
6640
- z22.object({
6641
- id: z22.string().describe('Metafield GID (e.g., "gid://shopify/Metafield/123")'),
6642
- namespace: z22.string().describe("Metafield namespace (grouping identifier)"),
6643
- key: z22.string().describe("Metafield key (field name)"),
6644
- value: z22.string().describe("Metafield value (the stored data)"),
6645
- type: z22.string().describe('Metafield type (e.g., "single_line_text_field", "json", "number_integer")'),
6646
- createdAt: z22.string().describe("Creation timestamp (ISO 8601)"),
6647
- updatedAt: z22.string().describe("Last update timestamp (ISO 8601)")
7350
+ var outputSchema24 = z26.array(
7351
+ z26.object({
7352
+ id: z26.string().describe('Metafield GID (e.g., "gid://shopify/Metafield/123")'),
7353
+ namespace: z26.string().describe("Metafield namespace (grouping identifier)"),
7354
+ key: z26.string().describe("Metafield key (field name)"),
7355
+ value: z26.string().describe("Metafield value (the stored data)"),
7356
+ type: z26.string().describe('Metafield type (e.g., "single_line_text_field", "json", "number_integer")'),
7357
+ createdAt: z26.string().describe("Creation timestamp (ISO 8601)"),
7358
+ updatedAt: z26.string().describe("Last update timestamp (ISO 8601)")
6648
7359
  })
6649
7360
  );
6650
7361
  var handleGetProductMetafields = async (context, params) => {
@@ -6669,14 +7380,21 @@ function registerGetProductMetafieldsTool() {
6669
7380
  name: "get-product-metafields",
6670
7381
  title: "Get Product Metafields",
6671
7382
  description: 'Retrieve metafields attached to a product. Metafields store custom data like SEO schema markup, product specifications, or custom attributes. Filter by namespace to get specific app/integration data, or by keys to retrieve specific fields. Common namespaces include "custom" (user-defined), "seo" (search optimization), and app-specific namespaces. Returns all matching metafields with their id, namespace, key, value, type, and timestamps. **Prerequisites:** get-product to obtain the product ID. **Follow-ups:** set-product-metafields to update values, delete-product-metafields to remove.',
6672
- inputSchema: inputSchema21,
6673
- outputSchema: outputSchema21,
7383
+ inputSchema: inputSchema24,
7384
+ outputSchema: outputSchema24,
6674
7385
  // AI Agent Optimization (Story 5.5)
6675
7386
  category: "seo",
6676
7387
  relationships: {
6677
7388
  relatedTools: ["get-product", "set-product-metafields"],
6678
7389
  prerequisites: ["get-product"],
6679
7390
  followUps: ["set-product-metafields", "delete-product-metafields"]
7391
+ },
7392
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7393
+ annotations: {
7394
+ readOnlyHint: true,
7395
+ destructiveHint: false,
7396
+ idempotentHint: true,
7397
+ openWorldHint: true
6680
7398
  }
6681
7399
  },
6682
7400
  handleGetProductMetafields
@@ -6684,51 +7402,51 @@ function registerGetProductMetafieldsTool() {
6684
7402
  }
6685
7403
 
6686
7404
  // src/tools/get-product.ts
6687
- import { z as z23 } from "zod";
6688
- var inputSchema22 = z23.object({
6689
- id: z23.string().optional().describe(
7405
+ import { z as z27 } from "zod";
7406
+ var inputSchema25 = z27.object({
7407
+ id: productIdSchema.optional().describe(
6690
7408
  'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Use either id or handle.'
6691
7409
  ),
6692
- handle: z23.string().optional().describe(
7410
+ handle: z27.string().optional().describe(
6693
7411
  'Product URL handle/slug (e.g., "premium-cotton-t-shirt"). Use either id or handle.'
6694
7412
  )
6695
7413
  }).refine((data) => data.id || data.handle, {
6696
7414
  message: "Either id or handle must be provided"
6697
7415
  });
6698
- var outputSchema22 = z23.object({
6699
- id: z23.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
6700
- title: z23.string().describe("Product title"),
6701
- handle: z23.string().describe("URL handle/slug"),
6702
- description: z23.string().nullable().describe("Product description (HTML)"),
6703
- vendor: z23.string().describe("Vendor/brand name"),
6704
- productType: z23.string().describe("Product type"),
6705
- status: z23.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
6706
- tags: z23.array(z23.string()).describe("Product tags"),
6707
- seo: z23.object({
6708
- title: z23.string().nullable().describe("SEO title for search engines"),
6709
- description: z23.string().nullable().describe("SEO description/meta description")
7416
+ var outputSchema25 = z27.object({
7417
+ id: z27.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
7418
+ title: z27.string().describe("Product title"),
7419
+ handle: z27.string().describe("URL handle/slug"),
7420
+ description: z27.string().nullable().describe("Product description (HTML)"),
7421
+ vendor: z27.string().describe("Vendor/brand name"),
7422
+ productType: z27.string().describe("Product type"),
7423
+ status: z27.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
7424
+ tags: z27.array(z27.string()).describe("Product tags"),
7425
+ seo: z27.object({
7426
+ title: z27.string().nullable().describe("SEO title for search engines"),
7427
+ description: z27.string().nullable().describe("SEO description/meta description")
6710
7428
  }).describe("SEO metadata for search engine optimization"),
6711
- createdAt: z23.string().describe("Creation timestamp (ISO 8601)"),
6712
- updatedAt: z23.string().describe("Last update timestamp (ISO 8601)"),
6713
- variants: z23.array(
6714
- z23.object({
6715
- id: z23.string().describe("Variant GID"),
6716
- title: z23.string().describe("Variant title"),
6717
- price: z23.string().describe("Price"),
6718
- compareAtPrice: z23.string().nullable().describe("Compare at price"),
6719
- sku: z23.string().nullable().describe("SKU"),
6720
- barcode: z23.string().nullable().describe("Barcode"),
6721
- inventoryQuantity: z23.number().nullable().describe("Inventory quantity")
7429
+ createdAt: z27.string().describe("Creation timestamp (ISO 8601)"),
7430
+ updatedAt: z27.string().describe("Last update timestamp (ISO 8601)"),
7431
+ variants: z27.array(
7432
+ z27.object({
7433
+ id: z27.string().describe("Variant GID"),
7434
+ title: z27.string().describe("Variant title"),
7435
+ price: z27.string().describe("Price"),
7436
+ compareAtPrice: z27.string().nullable().describe("Compare at price"),
7437
+ sku: z27.string().nullable().describe("SKU"),
7438
+ barcode: z27.string().nullable().describe("Barcode"),
7439
+ inventoryQuantity: z27.number().nullable().describe("Inventory quantity")
6722
7440
  })
6723
7441
  ).describe("Product variants"),
6724
- images: z23.array(
6725
- z23.object({
6726
- id: z23.string().describe("Image GID"),
6727
- url: z23.string().describe("Image URL"),
6728
- altText: z23.string().nullable().describe("Alt text")
7442
+ images: z27.array(
7443
+ z27.object({
7444
+ id: z27.string().describe("Image GID"),
7445
+ url: z27.string().describe("Image URL"),
7446
+ altText: z27.string().nullable().describe("Alt text")
6729
7447
  })
6730
7448
  ).describe("Product images"),
6731
- totalInventory: z23.number().describe("Total inventory across variants")
7449
+ totalInventory: z27.number().describe("Total inventory across variants")
6732
7450
  });
6733
7451
  var handleGetProduct = async (context, params) => {
6734
7452
  log.debug(`Getting product on shop: ${context.shopDomain}`);
@@ -6767,8 +7485,8 @@ function registerGetProductTool() {
6767
7485
  name: "get-product",
6768
7486
  title: "Get Product",
6769
7487
  description: "Retrieve details of a specific product from the Shopify store. Provide either the product ID (GID format) or handle (URL slug). Returns full product details including title, description, vendor, type, status, tags, SEO metadata (title/description for search engines), variants (with pricing/SKU/inventory), and images. **Prerequisites:** Use list-products to find product IDs or handles. **Follow-ups:** update-product, delete-product, add-product-image, get-product-metafields.",
6770
- inputSchema: inputSchema22,
6771
- outputSchema: outputSchema22,
7488
+ inputSchema: inputSchema25,
7489
+ outputSchema: outputSchema25,
6772
7490
  // AI Agent Optimization (Story 5.5)
6773
7491
  category: "product",
6774
7492
  relationships: {
@@ -6779,6 +7497,13 @@ function registerGetProductTool() {
6779
7497
  "add-product-image",
6780
7498
  "get-product-metafields"
6781
7499
  ]
7500
+ },
7501
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7502
+ annotations: {
7503
+ readOnlyHint: true,
7504
+ destructiveHint: false,
7505
+ idempotentHint: true,
7506
+ openWorldHint: true
6782
7507
  }
6783
7508
  },
6784
7509
  handleGetProduct
@@ -6786,36 +7511,36 @@ function registerGetProductTool() {
6786
7511
  }
6787
7512
 
6788
7513
  // src/tools/list-articles.ts
6789
- import { z as z24 } from "zod";
6790
- var inputSchema23 = z24.object({
6791
- first: z24.number().int().min(1).max(50).optional().default(10).describe("Number of articles to return (1-50). Defaults to 10."),
6792
- cursor: z24.string().optional().describe(
7514
+ import { z as z28 } from "zod";
7515
+ var inputSchema26 = z28.object({
7516
+ first: z28.number().int().min(1).max(50).optional().default(10).describe("Number of articles to return (1-50). Defaults to 10."),
7517
+ cursor: z28.string().optional().describe(
6793
7518
  "Pagination cursor from a previous response. Use the endCursor from the previous response to get the next page of results."
6794
7519
  ),
6795
- query: z24.string().optional().describe(
7520
+ query: z28.string().optional().describe(
6796
7521
  "Search query to filter articles. Supports Shopify search syntax. Examples: 'title:Guide', 'tag:tutorial', 'blog_id:123456789'."
6797
7522
  ),
6798
- blogId: z24.string().optional().describe(
7523
+ blogId: z28.string().optional().describe(
6799
7524
  '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".'
6800
7525
  ),
6801
- sortKey: z24.enum(["ID", "TITLE", "UPDATED_AT", "PUBLISHED_AT", "AUTHOR", "BLOG_TITLE"]).optional().describe("Sort order for results. Defaults to ID.")
7526
+ sortKey: z28.enum(["ID", "TITLE", "UPDATED_AT", "PUBLISHED_AT", "AUTHOR", "BLOG_TITLE"]).optional().describe("Sort order for results. Defaults to ID.")
6802
7527
  });
6803
- var outputSchema23 = z24.object({
6804
- articles: z24.array(
6805
- z24.object({
6806
- id: z24.string().describe("Article GID"),
6807
- title: z24.string().describe("Article title"),
6808
- handle: z24.string().describe("URL handle/slug"),
6809
- blog: z24.object({
6810
- id: z24.string().describe("Parent blog GID"),
6811
- title: z24.string().describe("Parent blog title")
7528
+ var outputSchema26 = z28.object({
7529
+ articles: z28.array(
7530
+ z28.object({
7531
+ id: z28.string().describe("Article GID"),
7532
+ title: z28.string().describe("Article title"),
7533
+ handle: z28.string().describe("URL handle/slug"),
7534
+ blog: z28.object({
7535
+ id: z28.string().describe("Parent blog GID"),
7536
+ title: z28.string().describe("Parent blog title")
6812
7537
  }),
6813
- isPublished: z24.boolean().describe("Whether article is visible on storefront"),
6814
- publishedAt: z24.string().nullable().describe("ISO timestamp when published")
7538
+ isPublished: z28.boolean().describe("Whether article is visible on storefront"),
7539
+ publishedAt: z28.string().nullable().describe("ISO timestamp when published")
6815
7540
  })
6816
7541
  ),
6817
- hasNextPage: z24.boolean().describe("Whether more articles are available"),
6818
- endCursor: z24.string().nullable().describe("Cursor for next page pagination")
7542
+ hasNextPage: z28.boolean().describe("Whether more articles are available"),
7543
+ endCursor: z28.string().nullable().describe("Cursor for next page pagination")
6819
7544
  });
6820
7545
  var handleListArticles = async (context, params) => {
6821
7546
  log.debug(`Listing articles on shop: ${context.shopDomain}`);
@@ -6859,12 +7584,19 @@ function registerListArticlesTool() {
6859
7584
  name: "list-articles",
6860
7585
  title: "List Articles",
6861
7586
  description: "List articles with optional filtering and pagination. Use blogId to filter articles from a specific blog. Use query to search by title or tags. Returns article ID, title, handle, blog reference, and publish status. Useful for: finding articles to edit, auditing blog content, bulk operations. **Typical workflow:** list-blogs \u2192 list-articles \u2192 update-article. **Prerequisites:** None (optional Blog ID for filtering). **Follow-ups:** update-article, delete-article, create-article. Returns a paginated list of article summaries with a response summary.",
6862
- inputSchema: inputSchema23,
6863
- outputSchema: outputSchema23,
7587
+ inputSchema: inputSchema26,
7588
+ outputSchema: outputSchema26,
6864
7589
  category: "content",
6865
7590
  relationships: {
6866
7591
  relatedTools: ["list-blogs", "create-article", "update-article"],
6867
7592
  followUps: ["update-article", "delete-article", "create-article"]
7593
+ },
7594
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7595
+ annotations: {
7596
+ readOnlyHint: true,
7597
+ destructiveHint: false,
7598
+ idempotentHint: true,
7599
+ openWorldHint: true
6868
7600
  }
6869
7601
  },
6870
7602
  handleListArticles
@@ -6872,29 +7604,29 @@ function registerListArticlesTool() {
6872
7604
  }
6873
7605
 
6874
7606
  // src/tools/list-blogs.ts
6875
- import { z as z25 } from "zod";
6876
- var inputSchema24 = z25.object({
6877
- first: z25.number().int().min(1).max(50).optional().default(10).describe("Number of blogs to return (1-50). Defaults to 10."),
6878
- cursor: z25.string().optional().describe(
7607
+ import { z as z29 } from "zod";
7608
+ var inputSchema27 = z29.object({
7609
+ first: z29.number().int().min(1).max(50).optional().default(10).describe("Number of blogs to return (1-50). Defaults to 10."),
7610
+ cursor: z29.string().optional().describe(
6879
7611
  "Pagination cursor from a previous response. Use the endCursor from the previous response to get the next page of results."
6880
7612
  ),
6881
- query: z25.string().optional().describe(
7613
+ query: z29.string().optional().describe(
6882
7614
  "Search query to filter blogs. Supports Shopify search syntax. Examples: 'title:News', 'handle:company-blog'."
6883
7615
  ),
6884
- sortKey: z25.enum(["ID", "TITLE", "UPDATED_AT"]).optional().describe("Sort order for results. Defaults to ID.")
7616
+ sortKey: z29.enum(["ID", "TITLE", "UPDATED_AT"]).optional().describe("Sort order for results. Defaults to ID.")
6885
7617
  });
6886
- var outputSchema24 = z25.object({
6887
- blogs: z25.array(
6888
- z25.object({
6889
- id: z25.string().describe("Blog GID"),
6890
- title: z25.string().describe("Blog title"),
6891
- handle: z25.string().describe("URL handle/slug"),
6892
- commentPolicy: z25.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy"),
6893
- articlesCount: z25.number().describe("Number of articles in the blog")
7618
+ var outputSchema27 = z29.object({
7619
+ blogs: z29.array(
7620
+ z29.object({
7621
+ id: z29.string().describe("Blog GID"),
7622
+ title: z29.string().describe("Blog title"),
7623
+ handle: z29.string().describe("URL handle/slug"),
7624
+ commentPolicy: z29.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy"),
7625
+ articlesCount: z29.number().describe("Number of articles in the blog")
6894
7626
  })
6895
7627
  ),
6896
- hasNextPage: z25.boolean().describe("Whether more blogs are available"),
6897
- endCursor: z25.string().nullable().describe("Cursor for next page pagination")
7628
+ hasNextPage: z29.boolean().describe("Whether more blogs are available"),
7629
+ endCursor: z29.string().nullable().describe("Cursor for next page pagination")
6898
7630
  });
6899
7631
  var handleListBlogs = async (context, params) => {
6900
7632
  log.debug(`Listing blogs on shop: ${context.shopDomain}`);
@@ -6932,12 +7664,19 @@ function registerListBlogsTool() {
6932
7664
  name: "list-blogs",
6933
7665
  title: "List Blogs",
6934
7666
  description: "List all blogs with optional filtering and pagination. Use the query parameter to search blogs by title or handle. Returns blog ID, title, handle, and article count. Useful for: finding blogs to manage, auditing content, selecting a blog for new articles. **Typical workflow:** list-blogs \u2192 list-articles. **Prerequisites:** None. **Follow-ups:** create-article, update-blog, create-blog. Returns a paginated list of blog summaries with a response summary.",
6935
- inputSchema: inputSchema24,
6936
- outputSchema: outputSchema24,
7667
+ inputSchema: inputSchema27,
7668
+ outputSchema: outputSchema27,
6937
7669
  category: "content",
6938
7670
  relationships: {
6939
7671
  relatedTools: ["create-blog", "update-blog", "list-articles"],
6940
7672
  followUps: ["create-article", "update-blog", "create-blog"]
7673
+ },
7674
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7675
+ annotations: {
7676
+ readOnlyHint: true,
7677
+ destructiveHint: false,
7678
+ idempotentHint: true,
7679
+ openWorldHint: true
6941
7680
  }
6942
7681
  },
6943
7682
  handleListBlogs
@@ -6945,40 +7684,40 @@ function registerListBlogsTool() {
6945
7684
  }
6946
7685
 
6947
7686
  // src/tools/list-collections.ts
6948
- import { z as z26 } from "zod";
6949
- var inputSchema25 = z26.object({
6950
- first: z26.number().int().min(1).max(50).optional().default(10).describe("Number of collections to return (1-50). Default: 10"),
6951
- after: z26.string().optional().describe(
7687
+ import { z as z30 } from "zod";
7688
+ var inputSchema28 = z30.object({
7689
+ first: z30.number().int().min(1).max(50).optional().default(10).describe("Number of collections to return (1-50). Default: 10"),
7690
+ after: z30.string().optional().describe(
6952
7691
  "Pagination cursor from previous response. Use the endCursor value from the previous page to get the next page of results."
6953
7692
  ),
6954
- query: z26.string().optional().describe(
7693
+ query: z30.string().optional().describe(
6955
7694
  'Search query to filter collections. Searches across collection titles and handles. Example: "summer" finds collections with "summer" in the title or handle.'
6956
7695
  ),
6957
- sortKey: z26.enum(["TITLE", "UPDATED_AT", "ID", "RELEVANCE"]).optional().describe(
7696
+ sortKey: z30.enum(["TITLE", "UPDATED_AT", "ID", "RELEVANCE"]).optional().describe(
6958
7697
  "Sort collections by: TITLE (alphabetical), UPDATED_AT (most recently updated first), ID (by collection ID), or RELEVANCE (best match when using query filter)"
6959
7698
  )
6960
7699
  });
6961
- var outputSchema25 = z26.object({
6962
- collections: z26.array(
6963
- z26.object({
6964
- id: z26.string().describe("Collection GID"),
6965
- title: z26.string().describe("Collection title"),
6966
- handle: z26.string().describe("URL handle"),
6967
- seo: z26.object({
6968
- title: z26.string().nullable().describe("SEO title"),
6969
- description: z26.string().nullable().describe("SEO description")
7700
+ var outputSchema28 = z30.object({
7701
+ collections: z30.array(
7702
+ z30.object({
7703
+ id: z30.string().describe("Collection GID"),
7704
+ title: z30.string().describe("Collection title"),
7705
+ handle: z30.string().describe("URL handle"),
7706
+ seo: z30.object({
7707
+ title: z30.string().nullable().describe("SEO title"),
7708
+ description: z30.string().nullable().describe("SEO description")
6970
7709
  }),
6971
- productsCount: z26.number().describe("Number of products in collection")
7710
+ productsCount: z30.number().describe("Number of products in collection")
6972
7711
  })
6973
7712
  ),
6974
- pageInfo: z26.object({
6975
- hasNextPage: z26.boolean().describe("Whether more pages exist"),
6976
- endCursor: z26.string().nullable().describe("Cursor for next page")
7713
+ pageInfo: z30.object({
7714
+ hasNextPage: z30.boolean().describe("Whether more pages exist"),
7715
+ endCursor: z30.string().nullable().describe("Cursor for next page")
6977
7716
  }),
6978
- summary: z26.object({
6979
- totalReturned: z26.number().describe("Number of collections in this response"),
6980
- hasMore: z26.boolean().describe("Whether more collections are available"),
6981
- hint: z26.string().describe("Suggestion for next action")
7717
+ summary: z30.object({
7718
+ totalReturned: z30.number().describe("Number of collections in this response"),
7719
+ hasMore: z30.boolean().describe("Whether more collections are available"),
7720
+ hint: z30.string().describe("Suggestion for next action")
6982
7721
  }).describe("AI-friendly summary for context management")
6983
7722
  });
6984
7723
  var handleListCollections = async (context, params) => {
@@ -7005,13 +7744,20 @@ function registerListCollectionsTool() {
7005
7744
  name: "list-collections",
7006
7745
  title: "List Collections",
7007
7746
  description: 'List and search collections with pagination and filtering. Use the query parameter for text search across collection titles and handles. Supports sorting by TITLE, UPDATED_AT, ID, or RELEVANCE. Returns up to 50 collections per page. Response includes summary with totalReturned, hasMore, and pagination hint. **Pagination:** Use "after" cursor from pageInfo.endCursor to get next page. **Follow-ups:** get-collection (for full details), update-collection, add-products-to-collection.',
7008
- inputSchema: inputSchema25,
7009
- outputSchema: outputSchema25,
7747
+ inputSchema: inputSchema28,
7748
+ outputSchema: outputSchema28,
7010
7749
  // AI Agent Optimization (Story 5.5)
7011
7750
  category: "collection",
7012
7751
  relationships: {
7013
7752
  relatedTools: ["get-collection"],
7014
7753
  followUps: ["get-collection", "update-collection", "add-products-to-collection"]
7754
+ },
7755
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7756
+ annotations: {
7757
+ readOnlyHint: true,
7758
+ destructiveHint: false,
7759
+ idempotentHint: true,
7760
+ openWorldHint: true
7015
7761
  }
7016
7762
  },
7017
7763
  handleListCollections
@@ -7019,47 +7765,47 @@ function registerListCollectionsTool() {
7019
7765
  }
7020
7766
 
7021
7767
  // src/tools/list-low-inventory.ts
7022
- import { z as z27 } from "zod";
7023
- var inputSchema26 = z27.object({
7024
- threshold: z27.number().int().min(0).default(10).describe(
7768
+ import { z as z31 } from "zod";
7769
+ var inputSchema29 = z31.object({
7770
+ threshold: z31.number().int().min(0).default(10).describe(
7025
7771
  "Products with total inventory at or below this number will be included. Default: 10. Use 0 to find only out-of-stock products."
7026
7772
  ),
7027
- includeZero: z27.boolean().default(true).describe(
7773
+ includeZero: z31.boolean().default(true).describe(
7028
7774
  "Include products with zero inventory. Default: true. Set to false to see only low-stock (not out-of-stock) products."
7029
7775
  ),
7030
- status: z27.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe(
7776
+ status: z31.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe(
7031
7777
  "Filter by product status. Default: ACTIVE. Use DRAFT to check pre-launch products, ARCHIVED for historical."
7032
7778
  ),
7033
- first: z27.number().int().min(1).max(100).default(50).describe("Number of products to return per page. Default: 50, max: 100."),
7034
- after: z27.string().optional().describe("Pagination cursor from previous response. Use pageInfo.endCursor to get next page.")
7779
+ first: z31.number().int().min(1).max(100).default(50).describe("Number of products to return per page. Default: 50, max: 100."),
7780
+ after: z31.string().optional().describe("Pagination cursor from previous response. Use pageInfo.endCursor to get next page.")
7035
7781
  });
7036
- var outputSchema26 = z27.object({
7037
- products: z27.array(
7038
- z27.object({
7039
- productId: z27.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
7040
- productTitle: z27.string().describe("Product title"),
7041
- status: z27.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
7042
- totalInventory: z27.number().describe("Total inventory across all variants"),
7043
- isOutOfStock: z27.boolean().describe("True if total inventory is zero"),
7044
- variants: z27.array(
7045
- z27.object({
7046
- variantId: z27.string().describe("Variant GID"),
7047
- variantTitle: z27.string().describe('Variant title (e.g., "Red / Large")'),
7048
- sku: z27.string().nullable().describe("SKU (Stock Keeping Unit)"),
7049
- inventoryItemId: z27.string().describe("Inventory item GID"),
7050
- available: z27.number().describe("Available quantity for sale")
7782
+ var outputSchema29 = z31.object({
7783
+ products: z31.array(
7784
+ z31.object({
7785
+ productId: z31.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
7786
+ productTitle: z31.string().describe("Product title"),
7787
+ status: z31.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
7788
+ totalInventory: z31.number().describe("Total inventory across all variants"),
7789
+ isOutOfStock: z31.boolean().describe("True if total inventory is zero"),
7790
+ variants: z31.array(
7791
+ z31.object({
7792
+ variantId: z31.string().describe("Variant GID"),
7793
+ variantTitle: z31.string().describe('Variant title (e.g., "Red / Large")'),
7794
+ sku: z31.string().nullable().describe("SKU (Stock Keeping Unit)"),
7795
+ inventoryItemId: z31.string().describe("Inventory item GID"),
7796
+ available: z31.number().describe("Available quantity for sale")
7051
7797
  })
7052
7798
  ).describe("Variant breakdown with inventory")
7053
7799
  })
7054
7800
  ).describe("Products with low inventory, sorted by total inventory ascending"),
7055
- pageInfo: z27.object({
7056
- hasNextPage: z27.boolean().describe("Whether more products exist after this page"),
7057
- endCursor: z27.string().nullable().describe('Cursor for the last item - use as "after" to get next page')
7801
+ pageInfo: z31.object({
7802
+ hasNextPage: z31.boolean().describe("Whether more products exist after this page"),
7803
+ endCursor: z31.string().nullable().describe('Cursor for the last item - use as "after" to get next page')
7058
7804
  }).describe("Pagination information"),
7059
- summary: z27.object({
7060
- totalProducts: z27.number().describe("Number of products in this response"),
7061
- outOfStockCount: z27.number().describe("Number of products with zero inventory"),
7062
- lowStockCount: z27.number().describe("Number of products with inventory > 0 but below threshold")
7805
+ summary: z31.object({
7806
+ totalProducts: z31.number().describe("Number of products in this response"),
7807
+ outOfStockCount: z31.number().describe("Number of products with zero inventory"),
7808
+ lowStockCount: z31.number().describe("Number of products with inventory > 0 but below threshold")
7063
7809
  }).describe("Summary statistics")
7064
7810
  });
7065
7811
  var handleListLowInventory = async (context, params) => {
@@ -7083,44 +7829,125 @@ function registerListLowInventoryTool() {
7083
7829
  name: "list-low-inventory",
7084
7830
  title: "List Low Inventory",
7085
7831
  description: 'Find products with low or zero inventory that may need restocking. Returns products sorted by inventory level (lowest first). Use threshold parameter to customize what counts as "low stock" (default: 10 units). Includes variant breakdown and summary statistics showing how many products are out-of-stock vs. low-stock. Perfect for daily inventory checks, restocking alerts, and identifying products at risk of stockout. Use includeZero=false to exclude completely sold-out items. Use status=DRAFT to check pre-launch products before publishing. **Typical workflow:** list-low-inventory \u2192 get-inventory \u2192 update-inventory. **Prerequisites:** None. **Follow-ups:** get-inventory, update-inventory. Returns paginated products with inventory levels and summary statistics.',
7086
- inputSchema: inputSchema26,
7087
- outputSchema: outputSchema26,
7832
+ inputSchema: inputSchema29,
7833
+ outputSchema: outputSchema29,
7088
7834
  category: "inventory",
7089
7835
  relationships: {
7090
7836
  relatedTools: ["get-inventory", "update-inventory", "get-bulk-inventory"],
7091
7837
  followUps: ["get-inventory", "update-inventory"]
7838
+ },
7839
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7840
+ annotations: {
7841
+ readOnlyHint: true,
7842
+ destructiveHint: false,
7843
+ idempotentHint: true,
7844
+ openWorldHint: true
7092
7845
  }
7093
7846
  },
7094
7847
  handleListLowInventory
7095
7848
  );
7096
7849
  }
7097
7850
 
7851
+ // src/tools/list-markets.ts
7852
+ import { z as z32 } from "zod";
7853
+ var inputSchema30 = z32.object({
7854
+ first: z32.number().int().min(1).max(50).optional().default(10).describe("Number of markets to return (1-50). Default: 10"),
7855
+ after: z32.string().optional().describe(
7856
+ "Pagination cursor from previous response. Use the endCursor value from the previous page to get the next page of results."
7857
+ )
7858
+ });
7859
+ var outputSchema30 = z32.object({
7860
+ markets: z32.array(
7861
+ z32.object({
7862
+ id: z32.string().describe("Market GID"),
7863
+ name: z32.string().describe("Market name (not shown to customers)"),
7864
+ handle: z32.string().describe("URL handle"),
7865
+ enabled: z32.boolean().describe("Whether the market is enabled"),
7866
+ status: z32.enum(["ACTIVE", "INACTIVE"]).describe("Market status"),
7867
+ type: z32.string().describe("Market type (PRIMARY, SINGLE_COUNTRY, MULTI_COUNTRY, CONTINENT)"),
7868
+ currencySettings: z32.object({
7869
+ baseCurrency: z32.object({
7870
+ currencyCode: z32.string().describe("Base currency code (e.g., USD, EUR)")
7871
+ }),
7872
+ localCurrencies: z32.boolean().describe("Whether local currencies are enabled")
7873
+ }).optional()
7874
+ })
7875
+ ),
7876
+ pageInfo: z32.object({
7877
+ hasNextPage: z32.boolean().describe("Whether more pages exist"),
7878
+ endCursor: z32.string().nullable().describe("Cursor for next page")
7879
+ }),
7880
+ summary: z32.object({
7881
+ totalReturned: z32.number().describe("Number of markets in this response"),
7882
+ hasMore: z32.boolean().describe("Whether more markets are available"),
7883
+ hint: z32.string().describe("Suggestion for next action")
7884
+ }).describe("AI-friendly summary for context management")
7885
+ });
7886
+ var handleListMarkets = async (context, params) => {
7887
+ log.debug(`Listing markets on shop: ${context.shopDomain}`);
7888
+ const result = await listMarkets(params.first, params.after);
7889
+ log.debug(`Listed ${result.markets.length} markets via tool`);
7890
+ return {
7891
+ ...result,
7892
+ summary: {
7893
+ totalReturned: result.markets.length,
7894
+ hasMore: result.pageInfo.hasNextPage,
7895
+ hint: result.pageInfo.hasNextPage ? `Use "after" parameter with value "${result.pageInfo.endCursor}" to fetch next page` : "All markets returned"
7896
+ }
7897
+ };
7898
+ };
7899
+ function registerListMarketsTool() {
7900
+ registerContextAwareTool(
7901
+ {
7902
+ name: "list-markets",
7903
+ title: "List Markets",
7904
+ description: 'List all configured markets in the Shopify store with pagination. Markets enable regional targeting for international stores. Returns market id, name, handle, status, type, and currency settings. Response includes summary with totalReturned, hasMore, and pagination hint. **Pagination:** Use "after" cursor from pageInfo.endCursor to get next page. **Prerequisites:** Store must have Markets feature enabled. **Follow-ups:** get-market (for full details including web presences), create-market.',
7905
+ inputSchema: inputSchema30,
7906
+ outputSchema: outputSchema30,
7907
+ // AI Agent Optimization
7908
+ category: "market",
7909
+ relationships: {
7910
+ relatedTools: ["get-market"],
7911
+ followUps: ["get-market", "create-market", "update-market"]
7912
+ },
7913
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7914
+ annotations: {
7915
+ readOnlyHint: true,
7916
+ destructiveHint: false,
7917
+ idempotentHint: true,
7918
+ openWorldHint: true
7919
+ }
7920
+ },
7921
+ handleListMarkets
7922
+ );
7923
+ }
7924
+
7098
7925
  // src/tools/list-pages.ts
7099
- import { z as z28 } from "zod";
7100
- var inputSchema27 = z28.object({
7101
- first: z28.number().int().min(1).max(50).optional().describe(
7926
+ import { z as z33 } from "zod";
7927
+ var inputSchema31 = z33.object({
7928
+ first: z33.number().int().min(1).max(50).optional().describe(
7102
7929
  "Number of pages to return per request. Default: 10, Maximum: 50. Use pagination (cursor) to retrieve more results."
7103
7930
  ),
7104
- cursor: z28.string().optional().describe(
7931
+ cursor: z33.string().optional().describe(
7105
7932
  "Pagination cursor from a previous list-pages response (endCursor). Use this to get the next page of results."
7106
7933
  ),
7107
- query: z28.string().optional().describe(
7934
+ query: z33.string().optional().describe(
7108
7935
  "Search query to filter pages. Supports Shopify search syntax. Examples: 'title:About', 'handle:contact', 'created_at:>2024-01-01'."
7109
7936
  ),
7110
- sortKey: z28.enum(["ID", "TITLE", "UPDATED_AT", "PUBLISHED_AT"]).optional().describe("Field to sort pages by. Options: ID (default), TITLE, UPDATED_AT, PUBLISHED_AT.")
7937
+ sortKey: z33.enum(["ID", "TITLE", "UPDATED_AT", "PUBLISHED_AT"]).optional().describe("Field to sort pages by. Options: ID (default), TITLE, UPDATED_AT, PUBLISHED_AT.")
7111
7938
  });
7112
- var outputSchema27 = z28.object({
7113
- pages: z28.array(
7114
- z28.object({
7115
- id: z28.string().describe("Page GID"),
7116
- title: z28.string().describe("Page title"),
7117
- handle: z28.string().describe("URL handle/slug"),
7118
- isPublished: z28.boolean().describe("Whether page is visible"),
7119
- updatedAt: z28.string().describe("ISO timestamp of last update")
7939
+ var outputSchema31 = z33.object({
7940
+ pages: z33.array(
7941
+ z33.object({
7942
+ id: z33.string().describe("Page GID"),
7943
+ title: z33.string().describe("Page title"),
7944
+ handle: z33.string().describe("URL handle/slug"),
7945
+ isPublished: z33.boolean().describe("Whether page is visible"),
7946
+ updatedAt: z33.string().describe("ISO timestamp of last update")
7120
7947
  })
7121
7948
  ),
7122
- hasNextPage: z28.boolean().describe("Whether more pages exist"),
7123
- endCursor: z28.string().nullable().describe("Cursor for next page of results")
7949
+ hasNextPage: z33.boolean().describe("Whether more pages exist"),
7950
+ endCursor: z33.string().nullable().describe("Cursor for next page of results")
7124
7951
  });
7125
7952
  var handleListPages = async (context, params) => {
7126
7953
  log.debug(`Listing pages on shop: ${context.shopDomain}`);
@@ -7146,12 +7973,19 @@ function registerListPagesTool() {
7146
7973
  name: "list-pages",
7147
7974
  title: "List Pages",
7148
7975
  description: "List all pages with optional filtering and pagination. Use the query parameter to search pages by title or handle. Returns page ID, title, handle, and publish status. Use pagination (cursor) for stores with many pages. Useful for: finding pages to edit, auditing content, bulk operations. **Typical workflow:** list-pages \u2192 get-page. **Prerequisites:** None. **Follow-ups:** get-page, update-page, create-page. Returns a paginated list of page summaries with a response summary.",
7149
- inputSchema: inputSchema27,
7150
- outputSchema: outputSchema27,
7976
+ inputSchema: inputSchema31,
7977
+ outputSchema: outputSchema31,
7151
7978
  category: "content",
7152
7979
  relationships: {
7153
7980
  relatedTools: ["get-page", "create-page"],
7154
7981
  followUps: ["get-page", "update-page", "create-page"]
7982
+ },
7983
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
7984
+ annotations: {
7985
+ readOnlyHint: true,
7986
+ destructiveHint: false,
7987
+ idempotentHint: true,
7988
+ openWorldHint: true
7155
7989
  }
7156
7990
  },
7157
7991
  handleListPages
@@ -7159,46 +7993,46 @@ function registerListPagesTool() {
7159
7993
  }
7160
7994
 
7161
7995
  // src/tools/list-products.ts
7162
- import { z as z29 } from "zod";
7163
- var inputSchema28 = z29.object({
7164
- first: z29.number().int().min(1).max(50).optional().describe("Number of products to return (1-50). Default: 10"),
7165
- after: z29.string().optional().describe("Pagination cursor from previous response's endCursor. Omit for first page."),
7166
- query: z29.string().optional().describe(
7996
+ import { z as z34 } from "zod";
7997
+ var inputSchema32 = z34.object({
7998
+ first: z34.number().int().min(1).max(50).optional().describe("Number of products to return (1-50). Default: 10"),
7999
+ after: z34.string().optional().describe("Pagination cursor from previous response's endCursor. Omit for first page."),
8000
+ query: z34.string().optional().describe(
7167
8001
  'Search query for title, description, tags. Example: "summer dress", "organic cotton"'
7168
8002
  ),
7169
- status: z29.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe(
8003
+ status: z34.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe(
7170
8004
  "Filter by product status. ACTIVE = published, DRAFT = not published, ARCHIVED = hidden"
7171
8005
  ),
7172
- vendor: z29.string().optional().describe('Filter by exact vendor name. Example: "Acme Corp", "Nike"'),
7173
- productType: z29.string().optional().describe('Filter by exact product type. Example: "T-Shirts", "Shoes"'),
7174
- sortBy: z29.enum(["TITLE", "CREATED_AT", "UPDATED_AT", "INVENTORY_TOTAL"]).optional().describe("Sort field. TITLE, CREATED_AT (default), UPDATED_AT, or INVENTORY_TOTAL"),
7175
- sortOrder: z29.enum(["ASC", "DESC"]).optional().describe("Sort direction. ASC (default) for ascending, DESC for descending")
8006
+ vendor: z34.string().optional().describe('Filter by exact vendor name. Example: "Acme Corp", "Nike"'),
8007
+ productType: z34.string().optional().describe('Filter by exact product type. Example: "T-Shirts", "Shoes"'),
8008
+ sortBy: z34.enum(["TITLE", "CREATED_AT", "UPDATED_AT", "INVENTORY_TOTAL"]).optional().describe("Sort field. TITLE, CREATED_AT (default), UPDATED_AT, or INVENTORY_TOTAL"),
8009
+ sortOrder: z34.enum(["ASC", "DESC"]).optional().describe("Sort direction. ASC (default) for ascending, DESC for descending")
7176
8010
  });
7177
- var outputSchema28 = z29.object({
7178
- products: z29.array(
7179
- z29.object({
7180
- id: z29.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
7181
- title: z29.string().describe("Product title"),
7182
- handle: z29.string().describe("URL handle/slug"),
7183
- status: z29.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
7184
- vendor: z29.string().describe("Vendor/brand name"),
7185
- productType: z29.string().describe("Product type"),
7186
- totalInventory: z29.number().describe("Total inventory across all variants"),
7187
- primaryVariantPrice: z29.string().describe('Price of the first variant (e.g., "29.99")'),
7188
- imageUrl: z29.string().nullable().describe("First image URL or null if no images")
8011
+ var outputSchema32 = z34.object({
8012
+ products: z34.array(
8013
+ z34.object({
8014
+ id: z34.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
8015
+ title: z34.string().describe("Product title"),
8016
+ handle: z34.string().describe("URL handle/slug"),
8017
+ status: z34.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
8018
+ vendor: z34.string().describe("Vendor/brand name"),
8019
+ productType: z34.string().describe("Product type"),
8020
+ totalInventory: z34.number().describe("Total inventory across all variants"),
8021
+ primaryVariantPrice: z34.string().describe('Price of the first variant (e.g., "29.99")'),
8022
+ imageUrl: z34.string().nullable().describe("First image URL or null if no images")
7189
8023
  })
7190
8024
  ).describe("Array of product summaries"),
7191
- pageInfo: z29.object({
7192
- hasNextPage: z29.boolean().describe("Whether more products exist after this page"),
7193
- hasPreviousPage: z29.boolean().describe("Whether products exist before this page"),
7194
- startCursor: z29.string().nullable().describe("Cursor for the first item in this page"),
7195
- endCursor: z29.string().nullable().describe('Cursor for the last item - use as "after" to get next page')
8025
+ pageInfo: z34.object({
8026
+ hasNextPage: z34.boolean().describe("Whether more products exist after this page"),
8027
+ hasPreviousPage: z34.boolean().describe("Whether products exist before this page"),
8028
+ startCursor: z34.string().nullable().describe("Cursor for the first item in this page"),
8029
+ endCursor: z34.string().nullable().describe('Cursor for the last item - use as "after" to get next page')
7196
8030
  }).describe("Pagination information"),
7197
- totalCount: z29.number().describe("Number of products returned in this page"),
7198
- summary: z29.object({
7199
- totalReturned: z29.number().describe("Number of products in this response"),
7200
- hasMore: z29.boolean().describe("Whether more products are available"),
7201
- hint: z29.string().describe("Suggestion for next action")
8031
+ totalCount: z34.number().describe("Number of products returned in this page"),
8032
+ summary: z34.object({
8033
+ totalReturned: z34.number().describe("Number of products in this response"),
8034
+ hasMore: z34.boolean().describe("Whether more products are available"),
8035
+ hint: z34.string().describe("Suggestion for next action")
7202
8036
  }).describe("AI-friendly summary for context management")
7203
8037
  });
7204
8038
  var handleListProducts = async (context, params) => {
@@ -7232,13 +8066,20 @@ function registerListProductsTool() {
7232
8066
  name: "list-products",
7233
8067
  title: "List Products",
7234
8068
  description: 'List products from the Shopify store with pagination and filtering. Returns product summaries including title, status, vendor, type, price, and inventory. Supports filtering by status (ACTIVE/DRAFT/ARCHIVED), vendor, productType, and search query. Sort by TITLE, CREATED_AT, UPDATED_AT, or INVENTORY_TOTAL. Response includes summary with totalReturned, hasMore, and pagination hint. **Pagination:** Use "after" cursor from pageInfo.endCursor to get next page. **Follow-ups:** get-product (for full details), update-product, delete-product.',
7235
- inputSchema: inputSchema28,
7236
- outputSchema: outputSchema28,
8069
+ inputSchema: inputSchema32,
8070
+ outputSchema: outputSchema32,
7237
8071
  // AI Agent Optimization (Story 5.5)
7238
8072
  category: "product",
7239
8073
  relationships: {
7240
8074
  relatedTools: ["get-product"],
7241
8075
  followUps: ["get-product", "update-product", "delete-product"]
8076
+ },
8077
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8078
+ annotations: {
8079
+ readOnlyHint: true,
8080
+ destructiveHint: false,
8081
+ idempotentHint: true,
8082
+ openWorldHint: true
7242
8083
  }
7243
8084
  },
7244
8085
  handleListProducts
@@ -7246,30 +8087,30 @@ function registerListProductsTool() {
7246
8087
  }
7247
8088
 
7248
8089
  // src/tools/list-redirects.ts
7249
- import { z as z30 } from "zod";
7250
- var inputSchema29 = z30.object({
7251
- first: z30.number().int().min(1).max(50).optional().default(10).describe("Number of redirects to return (1-50, default: 10)."),
7252
- cursor: z30.string().optional().describe(
8090
+ import { z as z35 } from "zod";
8091
+ var inputSchema33 = z35.object({
8092
+ first: z35.number().int().min(1).max(50).optional().default(10).describe("Number of redirects to return (1-50, default: 10)."),
8093
+ cursor: z35.string().optional().describe(
7253
8094
  "Pagination cursor from previous response (endCursor). Use this to fetch the next page of results."
7254
8095
  ),
7255
- query: z30.string().optional().describe(
8096
+ query: z35.string().optional().describe(
7256
8097
  "Search filter. Supports 'path:' and 'target:' prefixes. Examples: 'path:/old-' to find redirects starting with /old-, 'target:/products/' to find redirects pointing to /products/."
7257
8098
  )
7258
8099
  });
7259
- var outputSchema29 = z30.object({
7260
- redirects: z30.array(
7261
- z30.object({
7262
- id: z30.string().describe("Redirect GID"),
7263
- path: z30.string().describe("Source path"),
7264
- target: z30.string().describe("Target URL")
8100
+ var outputSchema33 = z35.object({
8101
+ redirects: z35.array(
8102
+ z35.object({
8103
+ id: z35.string().describe("Redirect GID"),
8104
+ path: z35.string().describe("Source path"),
8105
+ target: z35.string().describe("Target URL")
7265
8106
  })
7266
8107
  ),
7267
- hasNextPage: z30.boolean().describe("Whether more results exist"),
7268
- endCursor: z30.string().nullable().describe("Cursor for fetching next page"),
7269
- summary: z30.object({
7270
- totalReturned: z30.number().describe("Number of redirects in this response"),
7271
- hasMore: z30.boolean().describe("Whether more redirects are available"),
7272
- hint: z30.string().describe("Suggestion for next action")
8108
+ hasNextPage: z35.boolean().describe("Whether more results exist"),
8109
+ endCursor: z35.string().nullable().describe("Cursor for fetching next page"),
8110
+ summary: z35.object({
8111
+ totalReturned: z35.number().describe("Number of redirects in this response"),
8112
+ hasMore: z35.boolean().describe("Whether more redirects are available"),
8113
+ hint: z35.string().describe("Suggestion for next action")
7273
8114
  }).describe("AI-friendly summary for context management")
7274
8115
  });
7275
8116
  var handleListRedirects = async (context, params) => {
@@ -7307,13 +8148,20 @@ function registerListRedirectsTool() {
7307
8148
  name: "list-redirects",
7308
8149
  title: "List URL Redirects",
7309
8150
  description: `List all URL redirects with optional filtering and pagination. Use query parameter to search by path or target (supports 'path:' and 'target:' prefixes). Response includes summary with totalReturned, hasMore, and pagination hint. **Pagination:** Use "cursor" with endCursor value to get next page. **Follow-ups:** delete-redirect to remove unwanted redirects.`,
7310
- inputSchema: inputSchema29,
7311
- outputSchema: outputSchema29,
8151
+ inputSchema: inputSchema33,
8152
+ outputSchema: outputSchema33,
7312
8153
  // AI Agent Optimization (Story 5.5)
7313
8154
  category: "seo",
7314
8155
  relationships: {
7315
8156
  relatedTools: ["create-redirect"],
7316
8157
  followUps: ["delete-redirect"]
8158
+ },
8159
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8160
+ annotations: {
8161
+ readOnlyHint: true,
8162
+ destructiveHint: false,
8163
+ idempotentHint: true,
8164
+ openWorldHint: true
7317
8165
  }
7318
8166
  },
7319
8167
  handleListRedirects
@@ -7321,18 +8169,18 @@ function registerListRedirectsTool() {
7321
8169
  }
7322
8170
 
7323
8171
  // src/tools/remove-products-from-collection.ts
7324
- import { z as z31 } from "zod";
7325
- var inputSchema30 = z31.object({
7326
- collectionId: z31.string().min(1).describe(
8172
+ import { z as z36 } from "zod";
8173
+ var inputSchema34 = z36.object({
8174
+ collectionId: collectionIdSchema.describe(
7327
8175
  'Collection GID (e.g., "gid://shopify/Collection/123"). Use list-collections to find valid collection IDs.'
7328
8176
  ),
7329
- productIds: z31.array(z31.string().min(1)).min(1).max(250).describe(
8177
+ productIds: z36.array(productIdSchema).min(1).max(250).describe(
7330
8178
  '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.'
7331
8179
  )
7332
8180
  });
7333
- var outputSchema30 = z31.object({
7334
- success: z31.boolean().describe("Whether the operation succeeded"),
7335
- removedCount: z31.number().describe("Number of products removed from the collection")
8181
+ var outputSchema34 = z36.object({
8182
+ success: z36.boolean().describe("Whether the operation succeeded"),
8183
+ removedCount: z36.number().describe("Number of products removed from the collection")
7336
8184
  });
7337
8185
  var handleRemoveProductsFromCollection = async (context, params) => {
7338
8186
  log.debug(
@@ -7362,13 +8210,22 @@ function registerRemoveProductsFromCollectionTool() {
7362
8210
  name: "remove-products-from-collection",
7363
8211
  title: "Remove Products from Collection",
7364
8212
  description: "Remove products from a collection by their product IDs. This does NOT delete the products - they remain in the catalog but are no longer part of this collection. Maximum 250 products per call. **Prerequisites:** get-collection to verify collection contents before removing.",
7365
- inputSchema: inputSchema30,
7366
- outputSchema: outputSchema30,
8213
+ inputSchema: inputSchema34,
8214
+ outputSchema: outputSchema34,
7367
8215
  // AI Agent Optimization (Story 5.5)
7368
8216
  category: "collection",
7369
8217
  relationships: {
7370
8218
  relatedTools: ["add-products-to-collection", "get-collection"],
7371
8219
  prerequisites: ["get-collection"]
8220
+ },
8221
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8222
+ annotations: {
8223
+ readOnlyHint: false,
8224
+ destructiveHint: false,
8225
+ // Does not delete products, just removes from collection
8226
+ idempotentHint: true,
8227
+ // Removing already-removed products is idempotent
8228
+ openWorldHint: true
7372
8229
  }
7373
8230
  },
7374
8231
  handleRemoveProductsFromCollection
@@ -7376,7 +8233,7 @@ function registerRemoveProductsFromCollectionTool() {
7376
8233
  }
7377
8234
 
7378
8235
  // src/tools/reorder-product-images.ts
7379
- import { z as z32 } from "zod";
8236
+ import { z as z37 } from "zod";
7380
8237
  var PRODUCT_REORDER_MEDIA_MUTATION = `
7381
8238
  mutation ProductReorderMedia($id: ID!, $moves: [MoveInput!]!) {
7382
8239
  productReorderMedia(id: $id, moves: $moves) {
@@ -7391,23 +8248,23 @@ var PRODUCT_REORDER_MEDIA_MUTATION = `
7391
8248
  }
7392
8249
  }
7393
8250
  `;
7394
- var moveSchema = z32.object({
7395
- id: z32.string().min(1).describe('Image GID to move (e.g., "gid://shopify/MediaImage/123")'),
7396
- newPosition: z32.number().int().min(0).describe("Zero-based target position. Position 0 is the featured image.")
8251
+ var moveSchema = z37.object({
8252
+ id: imageIdSchema.describe('Image GID to move (e.g., "gid://shopify/MediaImage/123")'),
8253
+ newPosition: z37.number().int().min(0).describe("Zero-based target position. Position 0 is the featured image.")
7397
8254
  });
7398
- var inputSchema31 = z32.object({
7399
- productId: z32.string().min(1).describe(
8255
+ var inputSchema35 = z37.object({
8256
+ productId: productIdSchema.describe(
7400
8257
  'Shopify product GID (e.g., "gid://shopify/Product/123"). Required. Use list-products to find product IDs.'
7401
8258
  ),
7402
- moves: z32.array(moveSchema).min(1).describe(
8259
+ moves: z37.array(moveSchema).min(1).describe(
7403
8260
  "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)."
7404
8261
  )
7405
8262
  });
7406
- var outputSchema31 = z32.object({
7407
- success: z32.boolean().describe("Whether the reorder operation was initiated successfully"),
7408
- jobId: z32.string().nullable().describe("Background job ID for tracking (may be null if processed immediately)"),
7409
- jobDone: z32.boolean().describe("Whether the job completed immediately"),
7410
- message: z32.string().describe("Human-readable message describing the result")
8263
+ var outputSchema35 = z37.object({
8264
+ success: z37.boolean().describe("Whether the reorder operation was initiated successfully"),
8265
+ jobId: z37.string().nullable().describe("Background job ID for tracking (may be null if processed immediately)"),
8266
+ jobDone: z37.boolean().describe("Whether the job completed immediately"),
8267
+ message: z37.string().describe("Human-readable message describing the result")
7411
8268
  });
7412
8269
  var handleReorderProductImages = async (context, params) => {
7413
8270
  log.debug(`Reordering images on shop: ${context.shopDomain}`);
@@ -7475,13 +8332,21 @@ function registerReorderProductImagesTool() {
7475
8332
  name: "reorder-product-images",
7476
8333
  title: "Reorder Product Images",
7477
8334
  description: "Change the display order of product images. The first image (position 0) is typically the featured/hero image shown in product listings. Provide an array of move operations specifying which image IDs should move to which positions. Positions are 0-indexed. This operation runs asynchronously in Shopify's background. **Prerequisites:** get-product to obtain current image IDs and their positions.",
7478
- inputSchema: inputSchema31,
7479
- outputSchema: outputSchema31,
8335
+ inputSchema: inputSchema35,
8336
+ outputSchema: outputSchema35,
7480
8337
  // AI Agent Optimization (Story 5.5)
7481
8338
  category: "media",
7482
8339
  relationships: {
7483
8340
  relatedTools: ["add-product-image", "update-product-image"],
7484
8341
  prerequisites: ["get-product"]
8342
+ },
8343
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8344
+ annotations: {
8345
+ readOnlyHint: false,
8346
+ destructiveHint: false,
8347
+ idempotentHint: true,
8348
+ // Same order produces same result
8349
+ openWorldHint: true
7485
8350
  }
7486
8351
  },
7487
8352
  handleReorderProductImages
@@ -7489,45 +8354,45 @@ function registerReorderProductImagesTool() {
7489
8354
  }
7490
8355
 
7491
8356
  // src/tools/set-product-metafields.ts
7492
- import { z as z33 } from "zod";
8357
+ import { z as z38 } from "zod";
7493
8358
  var MAX_METAFIELDS_PER_CALL = 25;
7494
- var metafieldInputSchema = z33.object({
7495
- namespace: z33.string().min(1).describe(
8359
+ var metafieldInputSchema = z38.object({
8360
+ namespace: z38.string().min(1).describe(
7496
8361
  'Metafield namespace (grouping identifier, e.g., "custom", "seo", "my_app"). Use consistent namespaces to organize related metafields.'
7497
8362
  ),
7498
- key: z33.string().min(1).describe(
8363
+ key: z38.string().min(1).describe(
7499
8364
  'Metafield key (field name, e.g., "color_hex", "schema_markup"). Keys should be lowercase with underscores, unique within a namespace.'
7500
8365
  ),
7501
- value: z33.string().describe("The value to store. All values are stored as strings; JSON should be stringified."),
7502
- type: z33.string().min(1).describe(
8366
+ value: z38.string().describe("The value to store. All values are stored as strings; JSON should be stringified."),
8367
+ type: z38.string().min(1).describe(
7503
8368
  '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".'
7504
8369
  )
7505
8370
  });
7506
- var inputSchema32 = z33.object({
7507
- productId: z33.string().min(1).describe(
8371
+ var inputSchema36 = z38.object({
8372
+ productId: productIdSchema.describe(
7508
8373
  'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Use get-product or list-products to find product IDs.'
7509
8374
  ),
7510
- metafields: z33.array(metafieldInputSchema).min(1, "At least one metafield is required").max(
8375
+ metafields: z38.array(metafieldInputSchema).min(1, "At least one metafield is required").max(
7511
8376
  MAX_METAFIELDS_PER_CALL,
7512
8377
  `Maximum ${MAX_METAFIELDS_PER_CALL} metafields per call. Split into multiple calls for larger batches.`
7513
8378
  ).describe(
7514
8379
  `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.`
7515
8380
  )
7516
8381
  });
7517
- var outputSchema32 = z33.object({
7518
- success: z33.boolean().describe("Whether the operation succeeded"),
7519
- metafields: z33.array(
7520
- z33.object({
7521
- id: z33.string().describe("Metafield GID"),
7522
- namespace: z33.string().describe("Metafield namespace"),
7523
- key: z33.string().describe("Metafield key"),
7524
- value: z33.string().describe("Metafield value"),
7525
- type: z33.string().describe("Metafield type"),
7526
- createdAt: z33.string().describe("Creation timestamp (ISO 8601)"),
7527
- updatedAt: z33.string().describe("Last update timestamp (ISO 8601)")
8382
+ var outputSchema36 = z38.object({
8383
+ success: z38.boolean().describe("Whether the operation succeeded"),
8384
+ metafields: z38.array(
8385
+ z38.object({
8386
+ id: z38.string().describe("Metafield GID"),
8387
+ namespace: z38.string().describe("Metafield namespace"),
8388
+ key: z38.string().describe("Metafield key"),
8389
+ value: z38.string().describe("Metafield value"),
8390
+ type: z38.string().describe("Metafield type"),
8391
+ createdAt: z38.string().describe("Creation timestamp (ISO 8601)"),
8392
+ updatedAt: z38.string().describe("Last update timestamp (ISO 8601)")
7528
8393
  })
7529
8394
  ).describe("Created or updated metafields"),
7530
- count: z33.number().describe("Number of metafields processed")
8395
+ count: z38.number().describe("Number of metafields processed")
7531
8396
  });
7532
8397
  var handleSetProductMetafields = async (context, params) => {
7533
8398
  log.debug(
@@ -7563,14 +8428,22 @@ function registerSetProductMetafieldsTool() {
7563
8428
  name: "set-product-metafields",
7564
8429
  title: "Set Product Metafields",
7565
8430
  description: `Create or update up to ${MAX_METAFIELDS_PER_CALL} metafields on a product in a single operation. Each metafield requires: namespace (grouping identifier), key (field name), value (the data), and type. Existing metafields with matching namespace+key are updated; new ones are created. Common types: single_line_text_field, multi_line_text_field, number_integer, number_decimal, boolean, json. **Typical workflow:** create-product \u2192 set-product-metafields (add custom data). **Prerequisites:** get-product to obtain product ID, optionally get-product-metafields to check existing values. **Follow-ups:** get-product-metafields to verify changes.`,
7566
- inputSchema: inputSchema32,
7567
- outputSchema: outputSchema32,
8431
+ inputSchema: inputSchema36,
8432
+ outputSchema: outputSchema36,
7568
8433
  // AI Agent Optimization (Story 5.5)
7569
8434
  category: "seo",
7570
8435
  relationships: {
7571
8436
  relatedTools: ["get-product", "get-product-metafields"],
7572
8437
  prerequisites: ["get-product"],
7573
8438
  followUps: ["get-product-metafields"]
8439
+ },
8440
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8441
+ annotations: {
8442
+ readOnlyHint: false,
8443
+ destructiveHint: false,
8444
+ idempotentHint: true,
8445
+ // Same values produce same result
8446
+ openWorldHint: true
7574
8447
  }
7575
8448
  },
7576
8449
  handleSetProductMetafields
@@ -7578,47 +8451,47 @@ function registerSetProductMetafieldsTool() {
7578
8451
  }
7579
8452
 
7580
8453
  // src/tools/update-article.ts
7581
- import { z as z34 } from "zod";
7582
- var inputSchema33 = z34.object({
7583
- id: z34.string().describe(
8454
+ import { z as z39 } from "zod";
8455
+ var inputSchema37 = z39.object({
8456
+ id: articleIdSchema.describe(
7584
8457
  '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.'
7585
8458
  ),
7586
- title: z34.string().optional().describe("The new title for the article. Only provided if changing the title."),
7587
- authorName: z34.string().optional().describe("The new author name for the article."),
7588
- body: z34.string().optional().describe("The new HTML body content of the article. Supports HTML markup for formatting."),
7589
- summary: z34.string().optional().describe("A summary or excerpt of the article."),
7590
- tags: z34.array(z34.string()).optional().describe(
8459
+ title: z39.string().optional().describe("The new title for the article. Only provided if changing the title."),
8460
+ authorName: z39.string().optional().describe("The new author name for the article."),
8461
+ body: z39.string().optional().describe("The new HTML body content of the article. Supports HTML markup for formatting."),
8462
+ summary: z39.string().optional().describe("A summary or excerpt of the article."),
8463
+ tags: z39.array(z39.string()).optional().describe(
7591
8464
  "New tags for categorization. This replaces all existing tags. Example: ['guide', 'tutorial', 'beginner']."
7592
8465
  ),
7593
- image: z34.object({
7594
- url: z34.string().url().describe("The URL of the featured image"),
7595
- altText: z34.string().optional().describe("Alt text for accessibility")
8466
+ image: z39.object({
8467
+ url: z39.string().url().describe("The URL of the featured image"),
8468
+ altText: z39.string().optional().describe("Alt text for accessibility")
7596
8469
  }).optional().describe("Featured image for the article."),
7597
- isPublished: z34.boolean().optional().describe(
8470
+ isPublished: z39.boolean().optional().describe(
7598
8471
  "Whether the article should be visible on the storefront. Set to true to publish, false to unpublish."
7599
8472
  ),
7600
- publishDate: z34.string().optional().describe(
8473
+ publishDate: z39.string().optional().describe(
7601
8474
  "The date and time when the article should become visible (ISO 8601 format). Example: '2024-01-15T10:00:00Z'."
7602
8475
  ),
7603
- handle: z34.string().optional().describe(
8476
+ handle: z39.string().optional().describe(
7604
8477
  "The new URL handle/slug for the article. Set redirectNewHandle to true to automatically redirect the old URL to the new one."
7605
8478
  ),
7606
- templateSuffix: z34.string().optional().describe(
8479
+ templateSuffix: z39.string().optional().describe(
7607
8480
  "The suffix of the Liquid template used to render the article. For example, 'featured' would use the template 'article.featured.liquid'."
7608
8481
  ),
7609
- redirectNewHandle: z34.boolean().optional().describe(
8482
+ redirectNewHandle: z39.boolean().optional().describe(
7610
8483
  "Whether to automatically redirect the old URL to the new handle. Set to true when changing the handle to preserve SEO and existing links."
7611
8484
  )
7612
8485
  });
7613
- var outputSchema33 = z34.object({
7614
- id: z34.string().describe("Updated article GID"),
7615
- title: z34.string().describe("Article title"),
7616
- handle: z34.string().describe("URL handle/slug"),
7617
- blog: z34.object({
7618
- id: z34.string().describe("Parent blog GID"),
7619
- title: z34.string().describe("Parent blog title")
8486
+ var outputSchema37 = z39.object({
8487
+ id: z39.string().describe("Updated article GID"),
8488
+ title: z39.string().describe("Article title"),
8489
+ handle: z39.string().describe("URL handle/slug"),
8490
+ blog: z39.object({
8491
+ id: z39.string().describe("Parent blog GID"),
8492
+ title: z39.string().describe("Parent blog title")
7620
8493
  }),
7621
- isPublished: z34.boolean().describe("Whether article is visible on storefront")
8494
+ isPublished: z39.boolean().describe("Whether article is visible on storefront")
7622
8495
  });
7623
8496
  var handleUpdateArticle = async (context, params) => {
7624
8497
  log.debug(`Updating article on shop: ${context.shopDomain}`);
@@ -7667,13 +8540,20 @@ function registerUpdateArticleTool() {
7667
8540
  name: "update-article",
7668
8541
  title: "Update Article",
7669
8542
  description: "Update an existing article's attributes. You can update title, body content (HTML), summary, author, tags, featured image, publish status, and template suffix. Only provided fields are updated; others remain unchanged. Use redirectNewHandle: true when changing handles. Useful for: editing content, adding tags, publishing/unpublishing articles. **Typical workflow:** list-articles \u2192 update-article. **Prerequisites:** Article ID. **Follow-ups:** list-articles. Returns the updated article object.",
7670
- inputSchema: inputSchema33,
7671
- outputSchema: outputSchema33,
8543
+ inputSchema: inputSchema37,
8544
+ outputSchema: outputSchema37,
7672
8545
  category: "content",
7673
8546
  relationships: {
7674
8547
  relatedTools: ["list-articles", "create-article", "delete-article"],
7675
8548
  prerequisites: ["list-articles"],
7676
8549
  followUps: ["list-articles"]
8550
+ },
8551
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8552
+ annotations: {
8553
+ readOnlyHint: false,
8554
+ destructiveHint: false,
8555
+ idempotentHint: true,
8556
+ openWorldHint: true
7677
8557
  }
7678
8558
  },
7679
8559
  handleUpdateArticle
@@ -7681,30 +8561,30 @@ function registerUpdateArticleTool() {
7681
8561
  }
7682
8562
 
7683
8563
  // src/tools/update-blog.ts
7684
- import { z as z35 } from "zod";
7685
- var inputSchema34 = z35.object({
7686
- id: z35.string().describe(
8564
+ import { z as z40 } from "zod";
8565
+ var inputSchema38 = z40.object({
8566
+ id: blogIdSchema.describe(
7687
8567
  '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.'
7688
8568
  ),
7689
- title: z35.string().optional().describe("The new title for the blog. Only provided if changing the title."),
7690
- handle: z35.string().optional().describe(
8569
+ title: z40.string().optional().describe("The new title for the blog. Only provided if changing the title."),
8570
+ handle: z40.string().optional().describe(
7691
8571
  "The new URL handle/slug for the blog. Set redirectNewHandle to true to automatically redirect the old URL to the new one."
7692
8572
  ),
7693
- commentPolicy: z35.enum(["CLOSED", "MODERATE", "OPEN"]).optional().describe(
8573
+ commentPolicy: z40.enum(["CLOSED", "MODERATE", "OPEN"]).optional().describe(
7694
8574
  "Comment moderation policy for the blog. CLOSED: Comments disabled. MODERATE: Comments require approval. OPEN: Comments appear immediately."
7695
8575
  ),
7696
- templateSuffix: z35.string().optional().describe(
8576
+ templateSuffix: z40.string().optional().describe(
7697
8577
  "The suffix of the Liquid template used to render the blog. For example, 'news' would use the template 'blog.news.liquid'."
7698
8578
  ),
7699
- redirectNewHandle: z35.boolean().optional().describe(
8579
+ redirectNewHandle: z40.boolean().optional().describe(
7700
8580
  "Whether to automatically redirect the old URL to the new handle. Set to true when changing the handle to preserve SEO and existing links."
7701
8581
  )
7702
8582
  });
7703
- var outputSchema34 = z35.object({
7704
- id: z35.string().describe("Updated blog GID"),
7705
- title: z35.string().describe("Blog title"),
7706
- handle: z35.string().describe("URL handle/slug"),
7707
- commentPolicy: z35.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy")
8583
+ var outputSchema38 = z40.object({
8584
+ id: z40.string().describe("Updated blog GID"),
8585
+ title: z40.string().describe("Blog title"),
8586
+ handle: z40.string().describe("URL handle/slug"),
8587
+ commentPolicy: z40.enum(["CLOSED", "MODERATE", "OPEN"]).describe("Comment moderation policy")
7708
8588
  });
7709
8589
  var handleUpdateBlog = async (context, params) => {
7710
8590
  log.debug(`Updating blog on shop: ${context.shopDomain}`);
@@ -7747,13 +8627,20 @@ function registerUpdateBlogTool() {
7747
8627
  name: "update-blog",
7748
8628
  title: "Update Blog",
7749
8629
  description: "Update an existing blog's attributes. You can update title, handle (URL slug), comment policy (CLOSED, MODERATE, OPEN), and template suffix. Only provided fields are updated; others remain unchanged. Use redirectNewHandle: true when changing handles. Useful for: renaming blogs, changing comment settings, updating templates. **Typical workflow:** list-blogs \u2192 update-blog. **Prerequisites:** Blog ID. **Follow-ups:** list-blogs. Returns the updated blog object.",
7750
- inputSchema: inputSchema34,
7751
- outputSchema: outputSchema34,
8630
+ inputSchema: inputSchema38,
8631
+ outputSchema: outputSchema38,
7752
8632
  category: "content",
7753
8633
  relationships: {
7754
8634
  relatedTools: ["list-blogs", "create-blog", "delete-blog"],
7755
8635
  prerequisites: ["list-blogs"],
7756
8636
  followUps: ["list-blogs"]
8637
+ },
8638
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8639
+ annotations: {
8640
+ readOnlyHint: false,
8641
+ destructiveHint: false,
8642
+ idempotentHint: true,
8643
+ openWorldHint: true
7757
8644
  }
7758
8645
  },
7759
8646
  handleUpdateBlog
@@ -7761,21 +8648,21 @@ function registerUpdateBlogTool() {
7761
8648
  }
7762
8649
 
7763
8650
  // src/tools/update-collection.ts
7764
- import { z as z36 } from "zod";
7765
- var inputSchema35 = z36.object({
7766
- collectionId: z36.string().min(1).describe(
8651
+ import { z as z41 } from "zod";
8652
+ var inputSchema39 = z41.object({
8653
+ collectionId: collectionIdSchema.describe(
7767
8654
  'Collection ID to update (GID format, e.g., "gid://shopify/Collection/123"). Use list-collections or get-collection to find collection IDs.'
7768
8655
  ),
7769
- title: z36.string().min(1).optional().describe("New collection title"),
7770
- handle: z36.string().optional().describe(
8656
+ title: z41.string().min(1).optional().describe("New collection title"),
8657
+ handle: z41.string().optional().describe(
7771
8658
  "New URL handle/slug. When changing, set redirectNewHandle: true to create automatic redirect from the old URL."
7772
8659
  ),
7773
- descriptionHtml: z36.string().optional().describe("New HTML description"),
7774
- seo: z36.object({
7775
- title: z36.string().optional().describe("New SEO title"),
7776
- description: z36.string().optional().describe("New SEO meta description")
8660
+ descriptionHtml: z41.string().optional().describe("New HTML description"),
8661
+ seo: z41.object({
8662
+ title: z41.string().optional().describe("New SEO title"),
8663
+ description: z41.string().optional().describe("New SEO meta description")
7777
8664
  }).optional().describe("Updated SEO metadata"),
7778
- sortOrder: z36.enum([
8665
+ sortOrder: z41.enum([
7779
8666
  "ALPHA_ASC",
7780
8667
  "ALPHA_DESC",
7781
8668
  "BEST_SELLING",
@@ -7785,19 +8672,19 @@ var inputSchema35 = z36.object({
7785
8672
  "PRICE_ASC",
7786
8673
  "PRICE_DESC"
7787
8674
  ]).optional().describe("New product sort order within collection"),
7788
- image: z36.object({
7789
- src: z36.string().url().describe("New image URL (publicly accessible)"),
7790
- altText: z36.string().optional().describe("New alt text for accessibility")
8675
+ image: z41.object({
8676
+ src: z41.string().url().describe("New image URL (publicly accessible)"),
8677
+ altText: z41.string().optional().describe("New alt text for accessibility")
7791
8678
  }).optional().describe("New collection image"),
7792
- templateSuffix: z36.string().optional().describe("New Liquid template suffix"),
7793
- redirectNewHandle: z36.boolean().optional().describe(
8679
+ templateSuffix: z41.string().optional().describe("New Liquid template suffix"),
8680
+ redirectNewHandle: z41.boolean().optional().describe(
7794
8681
  "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."
7795
8682
  )
7796
8683
  });
7797
- var outputSchema35 = z36.object({
7798
- id: z36.string().describe("Updated collection GID"),
7799
- title: z36.string().describe("Collection title"),
7800
- handle: z36.string().describe("Collection URL handle")
8684
+ var outputSchema39 = z41.object({
8685
+ id: z41.string().describe("Updated collection GID"),
8686
+ title: z41.string().describe("Collection title"),
8687
+ handle: z41.string().describe("Collection URL handle")
7801
8688
  });
7802
8689
  var handleUpdateCollection = async (context, params) => {
7803
8690
  log.debug(`Updating collection on shop: ${context.shopDomain}`);
@@ -7836,14 +8723,22 @@ function registerUpdateCollectionTool() {
7836
8723
  name: "update-collection",
7837
8724
  title: "Update Collection",
7838
8725
  description: "Update an existing collection's attributes. Any field can be updated including title, handle, description, SEO fields, sort order, and image. When changing the handle, set redirectNewHandle: true for automatic URL redirect (important for SEO). Only provided fields are modified; omitted fields remain unchanged. **Prerequisites:** get-collection to view current values before updating. **Follow-ups:** add-products-to-collection, remove-products-from-collection.",
7839
- inputSchema: inputSchema35,
7840
- outputSchema: outputSchema35,
8726
+ inputSchema: inputSchema39,
8727
+ outputSchema: outputSchema39,
7841
8728
  // AI Agent Optimization (Story 5.5)
7842
8729
  category: "collection",
7843
8730
  relationships: {
7844
8731
  relatedTools: ["get-collection", "list-collections"],
7845
8732
  prerequisites: ["get-collection"],
7846
8733
  followUps: ["add-products-to-collection", "remove-products-from-collection"]
8734
+ },
8735
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8736
+ annotations: {
8737
+ readOnlyHint: false,
8738
+ destructiveHint: false,
8739
+ idempotentHint: true,
8740
+ // Same update produces same result
8741
+ openWorldHint: true
7847
8742
  }
7848
8743
  },
7849
8744
  handleUpdateCollection
@@ -7851,8 +8746,8 @@ function registerUpdateCollectionTool() {
7851
8746
  }
7852
8747
 
7853
8748
  // src/tools/update-inventory.ts
7854
- import { z as z37 } from "zod";
7855
- var inventoryReasonEnum = z37.enum([
8749
+ import { z as z42 } from "zod";
8750
+ var inventoryReasonEnum = z42.enum([
7856
8751
  "correction",
7857
8752
  "cycle_count_available",
7858
8753
  "damaged",
@@ -7871,20 +8766,20 @@ var inventoryReasonEnum = z37.enum([
7871
8766
  "safety_stock",
7872
8767
  "shrinkage"
7873
8768
  ]);
7874
- var inputSchema36 = z37.object({
7875
- inventoryItemId: z37.string().min(1).describe(
8769
+ var inputSchema40 = z42.object({
8770
+ inventoryItemId: inventoryItemIdSchema.describe(
7876
8771
  'The Shopify inventory item ID (e.g., "gid://shopify/InventoryItem/456"). Get this from get-inventory tool or from get-product response.'
7877
8772
  ),
7878
- locationId: z37.string().min(1).describe(
8773
+ locationId: locationIdSchema.describe(
7879
8774
  'The location ID where inventory should be updated (e.g., "gid://shopify/Location/789"). Get this from get-inventory tool.'
7880
8775
  ),
7881
- setQuantity: z37.number().int().min(0).optional().describe(
8776
+ setQuantity: z42.number().int().min(0).optional().describe(
7882
8777
  "Set inventory to this exact quantity. Example: 100. Use for absolute stock counts after physical inventory. Cannot be used together with adjustQuantity."
7883
8778
  ),
7884
- adjustQuantity: z37.number().int().optional().describe(
8779
+ adjustQuantity: z42.number().int().optional().describe(
7885
8780
  "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."
7886
8781
  ),
7887
- name: z37.enum(["available", "on_hand"]).optional().default("available").describe(
8782
+ name: z42.enum(["available", "on_hand"]).optional().default("available").describe(
7888
8783
  'Which quantity to modify. "available" (default) for sellable stock, "on_hand" for physical count.'
7889
8784
  ),
7890
8785
  reason: inventoryReasonEnum.optional().default("correction").describe(
@@ -7895,16 +8790,16 @@ var inputSchema36 = z37.object({
7895
8790
  }).refine((data) => !(data.setQuantity !== void 0 && data.adjustQuantity !== void 0), {
7896
8791
  message: "Cannot provide both setQuantity and adjustQuantity. Choose one."
7897
8792
  });
7898
- var outputSchema36 = z37.object({
7899
- success: z37.boolean().describe("Whether the operation succeeded"),
7900
- inventoryItemId: z37.string().describe("Inventory item GID"),
7901
- locationId: z37.string().describe("Location GID where inventory was updated"),
7902
- locationName: z37.string().describe("Human-readable location name"),
7903
- previousQuantity: z37.number().describe("Quantity before the change"),
7904
- newQuantity: z37.number().describe("Quantity after the change"),
7905
- delta: z37.number().describe("The actual change applied (positive or negative)"),
7906
- name: z37.string().describe("Which quantity was modified (available or on_hand)"),
7907
- reason: z37.string().describe("Reason for the adjustment")
8793
+ var outputSchema40 = z42.object({
8794
+ success: z42.boolean().describe("Whether the operation succeeded"),
8795
+ inventoryItemId: z42.string().describe("Inventory item GID"),
8796
+ locationId: z42.string().describe("Location GID where inventory was updated"),
8797
+ locationName: z42.string().describe("Human-readable location name"),
8798
+ previousQuantity: z42.number().describe("Quantity before the change"),
8799
+ newQuantity: z42.number().describe("Quantity after the change"),
8800
+ delta: z42.number().describe("The actual change applied (positive or negative)"),
8801
+ name: z42.string().describe("Which quantity was modified (available or on_hand)"),
8802
+ reason: z42.string().describe("Reason for the adjustment")
7908
8803
  });
7909
8804
  var handleUpdateInventory = async (context, params) => {
7910
8805
  log.debug(`Updating inventory on shop: ${context.shopDomain}`);
@@ -7978,47 +8873,113 @@ function registerUpdateInventoryTool() {
7978
8873
  name: "update-inventory",
7979
8874
  title: "Update Inventory",
7980
8875
  description: 'Update inventory quantity at a specific location. Use setQuantity for absolute values (e.g., setting stock to 50 after a count) or adjustQuantity for relative changes (e.g., +10 for received stock, -5 for shrinkage). Requires inventoryItemId (from get-inventory or get-product) and locationId (from get-inventory). Common reasons: "correction" (default), "received" (new stock), "shrinkage" (loss/theft), "damaged", "cycle_count_available" (stock take). Pre-validates that adjustments will not result in negative inventory. **Typical workflow:** get-inventory \u2192 update-inventory \u2192 get-inventory (to verify). **Prerequisites:** Inventory Item ID, Location ID. **Follow-ups:** get-inventory. Returns before/after quantities with delta.',
7981
- inputSchema: inputSchema36,
7982
- outputSchema: outputSchema36,
8876
+ inputSchema: inputSchema40,
8877
+ outputSchema: outputSchema40,
7983
8878
  category: "inventory",
7984
8879
  relationships: {
7985
8880
  relatedTools: ["get-inventory", "get-bulk-inventory", "list-low-inventory"],
7986
8881
  prerequisites: ["get-inventory"],
7987
8882
  followUps: ["get-inventory"]
8883
+ },
8884
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8885
+ annotations: {
8886
+ readOnlyHint: false,
8887
+ destructiveHint: false,
8888
+ idempotentHint: true,
8889
+ openWorldHint: true
7988
8890
  }
7989
8891
  },
7990
8892
  handleUpdateInventory
7991
8893
  );
7992
8894
  }
7993
8895
 
8896
+ // src/tools/update-market.ts
8897
+ import { z as z43 } from "zod";
8898
+ var inputSchema41 = z43.object({
8899
+ id: marketIdSchema.describe(
8900
+ 'Shopify Market GID to update (e.g., "gid://shopify/Market/123"). Use list-markets to find market IDs.'
8901
+ ),
8902
+ name: z43.string().min(1).optional().describe("New market name"),
8903
+ handle: z43.string().optional().describe("New URL handle/slug for the market"),
8904
+ enabled: z43.boolean().optional().describe("Whether the market should be enabled")
8905
+ });
8906
+ var outputSchema41 = z43.object({
8907
+ id: z43.string().describe("Updated market GID"),
8908
+ name: z43.string().describe("Market name"),
8909
+ handle: z43.string().describe("URL handle"),
8910
+ enabled: z43.boolean().describe("Whether the market is enabled")
8911
+ });
8912
+ var handleUpdateMarket = async (context, params) => {
8913
+ log.debug(`Updating market ${params.id} on shop: ${context.shopDomain}`);
8914
+ const input = {};
8915
+ if (params.name !== void 0) {
8916
+ input.name = params.name;
8917
+ }
8918
+ if (params.handle !== void 0) {
8919
+ input.handle = params.handle;
8920
+ }
8921
+ if (params.enabled !== void 0) {
8922
+ input.enabled = params.enabled;
8923
+ }
8924
+ const market = await updateMarket(params.id, input);
8925
+ log.debug(`Updated market: ${market.id}`);
8926
+ return market;
8927
+ };
8928
+ function registerUpdateMarketTool() {
8929
+ registerContextAwareTool(
8930
+ {
8931
+ name: "update-market",
8932
+ title: "Update Market",
8933
+ description: "Update an existing market's properties. Only provided fields are updated; omitted fields remain unchanged. Can update: name, handle, enabled status. **Prerequisites:** get-market to view current values before updating. **Follow-ups:** get-market (to verify), delete-market.",
8934
+ inputSchema: inputSchema41,
8935
+ outputSchema: outputSchema41,
8936
+ // AI Agent Optimization
8937
+ category: "market",
8938
+ relationships: {
8939
+ relatedTools: ["get-market", "list-markets"],
8940
+ followUps: ["get-market", "delete-market"]
8941
+ },
8942
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
8943
+ annotations: {
8944
+ readOnlyHint: false,
8945
+ destructiveHint: false,
8946
+ idempotentHint: true,
8947
+ // Same update with same values produces same result
8948
+ openWorldHint: true
8949
+ }
8950
+ },
8951
+ handleUpdateMarket
8952
+ );
8953
+ }
8954
+
7994
8955
  // src/tools/update-page.ts
7995
- import { z as z38 } from "zod";
7996
- var inputSchema37 = z38.object({
7997
- id: z38.string().min(1).describe(
8956
+ import { z as z44 } from "zod";
8957
+ var inputSchema42 = z44.object({
8958
+ id: pageIdSchema.describe(
7998
8959
  '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.'
7999
8960
  ),
8000
- title: z38.string().optional().describe("The new title for the page. Only provide if you want to change it."),
8001
- body: z38.string().optional().describe(
8961
+ title: z44.string().optional().describe("The new title for the page. Only provide if you want to change it."),
8962
+ body: z44.string().optional().describe(
8002
8963
  "The new HTML body content for the page. Supports HTML markup. Only provide if you want to change it."
8003
8964
  ),
8004
- handle: z38.string().optional().describe(
8965
+ handle: z44.string().optional().describe(
8005
8966
  "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."
8006
8967
  ),
8007
- isPublished: z38.boolean().optional().describe(
8968
+ isPublished: z44.boolean().optional().describe(
8008
8969
  "Whether the page should be visible on the storefront. Set to true to publish, false to unpublish."
8009
8970
  ),
8010
- templateSuffix: z38.string().optional().describe(
8971
+ templateSuffix: z44.string().optional().describe(
8011
8972
  "The suffix of the Liquid template used to render the page. Set to empty string to use the default template."
8012
8973
  ),
8013
- redirectNewHandle: z38.boolean().optional().describe(
8974
+ redirectNewHandle: z44.boolean().optional().describe(
8014
8975
  "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."
8015
8976
  )
8016
8977
  });
8017
- var outputSchema37 = z38.object({
8018
- id: z38.string().describe("Updated page GID"),
8019
- title: z38.string().describe("Page title"),
8020
- handle: z38.string().describe("URL handle/slug"),
8021
- isPublished: z38.boolean().describe("Whether page is visible on storefront")
8978
+ var outputSchema42 = z44.object({
8979
+ id: z44.string().describe("Updated page GID"),
8980
+ title: z44.string().describe("Page title"),
8981
+ handle: z44.string().describe("URL handle/slug"),
8982
+ isPublished: z44.boolean().describe("Whether page is visible on storefront")
8022
8983
  });
8023
8984
  var handleUpdatePage = async (context, params) => {
8024
8985
  log.debug(`Updating page on shop: ${context.shopDomain}`);
@@ -8062,13 +9023,20 @@ function registerUpdatePageTool() {
8062
9023
  name: "update-page",
8063
9024
  title: "Update Page",
8064
9025
  description: "Update an existing page's attributes. You can update title, body content (HTML), handle (URL slug), publish status, and template suffix. Only provided fields are updated; others remain unchanged. Use redirectNewHandle: true when changing handles to preserve SEO. Useful for: editing content, publishing/unpublishing pages, changing URLs. **Typical workflow:** get-page \u2192 update-page. **Prerequisites:** Page ID. **Follow-ups:** get-page. Returns the updated page object.",
8065
- inputSchema: inputSchema37,
8066
- outputSchema: outputSchema37,
9026
+ inputSchema: inputSchema42,
9027
+ outputSchema: outputSchema42,
8067
9028
  category: "content",
8068
9029
  relationships: {
8069
9030
  relatedTools: ["get-page", "list-pages"],
8070
9031
  prerequisites: ["get-page"],
8071
9032
  followUps: ["get-page"]
9033
+ },
9034
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
9035
+ annotations: {
9036
+ readOnlyHint: false,
9037
+ destructiveHint: false,
9038
+ idempotentHint: true,
9039
+ openWorldHint: true
8072
9040
  }
8073
9041
  },
8074
9042
  handleUpdatePage
@@ -8076,7 +9044,7 @@ function registerUpdatePageTool() {
8076
9044
  }
8077
9045
 
8078
9046
  // src/tools/update-product-image.ts
8079
- import { z as z39 } from "zod";
9047
+ import { z as z45 } from "zod";
8080
9048
  var FILE_UPDATE_MUTATION = `
8081
9049
  mutation FileUpdate($files: [FileUpdateInput!]!) {
8082
9050
  fileUpdate(files: $files) {
@@ -8096,18 +9064,18 @@ var FILE_UPDATE_MUTATION = `
8096
9064
  }
8097
9065
  }
8098
9066
  `;
8099
- var inputSchema38 = z39.object({
8100
- imageId: z39.string().min(1).describe(
9067
+ var inputSchema43 = z45.object({
9068
+ imageId: imageIdSchema.describe(
8101
9069
  'Shopify image GID (e.g., "gid://shopify/MediaImage/123"). Required. Use get-product to find image IDs in the media field.'
8102
9070
  ),
8103
- altText: z39.string().describe(
9071
+ altText: z45.string().describe(
8104
9072
  "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."
8105
9073
  )
8106
9074
  });
8107
- var outputSchema38 = z39.object({
8108
- id: z39.string().describe('Image GID (e.g., "gid://shopify/MediaImage/123")'),
8109
- url: z39.string().nullable().describe("Shopify CDN URL for the image"),
8110
- altText: z39.string().nullable().describe("Updated alt text for the image")
9075
+ var outputSchema43 = z45.object({
9076
+ id: z45.string().describe('Image GID (e.g., "gid://shopify/MediaImage/123")'),
9077
+ url: z45.string().nullable().describe("Shopify CDN URL for the image"),
9078
+ altText: z45.string().nullable().describe("Updated alt text for the image")
8111
9079
  });
8112
9080
  var handleUpdateProductImage = async (context, params) => {
8113
9081
  log.debug(`Updating image alt text on shop: ${context.shopDomain}`);
@@ -8166,14 +9134,22 @@ function registerUpdateProductImageTool() {
8166
9134
  name: "update-product-image",
8167
9135
  title: "Update Product Image",
8168
9136
  description: "Update the alt text of a product image for SEO and accessibility. Alt text describes the image for screen readers and helps search engines understand image content. Pass an empty string to clear alt text. **Typical workflow:** add-product-image \u2192 update-product-image (set alt text). **Prerequisites:** add-product-image or get-product to find image IDs. **Follow-ups:** reorder-product-images.",
8169
- inputSchema: inputSchema38,
8170
- outputSchema: outputSchema38,
9137
+ inputSchema: inputSchema43,
9138
+ outputSchema: outputSchema43,
8171
9139
  // AI Agent Optimization (Story 5.5)
8172
9140
  category: "media",
8173
9141
  relationships: {
8174
9142
  relatedTools: ["reorder-product-images", "add-product-image"],
8175
9143
  prerequisites: ["add-product-image"],
8176
9144
  followUps: ["reorder-product-images"]
9145
+ },
9146
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
9147
+ annotations: {
9148
+ readOnlyHint: false,
9149
+ destructiveHint: false,
9150
+ idempotentHint: true,
9151
+ // Same alt text update produces same result
9152
+ openWorldHint: true
8177
9153
  }
8178
9154
  },
8179
9155
  handleUpdateProductImage
@@ -8181,19 +9157,19 @@ function registerUpdateProductImageTool() {
8181
9157
  }
8182
9158
 
8183
9159
  // src/tools/update-product-variant.ts
8184
- import { z as z40 } from "zod";
8185
- var inputSchema39 = z40.object({
8186
- productId: z40.string().min(1).describe(
9160
+ import { z as z46 } from "zod";
9161
+ var inputSchema44 = z46.object({
9162
+ productId: productIdSchema.describe(
8187
9163
  'Shopify product GID (e.g., "gid://shopify/Product/123"). Required. Use get-product to find the product ID containing the variant.'
8188
9164
  ),
8189
- id: z40.string().min(1).describe(
9165
+ id: variantIdSchema.describe(
8190
9166
  'Shopify variant GID (e.g., "gid://shopify/ProductVariant/456"). Required. Use get-product to find variant IDs for a product.'
8191
9167
  ),
8192
- price: z40.string().optional().describe('New variant price as decimal string (e.g., "29.99")'),
8193
- compareAtPrice: z40.string().nullable().optional().describe(
9168
+ price: z46.string().optional().describe('New variant price as decimal string (e.g., "29.99")'),
9169
+ compareAtPrice: z46.string().nullable().optional().describe(
8194
9170
  '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.'
8195
9171
  ),
8196
- barcode: z40.string().nullable().optional().describe("Barcode (UPC, EAN, ISBN, etc.). Set to null to remove.")
9172
+ barcode: z46.string().nullable().optional().describe("Barcode (UPC, EAN, ISBN, etc.). Set to null to remove.")
8197
9173
  }).refine(
8198
9174
  (data) => {
8199
9175
  const { productId: _productId, id: _id, ...updateFields } = data;
@@ -8201,14 +9177,14 @@ var inputSchema39 = z40.object({
8201
9177
  },
8202
9178
  { message: "At least one field to update must be provided (price, compareAtPrice, or barcode)" }
8203
9179
  );
8204
- var outputSchema39 = z40.object({
8205
- id: z40.string().describe('Variant GID (e.g., "gid://shopify/ProductVariant/123")'),
8206
- title: z40.string().describe('Variant title (e.g., "Red / Medium")'),
8207
- price: z40.string().describe("Current price as decimal string"),
8208
- compareAtPrice: z40.string().nullable().describe("Compare at price for sale display"),
8209
- sku: z40.string().nullable().describe("Stock keeping unit"),
8210
- barcode: z40.string().nullable().describe("Barcode (UPC, EAN, etc.)"),
8211
- inventoryQuantity: z40.number().nullable().describe("Available inventory quantity")
9180
+ var outputSchema44 = z46.object({
9181
+ id: z46.string().describe('Variant GID (e.g., "gid://shopify/ProductVariant/123")'),
9182
+ title: z46.string().describe('Variant title (e.g., "Red / Medium")'),
9183
+ price: z46.string().describe("Current price as decimal string"),
9184
+ compareAtPrice: z46.string().nullable().describe("Compare at price for sale display"),
9185
+ sku: z46.string().nullable().describe("Stock keeping unit"),
9186
+ barcode: z46.string().nullable().describe("Barcode (UPC, EAN, etc.)"),
9187
+ inventoryQuantity: z46.number().nullable().describe("Available inventory quantity")
8212
9188
  });
8213
9189
  var handleUpdateProductVariant = async (context, params) => {
8214
9190
  log.debug(`Updating product variant on shop: ${context.shopDomain}`);
@@ -8237,13 +9213,21 @@ function registerUpdateProductVariantTool() {
8237
9213
  name: "update-product-variant",
8238
9214
  title: "Update Product Variant",
8239
9215
  description: 'Update an existing product variant in the Shopify store. Requires BOTH productId and variant id (GID format). Only provided fields are updated; others remain unchanged. Supports updating: price, compareAtPrice (for sale pricing), barcode. Returns the updated variant with all current details. To show sale pricing, set compareAtPrice higher than price (e.g., compareAtPrice: "39.99", price: "24.99"). **Prerequisites:** get-product to obtain productId and variant IDs. **Note:** SKU updates require separate inventory API calls.',
8240
- inputSchema: inputSchema39,
8241
- outputSchema: outputSchema39,
9216
+ inputSchema: inputSchema44,
9217
+ outputSchema: outputSchema44,
8242
9218
  // AI Agent Optimization (Story 5.5)
8243
9219
  category: "product",
8244
9220
  relationships: {
8245
9221
  relatedTools: ["get-product", "get-inventory"],
8246
9222
  prerequisites: ["get-product"]
9223
+ },
9224
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
9225
+ annotations: {
9226
+ readOnlyHint: false,
9227
+ destructiveHint: false,
9228
+ idempotentHint: true,
9229
+ // Same update produces same result
9230
+ openWorldHint: true
8247
9231
  }
8248
9232
  },
8249
9233
  handleUpdateProductVariant
@@ -8251,23 +9235,25 @@ function registerUpdateProductVariantTool() {
8251
9235
  }
8252
9236
 
8253
9237
  // src/tools/update-product.ts
8254
- import { z as z41 } from "zod";
8255
- var inputSchema40 = z41.object({
8256
- id: z41.string().min(1).describe('Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Required.'),
8257
- title: z41.string().min(1).optional().describe("New product title"),
8258
- description: z41.string().optional().describe("New product description (HTML supported)"),
8259
- vendor: z41.string().optional().describe("New product vendor/brand name"),
8260
- productType: z41.string().optional().describe("New product type for categorization"),
8261
- tags: z41.array(z41.string()).optional().describe("New tags (replaces existing tags)"),
8262
- status: z41.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe("New product status: ACTIVE (visible), DRAFT (hidden), ARCHIVED (hidden)"),
8263
- seo: z41.object({
8264
- title: z41.string().optional().describe("SEO title for search engine results"),
8265
- description: z41.string().optional().describe("SEO meta description for search engines")
9238
+ import { z as z47 } from "zod";
9239
+ var inputSchema45 = z47.object({
9240
+ id: productIdSchema.describe(
9241
+ 'Shopify product ID (GID format, e.g., "gid://shopify/Product/123"). Required.'
9242
+ ),
9243
+ title: z47.string().min(1).optional().describe("New product title"),
9244
+ description: z47.string().optional().describe("New product description (HTML supported)"),
9245
+ vendor: z47.string().optional().describe("New product vendor/brand name"),
9246
+ productType: z47.string().optional().describe("New product type for categorization"),
9247
+ tags: z47.array(z47.string()).optional().describe("New tags (replaces existing tags)"),
9248
+ status: z47.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).optional().describe("New product status: ACTIVE (visible), DRAFT (hidden), ARCHIVED (hidden)"),
9249
+ seo: z47.object({
9250
+ title: z47.string().optional().describe("SEO title for search engine results"),
9251
+ description: z47.string().optional().describe("SEO meta description for search engines")
8266
9252
  }).optional().describe("SEO metadata for search engine optimization"),
8267
- handle: z41.string().optional().describe(
9253
+ handle: z47.string().optional().describe(
8268
9254
  'New URL handle/slug (e.g., "my-product"). Use with redirectNewHandle for URL changes.'
8269
9255
  ),
8270
- redirectNewHandle: z41.boolean().optional().describe(
9256
+ redirectNewHandle: z47.boolean().optional().describe(
8271
9257
  "If true, creates automatic redirect from old handle to new handle. Use when changing handle."
8272
9258
  )
8273
9259
  }).refine(
@@ -8277,40 +9263,40 @@ var inputSchema40 = z41.object({
8277
9263
  },
8278
9264
  { message: "At least one field to update must be provided" }
8279
9265
  );
8280
- var outputSchema40 = z41.object({
8281
- id: z41.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
8282
- title: z41.string().describe("Product title"),
8283
- handle: z41.string().describe("URL handle/slug"),
8284
- description: z41.string().nullable().describe("Product description (HTML)"),
8285
- vendor: z41.string().describe("Vendor/brand name"),
8286
- productType: z41.string().describe("Product type"),
8287
- status: z41.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
8288
- tags: z41.array(z41.string()).describe("Product tags"),
8289
- seo: z41.object({
8290
- title: z41.string().nullable().describe("SEO title for search engines"),
8291
- description: z41.string().nullable().describe("SEO description/meta description")
9266
+ var outputSchema45 = z47.object({
9267
+ id: z47.string().describe('Product GID (e.g., "gid://shopify/Product/123")'),
9268
+ title: z47.string().describe("Product title"),
9269
+ handle: z47.string().describe("URL handle/slug"),
9270
+ description: z47.string().nullable().describe("Product description (HTML)"),
9271
+ vendor: z47.string().describe("Vendor/brand name"),
9272
+ productType: z47.string().describe("Product type"),
9273
+ status: z47.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).describe("Product status"),
9274
+ tags: z47.array(z47.string()).describe("Product tags"),
9275
+ seo: z47.object({
9276
+ title: z47.string().nullable().describe("SEO title for search engines"),
9277
+ description: z47.string().nullable().describe("SEO description/meta description")
8292
9278
  }).describe("SEO metadata for search engine optimization"),
8293
- createdAt: z41.string().describe("Creation timestamp (ISO 8601)"),
8294
- updatedAt: z41.string().describe("Last update timestamp (ISO 8601)"),
8295
- variants: z41.array(
8296
- z41.object({
8297
- id: z41.string().describe("Variant GID"),
8298
- title: z41.string().describe("Variant title"),
8299
- price: z41.string().describe("Price"),
8300
- compareAtPrice: z41.string().nullable().describe("Compare at price"),
8301
- sku: z41.string().nullable().describe("SKU"),
8302
- barcode: z41.string().nullable().describe("Barcode"),
8303
- inventoryQuantity: z41.number().nullable().describe("Inventory quantity")
9279
+ createdAt: z47.string().describe("Creation timestamp (ISO 8601)"),
9280
+ updatedAt: z47.string().describe("Last update timestamp (ISO 8601)"),
9281
+ variants: z47.array(
9282
+ z47.object({
9283
+ id: z47.string().describe("Variant GID"),
9284
+ title: z47.string().describe("Variant title"),
9285
+ price: z47.string().describe("Price"),
9286
+ compareAtPrice: z47.string().nullable().describe("Compare at price"),
9287
+ sku: z47.string().nullable().describe("SKU"),
9288
+ barcode: z47.string().nullable().describe("Barcode"),
9289
+ inventoryQuantity: z47.number().nullable().describe("Inventory quantity")
8304
9290
  })
8305
9291
  ).describe("Product variants"),
8306
- images: z41.array(
8307
- z41.object({
8308
- id: z41.string().describe("Image GID"),
8309
- url: z41.string().describe("Image URL"),
8310
- altText: z41.string().nullable().describe("Alt text")
9292
+ images: z47.array(
9293
+ z47.object({
9294
+ id: z47.string().describe("Image GID"),
9295
+ url: z47.string().describe("Image URL"),
9296
+ altText: z47.string().nullable().describe("Alt text")
8311
9297
  })
8312
9298
  ).describe("Product images"),
8313
- totalInventory: z41.number().describe("Total inventory across variants")
9299
+ totalInventory: z47.number().describe("Total inventory across variants")
8314
9300
  });
8315
9301
  var handleUpdateProduct = async (context, params) => {
8316
9302
  log.debug(`Updating product on shop: ${context.shopDomain}`);
@@ -8339,14 +9325,22 @@ function registerUpdateProductTool() {
8339
9325
  name: "update-product",
8340
9326
  title: "Update Product",
8341
9327
  description: "Update an existing product in the Shopify store. Requires the product ID (GID format). Only provided fields are updated; others remain unchanged. Supports updating: title, description, vendor, productType, tags, status, SEO metadata (seo.title, seo.description), and URL handle. Use handle + redirectNewHandle=true to change URLs with automatic redirect. Returns the updated product with all current details including SEO. **Prerequisites:** get-product to view current values before updating. **Follow-ups:** add-product-image, set-product-metafields.",
8342
- inputSchema: inputSchema40,
8343
- outputSchema: outputSchema40,
9328
+ inputSchema: inputSchema45,
9329
+ outputSchema: outputSchema45,
8344
9330
  // AI Agent Optimization (Story 5.5)
8345
9331
  category: "product",
8346
9332
  relationships: {
8347
9333
  relatedTools: ["list-products", "get-product"],
8348
9334
  prerequisites: ["get-product"],
8349
9335
  followUps: ["add-product-image", "set-product-metafields"]
9336
+ },
9337
+ // MCP Tool Annotations (Epic 9.5 - MCP Best Practices)
9338
+ annotations: {
9339
+ readOnlyHint: false,
9340
+ destructiveHint: false,
9341
+ idempotentHint: true,
9342
+ // Same update produces same result
9343
+ openWorldHint: true
8350
9344
  }
8351
9345
  },
8352
9346
  handleUpdateProduct
@@ -8362,7 +9356,9 @@ function setupToolHandlers(server) {
8362
9356
  tools: tools.map((tool) => ({
8363
9357
  name: tool.name,
8364
9358
  description: tool.description,
8365
- inputSchema: tool.inputSchema
9359
+ inputSchema: tool.inputSchema,
9360
+ // Include MCP tool annotations (Epic 9.5 - MCP Best Practices)
9361
+ annotations: tool.annotations
8366
9362
  }))
8367
9363
  };
8368
9364
  });
@@ -8447,6 +9443,11 @@ function registerAllTools(server) {
8447
9443
  registerGetInventoryTool();
8448
9444
  registerListLowInventoryTool();
8449
9445
  registerUpdateInventoryTool();
9446
+ registerCreateMarketTool();
9447
+ registerDeleteMarketTool();
9448
+ registerGetMarketTool();
9449
+ registerListMarketsTool();
9450
+ registerUpdateMarketTool();
8450
9451
  const toolCount = getRegisteredTools().length;
8451
9452
  log.debug(`Tool registration complete: ${toolCount} tools registered`);
8452
9453
  }