@translateimage/mcp-server 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 +23 -0
- package/README.md +533 -0
- package/dist/bin/http.d.ts +3 -0
- package/dist/bin/http.d.ts.map +1 -0
- package/dist/bin/http.js +51 -0
- package/dist/bin/stdio.d.ts +3 -0
- package/dist/bin/stdio.d.ts.map +1 -0
- package/dist/bin/stdio.js +14 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/schemas/common.d.ts +40 -0
- package/dist/src/schemas/common.d.ts.map +1 -0
- package/dist/src/schemas/common.js +31 -0
- package/dist/src/schemas/image-to-text.d.ts +69 -0
- package/dist/src/schemas/image-to-text.d.ts.map +1 -0
- package/dist/src/schemas/image-to-text.js +26 -0
- package/dist/src/schemas/index.d.ts +7 -0
- package/dist/src/schemas/index.d.ts.map +1 -0
- package/dist/src/schemas/index.js +6 -0
- package/dist/src/schemas/ocr.d.ts +333 -0
- package/dist/src/schemas/ocr.d.ts.map +1 -0
- package/dist/src/schemas/ocr.js +46 -0
- package/dist/src/schemas/shopify.d.ts +860 -0
- package/dist/src/schemas/shopify.d.ts.map +1 -0
- package/dist/src/schemas/shopify.js +183 -0
- package/dist/src/schemas/text-removal.d.ts +60 -0
- package/dist/src/schemas/text-removal.d.ts.map +1 -0
- package/dist/src/schemas/text-removal.js +16 -0
- package/dist/src/schemas/translate.d.ts +197 -0
- package/dist/src/schemas/translate.d.ts.map +1 -0
- package/dist/src/schemas/translate.js +70 -0
- package/dist/src/server.d.ts +4 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +7 -0
- package/dist/src/tools/image-to-text.d.ts +4 -0
- package/dist/src/tools/image-to-text.d.ts.map +1 -0
- package/dist/src/tools/image-to-text.js +12 -0
- package/dist/src/tools/index.d.ts +8 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +202 -0
- package/dist/src/tools/ocr.d.ts +4 -0
- package/dist/src/tools/ocr.d.ts.map +1 -0
- package/dist/src/tools/ocr.js +28 -0
- package/dist/src/tools/shopify/batch-translate.d.ts +26 -0
- package/dist/src/tools/shopify/batch-translate.d.ts.map +1 -0
- package/dist/src/tools/shopify/batch-translate.js +143 -0
- package/dist/src/tools/shopify/index.d.ts +19 -0
- package/dist/src/tools/shopify/index.d.ts.map +1 -0
- package/dist/src/tools/shopify/index.js +28 -0
- package/dist/src/tools/shopify/scan-products.d.ts +38 -0
- package/dist/src/tools/shopify/scan-products.d.ts.map +1 -0
- package/dist/src/tools/shopify/scan-products.js +178 -0
- package/dist/src/tools/shopify/shop-stats.d.ts +12 -0
- package/dist/src/tools/shopify/shop-stats.d.ts.map +1 -0
- package/dist/src/tools/shopify/shop-stats.js +89 -0
- package/dist/src/tools/shopify/translate-product.d.ts +19 -0
- package/dist/src/tools/shopify/translate-product.d.ts.map +1 -0
- package/dist/src/tools/shopify/translate-product.js +121 -0
- package/dist/src/tools/text-removal.d.ts +4 -0
- package/dist/src/tools/text-removal.d.ts.map +1 -0
- package/dist/src/tools/text-removal.js +10 -0
- package/dist/src/tools/translate.d.ts +4 -0
- package/dist/src/tools/translate.d.ts.map +1 -0
- package/dist/src/tools/translate.js +16 -0
- package/dist/src/utils/api-client.d.ts +46 -0
- package/dist/src/utils/api-client.d.ts.map +1 -0
- package/dist/src/utils/api-client.js +124 -0
- package/dist/src/utils/config.d.ts +7 -0
- package/dist/src/utils/config.d.ts.map +1 -0
- package/dist/src/utils/config.js +11 -0
- package/dist/src/utils/errors.d.ts +17 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +35 -0
- package/dist/src/utils/image.d.ts +34 -0
- package/dist/src/utils/image.d.ts.map +1 -0
- package/dist/src/utils/image.js +105 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +4 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { InvalidInputError } from "../../utils/errors.js";
|
|
2
|
+
import { TranslateImageApiClient } from "../../utils/api-client.js";
|
|
3
|
+
const MAX_PRODUCTS = 50;
|
|
4
|
+
const DEFAULT_MAX_PRODUCTS = 10;
|
|
5
|
+
const MAX_IMAGES_PER_CALL = 100;
|
|
6
|
+
const MAX_IMAGES_PER_PRODUCT = 50;
|
|
7
|
+
async function shopifyGraphQL(shopDomain, accessToken, query, variables) {
|
|
8
|
+
const response = await fetch(`https://${shopDomain}/admin/api/2024-01/graphql.json`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
"X-Shopify-Access-Token": accessToken,
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({ query, variables }),
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Shopify API error: ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
const data = await response.json();
|
|
20
|
+
if (data.errors) {
|
|
21
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
|
|
22
|
+
}
|
|
23
|
+
return data.data;
|
|
24
|
+
}
|
|
25
|
+
const PRODUCT_BY_ID_QUERY = `
|
|
26
|
+
query getProduct($id: ID!) {
|
|
27
|
+
product(id: $id) {
|
|
28
|
+
id
|
|
29
|
+
handle
|
|
30
|
+
title
|
|
31
|
+
media(first: 50) {
|
|
32
|
+
edges {
|
|
33
|
+
node {
|
|
34
|
+
... on MediaImage {
|
|
35
|
+
id
|
|
36
|
+
__typename
|
|
37
|
+
image {
|
|
38
|
+
url
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
const PRODUCTS_QUERY = `
|
|
48
|
+
query getProducts($first: Int!) {
|
|
49
|
+
products(first: $first) {
|
|
50
|
+
edges {
|
|
51
|
+
node {
|
|
52
|
+
id
|
|
53
|
+
handle
|
|
54
|
+
title
|
|
55
|
+
media(first: 50) {
|
|
56
|
+
edges {
|
|
57
|
+
node {
|
|
58
|
+
... on MediaImage {
|
|
59
|
+
id
|
|
60
|
+
__typename
|
|
61
|
+
image {
|
|
62
|
+
url
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
async function fetchImageAsBlob(imageUrl) {
|
|
74
|
+
const response = await fetch(imageUrl);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Failed to fetch image: ${response.status}`);
|
|
77
|
+
}
|
|
78
|
+
return response.blob();
|
|
79
|
+
}
|
|
80
|
+
function parseOcrResponse(ocrResult) {
|
|
81
|
+
const regions = ocrResult.regions.map((region) => ({
|
|
82
|
+
bounds: region.bounds,
|
|
83
|
+
text: Object.values(region.languages)[0] || "",
|
|
84
|
+
language: Object.keys(region.languages)[0] || "unknown",
|
|
85
|
+
confidence: region.probability,
|
|
86
|
+
}));
|
|
87
|
+
const avgConfidence = regions.length > 0
|
|
88
|
+
? regions.reduce((sum, r) => sum + r.confidence, 0) / regions.length
|
|
89
|
+
: 0;
|
|
90
|
+
return {
|
|
91
|
+
hasText: regions.length > 0 && ocrResult.text.trim().length > 0,
|
|
92
|
+
regions,
|
|
93
|
+
confidence: avgConfidence,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function handleScanProducts(input, config) {
|
|
97
|
+
const { shopDomain, accessToken } = input.shopifyCredentials;
|
|
98
|
+
const apiClient = new TranslateImageApiClient(config);
|
|
99
|
+
let products = [];
|
|
100
|
+
if (input.productId) {
|
|
101
|
+
const productData = await shopifyGraphQL(shopDomain, accessToken, PRODUCT_BY_ID_QUERY, { id: input.productId });
|
|
102
|
+
if (!productData.product) {
|
|
103
|
+
throw new InvalidInputError(`Product not found: ${input.productId}`);
|
|
104
|
+
}
|
|
105
|
+
products = [productData.product];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const maxProducts = Math.min(input.maxProducts ?? DEFAULT_MAX_PRODUCTS, MAX_PRODUCTS);
|
|
109
|
+
const productsData = await shopifyGraphQL(shopDomain, accessToken, PRODUCTS_QUERY, { first: maxProducts });
|
|
110
|
+
products = productsData.products.edges.map((e) => e.node);
|
|
111
|
+
}
|
|
112
|
+
const results = [];
|
|
113
|
+
let totalImagesScanned = 0;
|
|
114
|
+
let totalImagesWithText = 0;
|
|
115
|
+
let stoppedReason;
|
|
116
|
+
for (const product of products) {
|
|
117
|
+
const mediaImages = product.media.edges
|
|
118
|
+
.filter((edge) => edge.node.__typename === "MediaImage" && edge.node.image?.url)
|
|
119
|
+
.slice(0, MAX_IMAGES_PER_PRODUCT)
|
|
120
|
+
.map((edge) => ({
|
|
121
|
+
mediaId: edge.node.id,
|
|
122
|
+
url: edge.node.image.url,
|
|
123
|
+
}));
|
|
124
|
+
const imageScanResults = [];
|
|
125
|
+
let productImagesWithText = 0;
|
|
126
|
+
for (const media of mediaImages) {
|
|
127
|
+
if (totalImagesScanned >= MAX_IMAGES_PER_CALL) {
|
|
128
|
+
stoppedReason = `Max images per call limit reached (${MAX_IMAGES_PER_CALL})`;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const imageBlob = await fetchImageAsBlob(media.url);
|
|
133
|
+
const ocrResponse = await apiClient.ocr(imageBlob);
|
|
134
|
+
const parsed = parseOcrResponse(ocrResponse);
|
|
135
|
+
imageScanResults.push({
|
|
136
|
+
mediaId: media.mediaId,
|
|
137
|
+
imageUrl: media.url,
|
|
138
|
+
hasText: parsed.hasText,
|
|
139
|
+
confidence: parsed.confidence,
|
|
140
|
+
textRegions: parsed.regions,
|
|
141
|
+
});
|
|
142
|
+
if (parsed.hasText) {
|
|
143
|
+
productImagesWithText++;
|
|
144
|
+
totalImagesWithText++;
|
|
145
|
+
}
|
|
146
|
+
totalImagesScanned++;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
imageScanResults.push({
|
|
150
|
+
mediaId: media.mediaId,
|
|
151
|
+
imageUrl: media.url,
|
|
152
|
+
hasText: false,
|
|
153
|
+
confidence: 0,
|
|
154
|
+
textRegions: [],
|
|
155
|
+
});
|
|
156
|
+
totalImagesScanned++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
results.push({
|
|
160
|
+
productId: product.id,
|
|
161
|
+
productTitle: product.title,
|
|
162
|
+
imagesScanned: imageScanResults.length,
|
|
163
|
+
imagesWithText: productImagesWithText,
|
|
164
|
+
results: imageScanResults,
|
|
165
|
+
});
|
|
166
|
+
if (stoppedReason) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
productsScanned: results.length,
|
|
172
|
+
imagesScanned: totalImagesScanned,
|
|
173
|
+
imagesWithText: totalImagesWithText,
|
|
174
|
+
results,
|
|
175
|
+
isPartial: stoppedReason ? true : undefined,
|
|
176
|
+
stoppedReason,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServerConfig } from "../../utils/config.js";
|
|
2
|
+
import type { ShopStatsInput } from "../../schemas/shopify.js";
|
|
3
|
+
export interface ShopStatsResult {
|
|
4
|
+
shopName: string;
|
|
5
|
+
shopDomain: string;
|
|
6
|
+
totalProducts: number;
|
|
7
|
+
productsWithImages: number;
|
|
8
|
+
totalImages: number;
|
|
9
|
+
isPartial?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function handleShopStats(input: ShopStatsInput, _config: McpServerConfig): Promise<ShopStatsResult>;
|
|
12
|
+
//# sourceMappingURL=shop-stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shop-stats.d.ts","sourceRoot":"","sources":["../../../../src/tools/shopify/shop-stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AA2G/D,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,eAAe,CAAC,CAsD1B"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const MAX_PAGINATION_PAGES = 10;
|
|
2
|
+
const PRODUCTS_PER_PAGE = 250;
|
|
3
|
+
async function shopifyGraphQL(shopDomain, accessToken, query, variables) {
|
|
4
|
+
const response = await fetch(`https://${shopDomain}/admin/api/2024-01/graphql.json`, {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
"X-Shopify-Access-Token": accessToken,
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({ query, variables }),
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`Shopify API error: ${response.statusText}`);
|
|
14
|
+
}
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
if (data.errors) {
|
|
17
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
|
|
18
|
+
}
|
|
19
|
+
return data.data;
|
|
20
|
+
}
|
|
21
|
+
const SHOP_INFO_QUERY = `
|
|
22
|
+
query getShopInfo {
|
|
23
|
+
shop {
|
|
24
|
+
name
|
|
25
|
+
myshopifyDomain
|
|
26
|
+
}
|
|
27
|
+
productsCount {
|
|
28
|
+
count
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
const PRODUCTS_WITH_IMAGES_QUERY = `
|
|
33
|
+
query getProductsWithImages($first: Int!, $after: String) {
|
|
34
|
+
products(first: $first, after: $after) {
|
|
35
|
+
edges {
|
|
36
|
+
node {
|
|
37
|
+
id
|
|
38
|
+
media(first: 50) {
|
|
39
|
+
edges {
|
|
40
|
+
node {
|
|
41
|
+
... on MediaImage {
|
|
42
|
+
id
|
|
43
|
+
__typename
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
cursor
|
|
50
|
+
}
|
|
51
|
+
pageInfo {
|
|
52
|
+
hasNextPage
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
export async function handleShopStats(input, _config) {
|
|
58
|
+
const { shopDomain, accessToken } = input.shopifyCredentials;
|
|
59
|
+
const shopInfo = await shopifyGraphQL(shopDomain, accessToken, SHOP_INFO_QUERY);
|
|
60
|
+
const totalProducts = shopInfo.productsCount.count;
|
|
61
|
+
let productsWithImages = 0;
|
|
62
|
+
let totalImages = 0;
|
|
63
|
+
let cursor = null;
|
|
64
|
+
let pagesProcessed = 0;
|
|
65
|
+
let hasMorePages = true;
|
|
66
|
+
while (hasMorePages && pagesProcessed < MAX_PAGINATION_PAGES) {
|
|
67
|
+
const productsData = await shopifyGraphQL(shopDomain, accessToken, PRODUCTS_WITH_IMAGES_QUERY, { first: PRODUCTS_PER_PAGE, after: cursor });
|
|
68
|
+
for (const edge of productsData.products.edges) {
|
|
69
|
+
const imageCount = edge.node.media.edges.filter((mediaEdge) => mediaEdge.node.__typename === "MediaImage").length;
|
|
70
|
+
if (imageCount > 0) {
|
|
71
|
+
productsWithImages++;
|
|
72
|
+
totalImages += imageCount;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const edges = productsData.products.edges;
|
|
76
|
+
cursor = edges.length > 0 ? edges[edges.length - 1].cursor : null;
|
|
77
|
+
hasMorePages = productsData.products.pageInfo.hasNextPage;
|
|
78
|
+
pagesProcessed++;
|
|
79
|
+
}
|
|
80
|
+
const isPartial = hasMorePages && pagesProcessed >= MAX_PAGINATION_PAGES ? true : undefined;
|
|
81
|
+
return {
|
|
82
|
+
shopName: shopInfo.shop.name,
|
|
83
|
+
shopDomain: shopInfo.shop.myshopifyDomain,
|
|
84
|
+
totalProducts,
|
|
85
|
+
productsWithImages,
|
|
86
|
+
totalImages,
|
|
87
|
+
isPartial,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { McpServerConfig } from "../../utils/config.js";
|
|
2
|
+
import type { TranslateProductInput } from "../../schemas/shopify.js";
|
|
3
|
+
export interface TranslateProductResult {
|
|
4
|
+
productId: string;
|
|
5
|
+
productTitle: string;
|
|
6
|
+
status: "completed" | "failed" | "no_images";
|
|
7
|
+
imagesTranslated: number;
|
|
8
|
+
targetLanguage: string;
|
|
9
|
+
results: Array<{
|
|
10
|
+
mediaId: string;
|
|
11
|
+
originalUrl: string;
|
|
12
|
+
translatedImage: string;
|
|
13
|
+
status: "success" | "error";
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function handleTranslateProduct(input: TranslateProductInput, config: McpServerConfig): Promise<TranslateProductResult>;
|
|
19
|
+
//# sourceMappingURL=translate-product.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate-product.d.ts","sourceRoot":"","sources":["../../../../src/tools/shopify/translate-product.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAuFtE,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;IAC7C,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,KAAK,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAUD,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,qBAAqB,EAC5B,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,sBAAsB,CAAC,CA4EjC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { InvalidInputError } from "../../utils/errors.js";
|
|
2
|
+
import { TranslateImageApiClient } from "../../utils/api-client.js";
|
|
3
|
+
async function shopifyGraphQL(shopDomain, accessToken, query, variables) {
|
|
4
|
+
const response = await fetch(`https://${shopDomain}/admin/api/2024-01/graphql.json`, {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
"X-Shopify-Access-Token": accessToken,
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({ query, variables }),
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`Shopify API error: ${response.statusText}`);
|
|
14
|
+
}
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
if (data.errors) {
|
|
17
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
|
|
18
|
+
}
|
|
19
|
+
return data.data;
|
|
20
|
+
}
|
|
21
|
+
const PRODUCT_BY_ID_QUERY = `
|
|
22
|
+
query getProduct($id: ID!) {
|
|
23
|
+
product(id: $id) {
|
|
24
|
+
id
|
|
25
|
+
handle
|
|
26
|
+
title
|
|
27
|
+
featuredImage {
|
|
28
|
+
url
|
|
29
|
+
}
|
|
30
|
+
images(first: 50) {
|
|
31
|
+
edges {
|
|
32
|
+
node {
|
|
33
|
+
id
|
|
34
|
+
url
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
media(first: 50) {
|
|
39
|
+
edges {
|
|
40
|
+
node {
|
|
41
|
+
... on MediaImage {
|
|
42
|
+
id
|
|
43
|
+
__typename
|
|
44
|
+
image {
|
|
45
|
+
url
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
async function fetchImageAsBlob(imageUrl) {
|
|
55
|
+
const response = await fetch(imageUrl);
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`Failed to fetch image: ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
return response.blob();
|
|
60
|
+
}
|
|
61
|
+
export async function handleTranslateProduct(input, config) {
|
|
62
|
+
const { shopDomain, accessToken } = input.shopifyCredentials;
|
|
63
|
+
const apiClient = new TranslateImageApiClient(config);
|
|
64
|
+
const productData = await shopifyGraphQL(shopDomain, accessToken, PRODUCT_BY_ID_QUERY, { id: input.productId });
|
|
65
|
+
if (!productData.product) {
|
|
66
|
+
throw new InvalidInputError(`Product not found: ${input.productId}`);
|
|
67
|
+
}
|
|
68
|
+
const product = productData.product;
|
|
69
|
+
const mediaImages = product.media.edges
|
|
70
|
+
.filter((edge) => edge.node.__typename === "MediaImage" && edge.node.image?.url)
|
|
71
|
+
.map((edge) => ({
|
|
72
|
+
mediaId: edge.node.id,
|
|
73
|
+
url: edge.node.image.url,
|
|
74
|
+
}));
|
|
75
|
+
if (mediaImages.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
productId: input.productId,
|
|
78
|
+
productTitle: product.title,
|
|
79
|
+
status: "no_images",
|
|
80
|
+
imagesTranslated: 0,
|
|
81
|
+
targetLanguage: input.targetLanguage,
|
|
82
|
+
results: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const results = [];
|
|
86
|
+
let successCount = 0;
|
|
87
|
+
for (const media of mediaImages) {
|
|
88
|
+
try {
|
|
89
|
+
const imageBlob = await fetchImageAsBlob(media.url);
|
|
90
|
+
const translateResult = await apiClient.translate(imageBlob, {
|
|
91
|
+
target_lang: input.targetLanguage,
|
|
92
|
+
translator: input.translator,
|
|
93
|
+
font: "NotoSans",
|
|
94
|
+
});
|
|
95
|
+
results.push({
|
|
96
|
+
mediaId: media.mediaId,
|
|
97
|
+
originalUrl: media.url,
|
|
98
|
+
translatedImage: translateResult.resultImage,
|
|
99
|
+
status: "success",
|
|
100
|
+
});
|
|
101
|
+
successCount++;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
results.push({
|
|
105
|
+
mediaId: media.mediaId,
|
|
106
|
+
originalUrl: media.url,
|
|
107
|
+
translatedImage: "",
|
|
108
|
+
status: "error",
|
|
109
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
productId: input.productId,
|
|
115
|
+
productTitle: product.title,
|
|
116
|
+
status: successCount > 0 ? "completed" : "failed",
|
|
117
|
+
imagesTranslated: successCount,
|
|
118
|
+
targetLanguage: input.targetLanguage,
|
|
119
|
+
results,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { McpServerConfig } from "../utils/config.js";
|
|
2
|
+
import { TextRemovalInput, TextRemovalOutput } from "../schemas/text-removal.js";
|
|
3
|
+
export declare function handleTextRemoval(input: TextRemovalInput, config: McpServerConfig): Promise<TextRemovalOutput>;
|
|
4
|
+
//# sourceMappingURL=text-removal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-removal.d.ts","sourceRoot":"","sources":["../../../src/tools/text-removal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAEpC,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,iBAAiB,CAAC,CAS5B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TranslateImageApiClient } from "../utils/api-client.js";
|
|
2
|
+
import { resolveImageInput } from "../utils/image.js";
|
|
3
|
+
export async function handleTextRemoval(input, config) {
|
|
4
|
+
const client = new TranslateImageApiClient(config);
|
|
5
|
+
const imageBlob = await resolveImageInput(input.image);
|
|
6
|
+
const result = await client.removeText(imageBlob);
|
|
7
|
+
return {
|
|
8
|
+
cleanedImage: result.cleanedImage,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { McpServerConfig } from "../utils/config.js";
|
|
2
|
+
import { TranslateImageInput, TranslateImageOutput } from "../schemas/translate.js";
|
|
3
|
+
export declare function handleTranslateImage(input: TranslateImageInput, config: McpServerConfig): Promise<TranslateImageOutput>;
|
|
4
|
+
//# sourceMappingURL=translate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../../src/tools/translate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EAErB,MAAM,yBAAyB,CAAC;AAEjC,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,mBAAmB,EAC1B,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,oBAAoB,CAAC,CAe/B"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TranslateImageApiClient } from "../utils/api-client.js";
|
|
2
|
+
import { resolveImageInput } from "../utils/image.js";
|
|
3
|
+
export async function handleTranslateImage(input, config) {
|
|
4
|
+
const client = new TranslateImageApiClient(config);
|
|
5
|
+
const imageBlob = await resolveImageInput(input.image);
|
|
6
|
+
const result = await client.translate(imageBlob, {
|
|
7
|
+
target_lang: input.target_lang.toLowerCase(),
|
|
8
|
+
translator: input.translator,
|
|
9
|
+
font: input.font,
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
translatedImage: result.resultImage,
|
|
13
|
+
inpaintedImage: result.inpaintedImage,
|
|
14
|
+
textRegions: result.textRegions,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { McpServerConfig } from "./config.js";
|
|
2
|
+
export interface ApiResponse<T> {
|
|
3
|
+
success: boolean;
|
|
4
|
+
data?: T;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class TranslateImageApiClient {
|
|
8
|
+
private readonly apiKey;
|
|
9
|
+
private readonly baseUrl;
|
|
10
|
+
constructor(config: McpServerConfig);
|
|
11
|
+
translate(imageBlob: Blob, config: {
|
|
12
|
+
target_lang: string;
|
|
13
|
+
translator: string;
|
|
14
|
+
font?: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
resultImage: string;
|
|
17
|
+
inpaintedImage: string;
|
|
18
|
+
textRegions: unknown[];
|
|
19
|
+
}>;
|
|
20
|
+
ocr(imageBlob: Blob): Promise<{
|
|
21
|
+
text: string;
|
|
22
|
+
language: string;
|
|
23
|
+
regions: Array<{
|
|
24
|
+
bounds: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
languages: Record<string, string>;
|
|
31
|
+
probability: number;
|
|
32
|
+
}>;
|
|
33
|
+
}>;
|
|
34
|
+
removeText(imageBlob: Blob): Promise<{
|
|
35
|
+
cleanedImage: string;
|
|
36
|
+
}>;
|
|
37
|
+
imageToText(imageBlob: Blob, targetLanguages?: string[]): Promise<{
|
|
38
|
+
text: string;
|
|
39
|
+
languages: string[];
|
|
40
|
+
translations?: Record<string, string>;
|
|
41
|
+
}>;
|
|
42
|
+
private parseError;
|
|
43
|
+
private extractBase64;
|
|
44
|
+
private blobToBase64;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../../src/utils/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,MAAM,EAAE,eAAe;IAQ7B,SAAS,CACb,SAAS,EAAE,IAAI,EACf,MAAM,EAAE;QACN,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GACA,OAAO,CAAC;QACT,WAAW,EAAE,MAAM,CAAC;QACpB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,OAAO,EAAE,CAAC;KACxB,CAAC;IAqCI,GAAG,CAAC,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC;QAClC,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,KAAK,CAAC;YACb,MAAM,EAAE;gBAAE,CAAC,EAAE,MAAM,CAAC;gBAAC,CAAC,EAAE,MAAM,CAAC;gBAAC,KAAK,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,CAAA;aAAE,CAAC;YAChE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAClC,WAAW,EAAE,MAAM,CAAC;SACrB,CAAC,CAAC;KACJ,CAAC;IAoBI,UAAU,CAAC,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IA4B9D,WAAW,CACf,SAAS,EAAE,IAAI,EACf,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,OAAO,CAAC;QACT,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACvC,CAAC;YAuBY,UAAU;IASxB,OAAO,CAAC,aAAa;YASP,YAAY;CAI3B"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ApiKeyMissingError, ImageTranslationError } from "./errors.js";
|
|
2
|
+
export class TranslateImageApiClient {
|
|
3
|
+
apiKey;
|
|
4
|
+
baseUrl;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
if (!config.apiKey) {
|
|
7
|
+
throw new ApiKeyMissingError("TranslateImage");
|
|
8
|
+
}
|
|
9
|
+
this.apiKey = config.apiKey;
|
|
10
|
+
this.baseUrl = config.apiBaseUrl.replace(/\/$/, "");
|
|
11
|
+
}
|
|
12
|
+
async translate(imageBlob, config) {
|
|
13
|
+
const formData = new FormData();
|
|
14
|
+
formData.append("image", imageBlob);
|
|
15
|
+
formData.append("config", JSON.stringify(config));
|
|
16
|
+
const response = await fetch(`${this.baseUrl}/api/translate`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
20
|
+
},
|
|
21
|
+
body: formData,
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const error = await this.parseError(response);
|
|
25
|
+
throw new ImageTranslationError(error);
|
|
26
|
+
}
|
|
27
|
+
const contentType = response.headers.get("content-type");
|
|
28
|
+
if (contentType?.includes("application/json")) {
|
|
29
|
+
const json = await response.json();
|
|
30
|
+
return {
|
|
31
|
+
resultImage: this.extractBase64(json.resultImage),
|
|
32
|
+
inpaintedImage: this.extractBase64(json.inpaintedImage),
|
|
33
|
+
textRegions: json.textRegions || [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const blob = await response.blob();
|
|
37
|
+
const base64 = await this.blobToBase64(blob);
|
|
38
|
+
return {
|
|
39
|
+
resultImage: base64,
|
|
40
|
+
inpaintedImage: base64,
|
|
41
|
+
textRegions: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async ocr(imageBlob) {
|
|
45
|
+
const formData = new FormData();
|
|
46
|
+
formData.append("image", imageBlob);
|
|
47
|
+
const response = await fetch(`${this.baseUrl}/api/ocr`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
51
|
+
},
|
|
52
|
+
body: formData,
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const error = await this.parseError(response);
|
|
56
|
+
throw new ImageTranslationError(error);
|
|
57
|
+
}
|
|
58
|
+
return response.json();
|
|
59
|
+
}
|
|
60
|
+
async removeText(imageBlob) {
|
|
61
|
+
const formData = new FormData();
|
|
62
|
+
formData.append("image", imageBlob);
|
|
63
|
+
const response = await fetch(`${this.baseUrl}/api/remove-text`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
67
|
+
},
|
|
68
|
+
body: formData,
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const error = await this.parseError(response);
|
|
72
|
+
throw new ImageTranslationError(error);
|
|
73
|
+
}
|
|
74
|
+
const contentType = response.headers.get("content-type");
|
|
75
|
+
if (contentType?.includes("application/json")) {
|
|
76
|
+
const json = await response.json();
|
|
77
|
+
return { cleanedImage: this.extractBase64(json.cleanedImage) };
|
|
78
|
+
}
|
|
79
|
+
const blob = await response.blob();
|
|
80
|
+
const base64 = await this.blobToBase64(blob);
|
|
81
|
+
return { cleanedImage: base64 };
|
|
82
|
+
}
|
|
83
|
+
async imageToText(imageBlob, targetLanguages) {
|
|
84
|
+
const formData = new FormData();
|
|
85
|
+
formData.append("image", imageBlob);
|
|
86
|
+
if (targetLanguages && targetLanguages.length > 0) {
|
|
87
|
+
formData.append("config", JSON.stringify({ targetLanguages }));
|
|
88
|
+
}
|
|
89
|
+
const response = await fetch(`${this.baseUrl}/api/image-to-text`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
93
|
+
},
|
|
94
|
+
body: formData,
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = await this.parseError(response);
|
|
98
|
+
throw new ImageTranslationError(error);
|
|
99
|
+
}
|
|
100
|
+
return response.json();
|
|
101
|
+
}
|
|
102
|
+
async parseError(response) {
|
|
103
|
+
try {
|
|
104
|
+
const json = await response.json();
|
|
105
|
+
return json.error || `API error: ${response.status}`;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return `API error: ${response.status} ${response.statusText}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
extractBase64(dataUrl) {
|
|
112
|
+
if (!dataUrl)
|
|
113
|
+
return "";
|
|
114
|
+
if (dataUrl.startsWith("data:")) {
|
|
115
|
+
const [, base64] = dataUrl.split(",");
|
|
116
|
+
return base64 || "";
|
|
117
|
+
}
|
|
118
|
+
return dataUrl;
|
|
119
|
+
}
|
|
120
|
+
async blobToBase64(blob) {
|
|
121
|
+
const buffer = await blob.arrayBuffer();
|
|
122
|
+
return Buffer.from(buffer).toString("base64");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/utils/config.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,IAAI,eAAe,CAMnD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAI5D"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ApiKeyMissingError } from "./errors.js";
|
|
2
|
+
export function loadConfigFromEnv() {
|
|
3
|
+
const apiKey = process.env.TRANSLATEIMAGE_API_KEY || "";
|
|
4
|
+
const apiBaseUrl = process.env.TRANSLATEIMAGE_API_URL || "https://translateimage.io";
|
|
5
|
+
return { apiKey, apiBaseUrl };
|
|
6
|
+
}
|
|
7
|
+
export function validateConfig(config) {
|
|
8
|
+
if (!config.apiKey) {
|
|
9
|
+
throw new ApiKeyMissingError("TranslateImage");
|
|
10
|
+
}
|
|
11
|
+
}
|