byclaw-mcp 0.4.12 → 0.4.14
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 +32 -43
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -21,6 +21,26 @@ const SERVER_VERSION = (() => {
|
|
|
21
21
|
}
|
|
22
22
|
})();
|
|
23
23
|
const TOOL_PREFIX = `[byclaw-mcp v${SERVER_VERSION}] `;
|
|
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.';
|
|
24
44
|
const API_BASE = process.env.BYCLAW_API_URL || 'https://byclaw.io';
|
|
25
45
|
const API_KEY = process.env.BYCLAW_API_KEY || '';
|
|
26
46
|
// Brand fallback when the catalog row has no image. Clients (Claude Desktop
|
|
@@ -33,29 +53,6 @@ function imageUrlOf(p) {
|
|
|
33
53
|
const url = typeof p?.image_url === 'string' ? p.image_url.trim() : '';
|
|
34
54
|
return url.startsWith('https://') || url.startsWith('http://') ? url : IMAGE_FALLBACK;
|
|
35
55
|
}
|
|
36
|
-
async function fetchImageBase64(url) {
|
|
37
|
-
if (!url || url.includes('picsum.photos'))
|
|
38
|
-
return null;
|
|
39
|
-
try {
|
|
40
|
-
const res = await fetch(url, {
|
|
41
|
-
signal: AbortSignal.timeout(8000),
|
|
42
|
-
headers: { 'User-Agent': `Mozilla/5.0 (compatible; byclaw-mcp/${SERVER_VERSION})` },
|
|
43
|
-
redirect: 'follow',
|
|
44
|
-
});
|
|
45
|
-
if (!res.ok)
|
|
46
|
-
return null;
|
|
47
|
-
const contentType = res.headers.get('content-type') || 'image/jpeg';
|
|
48
|
-
if (!contentType.startsWith('image/'))
|
|
49
|
-
return null;
|
|
50
|
-
const buffer = await res.arrayBuffer();
|
|
51
|
-
if (buffer.byteLength < 100 || buffer.byteLength > 500000)
|
|
52
|
-
return null; // skip tiny errors or huge files
|
|
53
|
-
return { data: Buffer.from(buffer).toString('base64'), mimeType: contentType };
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
56
|
async function apiCall(path, options = {}) {
|
|
60
57
|
const headers = {
|
|
61
58
|
'Authorization': `Bearer ${API_KEY}`,
|
|
@@ -78,7 +75,7 @@ server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
|
|
|
78
75
|
server.setRequestHandler(types_js_1.GetPromptRequestSchema, async () => ({
|
|
79
76
|
messages: [{
|
|
80
77
|
role: 'user',
|
|
81
|
-
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` (
|
|
78
|
+
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.' },
|
|
82
79
|
}],
|
|
83
80
|
}));
|
|
84
81
|
// ═══════════════════════════════════════
|
|
@@ -89,7 +86,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
|
89
86
|
// ── Core: one-shot shopping ──
|
|
90
87
|
{
|
|
91
88
|
name: 'shop',
|
|
92
|
-
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).
|
|
89
|
+
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.',
|
|
93
90
|
inputSchema: {
|
|
94
91
|
type: 'object',
|
|
95
92
|
properties: {
|
|
@@ -105,7 +102,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
|
105
102
|
// ── Optional: browse & explore ──
|
|
106
103
|
{
|
|
107
104
|
name: 'search_products',
|
|
108
|
-
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)
|
|
105
|
+
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`.',
|
|
109
106
|
inputSchema: {
|
|
110
107
|
type: 'object',
|
|
111
108
|
properties: {
|
|
@@ -296,6 +293,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
296
293
|
alternatives: d.gift_picks.slice(1).map((p) => p.title),
|
|
297
294
|
};
|
|
298
295
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
296
|
+
_render_hint: RENDER_HINT,
|
|
299
297
|
type: 'gift_found',
|
|
300
298
|
advisor: advisorLabel,
|
|
301
299
|
advisor_profile: d.advisor_profile || null,
|
|
@@ -342,18 +340,11 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
342
340
|
}, null, 2) }] };
|
|
343
341
|
}
|
|
344
342
|
// ── NORMAL / FOLLOW-UP MODE ──
|
|
343
|
+
// No more server-side image prefetch — `image_url` is a plain
|
|
344
|
+
// string in each product object, the client (or the model in
|
|
345
|
+
// claude.ai-style clients) decides what to do with it. Cuts
|
|
346
|
+
// ~1-8s of network round-trip per call.
|
|
345
347
|
const content = [];
|
|
346
|
-
// Fetch all images in parallel (picked + up to 3 alternatives)
|
|
347
|
-
const allProducts = [d.picked, ...(d.alternatives || []).slice(0, 3)];
|
|
348
|
-
const allImages = await Promise.all(allProducts.map(async (p) => {
|
|
349
|
-
if (!p?.image_url)
|
|
350
|
-
return null;
|
|
351
|
-
return fetchImageBase64(p.image_url);
|
|
352
|
-
}));
|
|
353
|
-
// Add picked image only when we actually have a product_found
|
|
354
|
-
if (d.status === 'product_found' && allImages[0]) {
|
|
355
|
-
content.push({ type: 'image', data: allImages[0].data, mimeType: allImages[0].mimeType });
|
|
356
|
-
}
|
|
357
348
|
// F1: canonical fields. F6: no message_de/message_en. F2: no fallback to message.
|
|
358
349
|
if (d.status === 'no_match') {
|
|
359
350
|
content.push({ type: 'text', text: JSON.stringify({
|
|
@@ -369,6 +360,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
369
360
|
else {
|
|
370
361
|
const hasPick = !!d.picked;
|
|
371
362
|
content.push({ type: 'text', text: JSON.stringify({
|
|
363
|
+
_render_hint: RENDER_HINT,
|
|
372
364
|
type: d.status || 'product_found',
|
|
373
365
|
advisor: advisorLabel,
|
|
374
366
|
advisor_profile: d.advisor_profile || null,
|
|
@@ -396,11 +388,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
396
388
|
...(d.note ? { note: d.note } : {}),
|
|
397
389
|
}, null, 2) });
|
|
398
390
|
}
|
|
399
|
-
// Add alternative images
|
|
400
|
-
allImages.slice(1).forEach((img) => {
|
|
401
|
-
if (img)
|
|
402
|
-
content.push({ type: 'image', data: img.data, mimeType: img.mimeType });
|
|
403
|
-
});
|
|
404
391
|
// Track session for follow-ups (only on product_found, not no_match)
|
|
405
392
|
if (d.status !== 'no_match' && d.picked) {
|
|
406
393
|
lastShopContext = {
|
|
@@ -442,7 +429,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
442
429
|
result.data = result.data.map((p) => ({ ...p, image_url: imageUrlOf(p) }));
|
|
443
430
|
}
|
|
444
431
|
}
|
|
445
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
432
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _render_hint: RENDER_HINT, ...result }, null, 2) }] };
|
|
446
433
|
}
|
|
447
434
|
case 'leave_review': {
|
|
448
435
|
const lang = args?.language;
|
|
@@ -515,6 +502,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
515
502
|
// Gift responses
|
|
516
503
|
if (d.status === 'gift_found' && d.gift_picks) {
|
|
517
504
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
505
|
+
_render_hint: RENDER_HINT,
|
|
518
506
|
type: 'gift_found',
|
|
519
507
|
advisor: d.advisor_profile?.name || d.agent?.personality || null,
|
|
520
508
|
advisor_profile: d.advisor_profile || null,
|
|
@@ -530,6 +518,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
530
518
|
// (no fallback to d.message — that produced bit-identical strings on no_match)
|
|
531
519
|
const hasPick = !!d.picked;
|
|
532
520
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
521
|
+
_render_hint: RENDER_HINT,
|
|
533
522
|
type: d.status || 'no_match',
|
|
534
523
|
advisor: d.advisor_profile?.name || d.agent?.personality || null,
|
|
535
524
|
advisor_profile: d.advisor_profile || null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "byclaw-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
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
|
}
|