byclaw-mcp 0.4.13 → 0.4.15
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/dist/index.js +59 -65
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -21,24 +21,26 @@ const SERVER_VERSION = (() => {
|
|
|
21
21
|
}
|
|
22
22
|
})();
|
|
23
23
|
const TOOL_PREFIX = `[byclaw-mcp v${SERVER_VERSION}] `;
|
|
24
|
-
//
|
|
25
|
-
// - Claude Code / Cursor:
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
24
|
+
// Render guidance for the model. Stays consistent across clients now:
|
|
25
|
+
// - Claude Code / Cursor / browsers: inline images via ``
|
|
26
|
+
// work directly.
|
|
27
|
+
// - claude.ai web + Claude Desktop App: same markdown renders as a
|
|
28
|
+
// "Bild anzeigen" click-to-load placeholder (their external-image
|
|
29
|
+
// privacy gate). We accept this — Finding 093 ruled out HTML artifact
|
|
30
|
+
// widgets (sandbox CSP blocks our CDNs) and inline base64 (too many
|
|
31
|
+
// tokens / too slow). Placeholder is the cheapest acceptable UX.
|
|
32
|
+
// Surfaced in every shop / search_products / test_web_search response as
|
|
33
|
+
// the `_render_hint` field so the model never falls back to widget tricks.
|
|
34
|
+
const RENDER_HINT = 'Emit `` markdown for each product inline with the text. ' +
|
|
35
|
+
'Clients render this differently: Claude Code / Cursor / browsers show the ' +
|
|
36
|
+
'image immediately; claude.ai web + Claude Desktop App show a "Bild anzeigen" ' +
|
|
37
|
+
'click-to-load placeholder (their privacy gate for external images) — that ' +
|
|
38
|
+
'is acceptable, do not try to work around it with HTML widgets or any other ' +
|
|
39
|
+
'tool, the artifact sandbox CSP blocks our CDNs and the user just gets empty ' +
|
|
40
|
+
'boxes. The placeholder is the best we get in claude.ai today. Always wrap ' +
|
|
41
|
+
'each product\'s merchant link as a clickable "<merchant> →" using ' +
|
|
42
|
+
'affiliate_url. Keep replies tight — one image-markdown line + one buy line ' +
|
|
43
|
+
'per product.';
|
|
42
44
|
const API_BASE = process.env.BYCLAW_API_URL || 'https://byclaw.io';
|
|
43
45
|
const API_KEY = process.env.BYCLAW_API_KEY || '';
|
|
44
46
|
// Brand fallback when the catalog row has no image. Clients (Claude Desktop
|
|
@@ -51,39 +53,43 @@ function imageUrlOf(p) {
|
|
|
51
53
|
const url = typeof p?.image_url === 'string' ? p.image_url.trim() : '';
|
|
52
54
|
return url.startsWith('https://') || url.startsWith('http://') ? url : IMAGE_FALLBACK;
|
|
53
55
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
if (!res.ok)
|
|
64
|
-
return null;
|
|
65
|
-
const contentType = res.headers.get('content-type') || 'image/jpeg';
|
|
66
|
-
if (!contentType.startsWith('image/'))
|
|
67
|
-
return null;
|
|
68
|
-
const buffer = await res.arrayBuffer();
|
|
69
|
-
if (buffer.byteLength < 100 || buffer.byteLength > 500000)
|
|
70
|
-
return null; // skip tiny errors or huge files
|
|
71
|
-
return { data: Buffer.from(buffer).toString('base64'), mimeType: contentType };
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
56
|
+
// Hard cap on every backend request. Without it, if the backend is wedged
|
|
57
|
+
// (Anthropic 503 + SDK retry storm, ES lock, …) the MCP process would hang
|
|
58
|
+
// forever and the client (Claude Desktop) would only break out at its own
|
|
59
|
+
// 4 min cap with a "Server disconnected" disconnect — the very symptom
|
|
60
|
+
// Finding 094 described. 60 s is comfortably above normal backend latency
|
|
61
|
+
// (10-15 s for /api/mcp/shop) but below the client timeout, so the user
|
|
62
|
+
// gets a real error message instead of a stall.
|
|
63
|
+
const BACKEND_TIMEOUT_MS = 60000;
|
|
77
64
|
async function apiCall(path, options = {}) {
|
|
78
65
|
const headers = {
|
|
79
66
|
'Authorization': `Bearer ${API_KEY}`,
|
|
80
67
|
'Content-Type': 'application/json',
|
|
81
68
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
71
|
+
...options,
|
|
72
|
+
signal: AbortSignal.timeout(BACKEND_TIMEOUT_MS),
|
|
73
|
+
headers: { ...headers, ...options.headers },
|
|
74
|
+
});
|
|
75
|
+
return res.json();
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// AbortError fires when the timeout above kicks in. Translate to an
|
|
79
|
+
// ok=false body the existing call sites already know how to print.
|
|
80
|
+
if (err?.name === 'AbortError' || err?.name === 'TimeoutError') {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: `Backend timeout after ${BACKEND_TIMEOUT_MS / 1000}s (${path})`,
|
|
84
|
+
retryable: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: `Backend request failed: ${err?.message || 'unknown error'} (${path})`,
|
|
90
|
+
retryable: false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
87
93
|
}
|
|
88
94
|
const server = new index_js_1.Server({ name: 'byclaw', version: SERVER_VERSION }, { capabilities: { tools: {}, prompts: {} } });
|
|
89
95
|
// Prompts
|
|
@@ -96,7 +102,7 @@ server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
|
|
|
96
102
|
server.setRequestHandler(types_js_1.GetPromptRequestSchema, async () => ({
|
|
97
103
|
messages: [{
|
|
98
104
|
role: 'user',
|
|
99
|
-
content: { type: 'text', text: 'You are connected to byclaw.io — a product comparison platform that searches across thousands of online shops to find the best products. When the user asks for a product, use the "shop" tool.\n\nThe tool returns structured JSON with: `picked` (title, price, brand, merchant, affiliate_url, image_url), `alternatives` (
|
|
105
|
+
content: { type: 'text', text: 'You are connected to byclaw.io — a product comparison platform that searches across thousands of online shops to find the best products. When the user asks for a product, use the "shop" tool.\n\nThe tool returns structured JSON with: `picked` (title, price, brand, merchant, affiliate_url, image_url), `alternatives` (same fields), `compared` (int), `reasoning` (string), `review` (string), `disclosure` (string), and `_render_hint` (string).\n\n**Presenting the result:**\n1. For each product (picked first, then each alternative), emit one image markdown line `` and one buy line as a clickable `[<merchant> →](affiliate_url)`. Keep replies tight — image line + buy line per product, plus a one-sentence rationale below the picked.\n2. Claude Code / Cursor / browsers render the image inline. claude.ai web + Claude Desktop App show a "Bild anzeigen" click-to-load placeholder — accept that, do not try HTML widgets or other workarounds (the artifact sandbox blocks our CDNs).\n3. ALWAYS display the `disclosure` text verbatim at the end as a small italic note — this is legally required.\n\nbyclaw.io is operated by NJUDEV S.L., a European company.' },
|
|
100
106
|
}],
|
|
101
107
|
}));
|
|
102
108
|
// ═══════════════════════════════════════
|
|
@@ -107,7 +113,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
|
107
113
|
// ── Core: one-shot shopping ──
|
|
108
114
|
{
|
|
109
115
|
name: 'shop',
|
|
110
|
-
description: TOOL_PREFIX + 'Search and compare products across multiple shops. Returns a JSON object with `picked` (the recommendation) and `alternatives` (other candidates). Each product object includes `title`, `price`, `brand`, `merchant`, `affiliate_url`, and `image_url` (absolute HTTPS URL).
|
|
116
|
+
description: TOOL_PREFIX + 'Search and compare products across multiple shops. Returns a JSON object with `picked` (the recommendation) and `alternatives` (other candidates). Each product object includes `title`, `price`, `brand`, `merchant`, `affiliate_url`, and `image_url` (absolute HTTPS URL). Emit `` markdown for each product so the image renders inline in clients that support it (Claude Code, Cursor, browsers). claude.ai web / Claude Desktop App will show a click-to-load placeholder for those markdown images — that is fine, accept it. Do NOT try to build HTML widgets or other workarounds; the artifact sandbox blocks our product CDNs. See `_render_hint` in every response for the exact rendering recipe.',
|
|
111
117
|
inputSchema: {
|
|
112
118
|
type: 'object',
|
|
113
119
|
properties: {
|
|
@@ -123,7 +129,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
|
123
129
|
// ── Optional: browse & explore ──
|
|
124
130
|
{
|
|
125
131
|
name: 'search_products',
|
|
126
|
-
description: TOOL_PREFIX + 'Browse products without triggering a recommendation. Use this only if the user explicitly wants to see a list of options. Each result item has `image_url` (absolute HTTPS URL).
|
|
132
|
+
description: TOOL_PREFIX + 'Browse products without triggering a recommendation. Use this only if the user explicitly wants to see a list of options. Each result item has `image_url` (absolute HTTPS URL). Emit `` markdown inline; claude.ai web / Desktop will gate it behind a click-to-load placeholder, other clients render directly. See `_render_hint`.',
|
|
127
133
|
inputSchema: {
|
|
128
134
|
type: 'object',
|
|
129
135
|
properties: {
|
|
@@ -361,18 +367,11 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
361
367
|
}, null, 2) }] };
|
|
362
368
|
}
|
|
363
369
|
// ── NORMAL / FOLLOW-UP MODE ──
|
|
370
|
+
// No more server-side image prefetch — `image_url` is a plain
|
|
371
|
+
// string in each product object, the client (or the model in
|
|
372
|
+
// claude.ai-style clients) decides what to do with it. Cuts
|
|
373
|
+
// ~1-8s of network round-trip per call.
|
|
364
374
|
const content = [];
|
|
365
|
-
// Fetch all images in parallel (picked + up to 3 alternatives)
|
|
366
|
-
const allProducts = [d.picked, ...(d.alternatives || []).slice(0, 3)];
|
|
367
|
-
const allImages = await Promise.all(allProducts.map(async (p) => {
|
|
368
|
-
if (!p?.image_url)
|
|
369
|
-
return null;
|
|
370
|
-
return fetchImageBase64(p.image_url);
|
|
371
|
-
}));
|
|
372
|
-
// Add picked image only when we actually have a product_found
|
|
373
|
-
if (d.status === 'product_found' && allImages[0]) {
|
|
374
|
-
content.push({ type: 'image', data: allImages[0].data, mimeType: allImages[0].mimeType });
|
|
375
|
-
}
|
|
376
375
|
// F1: canonical fields. F6: no message_de/message_en. F2: no fallback to message.
|
|
377
376
|
if (d.status === 'no_match') {
|
|
378
377
|
content.push({ type: 'text', text: JSON.stringify({
|
|
@@ -416,11 +415,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
416
415
|
...(d.note ? { note: d.note } : {}),
|
|
417
416
|
}, null, 2) });
|
|
418
417
|
}
|
|
419
|
-
// Add alternative images
|
|
420
|
-
allImages.slice(1).forEach((img) => {
|
|
421
|
-
if (img)
|
|
422
|
-
content.push({ type: 'image', data: img.data, mimeType: img.mimeType });
|
|
423
|
-
});
|
|
424
418
|
// Track session for follow-ups (only on product_found, not no_match)
|
|
425
419
|
if (d.status !== 'no_match' && d.picked) {
|
|
426
420
|
lastShopContext = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "byclaw-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.15",
|
|
4
4
|
"description": "MCP Server for byclaw.io — Your AI shopping advisor. Connects Claude Desktop and other MCP clients to the byclaw.io product catalog.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
39
|
+
"tsx": "^4.0.0",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
41
|
}
|
|
42
42
|
}
|