@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.
- package/LICENSE +21 -0
- package/README.md +531 -0
- package/dist/index.js +107 -0
- package/dist/lib/formatters.js +72 -0
- package/dist/lib/shopifyAuth.js +96 -0
- package/dist/lib/toolUtils.js +36 -0
- package/dist/tools/completeDraftOrder.js +76 -0
- package/dist/tools/createCustomer.js +142 -0
- package/dist/tools/createDraftOrder.js +160 -0
- package/dist/tools/createFulfillment.js +100 -0
- package/dist/tools/createProduct.js +123 -0
- package/dist/tools/createRefund.js +115 -0
- package/dist/tools/deleteCustomer.js +47 -0
- package/dist/tools/deleteMetafields.js +54 -0
- package/dist/tools/deleteProduct.js +43 -0
- package/dist/tools/deleteProductVariants.js +82 -0
- package/dist/tools/getCollectionById.js +123 -0
- package/dist/tools/getCollections.js +88 -0
- package/dist/tools/getCustomerById.js +122 -0
- package/dist/tools/getCustomerOrders.js +131 -0
- package/dist/tools/getCustomers.js +125 -0
- package/dist/tools/getFulfillmentOrders.js +106 -0
- package/dist/tools/getInventoryItems.js +86 -0
- package/dist/tools/getInventoryLevels.js +82 -0
- package/dist/tools/getLocations.js +85 -0
- package/dist/tools/getMarkets.js +91 -0
- package/dist/tools/getMetafieldDefinitions.js +112 -0
- package/dist/tools/getMetafields.js +68 -0
- package/dist/tools/getOrderById.js +212 -0
- package/dist/tools/getOrderRefundDetails.js +131 -0
- package/dist/tools/getOrderTransactions.js +85 -0
- package/dist/tools/getOrders.js +148 -0
- package/dist/tools/getPriceLists.js +92 -0
- package/dist/tools/getProductById.js +171 -0
- package/dist/tools/getProductVariantsDetailed.js +139 -0
- package/dist/tools/getProducts.js +155 -0
- package/dist/tools/getShopInfo.js +74 -0
- package/dist/tools/manageCustomerAddress.js +149 -0
- package/dist/tools/manageProductOptions.js +293 -0
- package/dist/tools/manageProductVariants.js +203 -0
- package/dist/tools/manageTags.js +79 -0
- package/dist/tools/mergeCustomers.js +74 -0
- package/dist/tools/orderCancel.js +77 -0
- package/dist/tools/orderCloseOpen.js +74 -0
- package/dist/tools/orderMarkAsPaid.js +51 -0
- package/dist/tools/registry.js +106 -0
- package/dist/tools/setInventoryQuantities.js +74 -0
- package/dist/tools/setMetafields.js +61 -0
- package/dist/tools/updateCustomer.js +119 -0
- package/dist/tools/updateOrder.js +131 -0
- package/dist/tools/updateProduct.js +132 -0
- 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 };
|