shopify-store-mcp 1.0.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 +7 -0
- package/README.md +172 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +65 -0
- package/dist/db.d.ts +19 -0
- package/dist/db.js +161 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +93 -0
- package/dist/graphql/admin/common/collections.d.ts +8 -0
- package/dist/graphql/admin/common/collections.js +44 -0
- package/dist/graphql/admin/common/customers.d.ts +13 -0
- package/dist/graphql/admin/common/customers.js +112 -0
- package/dist/graphql/admin/common/orders.d.ts +13 -0
- package/dist/graphql/admin/common/orders.js +142 -0
- package/dist/graphql/admin/common/products.d.ts +23 -0
- package/dist/graphql/admin/common/products.js +159 -0
- package/dist/graphql/admin/common/shop.d.ts +7 -0
- package/dist/graphql/admin/common/shop.js +38 -0
- package/dist/graphql/admin/index.d.ts +15 -0
- package/dist/graphql/admin/index.js +18 -0
- package/dist/graphql/admin/specialized/bulk.d.ts +33 -0
- package/dist/graphql/admin/specialized/bulk.js +132 -0
- package/dist/graphql/admin/specialized/files.d.ts +22 -0
- package/dist/graphql/admin/specialized/files.js +170 -0
- package/dist/graphql/admin/specialized/inventory.d.ts +13 -0
- package/dist/graphql/admin/specialized/inventory.js +78 -0
- package/dist/graphql/admin/specialized/metafields.d.ts +22 -0
- package/dist/graphql/admin/specialized/metafields.js +100 -0
- package/dist/graphql/admin/specialized/metaobjects.d.ts +36 -0
- package/dist/graphql/admin/specialized/metaobjects.js +239 -0
- package/dist/graphql/admin/specialized/search.d.ts +21 -0
- package/dist/graphql/admin/specialized/search.js +100 -0
- package/dist/graphql/collections.d.ts +1 -0
- package/dist/graphql/collections.js +37 -0
- package/dist/graphql/customers.d.ts +2 -0
- package/dist/graphql/customers.js +98 -0
- package/dist/graphql/inventory.d.ts +2 -0
- package/dist/graphql/inventory.js +67 -0
- package/dist/graphql/metafields.d.ts +2 -0
- package/dist/graphql/metafields.js +43 -0
- package/dist/graphql/orders.d.ts +2 -0
- package/dist/graphql/orders.js +116 -0
- package/dist/graphql/products.d.ts +4 -0
- package/dist/graphql/products.js +140 -0
- package/dist/graphql/shop.d.ts +1 -0
- package/dist/graphql/shop.js +32 -0
- package/dist/graphql/storefront/common/cart.d.ts +23 -0
- package/dist/graphql/storefront/common/cart.js +210 -0
- package/dist/graphql/storefront/common/collections.d.ts +11 -0
- package/dist/graphql/storefront/common/collections.js +114 -0
- package/dist/graphql/storefront/common/products.d.ts +14 -0
- package/dist/graphql/storefront/common/products.js +155 -0
- package/dist/graphql/storefront/index.d.ts +7 -0
- package/dist/graphql/storefront/index.js +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +97 -0
- package/dist/logger.d.ts +58 -0
- package/dist/logger.js +165 -0
- package/dist/prompts/index.d.ts +2 -0
- package/dist/prompts/index.js +169 -0
- package/dist/queue.d.ts +73 -0
- package/dist/queue.js +120 -0
- package/dist/resources/index.d.ts +3 -0
- package/dist/resources/index.js +180 -0
- package/dist/shopify-client.d.ts +16 -0
- package/dist/shopify-client.js +39 -0
- package/dist/tools/graphql.d.ts +3 -0
- package/dist/tools/graphql.js +41 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/infrastructure.d.ts +6 -0
- package/dist/tools/infrastructure.js +215 -0
- package/dist/tools/shop.d.ts +3 -0
- package/dist/tools/shop.js +28 -0
- package/dist/tools/smart-bulk.d.ts +7 -0
- package/dist/tools/smart-bulk.js +286 -0
- package/dist/tools/smart-files.d.ts +7 -0
- package/dist/tools/smart-files.js +169 -0
- package/dist/tools/smart-metaobjects.d.ts +7 -0
- package/dist/tools/smart-metaobjects.js +186 -0
- package/dist/tools/smart-schema.d.ts +7 -0
- package/dist/tools/smart-schema.js +138 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +2 -0
- package/dist/utils/polling.d.ts +53 -0
- package/dist/utils/polling.js +77 -0
- package/package.json +83 -0
- package/prisma/schema.prisma +82 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart bulk operation tools
|
|
3
|
+
* Handles bulk export and import with polling
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { formatSuccessResponse, formatGraphQLErrors, formatErrorResponse, formatUserErrors, } from "../errors.js";
|
|
7
|
+
import { BULK_OPERATION_RUN_QUERY, GET_CURRENT_BULK_OPERATION, STAGED_UPLOADS_CREATE, BULK_OPERATION_RUN_MUTATION, } from "../graphql/admin/index.js";
|
|
8
|
+
import { pollUntil } from "../utils/polling.js";
|
|
9
|
+
import { enqueue } from "../queue.js";
|
|
10
|
+
import { logOperation } from "../logger.js";
|
|
11
|
+
// Bulk operation status constants
|
|
12
|
+
const BULK_STATUS = {
|
|
13
|
+
CREATED: "CREATED",
|
|
14
|
+
RUNNING: "RUNNING",
|
|
15
|
+
COMPLETED: "COMPLETED",
|
|
16
|
+
FAILED: "FAILED",
|
|
17
|
+
CANCELED: "CANCELED",
|
|
18
|
+
CANCELING: "CANCELING",
|
|
19
|
+
};
|
|
20
|
+
export function registerSmartBulkTools(server, client, storeDomain) {
|
|
21
|
+
// BULK EXPORT
|
|
22
|
+
server.registerTool("bulk_export", {
|
|
23
|
+
title: "Bulk Export Data",
|
|
24
|
+
description: "Export large amounts of data from Shopify using bulk operations. " +
|
|
25
|
+
"This is a SMART tool that: starts the bulk query, polls until complete, returns download URL. " +
|
|
26
|
+
"Use this for exporting products, orders, customers, etc. in JSONL format. " +
|
|
27
|
+
"The query should be the inner query WITHOUT the top-level 'query' keyword. " +
|
|
28
|
+
"Example: { products { edges { node { id title } } } }",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
query: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe("The GraphQL query to execute. Provide the inner query without 'query' keyword. " +
|
|
33
|
+
"Example: { products { edges { node { id title handle status } } } }"),
|
|
34
|
+
},
|
|
35
|
+
annotations: {
|
|
36
|
+
readOnlyHint: true,
|
|
37
|
+
destructiveHint: false,
|
|
38
|
+
idempotentHint: false,
|
|
39
|
+
openWorldHint: false,
|
|
40
|
+
},
|
|
41
|
+
}, async ({ query }) => {
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
try {
|
|
44
|
+
// Step 1: Start the bulk query
|
|
45
|
+
const startResponse = await enqueue(() => client.request(BULK_OPERATION_RUN_QUERY, {
|
|
46
|
+
variables: { query },
|
|
47
|
+
}));
|
|
48
|
+
if (startResponse.errors) {
|
|
49
|
+
await logOperation({
|
|
50
|
+
storeDomain,
|
|
51
|
+
toolName: "bulk_export",
|
|
52
|
+
query: BULK_OPERATION_RUN_QUERY,
|
|
53
|
+
variables: { query },
|
|
54
|
+
response: startResponse,
|
|
55
|
+
success: false,
|
|
56
|
+
errorMessage: "GraphQL errors",
|
|
57
|
+
durationMs: Date.now() - startTime,
|
|
58
|
+
});
|
|
59
|
+
return formatGraphQLErrors(startResponse);
|
|
60
|
+
}
|
|
61
|
+
const startData = startResponse.data;
|
|
62
|
+
if (startData.bulkOperationRunQuery.userErrors.length > 0) {
|
|
63
|
+
await logOperation({
|
|
64
|
+
storeDomain,
|
|
65
|
+
toolName: "bulk_export",
|
|
66
|
+
query: BULK_OPERATION_RUN_QUERY,
|
|
67
|
+
variables: { query },
|
|
68
|
+
response: startResponse,
|
|
69
|
+
success: false,
|
|
70
|
+
errorMessage: startData.bulkOperationRunQuery.userErrors.map(e => e.message).join(", "),
|
|
71
|
+
durationMs: Date.now() - startTime,
|
|
72
|
+
});
|
|
73
|
+
return formatUserErrors(startData.bulkOperationRunQuery.userErrors);
|
|
74
|
+
}
|
|
75
|
+
const bulkOp = startData.bulkOperationRunQuery.bulkOperation;
|
|
76
|
+
if (!bulkOp) {
|
|
77
|
+
return formatErrorResponse("No bulk operation returned");
|
|
78
|
+
}
|
|
79
|
+
// Step 2: Poll until complete
|
|
80
|
+
const result = await pollUntil(async () => {
|
|
81
|
+
const statusResponse = await enqueue(() => client.request(GET_CURRENT_BULK_OPERATION, {}));
|
|
82
|
+
if (statusResponse.errors) {
|
|
83
|
+
return { done: true, error: "Failed to check bulk operation status" };
|
|
84
|
+
}
|
|
85
|
+
const current = statusResponse.data.currentBulkOperation;
|
|
86
|
+
if (!current || current.id !== bulkOp.id) {
|
|
87
|
+
return { done: true, error: "Bulk operation not found or changed" };
|
|
88
|
+
}
|
|
89
|
+
if (current.status === BULK_STATUS.COMPLETED) {
|
|
90
|
+
return {
|
|
91
|
+
done: true,
|
|
92
|
+
result: {
|
|
93
|
+
id: current.id,
|
|
94
|
+
status: "COMPLETED",
|
|
95
|
+
url: current.url,
|
|
96
|
+
objectCount: current.objectCount,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (current.status === BULK_STATUS.FAILED) {
|
|
101
|
+
return {
|
|
102
|
+
done: true,
|
|
103
|
+
error: `Bulk operation failed: ${current.errorCode || "Unknown error"}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (current.status === BULK_STATUS.CANCELED) {
|
|
107
|
+
return { done: true, error: "Bulk operation was canceled" };
|
|
108
|
+
}
|
|
109
|
+
// Still running
|
|
110
|
+
return { done: false };
|
|
111
|
+
}, {
|
|
112
|
+
intervalMs: 5000, // Check every 5 seconds
|
|
113
|
+
timeoutMs: 300000, // 5 minutes max
|
|
114
|
+
});
|
|
115
|
+
await logOperation({
|
|
116
|
+
storeDomain,
|
|
117
|
+
toolName: "bulk_export",
|
|
118
|
+
query: BULK_OPERATION_RUN_QUERY,
|
|
119
|
+
variables: { query },
|
|
120
|
+
response: result,
|
|
121
|
+
success: true,
|
|
122
|
+
durationMs: Date.now() - startTime,
|
|
123
|
+
});
|
|
124
|
+
return formatSuccessResponse({
|
|
125
|
+
success: true,
|
|
126
|
+
bulkOperation: result,
|
|
127
|
+
message: `Bulk export completed. Download URL: ${result.url}`,
|
|
128
|
+
downloadUrl: result.url,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await logOperation({
|
|
133
|
+
storeDomain,
|
|
134
|
+
toolName: "bulk_export",
|
|
135
|
+
query: BULK_OPERATION_RUN_QUERY,
|
|
136
|
+
variables: { query },
|
|
137
|
+
success: false,
|
|
138
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
139
|
+
durationMs: Date.now() - startTime,
|
|
140
|
+
});
|
|
141
|
+
return formatErrorResponse(error);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// BULK IMPORT
|
|
145
|
+
server.registerTool("bulk_import", {
|
|
146
|
+
title: "Bulk Import Data",
|
|
147
|
+
description: "Import large amounts of data to Shopify using bulk mutations. " +
|
|
148
|
+
"This is a SMART tool that: creates staged upload, uploads JSONL, runs bulk mutation, polls until complete. " +
|
|
149
|
+
"The mutation should be a single mutation that will run for each line in the JSONL. " +
|
|
150
|
+
"The JSONL file should have one JSON object per line with the mutation variables.",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
mutation: z
|
|
153
|
+
.string()
|
|
154
|
+
.describe("The GraphQL mutation to execute per JSONL line. " +
|
|
155
|
+
"Example: mutation productUpdate($input: ProductInput!) { productUpdate(input: $input) { product { id } userErrors { message } } }"),
|
|
156
|
+
jsonlContent: z
|
|
157
|
+
.string()
|
|
158
|
+
.describe("The JSONL content to import. One JSON object per line with mutation variables. " +
|
|
159
|
+
"Example: {\"input\": {\"id\": \"gid://shopify/Product/123\", \"title\": \"New Title\"}}"),
|
|
160
|
+
},
|
|
161
|
+
annotations: {
|
|
162
|
+
readOnlyHint: false,
|
|
163
|
+
destructiveHint: true,
|
|
164
|
+
idempotentHint: false,
|
|
165
|
+
openWorldHint: false,
|
|
166
|
+
},
|
|
167
|
+
}, async ({ mutation, jsonlContent }) => {
|
|
168
|
+
const startTime = Date.now();
|
|
169
|
+
try {
|
|
170
|
+
// Step 1: Create staged upload
|
|
171
|
+
const filename = `bulk-import-${Date.now()}.jsonl`;
|
|
172
|
+
const stagedResponse = await enqueue(() => client.request(STAGED_UPLOADS_CREATE, {
|
|
173
|
+
variables: {
|
|
174
|
+
input: [
|
|
175
|
+
{
|
|
176
|
+
filename,
|
|
177
|
+
mimeType: "text/jsonl",
|
|
178
|
+
httpMethod: "POST",
|
|
179
|
+
resource: "BULK_MUTATION_VARIABLES",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
}));
|
|
184
|
+
if (stagedResponse.errors) {
|
|
185
|
+
return formatGraphQLErrors(stagedResponse);
|
|
186
|
+
}
|
|
187
|
+
const stagedData = stagedResponse.data;
|
|
188
|
+
if (stagedData.stagedUploadsCreate.userErrors.length > 0) {
|
|
189
|
+
return formatUserErrors(stagedData.stagedUploadsCreate.userErrors);
|
|
190
|
+
}
|
|
191
|
+
const target = stagedData.stagedUploadsCreate.stagedTargets[0];
|
|
192
|
+
if (!target) {
|
|
193
|
+
return formatErrorResponse("No staged upload target returned");
|
|
194
|
+
}
|
|
195
|
+
// Step 2: Upload JSONL to staged URL
|
|
196
|
+
const formData = new FormData();
|
|
197
|
+
for (const param of target.parameters) {
|
|
198
|
+
formData.append(param.name, param.value);
|
|
199
|
+
}
|
|
200
|
+
formData.append("file", new Blob([jsonlContent], { type: "text/jsonl" }), filename);
|
|
201
|
+
const uploadResponse = await fetch(target.url, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
body: formData,
|
|
204
|
+
});
|
|
205
|
+
if (!uploadResponse.ok) {
|
|
206
|
+
return formatErrorResponse(`Failed to upload JSONL: ${uploadResponse.statusText}`);
|
|
207
|
+
}
|
|
208
|
+
// Step 3: Run bulk mutation
|
|
209
|
+
const bulkResponse = await enqueue(() => client.request(BULK_OPERATION_RUN_MUTATION, {
|
|
210
|
+
variables: {
|
|
211
|
+
mutation,
|
|
212
|
+
stagedUploadPath: target.resourceUrl,
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
if (bulkResponse.errors) {
|
|
216
|
+
return formatGraphQLErrors(bulkResponse);
|
|
217
|
+
}
|
|
218
|
+
const bulkData = bulkResponse.data;
|
|
219
|
+
if (bulkData.bulkOperationRunMutation.userErrors.length > 0) {
|
|
220
|
+
return formatUserErrors(bulkData.bulkOperationRunMutation.userErrors);
|
|
221
|
+
}
|
|
222
|
+
const bulkOp = bulkData.bulkOperationRunMutation.bulkOperation;
|
|
223
|
+
if (!bulkOp) {
|
|
224
|
+
return formatErrorResponse("No bulk operation returned");
|
|
225
|
+
}
|
|
226
|
+
// Step 4: Poll until complete
|
|
227
|
+
const result = await pollUntil(async () => {
|
|
228
|
+
const statusResponse = await enqueue(() => client.request(GET_CURRENT_BULK_OPERATION, {}));
|
|
229
|
+
if (statusResponse.errors) {
|
|
230
|
+
return { done: true, error: "Failed to check bulk operation status" };
|
|
231
|
+
}
|
|
232
|
+
const current = statusResponse.data.currentBulkOperation;
|
|
233
|
+
if (!current || current.id !== bulkOp.id) {
|
|
234
|
+
return { done: true, error: "Bulk operation not found or changed" };
|
|
235
|
+
}
|
|
236
|
+
if (current.status === BULK_STATUS.COMPLETED) {
|
|
237
|
+
return {
|
|
238
|
+
done: true,
|
|
239
|
+
result: {
|
|
240
|
+
id: current.id,
|
|
241
|
+
status: "COMPLETED",
|
|
242
|
+
objectCount: current.objectCount,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (current.status === BULK_STATUS.FAILED) {
|
|
247
|
+
return {
|
|
248
|
+
done: true,
|
|
249
|
+
error: `Bulk operation failed: ${current.errorCode || "Unknown error"}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return { done: false };
|
|
253
|
+
}, {
|
|
254
|
+
intervalMs: 5000,
|
|
255
|
+
timeoutMs: 300000,
|
|
256
|
+
});
|
|
257
|
+
await logOperation({
|
|
258
|
+
storeDomain,
|
|
259
|
+
toolName: "bulk_import",
|
|
260
|
+
query: BULK_OPERATION_RUN_MUTATION,
|
|
261
|
+
variables: { mutation, jsonlLines: jsonlContent.split("\n").length },
|
|
262
|
+
response: result,
|
|
263
|
+
success: true,
|
|
264
|
+
durationMs: Date.now() - startTime,
|
|
265
|
+
});
|
|
266
|
+
return formatSuccessResponse({
|
|
267
|
+
success: true,
|
|
268
|
+
bulkOperation: result,
|
|
269
|
+
message: `Bulk import completed. ${result.objectCount} objects processed.`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
await logOperation({
|
|
274
|
+
storeDomain,
|
|
275
|
+
toolName: "bulk_import",
|
|
276
|
+
query: BULK_OPERATION_RUN_MUTATION,
|
|
277
|
+
variables: { mutation },
|
|
278
|
+
success: false,
|
|
279
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
280
|
+
durationMs: Date.now() - startTime,
|
|
281
|
+
});
|
|
282
|
+
return formatErrorResponse(error);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=smart-bulk.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart file upload tool
|
|
3
|
+
* Handles the full workflow: create file → poll status → return ready URL
|
|
4
|
+
*/
|
|
5
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import type { AdminApiClient } from "../shopify-client.js";
|
|
7
|
+
export declare function registerSmartFileTools(server: McpServer, client: AdminApiClient, storeDomain: string): void;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart file upload tool
|
|
3
|
+
* Handles the full workflow: create file → poll status → return ready URL
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { formatSuccessResponse, formatGraphQLErrors, formatErrorResponse, formatUserErrors, } from "../errors.js";
|
|
7
|
+
import { FILE_CREATE, GET_FILE } from "../graphql/admin/index.js";
|
|
8
|
+
import { pollUntil } from "../utils/polling.js";
|
|
9
|
+
import { enqueue } from "../queue.js";
|
|
10
|
+
import { logOperation } from "../logger.js";
|
|
11
|
+
// File status constants
|
|
12
|
+
const FILE_STATUS = {
|
|
13
|
+
PROCESSING: "PROCESSING",
|
|
14
|
+
READY: "READY",
|
|
15
|
+
FAILED: "FAILED",
|
|
16
|
+
UPLOADED: "UPLOADED",
|
|
17
|
+
};
|
|
18
|
+
export function registerSmartFileTools(server, client, storeDomain) {
|
|
19
|
+
server.registerTool("upload_file", {
|
|
20
|
+
title: "Upload File",
|
|
21
|
+
description: "Upload a file to Shopify from an external URL. " +
|
|
22
|
+
"This is a SMART tool that handles the full workflow: " +
|
|
23
|
+
"creates the file, polls until it's ready, and returns the final CDN URL. " +
|
|
24
|
+
"Supports images, videos, and generic files. " +
|
|
25
|
+
"Use this instead of raw fileCreate when you need the final URL immediately.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
url: z
|
|
28
|
+
.string()
|
|
29
|
+
.url()
|
|
30
|
+
.describe("The external URL of the file to upload. Shopify will fetch this."),
|
|
31
|
+
filename: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Optional filename. If not provided, extracted from URL."),
|
|
35
|
+
alt: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Alt text for images. Recommended for accessibility."),
|
|
39
|
+
contentType: z
|
|
40
|
+
.enum(["IMAGE", "VIDEO", "FILE"])
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Content type hint. IMAGE for images, VIDEO for videos, FILE for documents/other. " +
|
|
43
|
+
"If not provided, Shopify will auto-detect."),
|
|
44
|
+
duplicateResolution: z
|
|
45
|
+
.enum(["APPEND_UUID", "REPLACE", "RAISE_ERROR"])
|
|
46
|
+
.default("APPEND_UUID")
|
|
47
|
+
.describe("How to handle duplicate filenames: " +
|
|
48
|
+
"APPEND_UUID (default) adds unique suffix, " +
|
|
49
|
+
"REPLACE overwrites existing, " +
|
|
50
|
+
"RAISE_ERROR fails if exists."),
|
|
51
|
+
},
|
|
52
|
+
annotations: {
|
|
53
|
+
readOnlyHint: false,
|
|
54
|
+
destructiveHint: false,
|
|
55
|
+
idempotentHint: false,
|
|
56
|
+
openWorldHint: true,
|
|
57
|
+
},
|
|
58
|
+
}, async ({ url, filename, alt, contentType, duplicateResolution }) => {
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
try {
|
|
61
|
+
// Step 1: Create the file
|
|
62
|
+
const createResponse = await enqueue(() => client.request(FILE_CREATE, {
|
|
63
|
+
variables: {
|
|
64
|
+
files: [
|
|
65
|
+
{
|
|
66
|
+
originalSource: url,
|
|
67
|
+
filename: filename || undefined,
|
|
68
|
+
alt: alt || undefined,
|
|
69
|
+
contentType: contentType || undefined,
|
|
70
|
+
duplicateResolutionMode: duplicateResolution,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
if (createResponse.errors) {
|
|
76
|
+
await logOperation({
|
|
77
|
+
storeDomain,
|
|
78
|
+
toolName: "upload_file",
|
|
79
|
+
query: FILE_CREATE,
|
|
80
|
+
variables: { url, filename, alt, contentType, duplicateResolution },
|
|
81
|
+
response: createResponse,
|
|
82
|
+
success: false,
|
|
83
|
+
errorMessage: "GraphQL errors",
|
|
84
|
+
durationMs: Date.now() - startTime,
|
|
85
|
+
});
|
|
86
|
+
return formatGraphQLErrors(createResponse);
|
|
87
|
+
}
|
|
88
|
+
const createData = createResponse.data;
|
|
89
|
+
if (createData.fileCreate.userErrors.length > 0) {
|
|
90
|
+
await logOperation({
|
|
91
|
+
storeDomain,
|
|
92
|
+
toolName: "upload_file",
|
|
93
|
+
query: FILE_CREATE,
|
|
94
|
+
variables: { url, filename, alt, contentType, duplicateResolution },
|
|
95
|
+
response: createResponse,
|
|
96
|
+
success: false,
|
|
97
|
+
errorMessage: createData.fileCreate.userErrors.map(e => e.message).join(", "),
|
|
98
|
+
durationMs: Date.now() - startTime,
|
|
99
|
+
});
|
|
100
|
+
return formatUserErrors(createData.fileCreate.userErrors);
|
|
101
|
+
}
|
|
102
|
+
const createdFile = createData.fileCreate.files[0];
|
|
103
|
+
if (!createdFile) {
|
|
104
|
+
return formatErrorResponse("No file returned from fileCreate");
|
|
105
|
+
}
|
|
106
|
+
// Step 2: Poll until file is ready (or failed)
|
|
107
|
+
const fileId = createdFile.id;
|
|
108
|
+
const readyFile = await pollUntil(async () => {
|
|
109
|
+
const statusResponse = await enqueue(() => client.request(GET_FILE, { variables: { id: fileId } }));
|
|
110
|
+
if (statusResponse.errors) {
|
|
111
|
+
return { done: true, error: "Failed to check file status" };
|
|
112
|
+
}
|
|
113
|
+
const node = statusResponse.data.node;
|
|
114
|
+
if (!node) {
|
|
115
|
+
return { done: true, error: "File not found" };
|
|
116
|
+
}
|
|
117
|
+
if (node.fileStatus === FILE_STATUS.READY) {
|
|
118
|
+
// Extract the final URL based on file type
|
|
119
|
+
const finalUrl = node.url || node.image?.url || node.originalSource?.url;
|
|
120
|
+
return {
|
|
121
|
+
done: true,
|
|
122
|
+
result: {
|
|
123
|
+
id: fileId,
|
|
124
|
+
status: "READY",
|
|
125
|
+
url: finalUrl,
|
|
126
|
+
...node,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (node.fileStatus === FILE_STATUS.FAILED) {
|
|
131
|
+
const errorMsg = node.fileErrors?.map(e => e.message).join(", ") || "File processing failed";
|
|
132
|
+
return { done: true, error: errorMsg };
|
|
133
|
+
}
|
|
134
|
+
// Still processing
|
|
135
|
+
return { done: false };
|
|
136
|
+
}, {
|
|
137
|
+
intervalMs: 2000,
|
|
138
|
+
timeoutMs: 60000, // 60 seconds max
|
|
139
|
+
});
|
|
140
|
+
await logOperation({
|
|
141
|
+
storeDomain,
|
|
142
|
+
toolName: "upload_file",
|
|
143
|
+
query: FILE_CREATE,
|
|
144
|
+
variables: { url, filename, alt, contentType, duplicateResolution },
|
|
145
|
+
response: readyFile,
|
|
146
|
+
success: true,
|
|
147
|
+
durationMs: Date.now() - startTime,
|
|
148
|
+
});
|
|
149
|
+
return formatSuccessResponse({
|
|
150
|
+
success: true,
|
|
151
|
+
file: readyFile,
|
|
152
|
+
message: `File uploaded successfully. CDN URL: ${readyFile.url}`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
await logOperation({
|
|
157
|
+
storeDomain,
|
|
158
|
+
toolName: "upload_file",
|
|
159
|
+
query: FILE_CREATE,
|
|
160
|
+
variables: { url, filename, alt, contentType, duplicateResolution },
|
|
161
|
+
success: false,
|
|
162
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
163
|
+
durationMs: Date.now() - startTime,
|
|
164
|
+
});
|
|
165
|
+
return formatErrorResponse(error);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=smart-files.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart metaobject tools
|
|
3
|
+
* Handles upsert (get or create/update) in one operation
|
|
4
|
+
*/
|
|
5
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import type { AdminApiClient } from "../shopify-client.js";
|
|
7
|
+
export declare function registerSmartMetaobjectTools(server: McpServer, client: AdminApiClient, storeDomain: string): void;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart metaobject tools
|
|
3
|
+
* Handles upsert (get or create/update) in one operation
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { formatSuccessResponse, formatGraphQLErrors, formatErrorResponse, formatUserErrors, } from "../errors.js";
|
|
7
|
+
import { GET_METAOBJECT_BY_HANDLE, METAOBJECT_CREATE, METAOBJECT_UPDATE, } from "../graphql/admin/index.js";
|
|
8
|
+
import { enqueue } from "../queue.js";
|
|
9
|
+
import { logOperation } from "../logger.js";
|
|
10
|
+
export function registerSmartMetaobjectTools(server, client, storeDomain) {
|
|
11
|
+
server.registerTool("upsert_metaobject", {
|
|
12
|
+
title: "Upsert Metaobject",
|
|
13
|
+
description: "Create or update a metaobject by handle (idempotent). " +
|
|
14
|
+
"This is a SMART tool that: checks if metaobject exists → creates if not → updates if exists. " +
|
|
15
|
+
"Use this when you want to ensure a metaobject exists with certain data, " +
|
|
16
|
+
"regardless of whether it already exists. " +
|
|
17
|
+
"The handle must be unique within the metaobject type.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe("The metaobject type (e.g., 'color_swatch', 'brand', 'faq'). " +
|
|
22
|
+
"Must match an existing metaobject definition in the store."),
|
|
23
|
+
handle: z
|
|
24
|
+
.string()
|
|
25
|
+
.describe("Unique handle for this metaobject within its type. " +
|
|
26
|
+
"Used to identify the metaobject for upsert."),
|
|
27
|
+
fields: z
|
|
28
|
+
.array(z.object({
|
|
29
|
+
key: z.string().describe("Field key as defined in the metaobject definition"),
|
|
30
|
+
value: z.string().describe("Field value (JSON string for complex types)"),
|
|
31
|
+
}))
|
|
32
|
+
.describe("Array of field key-value pairs to set on the metaobject"),
|
|
33
|
+
publishStatus: z
|
|
34
|
+
.enum(["ACTIVE", "DRAFT"])
|
|
35
|
+
.default("ACTIVE")
|
|
36
|
+
.describe("Publish status. ACTIVE makes it visible on storefront if publishable."),
|
|
37
|
+
},
|
|
38
|
+
annotations: {
|
|
39
|
+
readOnlyHint: false,
|
|
40
|
+
destructiveHint: true,
|
|
41
|
+
idempotentHint: true, // Same input = same result
|
|
42
|
+
openWorldHint: false,
|
|
43
|
+
},
|
|
44
|
+
}, async ({ type, handle, fields, publishStatus }) => {
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
try {
|
|
47
|
+
// Step 1: Check if metaobject already exists
|
|
48
|
+
const existingResponse = await enqueue(() => client.request(GET_METAOBJECT_BY_HANDLE, {
|
|
49
|
+
variables: {
|
|
50
|
+
handle: { type, handle },
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
if (existingResponse.errors) {
|
|
54
|
+
await logOperation({
|
|
55
|
+
storeDomain,
|
|
56
|
+
toolName: "upsert_metaobject",
|
|
57
|
+
query: GET_METAOBJECT_BY_HANDLE,
|
|
58
|
+
variables: { type, handle },
|
|
59
|
+
response: existingResponse,
|
|
60
|
+
success: false,
|
|
61
|
+
errorMessage: "GraphQL errors checking existing",
|
|
62
|
+
durationMs: Date.now() - startTime,
|
|
63
|
+
});
|
|
64
|
+
return formatGraphQLErrors(existingResponse);
|
|
65
|
+
}
|
|
66
|
+
const existing = existingResponse.data.metaobjectByHandle;
|
|
67
|
+
let result;
|
|
68
|
+
let action;
|
|
69
|
+
if (existing) {
|
|
70
|
+
// Step 2a: Update existing metaobject
|
|
71
|
+
action = "updated";
|
|
72
|
+
const updateResponse = await enqueue(() => client.request(METAOBJECT_UPDATE, {
|
|
73
|
+
variables: {
|
|
74
|
+
id: existing.id,
|
|
75
|
+
metaobject: {
|
|
76
|
+
fields,
|
|
77
|
+
capabilities: publishStatus
|
|
78
|
+
? { publishable: { status: publishStatus } }
|
|
79
|
+
: undefined,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
if (updateResponse.errors) {
|
|
84
|
+
await logOperation({
|
|
85
|
+
storeDomain,
|
|
86
|
+
toolName: "upsert_metaobject",
|
|
87
|
+
query: METAOBJECT_UPDATE,
|
|
88
|
+
variables: { id: existing.id, fields },
|
|
89
|
+
response: updateResponse,
|
|
90
|
+
success: false,
|
|
91
|
+
errorMessage: "GraphQL errors during update",
|
|
92
|
+
durationMs: Date.now() - startTime,
|
|
93
|
+
});
|
|
94
|
+
return formatGraphQLErrors(updateResponse);
|
|
95
|
+
}
|
|
96
|
+
const updateData = updateResponse.data;
|
|
97
|
+
if (updateData.metaobjectUpdate.userErrors.length > 0) {
|
|
98
|
+
await logOperation({
|
|
99
|
+
storeDomain,
|
|
100
|
+
toolName: "upsert_metaobject",
|
|
101
|
+
query: METAOBJECT_UPDATE,
|
|
102
|
+
variables: { id: existing.id, fields },
|
|
103
|
+
response: updateResponse,
|
|
104
|
+
success: false,
|
|
105
|
+
errorMessage: updateData.metaobjectUpdate.userErrors.map(e => e.message).join(", "),
|
|
106
|
+
durationMs: Date.now() - startTime,
|
|
107
|
+
});
|
|
108
|
+
return formatUserErrors(updateData.metaobjectUpdate.userErrors);
|
|
109
|
+
}
|
|
110
|
+
result = updateData.metaobjectUpdate.metaobject;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Step 2b: Create new metaobject
|
|
114
|
+
action = "created";
|
|
115
|
+
const createResponse = await enqueue(() => client.request(METAOBJECT_CREATE, {
|
|
116
|
+
variables: {
|
|
117
|
+
metaobject: {
|
|
118
|
+
type,
|
|
119
|
+
handle,
|
|
120
|
+
fields,
|
|
121
|
+
capabilities: publishStatus
|
|
122
|
+
? { publishable: { status: publishStatus } }
|
|
123
|
+
: undefined,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
127
|
+
if (createResponse.errors) {
|
|
128
|
+
await logOperation({
|
|
129
|
+
storeDomain,
|
|
130
|
+
toolName: "upsert_metaobject",
|
|
131
|
+
query: METAOBJECT_CREATE,
|
|
132
|
+
variables: { type, handle, fields },
|
|
133
|
+
response: createResponse,
|
|
134
|
+
success: false,
|
|
135
|
+
errorMessage: "GraphQL errors during create",
|
|
136
|
+
durationMs: Date.now() - startTime,
|
|
137
|
+
});
|
|
138
|
+
return formatGraphQLErrors(createResponse);
|
|
139
|
+
}
|
|
140
|
+
const createData = createResponse.data;
|
|
141
|
+
if (createData.metaobjectCreate.userErrors.length > 0) {
|
|
142
|
+
await logOperation({
|
|
143
|
+
storeDomain,
|
|
144
|
+
toolName: "upsert_metaobject",
|
|
145
|
+
query: METAOBJECT_CREATE,
|
|
146
|
+
variables: { type, handle, fields },
|
|
147
|
+
response: createResponse,
|
|
148
|
+
success: false,
|
|
149
|
+
errorMessage: createData.metaobjectCreate.userErrors.map(e => e.message).join(", "),
|
|
150
|
+
durationMs: Date.now() - startTime,
|
|
151
|
+
});
|
|
152
|
+
return formatUserErrors(createData.metaobjectCreate.userErrors);
|
|
153
|
+
}
|
|
154
|
+
result = createData.metaobjectCreate.metaobject;
|
|
155
|
+
}
|
|
156
|
+
await logOperation({
|
|
157
|
+
storeDomain,
|
|
158
|
+
toolName: "upsert_metaobject",
|
|
159
|
+
query: action === "created" ? METAOBJECT_CREATE : METAOBJECT_UPDATE,
|
|
160
|
+
variables: { type, handle, fields },
|
|
161
|
+
response: result,
|
|
162
|
+
success: true,
|
|
163
|
+
durationMs: Date.now() - startTime,
|
|
164
|
+
});
|
|
165
|
+
return formatSuccessResponse({
|
|
166
|
+
success: true,
|
|
167
|
+
action,
|
|
168
|
+
metaobject: result,
|
|
169
|
+
message: `Metaobject ${action} successfully.`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
await logOperation({
|
|
174
|
+
storeDomain,
|
|
175
|
+
toolName: "upsert_metaobject",
|
|
176
|
+
query: "upsert_metaobject",
|
|
177
|
+
variables: { type, handle, fields },
|
|
178
|
+
success: false,
|
|
179
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
180
|
+
durationMs: Date.now() - startTime,
|
|
181
|
+
});
|
|
182
|
+
return formatErrorResponse(error);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=smart-metaobjects.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart schema discovery tool
|
|
3
|
+
* Combines metafield definitions and metaobject definitions in one call
|
|
4
|
+
*/
|
|
5
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import type { AdminApiClient } from "../shopify-client.js";
|
|
7
|
+
export declare function registerSmartSchemaTools(server: McpServer, client: AdminApiClient, storeDomain: string): void;
|