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.
Files changed (2) hide show
  1. package/dist/index.js +32 -43
  2. 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 `![title](image_url)`
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 `![title](image_url)` 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` (each with title, price, brand, merchant, affiliate_url, image_url), `compared` (int), `reasoning` (string), `review` (string), and `disclosure` (string).\n\nWhen presenting the result:\n1. Render the picked product image inline using markdown image syntax: `![title](image_url)`. Do the same for every alternative users need to SEE what they\'re being recommended.\n2. Show the recommended product with its price and the advisor\'s reasoning\n3. Link the recommendation using its `affiliate_url` field (format: "<merchant> →" as a clickable link)\n4. List every alternative with its title, price, merchant and link each using its `affiliate_url` field — do not show alternatives as plain text, they must be clickable links and visible images\n5. ALWAYS display the `disclosure` text verbatim at the end, as a small italic note — this is legally required\n\nThe server also emits native MCP image content blocks alongside the JSON for clients that render them natively (Claude Code). Markdown-only clients (Claude Desktop today) should render via `image_url`. If your client supports both, prefer markdown so the order matches the textual context.\n\nbyclaw.io is operated by NJUDEV S.L., a European company.' },
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 `![title](image_url)` 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). Render every product image inline using markdown image syntax: `![title](image_url)`. The server also emits native MCP image content blocks for clients that render them natively use whichever your client supports, prefer markdown when in doubt.',
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 `![title](image_url)` 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) render it inline with markdown `![title](image_url)` so users see what each product looks like.',
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 `![title](image_url)` 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.12",
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
- "typescript": "^5.0.0",
40
- "tsx": "^4.0.0"
39
+ "tsx": "^4.0.0",
40
+ "typescript": "^5.0.0"
41
41
  }
42
42
  }