@klaudworks/shopify-mcp 1.1.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +531 -0
  3. package/dist/index.js +107 -0
  4. package/dist/lib/formatters.js +72 -0
  5. package/dist/lib/shopifyAuth.js +96 -0
  6. package/dist/lib/toolUtils.js +36 -0
  7. package/dist/tools/completeDraftOrder.js +76 -0
  8. package/dist/tools/createCustomer.js +142 -0
  9. package/dist/tools/createDraftOrder.js +160 -0
  10. package/dist/tools/createFulfillment.js +100 -0
  11. package/dist/tools/createProduct.js +123 -0
  12. package/dist/tools/createRefund.js +115 -0
  13. package/dist/tools/deleteCustomer.js +47 -0
  14. package/dist/tools/deleteMetafields.js +54 -0
  15. package/dist/tools/deleteProduct.js +43 -0
  16. package/dist/tools/deleteProductVariants.js +82 -0
  17. package/dist/tools/getCollectionById.js +123 -0
  18. package/dist/tools/getCollections.js +88 -0
  19. package/dist/tools/getCustomerById.js +122 -0
  20. package/dist/tools/getCustomerOrders.js +131 -0
  21. package/dist/tools/getCustomers.js +125 -0
  22. package/dist/tools/getFulfillmentOrders.js +106 -0
  23. package/dist/tools/getInventoryItems.js +86 -0
  24. package/dist/tools/getInventoryLevels.js +82 -0
  25. package/dist/tools/getLocations.js +85 -0
  26. package/dist/tools/getMarkets.js +91 -0
  27. package/dist/tools/getMetafieldDefinitions.js +112 -0
  28. package/dist/tools/getMetafields.js +68 -0
  29. package/dist/tools/getOrderById.js +212 -0
  30. package/dist/tools/getOrderRefundDetails.js +131 -0
  31. package/dist/tools/getOrderTransactions.js +85 -0
  32. package/dist/tools/getOrders.js +148 -0
  33. package/dist/tools/getPriceLists.js +92 -0
  34. package/dist/tools/getProductById.js +171 -0
  35. package/dist/tools/getProductVariantsDetailed.js +139 -0
  36. package/dist/tools/getProducts.js +155 -0
  37. package/dist/tools/getShopInfo.js +74 -0
  38. package/dist/tools/manageCustomerAddress.js +149 -0
  39. package/dist/tools/manageProductOptions.js +293 -0
  40. package/dist/tools/manageProductVariants.js +203 -0
  41. package/dist/tools/manageTags.js +79 -0
  42. package/dist/tools/mergeCustomers.js +74 -0
  43. package/dist/tools/orderCancel.js +77 -0
  44. package/dist/tools/orderCloseOpen.js +74 -0
  45. package/dist/tools/orderMarkAsPaid.js +51 -0
  46. package/dist/tools/registry.js +106 -0
  47. package/dist/tools/setInventoryQuantities.js +74 -0
  48. package/dist/tools/setMetafields.js +61 -0
  49. package/dist/tools/updateCustomer.js +119 -0
  50. package/dist/tools/updateOrder.js +131 -0
  51. package/dist/tools/updateProduct.js +132 -0
  52. package/package.json +66 -0
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ // ── Shared Zod schemas ────────────────────────────────────────────────
3
+ /**
4
+ * Mailing address schema shared by createDraftOrder (shipping + billing)
5
+ * and manageCustomerAddress. Uses countryCode/provinceCode (API input type).
6
+ *
7
+ * NOTE: updateOrder.shippingAddress intentionally uses country/province
8
+ * (different Shopify input type) and is NOT shared here.
9
+ */
10
+ export const shippingAddressSchema = z.object({
11
+ address1: z.string().optional(),
12
+ address2: z.string().optional(),
13
+ city: z.string().optional(),
14
+ company: z.string().optional(),
15
+ countryCode: z.string().optional().describe("Two-letter country code"),
16
+ firstName: z.string().optional(),
17
+ lastName: z.string().optional(),
18
+ phone: z.string().optional().describe("Phone in E.164 format, e.g. +16135551111"),
19
+ provinceCode: z.string().optional(),
20
+ zip: z.string().optional(),
21
+ });
22
+ /**
23
+ * Format a line-item connection into a clean array.
24
+ * Used by getOrders, getOrderById, and getCustomerOrders.
25
+ */
26
+ export function formatLineItems(lineItems) {
27
+ return lineItems.edges.map((edge) => {
28
+ const item = edge.node;
29
+ return {
30
+ id: item.id,
31
+ title: item.title,
32
+ quantity: item.quantity,
33
+ originalTotal: item.originalTotalSet.shopMoney,
34
+ variant: item.variant
35
+ ? {
36
+ id: item.variant.id,
37
+ title: item.variant.title,
38
+ sku: item.variant.sku,
39
+ }
40
+ : null,
41
+ };
42
+ });
43
+ }
44
+ /**
45
+ * Format a raw order node into the standard order summary shape.
46
+ * Used by getOrders, getCustomerOrders, and getOrderById (as a base).
47
+ */
48
+ export function formatOrderSummary(order) {
49
+ return {
50
+ id: order.id,
51
+ name: order.name,
52
+ createdAt: order.createdAt,
53
+ financialStatus: order.displayFinancialStatus,
54
+ fulfillmentStatus: order.displayFulfillmentStatus,
55
+ totalPrice: order.totalPriceSet.shopMoney,
56
+ subtotalPrice: order.subtotalPriceSet.shopMoney,
57
+ totalShippingPrice: order.totalShippingPriceSet.shopMoney,
58
+ totalTax: order.totalTaxSet.shopMoney,
59
+ customer: order.customer
60
+ ? {
61
+ id: order.customer.id,
62
+ firstName: order.customer.firstName,
63
+ lastName: order.customer.lastName,
64
+ email: order.customer.defaultEmailAddress?.emailAddress || null,
65
+ }
66
+ : null,
67
+ shippingAddress: order.shippingAddress,
68
+ lineItems: formatLineItems(order.lineItems),
69
+ tags: order.tags,
70
+ note: order.note,
71
+ };
72
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Shopify OAuth Client Credentials flow.
3
+ *
4
+ * As of January 1 2026 Shopify no longer exposes static Admin API access
5
+ * tokens in the UI. New apps created in the Dev Dashboard receive a
6
+ * client_id + client_secret pair which must be exchanged for a short-lived
7
+ * access token (expires_in ≈ 86 400 s / 24 h).
8
+ *
9
+ * This module handles the token exchange and transparent refresh so the
10
+ * rest of the codebase can keep using a plain access-token string.
11
+ */
12
+ // Refresh 5 minutes before actual expiry to avoid race conditions.
13
+ const REFRESH_MARGIN_MS = 5 * 60 * 1000;
14
+ export class ShopifyAuth {
15
+ constructor(config) {
16
+ this.accessToken = null;
17
+ this.expiresAt = 0;
18
+ this.refreshTimer = null;
19
+ this.graphqlClient = null;
20
+ this.config = config;
21
+ }
22
+ /** Attach the GraphQL client so the token can be hot-swapped on refresh. */
23
+ setGraphQLClient(client) {
24
+ this.graphqlClient = client;
25
+ }
26
+ /** Fetch an initial token. Must be called before the server starts. */
27
+ async initialize() {
28
+ await this.fetchToken();
29
+ this.scheduleRefresh();
30
+ return this.accessToken;
31
+ }
32
+ /** Return the current (valid) access token. */
33
+ getAccessToken() {
34
+ if (!this.accessToken) {
35
+ throw new Error("ShopifyAuth not initialized — call initialize() first");
36
+ }
37
+ return this.accessToken;
38
+ }
39
+ /** Stop the background refresh timer (for clean shutdown). */
40
+ destroy() {
41
+ if (this.refreshTimer) {
42
+ clearTimeout(this.refreshTimer);
43
+ this.refreshTimer = null;
44
+ }
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Internal
48
+ // ---------------------------------------------------------------------------
49
+ async fetchToken() {
50
+ const url = `https://${this.config.shopDomain}/admin/oauth/access_token`;
51
+ const body = new URLSearchParams({
52
+ grant_type: "client_credentials",
53
+ client_id: this.config.clientId,
54
+ client_secret: this.config.clientSecret,
55
+ });
56
+ const res = await fetch(url, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
59
+ body,
60
+ });
61
+ if (!res.ok) {
62
+ // Cap the echoed body: it should only ever be a short OAuth error like
63
+ // {"error":"invalid_client"}, but truncate defensively so an unexpected
64
+ // large/sensitive response is never propagated wholesale into logs.
65
+ const text = (await res.text()).slice(0, 200);
66
+ throw new Error(`Shopify token exchange failed (${res.status}): ${text}`);
67
+ }
68
+ const data = (await res.json());
69
+ this.accessToken = data.access_token;
70
+ this.expiresAt = Date.now() + data.expires_in * 1000;
71
+ // Hot-swap the header on the existing GraphQL client so every tool
72
+ // automatically picks up the new token.
73
+ if (this.graphqlClient) {
74
+ this.graphqlClient.setHeader("X-Shopify-Access-Token", this.accessToken);
75
+ }
76
+ }
77
+ scheduleRefresh() {
78
+ const msUntilRefresh = this.expiresAt - Date.now() - REFRESH_MARGIN_MS;
79
+ const delay = Math.max(msUntilRefresh, 0);
80
+ this.refreshTimer = setTimeout(async () => {
81
+ try {
82
+ await this.fetchToken();
83
+ this.scheduleRefresh();
84
+ }
85
+ catch (err) {
86
+ console.error("Failed to refresh Shopify access token:", err);
87
+ // Retry in 60 s rather than dying.
88
+ this.refreshTimer = setTimeout(() => this.scheduleRefresh(), 60000);
89
+ }
90
+ }, delay);
91
+ // Allow the Node process to exit even if the timer is pending.
92
+ if (this.refreshTimer && typeof this.refreshTimer === "object" && "unref" in this.refreshTimer) {
93
+ this.refreshTimer.unref();
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,36 @@
1
+ // ── Utility functions ─────────────────────────────────────────────────
2
+ /**
3
+ * Throw a formatted error if Shopify userErrors array is non-empty.
4
+ */
5
+ export function checkUserErrors(errors, operation) {
6
+ if (errors.length > 0) {
7
+ throw new Error(`Failed to ${operation}: ${errors
8
+ .map((e) => `${e.field}: ${e.message}`)
9
+ .join(", ")}`);
10
+ }
11
+ }
12
+ /**
13
+ * Catch handler that doesn't re-wrap errors already thrown by checkUserErrors.
14
+ * Fixes the double-wrapping bug where "Failed to X: Failed to X: actual message"
15
+ * was produced by every mutation tool.
16
+ */
17
+ export function handleToolError(operation, error) {
18
+ // If the error already has our "Failed to" prefix, re-throw as-is
19
+ if (error instanceof Error && error.message.startsWith("Failed to ")) {
20
+ throw error;
21
+ }
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ throw new Error(`Failed to ${operation}: ${message}`);
24
+ }
25
+ /**
26
+ * Extract nodes from a Shopify connection's edges array.
27
+ */
28
+ export function edgesToNodes(connection) {
29
+ return connection.edges.map((edge) => edge.node);
30
+ }
31
+ /**
32
+ * Extract shopMoney from a Shopify MoneyBag (e.g. totalPriceSet.shopMoney).
33
+ */
34
+ export function shopMoney(moneyBag) {
35
+ return moneyBag?.shopMoney ?? null;
36
+ }
@@ -0,0 +1,76 @@
1
+ import { gql } from "graphql-request";
2
+ import { z } from "zod";
3
+ import { checkUserErrors, handleToolError } from "../lib/toolUtils.js";
4
+ const CompleteDraftOrderInputSchema = z.object({
5
+ draftOrderId: z.string().describe("The draft order GID, e.g. gid://shopify/DraftOrder/123"),
6
+ paymentGatewayId: z.string().optional().describe("Payment gateway GID (optional)"),
7
+ });
8
+ let shopifyClient;
9
+ const completeDraftOrder = {
10
+ name: "complete-draft-order",
11
+ description: "Complete a draft order, converting it into a real order. Optionally specify a payment gateway.",
12
+ schema: CompleteDraftOrderInputSchema,
13
+ initialize(client) {
14
+ shopifyClient = client;
15
+ },
16
+ execute: async (input) => {
17
+ try {
18
+ const query = gql `
19
+ #graphql
20
+
21
+ mutation draftOrderComplete($id: ID!, $paymentGatewayId: ID) {
22
+ draftOrderComplete(id: $id, paymentGatewayId: $paymentGatewayId) {
23
+ draftOrder {
24
+ id
25
+ status
26
+ order {
27
+ id
28
+ name
29
+ displayFinancialStatus
30
+ displayFulfillmentStatus
31
+ totalPriceSet {
32
+ shopMoney {
33
+ amount
34
+ currencyCode
35
+ }
36
+ }
37
+ }
38
+ }
39
+ userErrors {
40
+ field
41
+ message
42
+ }
43
+ }
44
+ }
45
+ `;
46
+ const variables = {
47
+ id: input.draftOrderId,
48
+ };
49
+ if (input.paymentGatewayId) {
50
+ variables.paymentGatewayId = input.paymentGatewayId;
51
+ }
52
+ const data = (await shopifyClient.request(query, variables));
53
+ checkUserErrors(data.draftOrderComplete.userErrors, "complete draft order");
54
+ const draft = data.draftOrderComplete.draftOrder;
55
+ return {
56
+ draftOrder: {
57
+ id: draft.id,
58
+ status: draft.status,
59
+ order: draft.order
60
+ ? {
61
+ id: draft.order.id,
62
+ name: draft.order.name,
63
+ financialStatus: draft.order.displayFinancialStatus,
64
+ fulfillmentStatus: draft.order.displayFulfillmentStatus,
65
+ totalPrice: draft.order.totalPriceSet?.shopMoney,
66
+ }
67
+ : null,
68
+ },
69
+ };
70
+ }
71
+ catch (error) {
72
+ handleToolError("complete draft order", error);
73
+ }
74
+ },
75
+ };
76
+ export { completeDraftOrder };
@@ -0,0 +1,142 @@
1
+ import { gql } from "graphql-request";
2
+ import { z } from "zod";
3
+ import { checkUserErrors, handleToolError, edgesToNodes } from "../lib/toolUtils.js";
4
+ // Input schema for creating a customer
5
+ const CreateCustomerInputSchema = z.object({
6
+ firstName: z.string().optional(),
7
+ lastName: z.string().optional(),
8
+ email: z.string().email().optional(),
9
+ phone: z.string().optional(),
10
+ tags: z.array(z.string()).optional(),
11
+ note: z.string().optional(),
12
+ taxExempt: z.boolean().optional(),
13
+ metafields: z
14
+ .array(z.object({
15
+ namespace: z.string(),
16
+ key: z.string(),
17
+ value: z.string(),
18
+ type: z.string().optional()
19
+ }))
20
+ .optional(),
21
+ addresses: z
22
+ .array(z.object({
23
+ address1: z.string().optional(),
24
+ address2: z.string().optional(),
25
+ city: z.string().optional(),
26
+ provinceCode: z.string().optional(),
27
+ zip: z.string().optional(),
28
+ countryCode: z.string().optional(),
29
+ phone: z.string().optional()
30
+ }))
31
+ .optional()
32
+ });
33
+ // Will be initialized in index.ts
34
+ let shopifyClient;
35
+ const createCustomer = {
36
+ name: "create-customer",
37
+ description: "Create a new customer",
38
+ schema: CreateCustomerInputSchema,
39
+ // Add initialize method to set up the GraphQL client
40
+ initialize(client) {
41
+ shopifyClient = client;
42
+ },
43
+ execute: async (input) => {
44
+ try {
45
+ const query = gql `
46
+ #graphql
47
+
48
+ mutation customerCreate($input: CustomerInput!) {
49
+ customerCreate(input: $input) {
50
+ customer {
51
+ id
52
+ firstName
53
+ lastName
54
+ defaultEmailAddress {
55
+ emailAddress
56
+ }
57
+ defaultPhoneNumber {
58
+ phoneNumber
59
+ }
60
+ tags
61
+ note
62
+ taxExempt
63
+ createdAt
64
+ updatedAt
65
+ defaultAddress {
66
+ address1
67
+ address2
68
+ city
69
+ provinceCode
70
+ zip
71
+ country
72
+ phone
73
+ }
74
+ addressesV2(first: 10) {
75
+ edges {
76
+ node {
77
+ address1
78
+ address2
79
+ city
80
+ provinceCode
81
+ zip
82
+ country
83
+ phone
84
+ }
85
+ }
86
+ }
87
+ metafields(first: 10) {
88
+ edges {
89
+ node {
90
+ id
91
+ namespace
92
+ key
93
+ value
94
+ }
95
+ }
96
+ }
97
+ }
98
+ userErrors {
99
+ field
100
+ message
101
+ }
102
+ }
103
+ }
104
+ `;
105
+ const variables = {
106
+ input: {
107
+ ...input
108
+ }
109
+ };
110
+ const data = (await shopifyClient.request(query, variables));
111
+ checkUserErrors(data.customerCreate.userErrors, "create customer");
112
+ // Format and return the created customer
113
+ const customer = data.customerCreate.customer;
114
+ // Format metafields if they exist
115
+ const metafields = customer.metafields?.edges.map((edge) => edge.node) || [];
116
+ const addresses = customer.addressesV2
117
+ ? edgesToNodes(customer.addressesV2)
118
+ : [];
119
+ return {
120
+ customer: {
121
+ id: customer.id,
122
+ firstName: customer.firstName,
123
+ lastName: customer.lastName,
124
+ email: customer.defaultEmailAddress?.emailAddress || null,
125
+ phone: customer.defaultPhoneNumber?.phoneNumber || null,
126
+ tags: customer.tags,
127
+ note: customer.note,
128
+ taxExempt: customer.taxExempt,
129
+ createdAt: customer.createdAt,
130
+ updatedAt: customer.updatedAt,
131
+ defaultAddress: customer.defaultAddress,
132
+ addresses,
133
+ metafields
134
+ }
135
+ };
136
+ }
137
+ catch (error) {
138
+ handleToolError("create customer", error);
139
+ }
140
+ }
141
+ };
142
+ export { createCustomer };
@@ -0,0 +1,160 @@
1
+ import { gql } from "graphql-request";
2
+ import { z } from "zod";
3
+ import { checkUserErrors, handleToolError } from "../lib/toolUtils.js";
4
+ import { shippingAddressSchema } from "../lib/formatters.js";
5
+ const CreateDraftOrderInputSchema = z.object({
6
+ lineItems: z
7
+ .array(z.object({
8
+ variantId: z.string().optional().describe("Product variant GID. Required for existing products, omit for custom line items."),
9
+ title: z.string().optional().describe("Title for custom line items (ignored when variantId is set)"),
10
+ quantity: z.number().describe("Quantity of the line item"),
11
+ originalUnitPriceWithCurrency: z
12
+ .object({
13
+ amount: z.string().describe("Price amount as string"),
14
+ currencyCode: z.string().describe("Currency code, e.g. 'USD'"),
15
+ })
16
+ .optional()
17
+ .describe("Custom price for custom line items"),
18
+ sku: z.string().optional().describe("SKU for custom line items"),
19
+ taxable: z.boolean().optional().describe("Whether custom line item is taxable"),
20
+ requiresShipping: z.boolean().optional().describe("Whether custom line item requires shipping"),
21
+ }))
22
+ .min(1)
23
+ .describe("Line items (max 499). Use variantId for existing products or title+price for custom items."),
24
+ customerId: z.string().optional().describe("Customer GID to associate with the draft order"),
25
+ email: z.string().optional().describe("Customer email"),
26
+ phone: z.string().optional().describe("Customer phone"),
27
+ note: z.string().optional().describe("Note for the draft order"),
28
+ tags: z.array(z.string()).optional().describe("Tags for the draft order"),
29
+ shippingAddress: shippingAddressSchema.optional(),
30
+ billingAddress: shippingAddressSchema.optional(),
31
+ useCustomerDefaultAddress: z.boolean().optional().describe("Use customer's default address"),
32
+ taxExempt: z.boolean().optional().describe("Whether the draft order is tax exempt"),
33
+ poNumber: z.string().optional().describe("Purchase order number"),
34
+ appliedDiscount: z
35
+ .object({
36
+ title: z.string().optional().describe("Discount title"),
37
+ description: z.string().optional(),
38
+ value: z.number().describe("Discount value"),
39
+ valueType: z.enum(["FIXED_AMOUNT", "PERCENTAGE"]).describe("Whether value is fixed or percentage"),
40
+ })
41
+ .optional()
42
+ .describe("Order-level discount"),
43
+ });
44
+ let shopifyClient;
45
+ const createDraftOrder = {
46
+ name: "create-draft-order",
47
+ description: "Create a draft order for phone/chat sales, invoicing, or wholesale. Supports custom line items, discounts, and customer association.",
48
+ schema: CreateDraftOrderInputSchema,
49
+ initialize(client) {
50
+ shopifyClient = client;
51
+ },
52
+ execute: async (input) => {
53
+ try {
54
+ const query = gql `
55
+ #graphql
56
+
57
+ mutation draftOrderCreate($input: DraftOrderInput!) {
58
+ draftOrderCreate(input: $input) {
59
+ draftOrder {
60
+ id
61
+ name
62
+ status
63
+ totalPriceSet {
64
+ shopMoney {
65
+ amount
66
+ currencyCode
67
+ }
68
+ }
69
+ subtotalPriceSet {
70
+ shopMoney {
71
+ amount
72
+ currencyCode
73
+ }
74
+ }
75
+ customer {
76
+ id
77
+ firstName
78
+ lastName
79
+ }
80
+ lineItems(first: 20) {
81
+ edges {
82
+ node {
83
+ id
84
+ title
85
+ quantity
86
+ originalTotalSet {
87
+ shopMoney {
88
+ amount
89
+ currencyCode
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ tags
96
+ note2
97
+ createdAt
98
+ }
99
+ userErrors {
100
+ field
101
+ message
102
+ }
103
+ }
104
+ }
105
+ `;
106
+ const draftInput = {
107
+ lineItems: input.lineItems,
108
+ };
109
+ if (input.customerId) {
110
+ draftInput.purchasingEntity = { customerId: input.customerId };
111
+ }
112
+ if (input.email)
113
+ draftInput.email = input.email;
114
+ if (input.phone)
115
+ draftInput.phone = input.phone;
116
+ if (input.note)
117
+ draftInput.note = input.note;
118
+ if (input.tags)
119
+ draftInput.tags = input.tags;
120
+ if (input.shippingAddress)
121
+ draftInput.shippingAddress = input.shippingAddress;
122
+ if (input.billingAddress)
123
+ draftInput.billingAddress = input.billingAddress;
124
+ if (input.useCustomerDefaultAddress !== undefined)
125
+ draftInput.useCustomerDefaultAddress = input.useCustomerDefaultAddress;
126
+ if (input.taxExempt !== undefined)
127
+ draftInput.taxExempt = input.taxExempt;
128
+ if (input.poNumber)
129
+ draftInput.poNumber = input.poNumber;
130
+ if (input.appliedDiscount)
131
+ draftInput.appliedDiscount = input.appliedDiscount;
132
+ const data = (await shopifyClient.request(query, { input: draftInput }));
133
+ checkUserErrors(data.draftOrderCreate.userErrors, "create draft order");
134
+ const draft = data.draftOrderCreate.draftOrder;
135
+ return {
136
+ draftOrder: {
137
+ id: draft.id,
138
+ name: draft.name,
139
+ status: draft.status,
140
+ totalPrice: draft.totalPriceSet?.shopMoney,
141
+ subtotalPrice: draft.subtotalPriceSet?.shopMoney,
142
+ customer: draft.customer,
143
+ lineItems: draft.lineItems.edges.map((e) => ({
144
+ id: e.node.id,
145
+ title: e.node.title,
146
+ quantity: e.node.quantity,
147
+ originalTotal: e.node.originalTotalSet?.shopMoney,
148
+ })),
149
+ tags: draft.tags,
150
+ note: draft.note2,
151
+ createdAt: draft.createdAt,
152
+ },
153
+ };
154
+ }
155
+ catch (error) {
156
+ handleToolError("create draft order", error);
157
+ }
158
+ },
159
+ };
160
+ export { createDraftOrder };
@@ -0,0 +1,100 @@
1
+ import { gql } from "graphql-request";
2
+ import { z } from "zod";
3
+ import { checkUserErrors, handleToolError } from "../lib/toolUtils.js";
4
+ const CreateFulfillmentInputSchema = z.object({
5
+ lineItemsByFulfillmentOrder: z
6
+ .array(z.object({
7
+ fulfillmentOrderId: z.string().describe("The fulfillment order GID"),
8
+ fulfillmentOrderLineItems: z
9
+ .array(z.object({
10
+ id: z.string().describe("The fulfillment order line item GID"),
11
+ quantity: z.number().describe("Quantity to fulfill"),
12
+ }))
13
+ .optional()
14
+ .describe("Specific line items to fulfill. If omitted, all line items are fulfilled."),
15
+ }))
16
+ .min(1)
17
+ .describe("Fulfillment orders and their line items to fulfill"),
18
+ trackingInfo: z
19
+ .object({
20
+ number: z.string().optional().describe("Tracking number"),
21
+ url: z.string().optional().describe("Tracking URL"),
22
+ company: z.string().optional().describe("Tracking company name (use exact Shopify-known names)"),
23
+ })
24
+ .optional()
25
+ .describe("Tracking information for the shipment"),
26
+ notifyCustomer: z.boolean().default(false).describe("Whether to send shipping notification to customer"),
27
+ });
28
+ let shopifyClient;
29
+ const createFulfillment = {
30
+ name: "create-fulfillment",
31
+ description: "Create a fulfillment (mark items as shipped) with optional tracking info and customer notification.",
32
+ schema: CreateFulfillmentInputSchema,
33
+ initialize(client) {
34
+ shopifyClient = client;
35
+ },
36
+ execute: async (input) => {
37
+ try {
38
+ const query = gql `
39
+ #graphql
40
+
41
+ mutation fulfillmentCreate($fulfillment: FulfillmentInput!) {
42
+ fulfillmentCreate(fulfillment: $fulfillment) {
43
+ fulfillment {
44
+ id
45
+ status
46
+ createdAt
47
+ trackingInfo {
48
+ number
49
+ url
50
+ company
51
+ }
52
+ fulfillmentLineItems(first: 20) {
53
+ edges {
54
+ node {
55
+ id
56
+ quantity
57
+ lineItem {
58
+ title
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ userErrors {
65
+ field
66
+ message
67
+ }
68
+ }
69
+ }
70
+ `;
71
+ const fulfillment = {
72
+ lineItemsByFulfillmentOrder: input.lineItemsByFulfillmentOrder,
73
+ notifyCustomer: input.notifyCustomer,
74
+ };
75
+ if (input.trackingInfo) {
76
+ fulfillment.trackingInfo = input.trackingInfo;
77
+ }
78
+ const data = (await shopifyClient.request(query, { fulfillment }));
79
+ checkUserErrors(data.fulfillmentCreate.userErrors, "create fulfillment");
80
+ const f = data.fulfillmentCreate.fulfillment;
81
+ return {
82
+ fulfillment: {
83
+ id: f.id,
84
+ status: f.status,
85
+ createdAt: f.createdAt,
86
+ trackingInfo: f.trackingInfo,
87
+ lineItems: f.fulfillmentLineItems.edges.map((e) => ({
88
+ id: e.node.id,
89
+ quantity: e.node.quantity,
90
+ title: e.node.lineItem.title,
91
+ })),
92
+ },
93
+ };
94
+ }
95
+ catch (error) {
96
+ handleToolError("create fulfillment", error);
97
+ }
98
+ },
99
+ };
100
+ export { createFulfillment };