diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..cee304a
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index f563bb1..a7635c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
.env
node_modules
-build
\ No newline at end of file
+build
+*.md
+test-*.js
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 1fd6dec..6c2842a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,7 @@
MIT License
-Copyright (c) 2025 Amir Bengherbi
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index 8fb83c1..2441469 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,15 @@
# Shopify MCP Server
-MCP Server for Shopify API, enabling interaction with store data through GraphQL API. This server provides tools for managing products, customers, orders, and more.
+> 🔧 This is a fork of the original [shopify-mcp-server](https://github.com/amir-bengherbi/shopify-mcp-server) by Amir Bengherbi.
-
+MCP Server for Shopify API, enabling interaction with store data through GraphQL API. This fork includes additional tools for product and collection management.
+
+## What's Different
+
+This fork adds:
+- Additional tools for product/collection management (see New Tools section below)
+- Uses Shopify GraphQL Admin API version 2025-04
+- Includes automatic GID formatting for IDs
## Features
@@ -14,6 +21,8 @@ MCP Server for Shopify API, enabling interaction with store data through GraphQL
## Tools
+### Original Tools (from upstream)
+
1. `get-products`
* Get all products or search by title
* Inputs:
@@ -124,6 +133,87 @@ MCP Server for Shopify API, enabling interaction with store data through GraphQL
* `webhookId` (optional string): Webhook ID (required for unsubscribe)
* Returns: Webhook details or success message
+### New Tools (added in this fork)
+
+16. `create-product`
+ * Create a new product with variants and options
+ * Inputs:
+ * `title` (string): Product title
+ * `descriptionHtml` (optional string): Product description in HTML
+ * `vendor` (optional string): Product vendor
+ * `productType` (optional string): Product type
+ * `handle` (optional string): Product handle/slug
+ * `status` (optional enum): Product status (ACTIVE, ARCHIVED, DRAFT)
+ * `tags` (optional array): Product tags
+ * `productOptions` (optional array): Product options (e.g., Size, Color)
+ * `metafields` (optional array): Product metafields
+ * Returns: Created product details
+
+17. `update-product`
+ * Update an existing product
+ * Inputs:
+ * `id` (string): Product ID to update
+ * All other fields from create-product (optional)
+ * Returns: Updated product details
+
+18. `create-product-variants-bulk`
+ * Create multiple variants for a product
+ * Inputs:
+ * `productId` (string): Product ID to add variants to
+ * `variants` (array): Array of variant objects with optionValues, price, barcode, etc.
+ * Returns: Created variants details
+
+19. `update-product-variants-bulk`
+ * Update multiple variants for a product
+ * Inputs:
+ * `productId` (string): Product ID
+ * `variants` (array): Array of variant objects with id and fields to update
+ * Returns: Updated variants details
+
+20. `delete-product-variants-bulk`
+ * Delete multiple variants from a product
+ * Inputs:
+ * `productId` (string): Product ID
+ * `variantIds` (array): Array of variant IDs to delete
+ * Returns: Deletion confirmation
+
+21. `create-staged-uploads`
+ * Stage media files for upload to Shopify
+ * Inputs:
+ * `uploads` (array): Array of upload requests with filename, mimeType, resource type
+ * Returns: Staged upload URLs and parameters
+
+22. `create-product-media`
+ * Add media files to a product
+ * Inputs:
+ * `productId` (string): Product ID to add media to
+ * `media` (array): Array of media objects with originalSource URLs
+ * Returns: Created media details
+
+23. `set-metafields`
+ * Set metafields for products, variants, or other resources
+ * Inputs:
+ * `metafields` (array): Array of metafield objects with key, namespace, ownerId, type, value
+ * Returns: Created/updated metafield details
+
+24. `create-collection`
+ * Create a new collection
+ * Inputs:
+ * `title` (string): Collection title
+ * `descriptionHtml` (optional string): Collection description
+ * `handle` (optional string): Collection handle
+ * `products` (optional array): Product IDs to include
+ * `ruleSet` (optional object): Smart collection rules
+ * `metafields` (optional array): Collection metafields
+ * Returns: Created collection details
+
+25. `update-collection`
+ * Update an existing collection
+ * Inputs:
+ * `id` (string): Collection ID to update
+ * All other fields from create-collection (optional)
+ * Returns: Updated collection details
+
## Setup
### Shopify Access Token
@@ -150,14 +240,16 @@ More details on how to create a Shopify app can be found [here](https://help.sho
### Usage with Claude Desktop
+Since this is a fork, you'll need to run it locally. First clone and build this repository (see Development section below).
+
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"shopify": {
- "command": "npx",
- "args": ["-y", "shopify-mcp-server"],
+ "command": "node",
+ "args": ["/path/to/your/shopify-mcp-server/build/index.js"],
"env": {
"SHOPIFY_ACCESS_TOKEN": "",
"MYSHOPIFY_DOMAIN": ".myshopify.com"
@@ -167,6 +259,8 @@ Add to your `claude_desktop_config.json`:
}
```
+> **Note**: Replace `/path/to/your/shopify-mcp-server` with the actual path where you cloned this repository.
+
## Development
1. Clone the repository
@@ -194,19 +288,26 @@ npm test
- graphql-request - GraphQL client for Shopify API
- zod - Runtime type validation
+## API Version
+
+This server uses Shopify GraphQL Admin API version **2025-04**.
+
## Contributing
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) first.
-## License
+## Credits
-MIT
+- Original author: [Amir Bengherbi](https://github.com/amir-bengherbi)
+- Extended by: [Ryan Boyle](https://github.com/ipvr9) with [Claude (Opus 4)](https://claude.ai)
+- Built with the [Model Context Protocol](https://modelcontextprotocol.io)
-## Community
+## License
-- [MCP GitHub Discussions](https://github.com/modelcontextprotocol/servers/discussions)
-- [Report Issues](https://github.com/your-username/shopify-mcp-server/issues)
+MIT - See [LICENSE](LICENSE) file for details.
----
+## Support
-Built with ❤️ using the [Model Context Protocol](https://modelcontextprotocol.io)
+- [Report Issues](https://github.com/ipvr9/shopify-mcp-server/issues)
+- [Original Repository](https://github.com/amir-bengherbi/shopify-mcp-server)
+- [MCP GitHub Discussions](https://github.com/modelcontextprotocol/servers/discussions)
diff --git a/package-lock.json b/package-lock.json
index cd87fee..9ec3998 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,25 +1,26 @@
{
- "name": "shopify-tools",
- "version": "1.0.0",
+ "name": "shopify-mcp-server",
+ "version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "shopify-tools",
- "version": "1.0.0",
- "license": "ISC",
+ "name": "shopify-mcp-server",
+ "version": "1.0.1",
+ "license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
+ "graphql": "^16.11.0",
"graphql-request": "^7.1.2",
"zod": "^3.24.1"
},
"bin": {
- "shopify-tools": "build/index.js"
+ "shopify-mcp-server": "build/index.js"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^22.10.10",
- "dotenv": "^16.4.7",
+ "dotenv": "^16.5.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
@@ -1645,9 +1646,9 @@
}
},
"node_modules/dotenv": {
- "version": "16.4.7",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
- "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@@ -2007,11 +2008,10 @@
"license": "ISC"
},
"node_modules/graphql": {
- "version": "16.10.0",
- "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz",
- "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==",
+ "version": "16.11.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
+ "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
diff --git a/package.json b/package.json
index e58156a..44933b1 100644
--- a/package.json
+++ b/package.json
@@ -1,24 +1,24 @@
{
"name": "shopify-mcp-server",
- "version": "1.0.1",
+ "version": "1.1.0",
"main": "index.js",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\""
},
"keywords": [],
- "author": "Amir Bengherbi",
"license": "MIT",
"description": "MCP Server for Shopify API, enabling interaction with store data through GraphQL API.",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
+ "graphql": "^16.11.0",
"graphql-request": "^7.1.2",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^22.10.10",
- "dotenv": "^16.4.7",
+ "dotenv": "^16.5.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000..4c01c21
Binary files /dev/null and b/src/.DS_Store differ
diff --git a/src/ShopifyClient/ShopifyClient.ts b/src/ShopifyClient/ShopifyClient.ts
index c79a135..763367e 100644
--- a/src/ShopifyClient/ShopifyClient.ts
+++ b/src/ShopifyClient/ShopifyClient.ts
@@ -44,6 +44,26 @@ import {
ShopifyOrdersGraphqlQueryParams,
ShopifyOrdersGraphqlResponse,
ShopifyOrderGraphql,
+ // New imports for product management
+ ProductCreateInput,
+ ProductCreateResponse,
+ ProductUpdateInput,
+ ProductUpdateResponse,
+ ProductVariantsBulkInput,
+ ProductVariantsBulkCreateResponse,
+ ProductVariantsBulkUpdateResponse,
+ ProductVariantsBulkDeleteResponse,
+ StagedUploadInput,
+ StagedUploadsCreateResponse,
+ CreateMediaInput,
+ ProductCreateMediaResponse,
+ MetafieldsSetInput,
+ MetafieldsSetResponse,
+ CollectionCreateInput,
+ CollectionCreateResponse,
+ CollectionUpdateInput,
+ CollectionUpdateResponse,
+ UserError,
} from "./ShopifyClientPort.js";
import { gql } from "graphql-request";
@@ -100,7 +120,7 @@ const productFragment = gql`
export class ShopifyClient implements ShopifyClientPort {
private readonly logger = console;
- private SHOPIFY_API_VERSION = "2024-04";
+ private SHOPIFY_API_VERSION = "2025-04";
static getShopifyOrdersNextPage(link: Maybe): string | undefined {
if (!link) return;
@@ -230,6 +250,17 @@ export class ShopifyClient implements ShopifyClientPort {
const responseData = await response.json();
if (!response.ok || responseData?.errors) {
+ // Enhanced error logging for debugging
+ console.error('Shopify GraphQL Error Details:', {
+ status: response.status,
+ statusText: response.statusText,
+ url,
+ query: query.substring(0, 200) + '...',
+ variables,
+ responseData,
+ headers: Object.fromEntries(response.headers.entries())
+ });
+
const error = new Error("Shopify GraphQL Error");
throw Object.assign(error, {
response: { data: responseData, status: response.status },
@@ -898,9 +929,11 @@ export class ShopifyClient implements ShopifyClientPort {
shop: string,
queryParams: ShopifyLoadOrderQueryParams
): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
const res = await this.shopifyHTTPRequest<{ order: ShopifyOrder }>({
method: "GET",
- url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/orders/${queryParams.orderId}.json`,
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/orders/${queryParams.orderId}.json`,
accessToken,
params: {
fields: this.getOrdersFields(queryParams.fields),
@@ -916,6 +949,7 @@ export class ShopifyClient implements ShopifyClientPort {
queryParams: ShopifyCollectionsQueryParams,
next?: string
): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
const nextList = next?.split(",");
const customNext = nextList?.[0];
const smartNext = nextList?.[1];
@@ -928,7 +962,7 @@ export class ShopifyClient implements ShopifyClientPort {
const customRes =
await this.shopifyHTTPRequest({
method: "GET",
- url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/custom_collections.json`,
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/custom_collections.json`,
accessToken,
params: {
limit: queryParams.limit,
@@ -948,7 +982,7 @@ export class ShopifyClient implements ShopifyClientPort {
const smartRes =
await this.shopifyHTTPRequest({
method: "GET",
- url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/smart_collections.json`,
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/smart_collections.json`,
accessToken,
params: {
limit: queryParams.limit,
@@ -978,9 +1012,11 @@ export class ShopifyClient implements ShopifyClientPort {
accessToken: string,
shop: string
): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
const res = await this.shopifyHTTPRequest({
method: "GET",
- url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/shop.json`,
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/shop.json`,
accessToken,
});
@@ -1443,9 +1479,11 @@ export class ShopifyClient implements ShopifyClientPort {
limit?: number,
next?: string
): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
const res = await this.shopifyHTTPRequest<{ customers: any[] }>({
method: "GET",
- url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/customers.json`,
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/customers.json`,
accessToken,
params: {
limit: limit ?? 250,
@@ -1716,6 +1754,874 @@ export class ShopifyClient implements ShopifyClientPort {
}
}
+ // New product management methods implementation
+
+ /**
+ * Creates a new product in the Shopify store
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param productInput - Product creation input data
+ * @returns Promise with created product data and any errors
+ */
+ async createProduct(
+ accessToken: string,
+ shop: string,
+ productInput: ProductCreateInput
+ ): Promise {
+ // Validate required fields
+ if (!productInput.title || productInput.title.trim() === '') {
+ throw new Error('Product title is required');
+ }
+
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation productCreate($input: ProductInput!) {
+ productCreate(input: $input) {
+ product {
+ id
+ title
+ handle
+ status
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ input: {
+ title: productInput.title,
+ descriptionHtml: productInput.descriptionHtml,
+ vendor: productInput.vendor,
+ productType: productInput.productType,
+ handle: productInput.handle,
+ status: productInput.status,
+ tags: productInput.tags,
+ productOptions: productInput.productOptions,
+ metafields: productInput.metafields?.map(metafield => ({
+ key: metafield.key,
+ namespace: metafield.namespace,
+ value: metafield.value,
+ type: metafield.type
+ }))
+ }
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ productCreate: {
+ product: {
+ id: string;
+ title: string;
+ handle: string;
+ status: string;
+ };
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const product = res.data.data.productCreate.product;
+ const userErrors = res.data.data.productCreate.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ productInput
+ });
+ }
+
+ return {
+ id: product.id,
+ title: product.title,
+ handle: product.handle,
+ status: product.status
+ };
+ }
+
+ /**
+ * Updates an existing product in the Shopify store
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param productInput - Product update input data including ID
+ * @returns Promise with updated product data and any errors
+ */
+ async updateProduct(
+ accessToken: string,
+ shop: string,
+ productInput: ProductUpdateInput
+ ): Promise {
+ // Validate required fields
+ if (!productInput.id) {
+ throw new Error('Product ID is required for update');
+ }
+
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation ProductUpdate($product: ProductUpdateInput!) {
+ productUpdate(product: $product) {
+ product {
+ id
+ title
+ handle
+ status
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ product: {
+ id: this.ensureGid(productInput.id, 'Product'),
+ title: productInput.title,
+ descriptionHtml: productInput.descriptionHtml,
+ vendor: productInput.vendor,
+ productType: productInput.productType,
+ handle: productInput.handle,
+ status: productInput.status,
+ tags: productInput.tags,
+ metafields: productInput.metafields?.map(metafield => ({
+ key: metafield.key,
+ namespace: metafield.namespace,
+ value: metafield.value,
+ type: metafield.type
+ }))
+ }
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ productUpdate: {
+ product: {
+ id: string;
+ title: string;
+ handle: string;
+ status: string;
+ };
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const product = res.data.data.productUpdate.product;
+ const userErrors = res.data.data.productUpdate.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ productInput
+ });
+ }
+
+ return {
+ id: product.id,
+ title: product.title,
+ handle: product.handle,
+ status: product.status
+ };
+ }
+
+ /**
+ * Creates multiple product variants in bulk
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param productId - ID of the product to add variants to
+ * @param variants - Array of variant data to create
+ * @returns Promise with created variants data and any errors
+ */
+ async createProductVariantsBulk(
+ accessToken: string,
+ shop: string,
+ productId: string,
+ variants: ProductVariantsBulkInput[]
+ ): Promise {
+ // Validate inputs
+ if (!productId) {
+ throw new Error('Product ID is required');
+ }
+ if (!variants || variants.length === 0) {
+ throw new Error('At least one variant is required');
+ }
+
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation ProductVariantsBulkCreate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
+ productVariantsBulkCreate(productId: $productId, variants: $variants) {
+ productVariants {
+ id
+ title
+ price
+ sku
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ productId: this.ensureGid(productId, 'Product'),
+ variants: variants.map(variant => ({
+ optionValues: variant.optionValues,
+ price: variant.price,
+ compareAtPrice: variant.compareAtPrice,
+ barcode: variant.barcode,
+ inventoryPolicy: variant.inventoryPolicy,
+ metafields: variant.metafields?.map(metafield => ({
+ key: metafield.key,
+ namespace: metafield.namespace,
+ value: metafield.value,
+ type: metafield.type
+ }))
+ }))
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ productVariantsBulkCreate: {
+ productVariants: Array<{
+ id: string;
+ title: string;
+ price: string;
+ sku?: string;
+ }>;
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const productVariants = res.data.data.productVariantsBulkCreate.productVariants;
+ const userErrors = res.data.data.productVariantsBulkCreate.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ productId,
+ variants
+ });
+ }
+
+ return {
+ productVariants,
+ userErrors
+ };
+ }
+
+ /**
+ * Updates multiple product variants in bulk
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param productId - ID of the product containing the variants
+ * @param variants - Array of variant data to update
+ * @returns Promise with updated variants data and any errors
+ */
+ async updateProductVariantsBulk(
+ accessToken: string,
+ shop: string,
+ productId: string,
+ variants: ProductVariantsBulkInput[]
+ ): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation ProductVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
+ productVariantsBulkUpdate(productId: $productId, variants: $variants) {
+ productVariants {
+ id
+ title
+ price
+ sku
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ productId: this.ensureGid(productId, 'Product'),
+ variants: variants.map(variant => ({
+ id: variant.id ? this.ensureGid(variant.id, 'ProductVariant') : undefined,
+ optionValues: variant.optionValues,
+ price: variant.price,
+ compareAtPrice: variant.compareAtPrice,
+ barcode: variant.barcode,
+ inventoryPolicy: variant.inventoryPolicy,
+ metafields: variant.metafields?.map(metafield => ({
+ key: metafield.key,
+ namespace: metafield.namespace,
+ value: metafield.value,
+ type: metafield.type
+ }))
+ }))
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ productVariantsBulkUpdate: {
+ productVariants: Array<{
+ id: string;
+ title: string;
+ price: string;
+ sku?: string;
+ }>;
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const productVariants = res.data.data.productVariantsBulkUpdate.productVariants;
+ const userErrors = res.data.data.productVariantsBulkUpdate.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ productId,
+ variants
+ });
+ }
+
+ return {
+ productVariants,
+ userErrors
+ };
+ }
+
+ /**
+ * Deletes multiple product variants in bulk
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param productId - ID of the product containing the variants
+ * @param variantIds - Array of variant IDs to delete
+ * @returns Promise with deletion results and any errors
+ */
+ async deleteProductVariantsBulk(
+ accessToken: string,
+ shop: string,
+ productId: string,
+ variantIds: string[]
+ ): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation ProductVariantsBulkDelete($productId: ID!, $variantsIds: [ID!]!) {
+ productVariantsBulkDelete(productId: $productId, variantsIds: $variantsIds) {
+ product {
+ id
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ productId: this.ensureGid(productId, 'Product'),
+ variantsIds: variantIds.map(id => this.ensureGid(id, 'ProductVariant'))
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ productVariantsBulkDelete: {
+ product: {
+ id: string;
+ };
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const product = res.data.data.productVariantsBulkDelete.product;
+ const userErrors = res.data.data.productVariantsBulkDelete.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ productId,
+ variantIds
+ });
+ }
+
+ return {
+ product,
+ userErrors
+ };
+ }
+
+ /**
+ * Creates staged uploads for media files to be uploaded to Shopify
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param uploads - Array of upload configurations
+ * @returns Promise with staged upload targets and parameters
+ */
+ async createStagedUploads(
+ accessToken: string,
+ shop: string,
+ uploads: StagedUploadInput[]
+ ): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation StagedUploadsCreate($input: [StagedUploadInput!]!) {
+ stagedUploadsCreate(input: $input) {
+ stagedTargets {
+ url
+ resourceUrl
+ parameters {
+ name
+ value
+ }
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ input: uploads.map(upload => ({
+ filename: upload.filename,
+ mimeType: upload.mimeType,
+ httpMethod: upload.httpMethod,
+ resource: upload.resource,
+ fileSize: upload.fileSize
+ }))
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ stagedUploadsCreate: {
+ stagedTargets: Array<{
+ url: string;
+ resourceUrl: string;
+ parameters: Array<{
+ name: string;
+ value: string;
+ }>;
+ }>;
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const stagedTargets = res.data.data.stagedUploadsCreate.stagedTargets;
+ const userErrors = res.data.data.stagedUploadsCreate.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ uploads
+ });
+ }
+
+ return {
+ stagedTargets,
+ userErrors
+ };
+ }
+
+ /**
+ * Adds media files to a product after uploading them
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param productId - ID of the product to add media to
+ * @param media - Array of media configurations
+ * @returns Promise with created media data and any errors
+ */
+ async createProductMedia(
+ accessToken: string,
+ shop: string,
+ productId: string,
+ media: CreateMediaInput[]
+ ): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation ProductCreateMedia($media: [CreateMediaInput!]!, $productId: ID!) {
+ productCreateMedia(media: $media, productId: $productId) {
+ media {
+ alt
+ mediaContentType
+ status
+ }
+ mediaUserErrors {
+ field
+ message
+ }
+ product {
+ id
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ productId,
+ media: media.map(m => ({
+ alt: m.alt,
+ mediaContentType: m.mediaContentType,
+ originalSource: m.originalSource
+ }))
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ productCreateMedia: {
+ media: Array<{
+ alt?: string;
+ mediaContentType: string;
+ status: string;
+ }>;
+ mediaUserErrors: UserError[];
+ product: {
+ id: string;
+ };
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const mediaData = res.data.data.productCreateMedia.media;
+ const mediaUserErrors = res.data.data.productCreateMedia.mediaUserErrors;
+ const product = res.data.data.productCreateMedia.product;
+
+ if (mediaUserErrors.length > 0) {
+ throw getGraphqlShopifyUserError(mediaUserErrors, {
+ shop,
+ productId,
+ media
+ });
+ }
+
+ return {
+ media: mediaData,
+ mediaUserErrors,
+ product
+ };
+ }
+
+ /**
+ * Sets metafields for products, variants, or other resources
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param metafields - Array of metafield data to set
+ * @returns Promise with created/updated metafields and any errors
+ */
+ async setMetafields(
+ accessToken: string,
+ shop: string,
+ metafields: MetafieldsSetInput[]
+ ): Promise {
+ // Validate inputs
+ if (!metafields || metafields.length === 0) {
+ throw new Error('At least one metafield is required');
+ }
+
+ // Validate each metafield
+ metafields.forEach((metafield, index) => {
+ if (!metafield.key || !metafield.namespace || !metafield.ownerId || !metafield.type || metafield.value === undefined) {
+ throw new Error(`Metafield at index ${index} is missing required fields (key, namespace, ownerId, type, value)`);
+ }
+ });
+
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
+ metafieldsSet(metafields: $metafields) {
+ metafields {
+ id
+ key
+ namespace
+ value
+ type
+ createdAt
+ updatedAt
+ }
+ userErrors {
+ field
+ message
+ code
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ metafields: metafields.map(metafield => ({
+ key: metafield.key,
+ namespace: metafield.namespace,
+ ownerId: this.ensureGid(metafield.ownerId, this.inferResourceType(metafield.ownerId)),
+ type: metafield.type,
+ value: metafield.value
+ }))
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ metafieldsSet: {
+ metafields: Array<{
+ id: string;
+ key: string;
+ namespace: string;
+ value: string;
+ type: string;
+ createdAt: string;
+ updatedAt: string;
+ }>;
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const metafieldsData = res.data.data.metafieldsSet.metafields;
+ const userErrors = res.data.data.metafieldsSet.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ metafields
+ });
+ }
+
+ return {
+ metafields: metafieldsData,
+ userErrors
+ };
+ }
+
+ /**
+ * Creates a new collection in the Shopify store
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param collectionInput - Collection creation input data
+ * @returns Promise with created collection data and any errors
+ */
+ async createCollection(
+ accessToken: string,
+ shop: string,
+ collectionInput: CollectionCreateInput
+ ): Promise {
+ // Validate required fields
+ if (!collectionInput.title || collectionInput.title.trim() === '') {
+ throw new Error('Collection title is required');
+ }
+
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation CollectionCreate($input: CollectionInput!) {
+ collectionCreate(input: $input) {
+ collection {
+ id
+ title
+ handle
+ descriptionHtml
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ input: {
+ title: collectionInput.title,
+ descriptionHtml: collectionInput.descriptionHtml,
+ handle: collectionInput.handle,
+ products: collectionInput.products?.map(id => this.ensureGid(id, 'Product')),
+ ruleSet: collectionInput.ruleSet,
+ metafields: collectionInput.metafields?.map(metafield => ({
+ key: metafield.key,
+ namespace: metafield.namespace,
+ value: metafield.value,
+ type: metafield.type
+ }))
+ }
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ collectionCreate: {
+ collection: {
+ id: string;
+ title: string;
+ handle: string;
+ descriptionHtml?: string;
+ };
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const collection = res.data.data.collectionCreate.collection;
+ const userErrors = res.data.data.collectionCreate.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ collectionInput
+ });
+ }
+
+ return {
+ id: collection.id,
+ title: collection.title,
+ handle: collection.handle,
+ descriptionHtml: collection.descriptionHtml
+ };
+ }
+
+ /**
+ * Updates an existing collection in the Shopify store
+ * @param accessToken - Shopify API access token
+ * @param shop - Shop domain
+ * @param collectionInput - Collection update input data including ID
+ * @returns Promise with updated collection data and any errors
+ */
+ async updateCollection(
+ accessToken: string,
+ shop: string,
+ collectionInput: CollectionUpdateInput
+ ): Promise {
+ const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
+
+ const graphqlQuery = gql`
+ mutation CollectionUpdate($input: CollectionInput!) {
+ collectionUpdate(input: $input) {
+ collection {
+ id
+ title
+ handle
+ descriptionHtml
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const variables = {
+ input: {
+ id: this.ensureGid(collectionInput.id, 'Collection'),
+ title: collectionInput.title,
+ descriptionHtml: collectionInput.descriptionHtml,
+ handle: collectionInput.handle,
+ products: collectionInput.products?.map(id => this.ensureGid(id, 'Product')),
+ ruleSet: collectionInput.ruleSet,
+ metafields: collectionInput.metafields?.map(metafield => ({
+ key: metafield.key,
+ namespace: metafield.namespace,
+ value: metafield.value,
+ type: metafield.type
+ }))
+ }
+ };
+
+ const res = await this.shopifyGraphqlRequest<{
+ data: {
+ collectionUpdate: {
+ collection: {
+ id: string;
+ title: string;
+ handle: string;
+ descriptionHtml?: string;
+ };
+ userErrors: UserError[];
+ };
+ };
+ }>({
+ url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
+ accessToken,
+ query: graphqlQuery,
+ variables
+ });
+
+ const collection = res.data.data.collectionUpdate.collection;
+ const userErrors = res.data.data.collectionUpdate.userErrors;
+
+ if (userErrors.length > 0) {
+ throw getGraphqlShopifyUserError(userErrors, {
+ shop,
+ collectionInput
+ });
+ }
+
+ return {
+ id: collection.id,
+ title: collection.title,
+ handle: collection.handle,
+ descriptionHtml: collection.descriptionHtml
+ };
+ }
+
private getOrdersFields(fields?: string[]): string {
const defaultFields = [
"id",
@@ -1748,6 +2654,27 @@ export class ShopifyClient implements ShopifyClientPort {
return id;
}
+ public ensureGid(id: string, type: string): string {
+ if (id.startsWith('gid://shopify/')) {
+ return id;
+ }
+ return `gid://shopify/${type}/${id}`;
+ }
+
+ private inferResourceType(ownerId: string): string {
+ // If already a GID, extract the type
+ if (ownerId.startsWith('gid://shopify/')) {
+ const match = ownerId.match(/gid:\/\/shopify\/([^\/]+)\//);
+ if (match && match[1]) {
+ return match[1];
+ }
+ }
+
+ // Default to Product, but this could be enhanced with more context
+ // Common resource types: Product, ProductVariant, Collection, Customer, Order
+ return 'Product';
+ }
+
async getPriceRule(
accessToken: string,
shop: string,
diff --git a/src/ShopifyClient/ShopifyClientPort.ts b/src/ShopifyClient/ShopifyClientPort.ts
index 7de1813..4db2329 100644
--- a/src/ShopifyClient/ShopifyClientPort.ts
+++ b/src/ShopifyClient/ShopifyClientPort.ts
@@ -1066,4 +1066,278 @@ export interface ShopifyClientPort {
getIdFromGid(gid: string): string;
loadShopDetail(accessToken: string, shop: string): Promise;
+
+ // New product management methods
+ createProduct(
+ accessToken: string,
+ shop: string,
+ productInput: ProductCreateInput
+ ): Promise;
+
+ updateProduct(
+ accessToken: string,
+ shop: string,
+ productInput: ProductUpdateInput
+ ): Promise;
+
+ createProductVariantsBulk(
+ accessToken: string,
+ shop: string,
+ productId: string,
+ variants: ProductVariantsBulkInput[]
+ ): Promise;
+
+ updateProductVariantsBulk(
+ accessToken: string,
+ shop: string,
+ productId: string,
+ variants: ProductVariantsBulkInput[]
+ ): Promise;
+
+ deleteProductVariantsBulk(
+ accessToken: string,
+ shop: string,
+ productId: string,
+ variantIds: string[]
+ ): Promise;
+
+ createStagedUploads(
+ accessToken: string,
+ shop: string,
+ uploads: StagedUploadInput[]
+ ): Promise;
+
+ createProductMedia(
+ accessToken: string,
+ shop: string,
+ productId: string,
+ media: CreateMediaInput[]
+ ): Promise;
+
+ setMetafields(
+ accessToken: string,
+ shop: string,
+ metafields: MetafieldsSetInput[]
+ ): Promise;
+
+ createCollection(
+ accessToken: string,
+ shop: string,
+ collectionInput: CollectionCreateInput
+ ): Promise;
+
+ updateCollection(
+ accessToken: string,
+ shop: string,
+ collectionInput: CollectionUpdateInput
+ ): Promise;
}
+
+// New type definitions for product management
+
+export type ProductOptionInput = {
+ name: string;
+ values: Array<{ name: string }>;
+};
+
+export type ProductCreateInput = {
+ title: string;
+ descriptionHtml?: string;
+ vendor?: string;
+ productType?: string;
+ handle?: string;
+ productOptions?: ProductOptionInput[];
+ metafields?: MetafieldInput[];
+ status?: "ACTIVE" | "ARCHIVED" | "DRAFT";
+ tags?: string[];
+};
+
+export type ProductUpdateInput = {
+ id: string;
+ title?: string;
+ descriptionHtml?: string;
+ vendor?: string;
+ productType?: string;
+ handle?: string;
+ status?: "ACTIVE" | "ARCHIVED" | "DRAFT";
+ tags?: string[];
+ metafields?: MetafieldInput[];
+};
+
+export type ProductCreateResponse = {
+ id: string;
+ title: string;
+ handle: string;
+ status: string;
+};
+
+export type ProductUpdateResponse = {
+ id: string;
+ title: string;
+ handle: string;
+ status: string;
+};
+
+export type ProductVariantsBulkInput = {
+ id?: string; // Only for update operations
+ optionValues?: Array<{
+ optionName: string;
+ name: string;
+ }>;
+ price?: string;
+ compareAtPrice?: string;
+ barcode?: string;
+ inventoryPolicy?: "DENY" | "CONTINUE";
+ inventoryItem?: {
+ cost?: string;
+ tracked?: boolean;
+ };
+ mediaId?: string;
+ metafields?: MetafieldInput[];
+};
+
+export type ProductVariantsBulkCreateResponse = {
+ productVariants: Array<{
+ id: string;
+ title: string;
+ price: string;
+ sku?: string;
+ }>;
+ userErrors: UserError[];
+};
+
+export type ProductVariantsBulkUpdateResponse = {
+ productVariants: Array<{
+ id: string;
+ title: string;
+ price: string;
+ sku?: string;
+ }>;
+ userErrors: UserError[];
+};
+
+export type ProductVariantsBulkDeleteResponse = {
+ product: {
+ id: string;
+ };
+ userErrors: UserError[];
+};
+
+export type StagedUploadInput = {
+ filename: string;
+ mimeType: string;
+ httpMethod: "POST";
+ resource: "IMAGE" | "VIDEO" | "MODEL_3D";
+ fileSize?: string;
+};
+
+export type StagedUploadsCreateResponse = {
+ stagedTargets: Array<{
+ url: string;
+ resourceUrl: string;
+ parameters: Array<{
+ name: string;
+ value: string;
+ }>;
+ }>;
+ userErrors: UserError[];
+};
+
+export type CreateMediaInput = {
+ alt?: string;
+ mediaContentType: "IMAGE" | "VIDEO" | "EXTERNAL_VIDEO" | "MODEL_3D";
+ originalSource: string;
+};
+
+export type ProductCreateMediaResponse = {
+ media: Array<{
+ alt?: string;
+ mediaContentType: string;
+ status: string;
+ }>;
+ mediaUserErrors: UserError[];
+ product: {
+ id: string;
+ };
+};
+
+export type MetafieldInput = {
+ key: string;
+ namespace: string;
+ value: string;
+ type: string;
+};
+
+export type MetafieldsSetInput = {
+ key: string;
+ namespace: string;
+ ownerId: string;
+ type: string;
+ value: string;
+};
+
+export type MetafieldsSetResponse = {
+ metafields: Array<{
+ id: string;
+ key: string;
+ namespace: string;
+ value: string;
+ type: string;
+ createdAt: string;
+ updatedAt: string;
+ }>;
+ userErrors: UserError[];
+};
+
+export type CollectionCreateInput = {
+ title: string;
+ descriptionHtml?: string;
+ handle?: string;
+ metafields?: MetafieldInput[];
+ products?: string[];
+ ruleSet?: {
+ appliedDisjunctively: boolean;
+ rules: Array<{
+ column: string;
+ relation: string;
+ condition: string;
+ }>;
+ };
+};
+
+export type CollectionUpdateInput = {
+ id: string;
+ title?: string;
+ descriptionHtml?: string;
+ handle?: string;
+ metafields?: MetafieldInput[];
+ products?: string[];
+ ruleSet?: {
+ appliedDisjunctively: boolean;
+ rules: Array<{
+ column: string;
+ relation: string;
+ condition: string;
+ }>;
+ };
+};
+
+export type CollectionCreateResponse = {
+ id: string;
+ title: string;
+ handle: string;
+ descriptionHtml?: string;
+};
+
+export type CollectionUpdateResponse = {
+ id: string;
+ title: string;
+ handle: string;
+ descriptionHtml?: string;
+};
+
+export type UserError = {
+ field?: string[];
+ message: string;
+ code?: string;
+};
diff --git a/src/index.ts b/src/index.ts
index 5fe230e..9ad9aa6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -205,8 +205,11 @@ server.tool(
MYSHOPIFY_DOMAIN,
variantIds
);
+ const formattedVariants = variants.variants.map(variant =>
+ `Variant: ${variant.title}\nID: ${variant.id}\nPrice: ${variant.price}\nSKU: ${variant.sku || 'N/A'}\nProduct: ${variant.product.title}`
+ ).join('\n\n');
return {
- content: [{ type: "text", text: JSON.stringify(variants, null, 2) }],
+ content: [{ type: "text", text: formattedVariants }],
};
} catch (error) {
return handleError("Failed to retrieve variants", error);
@@ -232,7 +235,7 @@ server.tool(
next
);
return {
- content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(response) }],
};
} catch (error) {
return handleError("Failed to retrieve customers data", error);
@@ -332,7 +335,7 @@ server.tool(
{ orderId }
);
return {
- content: [{ type: "text", text: JSON.stringify(order, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(order) }],
};
} catch (error) {
return handleError("Failed to retrieve order", error);
@@ -392,7 +395,7 @@ server.tool(
discountInput
);
return {
- content: [{ type: "text", text: JSON.stringify(discount, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(discount) }],
};
} catch (error) {
return handleError("Failed to create discount", error);
@@ -445,7 +448,7 @@ server.tool(
draftOrderData
);
return {
- content: [{ type: "text", text: JSON.stringify(draftOrder, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(draftOrder) }],
};
} catch (error) {
return handleError("Failed to create draft order", error);
@@ -471,7 +474,7 @@ server.tool(
);
return {
content: [
- { type: "text", text: JSON.stringify(completedOrder, null, 2) },
+ { type: "text", text: safeJsonStringify(completedOrder) },
],
};
} catch (error) {
@@ -501,7 +504,7 @@ server.tool(
{ limit, name }
);
return {
- content: [{ type: "text", text: JSON.stringify(collections, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(collections) }],
};
} catch (error) {
return handleError("Failed to retrieve collections", error);
@@ -515,7 +518,7 @@ server.tool("get-shop", "Get shop details", {}, async () => {
try {
const shop = await client.loadShop(SHOPIFY_ACCESS_TOKEN, MYSHOPIFY_DOMAIN);
return {
- content: [{ type: "text", text: JSON.stringify(shop, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(shop) }],
};
} catch (error) {
return handleError("Failed to retrieve shop details", error);
@@ -534,7 +537,7 @@ server.tool(
MYSHOPIFY_DOMAIN
);
return {
- content: [{ type: "text", text: JSON.stringify(shopDetails, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(shopDetails) }],
};
} catch (error) {
return handleError("Failed to retrieve extended shop details", error);
@@ -571,7 +574,7 @@ server.tool(
topic
);
return {
- content: [{ type: "text", text: JSON.stringify(webhook, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(webhook) }],
};
}
case "find": {
@@ -582,7 +585,7 @@ server.tool(
topic
);
return {
- content: [{ type: "text", text: JSON.stringify(webhook, null, 2) }],
+ content: [{ type: "text", text: safeJsonStringify(webhook) }],
};
}
case "unsubscribe": {
@@ -607,6 +610,439 @@ server.tool(
}
);
+// Product Management Tools
+server.tool(
+ "create-product",
+ "Create a new product with variants and options",
+ {
+ title: z.string().describe("Product title"),
+ descriptionHtml: z.string().optional().describe("Product description in HTML"),
+ vendor: z.string().optional().describe("Product vendor"),
+ productType: z.string().optional().describe("Product type"),
+ handle: z.string().optional().describe("Product handle/slug"),
+ status: z.enum(["ACTIVE", "ARCHIVED", "DRAFT"]).optional().describe("Product status"),
+ tags: z.array(z.string()).optional().describe("Product tags"),
+ productOptions: z.array(z.object({
+ name: z.string(),
+ values: z.array(z.object({
+ name: z.string()
+ }))
+ })).optional().describe("Product options (e.g., Size, Color)"),
+ metafields: z.array(z.object({
+ key: z.string(),
+ namespace: z.string(),
+ value: z.string(),
+ type: z.string()
+ })).optional().describe("Product metafields")
+ },
+ async ({ title, descriptionHtml, vendor, productType, handle, status, tags, productOptions, metafields }) => {
+ const client = new ShopifyClient();
+ try {
+ const product = await client.createProduct(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ {
+ title,
+ descriptionHtml,
+ vendor,
+ productType,
+ handle,
+ status,
+ tags,
+ productOptions,
+ metafields
+ }
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(product) }],
+ };
+ } catch (error) {
+ return handleError("Failed to create product", error);
+ }
+ }
+);
+
+server.tool(
+ "update-product",
+ "Update an existing product",
+ {
+ id: z.string().describe("Product ID to update"),
+ title: z.string().optional().describe("Product title"),
+ descriptionHtml: z.string().optional().describe("Product description in HTML"),
+ vendor: z.string().optional().describe("Product vendor"),
+ productType: z.string().optional().describe("Product type"),
+ handle: z.string().optional().describe("Product handle/slug"),
+ status: z.enum(["ACTIVE", "ARCHIVED", "DRAFT"]).optional().describe("Product status"),
+ tags: z.array(z.string()).optional().describe("Product tags"),
+ metafields: z.array(z.object({
+ key: z.string(),
+ namespace: z.string(),
+ value: z.string(),
+ type: z.string()
+ })).optional().describe("Product metafields")
+ },
+ async ({ id, title, descriptionHtml, vendor, productType, handle, status, tags, metafields }) => {
+ const client = new ShopifyClient();
+ try {
+ const product = await client.updateProduct(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ {
+ id,
+ title,
+ descriptionHtml,
+ vendor,
+ productType,
+ handle,
+ status,
+ tags,
+ metafields
+ }
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(product) }],
+ };
+ } catch (error) {
+ return handleError("Failed to update product", error);
+ }
+ }
+);
+
+server.tool(
+ "create-product-variants-bulk",
+ "Create multiple product variants at once",
+ {
+ productId: z.string().describe("Product ID to add variants to"),
+ variants: z.array(z.object({
+ optionValues: z.array(z.object({
+ optionName: z.string(),
+ name: z.string()
+ })).optional().describe("Option values for this variant"),
+ price: z.string().optional().describe("Variant price"),
+ compareAtPrice: z.string().optional().describe("Compare at price"),
+ barcode: z.string().optional().describe("Barcode"),
+ inventoryPolicy: z.enum(["DENY", "CONTINUE"]).optional().describe("Inventory policy"),
+ inventoryManagement: z.enum(["SHOPIFY", "NOT_MANAGED"]).optional().describe("Inventory management"),
+ inventoryQuantity: z.number().optional().describe("Inventory quantity"),
+ sku: z.string().optional().describe("SKU"),
+ weight: z.number().optional().describe("Weight"),
+ weightUnit: z.enum(["GRAMS", "KILOGRAMS", "OUNCES", "POUNDS"]).optional().describe("Weight unit"),
+ requiresShipping: z.boolean().optional().describe("Requires shipping"),
+ metafields: z.array(z.object({
+ key: z.string(),
+ namespace: z.string(),
+ value: z.string(),
+ type: z.string()
+ })).optional().describe("Variant metafields")
+ })).describe("Array of variants to create")
+ },
+ async ({ productId, variants }) => {
+ const client = new ShopifyClient();
+ try {
+ const result = await client.createProductVariantsBulk(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ productId,
+ variants
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(result) }],
+ };
+ } catch (error) {
+ return handleError("Failed to create product variants", error);
+ }
+ }
+);
+
+server.tool(
+ "update-product-variants-bulk",
+ "Update multiple product variants at once",
+ {
+ productId: z.string().describe("Product ID"),
+ variants: z.array(z.object({
+ id: z.string().describe("Variant ID to update"),
+ optionValues: z.array(z.object({
+ optionName: z.string(),
+ name: z.string()
+ })).optional().describe("Option values for this variant"),
+ price: z.string().optional().describe("Variant price"),
+ compareAtPrice: z.string().optional().describe("Compare at price"),
+ barcode: z.string().optional().describe("Barcode"),
+ inventoryPolicy: z.enum(["DENY", "CONTINUE"]).optional().describe("Inventory policy"),
+ inventoryManagement: z.enum(["SHOPIFY", "NOT_MANAGED"]).optional().describe("Inventory management"),
+ inventoryQuantity: z.number().optional().describe("Inventory quantity"),
+ sku: z.string().optional().describe("SKU"),
+ weight: z.number().optional().describe("Weight"),
+ weightUnit: z.enum(["GRAMS", "KILOGRAMS", "OUNCES", "POUNDS"]).optional().describe("Weight unit"),
+ requiresShipping: z.boolean().optional().describe("Requires shipping"),
+ metafields: z.array(z.object({
+ key: z.string(),
+ namespace: z.string(),
+ value: z.string(),
+ type: z.string()
+ })).optional().describe("Variant metafields")
+ })).describe("Array of variants to update")
+ },
+ async ({ productId, variants }) => {
+ const client = new ShopifyClient();
+ try {
+ const result = await client.updateProductVariantsBulk(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ productId,
+ variants
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(result) }],
+ };
+ } catch (error) {
+ return handleError("Failed to update product variants", error);
+ }
+ }
+);
+
+server.tool(
+ "delete-product-variants-bulk",
+ "Delete multiple product variants at once",
+ {
+ productId: z.string().describe("Product ID"),
+ variantIds: z.array(z.string()).describe("Array of variant IDs to delete")
+ },
+ async ({ productId, variantIds }) => {
+ const client = new ShopifyClient();
+ try {
+ const result = await client.deleteProductVariantsBulk(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ productId,
+ variantIds
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(result) }],
+ };
+ } catch (error) {
+ return handleError("Failed to delete product variants", error);
+ }
+ }
+);
+
+server.tool(
+ "create-staged-uploads",
+ "Create staged uploads for media files",
+ {
+ uploads: z.array(z.object({
+ filename: z.string().describe("Filename"),
+ mimeType: z.string().describe("MIME type (e.g., image/jpeg)"),
+ httpMethod: z.literal("POST").describe("HTTP method"),
+ resource: z.enum(["IMAGE", "VIDEO", "MODEL_3D"]).describe("Resource type"),
+ fileSize: z.string().optional().describe("File size for videos and 3D models")
+ })).describe("Array of upload requests")
+ },
+ async ({ uploads }) => {
+ const client = new ShopifyClient();
+ try {
+ const result = await client.createStagedUploads(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ uploads
+ );
+
+ // Format as simple text instead of JSON to avoid circular references
+ let formattedResult = "Staged Upload Results:\n\n";
+
+ if (result.stagedTargets && result.stagedTargets.length > 0) {
+ result.stagedTargets.forEach((target, index) => {
+ formattedResult += `Target ${index + 1}:\n`;
+ formattedResult += ` Upload URL: ${target.url}\n`;
+ formattedResult += ` Resource URL: ${target.resourceUrl}\n`;
+ formattedResult += ` Parameters: ${target.parameters?.length || 0} parameters\n\n`;
+ });
+ }
+
+ if (result.userErrors && result.userErrors.length > 0) {
+ formattedResult += "Errors:\n";
+ result.userErrors.forEach(error => {
+ formattedResult += ` - ${error.message}\n`;
+ });
+ }
+
+ return {
+ content: [{ type: "text", text: formattedResult }],
+ };
+ } catch (error) {
+ return handleError("Failed to create staged uploads", error);
+ }
+ }
+);
+
+server.tool(
+ "create-product-media",
+ "Add media files to a product after uploading them",
+ {
+ productId: z.string().describe("Product ID to add media to"),
+ media: z.array(z.object({
+ alt: z.string().optional().describe("Alt text for the media"),
+ mediaContentType: z.enum(["IMAGE", "VIDEO", "EXTERNAL_VIDEO", "MODEL_3D"]).describe("Media content type"),
+ originalSource: z.string().describe("URL from staged upload")
+ })).describe("Array of media to add")
+ },
+ async ({ productId, media }) => {
+ const client = new ShopifyClient();
+ try {
+ const result = await client.createProductMedia(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ productId,
+ media
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(result) }],
+ };
+ } catch (error) {
+ return handleError("Failed to create product media", error);
+ }
+ }
+);
+
+server.tool(
+ "set-metafields",
+ "Set metafields for products, variants, or other resources",
+ {
+ metafields: z.array(z.object({
+ key: z.string().describe("Metafield key"),
+ namespace: z.string().describe("Metafield namespace"),
+ ownerId: z.string().describe("ID of the resource that owns the metafield"),
+ type: z.string().describe("Metafield type (e.g., single_line_text_field)"),
+ value: z.string().describe("Metafield value")
+ })).describe("Array of metafields to set")
+ },
+ async ({ metafields }) => {
+ const client = new ShopifyClient();
+ try {
+ const result = await client.setMetafields(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ metafields
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(result) }],
+ };
+ } catch (error) {
+ return handleError("Failed to set metafields", error);
+ }
+ }
+);
+
+server.tool(
+ "create-collection",
+ "Create a new collection",
+ {
+ title: z.string().describe("Collection title"),
+ descriptionHtml: z.string().optional().describe("Collection description in HTML"),
+ handle: z.string().optional().describe("Collection handle/slug"),
+ products: z.array(z.string()).optional().describe("Array of product IDs to include"),
+ ruleSet: z.object({
+ appliedDisjunctively: z.boolean(),
+ rules: z.array(z.object({
+ column: z.string(),
+ relation: z.string(),
+ condition: z.string()
+ }))
+ }).optional().describe("Smart collection rules"),
+ metafields: z.array(z.object({
+ key: z.string(),
+ namespace: z.string(),
+ value: z.string(),
+ type: z.string()
+ })).optional().describe("Collection metafields")
+ },
+ async ({ title, descriptionHtml, handle, products, ruleSet, metafields }) => {
+ const client = new ShopifyClient();
+ try {
+ const collection = await client.createCollection(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ {
+ title,
+ descriptionHtml,
+ handle,
+ products,
+ ruleSet,
+ metafields
+ }
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(collection) }],
+ };
+ } catch (error) {
+ return handleError("Failed to create collection", error);
+ }
+ }
+);
+
+server.tool(
+ "update-collection",
+ "Update an existing collection",
+ {
+ id: z.string().describe("Collection ID to update"),
+ title: z.string().optional().describe("Collection title"),
+ descriptionHtml: z.string().optional().describe("Collection description in HTML"),
+ handle: z.string().optional().describe("Collection handle/slug"),
+ products: z.array(z.string()).optional().describe("Array of product IDs to include"),
+ ruleSet: z.object({
+ appliedDisjunctively: z.boolean(),
+ rules: z.array(z.object({
+ column: z.string(),
+ relation: z.string(),
+ condition: z.string()
+ }))
+ }).optional().describe("Smart collection rules"),
+ metafields: z.array(z.object({
+ key: z.string(),
+ namespace: z.string(),
+ value: z.string(),
+ type: z.string()
+ })).optional().describe("Collection metafields")
+ },
+ async ({ id, title, descriptionHtml, handle, products, ruleSet, metafields }) => {
+ const client = new ShopifyClient();
+ try {
+ const collection = await client.updateCollection(
+ SHOPIFY_ACCESS_TOKEN,
+ MYSHOPIFY_DOMAIN,
+ {
+ id,
+ title,
+ descriptionHtml,
+ handle,
+ products,
+ ruleSet,
+ metafields
+ }
+ );
+ return {
+ content: [{ type: "text", text: safeJsonStringify(collection) }],
+ };
+ } catch (error) {
+ return handleError("Failed to update collection", error);
+ }
+ }
+);
+
+// Utility function to safely serialize responses avoiding circular references
+function safeJsonStringify(obj: any): string {
+ const seen = new Set();
+ return JSON.stringify(obj, (key, value) => {
+ if (typeof value === "object" && value !== null) {
+ if (seen.has(value)) {
+ return "[Circular Reference]";
+ }
+ seen.add(value);
+ }
+ return value;
+ }, 2);
+}
+
// Utility function to handle errors
function handleError(
defaultMessage: string,
@@ -616,8 +1052,12 @@ function handleError(
isError: boolean;
} {
let errorMessage = defaultMessage;
- if (error instanceof CustomError) {
+ if (error instanceof Error) {
errorMessage = `${defaultMessage}: ${error.message}`;
+ console.error("Full error details:", error);
+ } else {
+ errorMessage = `${defaultMessage}: ${String(error)}`;
+ console.error("Unknown error type:", error);
}
return {
content: [{ type: "text", text: errorMessage }],