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.
Files changed (2) hide show
  1. package/dist/index.js +59 -65
  2. 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
- // Different MCP clients render product images very differently:
25
- // - Claude Code / Cursor: render `![title](image_url)` markdown inline,
26
- // and also render native MCP image content blocks → markdown wins by
27
- // keeping ordering aligned with text.
28
- // - claude.ai web + Claude Desktop App: render NEITHER external markdown
29
- // images NOR MCP image content blocks from tool results in the user UI.
30
- // Inline images only show when wrapped in an HTML artifact (the model
31
- // calls a `visualize`/`show_widget`-style tool, e.g.
32
- // `mcp__visualize__show_widget`, with `<img src="...">` inside).
33
- // We surface this hint in every product_found tool result and in the tool
34
- // descriptions so the model can pick the right rendering for its client
35
- // without us having to detect the client server-side.
36
- const RENDER_HINT = 'If your client renders external images inline (Claude Code, Cursor, etc.), ' +
37
- 'emit `![title](image_url)` markdown for each product. ' +
38
- 'If your client does NOT render external images in chat (claude.ai web, ' +
39
- 'Claude Desktop App), wrap the picked product and every alternative in an ' +
40
- 'HTML artifact via the `visualize`/`show_widget` tool with `<img src="image_url">` ' +
41
- 'tags, so the user actually sees the products. The affiliate_url is the buy link.';
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.';
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
- async function fetchImageBase64(url) {
55
- if (!url || url.includes('picsum.photos'))
56
- return null;
57
- try {
58
- const res = await fetch(url, {
59
- signal: AbortSignal.timeout(8000),
60
- headers: { 'User-Agent': `Mozilla/5.0 (compatible; byclaw-mcp/${SERVER_VERSION})` },
61
- redirect: 'follow',
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
- const res = await fetch(`${API_BASE}${path}`, {
83
- ...options,
84
- headers: { ...headers, ...options.headers },
85
- });
86
- return res.json();
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` (each with title, price, brand, merchant, affiliate_url, image_url), `compared` (int), `reasoning` (string), `review` (string), `disclosure` (string), and `_render_hint` (string).\n\n**Image rendering** — different clients need different approaches and the response\'s `_render_hint` field tells you which one to use:\n • If running inside **claude.ai web or Claude Desktop App**, the chat UI does NOT render external markdown images or MCP image content blocks. You MUST surface the products via an HTML artifact call the `visualize` / `show_widget`-style tool and emit `<img src="image_url">` tags inside the widget so the user actually sees the products.\n • If running inside **Claude Code, Cursor, or another MCP client with inline image support**, emit `![title](image_url)` markdown for each product directly in your reply.\n\nWhen presenting the result:\n1. Follow `_render_hint` for images (HTML widget on claude.ai/Desktop, markdown elsewhere).\n2. Show the recommended product with its price and the advisor\'s reasoning.\n3. Link the recommendation using its `affiliate_url` field as a clickable link (label: "<merchant> ").\n4. List every alternative with its title, price, merchant and link each using its `affiliate_url` field never as plain text.\n5. 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.' },
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 `![title](image_url)` and one buy line as a clickable `[<merchant> →](affiliate_url)`. Keep replies tightimage 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). The response also includes a `_render_hint` field that tells you exactly how to display images for your current client: claude.ai / Claude Desktop need an HTML artifact via the `visualize`/`show_widget` tool with `<img>` tags; Claude Code / Cursor / other MCP clients with image support can use markdown `![title](image_url)` directly. ALWAYS follow that `_render_hint` so the user actually sees the products. The server additionally emits native MCP image content blocks for clients that render them natively.',
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 `![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.',
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). The response includes a `_render_hint` field follow it to pick markdown vs HTML-widget rendering for the current client (claude.ai/Desktop need an HTML widget; Claude Code/Cursor can use markdown `![title](image_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 `![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`.',
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.13",
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
- "typescript": "^5.0.0",
40
- "tsx": "^4.0.0"
39
+ "tsx": "^4.0.0",
40
+ "typescript": "^5.0.0"
41
41
  }
42
42
  }