byclaw-mcp 0.4.9 → 0.4.11

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 +83 -43
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,13 +6,23 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
6
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
7
  const API_BASE = process.env.BYCLAW_API_URL || 'https://byclaw.io';
8
8
  const API_KEY = process.env.BYCLAW_API_KEY || '';
9
+ // Brand fallback when the catalog row has no image. Clients (Claude Desktop
10
+ // in particular) currently can't render MCP image content blocks, so they
11
+ // rely on `image_url` strings in the JSON to emit `![title](url)` markdown.
12
+ // A string fallback keeps the markdown valid even when an upstream feed
13
+ // row drops its image — better a brand logo than a broken image tag.
14
+ const IMAGE_FALLBACK = `${API_BASE}/icon-512.png`;
15
+ function imageUrlOf(p) {
16
+ const url = typeof p?.image_url === 'string' ? p.image_url.trim() : '';
17
+ return url.startsWith('https://') || url.startsWith('http://') ? url : IMAGE_FALLBACK;
18
+ }
9
19
  async function fetchImageBase64(url) {
10
20
  if (!url || url.includes('picsum.photos'))
11
21
  return null;
12
22
  try {
13
23
  const res = await fetch(url, {
14
24
  signal: AbortSignal.timeout(8000),
15
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; byclaw-mcp/0.4.0)' },
25
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; byclaw-mcp/0.4.11)' },
16
26
  redirect: 'follow',
17
27
  });
18
28
  if (!res.ok)
@@ -40,7 +50,7 @@ async function apiCall(path, options = {}) {
40
50
  });
41
51
  return res.json();
42
52
  }
43
- const server = new index_js_1.Server({ name: 'byclaw', version: '0.4.0' }, { capabilities: { tools: {}, prompts: {} } });
53
+ const server = new index_js_1.Server({ name: 'byclaw', version: '0.4.11' }, { capabilities: { tools: {}, prompts: {} } });
44
54
  // Prompts
45
55
  server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
46
56
  prompts: [{
@@ -51,7 +61,7 @@ server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
51
61
  server.setRequestHandler(types_js_1.GetPromptRequestSchema, async () => ({
52
62
  messages: [{
53
63
  role: 'user',
54
- 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: recommendation (title, price, merchant, review, url), alternatives (each with title, price, brand, merchant, url), compared (int), and affiliate_notice (string).\n\nWhen presenting the result:\n1. Show the recommended product with its price and the advisor\'s reasoning\n2. Link the recommendation using its `url` field (format: "<merchant> →" as a clickable link)\n3. List every alternative with its title, price, merchant and link each using its `url` field — do not show alternatives as plain text, they must be clickable links\n4. ALWAYS display the `affiliate_notice` 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.' },
64
+ 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.' },
55
65
  }],
56
66
  }));
57
67
  // ═══════════════════════════════════════
@@ -62,7 +72,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
62
72
  // ── Core: one-shot shopping ──
63
73
  {
64
74
  name: 'shop',
65
- description: 'Search and compare products across multiple shops. Returns the best recommendation with price, merchant, review, and alternatives. Use when the user wants to find, compare, or buy a product.',
75
+ description: '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.',
66
76
  inputSchema: {
67
77
  type: 'object',
68
78
  properties: {
@@ -78,7 +88,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
78
88
  // ── Optional: browse & explore ──
79
89
  {
80
90
  name: 'search_products',
81
- description: 'Browse products without triggering a recommendation. Use this only if the user explicitly wants to see a list of options.',
91
+ description: '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.',
82
92
  inputSchema: {
83
93
  type: 'object',
84
94
  properties: {
@@ -250,8 +260,12 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
250
260
  });
251
261
  if (result?.ok) {
252
262
  const d = result.data;
253
- const disclosure = d.disclosure || 'ℹ️ Affiliate-Link. byclaw earns a small commission — no extra cost to you.';
254
- const personalityName = d.agent?.personality || d.advisor || '';
263
+ // F1: canonical schema same field names across MCP shop/test_web_search/Web API:
264
+ // type: product_found | clarification_needed | no_match | gift_found | gift_no_results | acknowledgement_legacy
265
+ // picked.affiliate_url (NOT recommendation.url)
266
+ // disclosure (NOT affiliate_notice)
267
+ // message (single, in caller's language; never message_de/message_en)
268
+ const personalityName = d.agent?.personality || '';
255
269
  const advisorLabel = d.advisor_profile?.name || personalityName.charAt(0).toUpperCase() + personalityName.slice(1) || 'Advisor';
256
270
  // ── GIFT MODE ──
257
271
  if (d.status === 'gift_found' && Array.isArray(d.gift_picks) && d.gift_picks.length > 0) {
@@ -268,19 +282,21 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
268
282
  type: 'gift_found',
269
283
  advisor: advisorLabel,
270
284
  advisor_profile: d.advisor_profile || null,
285
+ message: d.message || null,
271
286
  gift_picks: d.gift_picks.map((p) => ({
272
287
  title: p.title,
273
288
  price: p.price,
274
289
  brand: p.brand || '',
275
290
  merchant: p.merchant || '',
276
- reasoning: p.reasoning || '',
277
- url: p.affiliate_url || '',
291
+ reasoning: p.reasoning || null,
292
+ affiliate_url: p.affiliate_url || '',
293
+ image_url: imageUrlOf(p),
278
294
  })),
279
295
  gift_intent: {
280
296
  recipient: intent.recipient || '',
281
297
  summary: intent.summary || '',
282
298
  },
283
- affiliate_notice: disclosure,
299
+ disclosure: d.disclosure || null,
284
300
  ...(d.note ? { note: d.note } : {}),
285
301
  }, null, 2) }] };
286
302
  }
@@ -293,7 +309,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
293
309
  message: d.message || '',
294
310
  suggested_next: d.suggested_next || [],
295
311
  context_summary: d.context_summary || null,
296
- affiliate_notice: disclosure,
312
+ disclosure: d.disclosure || null,
313
+ ...(d.note ? { note: d.note } : {}),
314
+ }, null, 2) }] };
315
+ }
316
+ // ── ACKNOWLEDGEMENT (F3.a interim) ──
317
+ if (d.status === 'acknowledgement_legacy' || d.status === 'acknowledgement') {
318
+ return { content: [{ type: 'text', text: JSON.stringify({
319
+ type: d.status,
320
+ advisor: advisorLabel,
321
+ advisor_profile: d.advisor_profile || null,
322
+ message: d.message || '',
323
+ disclosure: d.disclosure || null,
297
324
  ...(d.note ? { note: d.note } : {}),
298
325
  }, null, 2) }] };
299
326
  }
@@ -306,47 +333,49 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
306
333
  return null;
307
334
  return fetchImageBase64(p.image_url);
308
335
  }));
309
- // Add picked image
310
- if (allImages[0]) {
336
+ // Add picked image only when we actually have a product_found
337
+ if (d.status === 'product_found' && allImages[0]) {
311
338
  content.push({ type: 'image', data: allImages[0].data, mimeType: allImages[0].mimeType });
312
339
  }
313
- // Add structured data handle no_match vs product_recommendation
340
+ // F1: canonical fields. F6: no message_de/message_en. F2: no fallback to message.
314
341
  if (d.status === 'no_match') {
315
342
  content.push({ type: 'text', text: JSON.stringify({
316
343
  type: 'no_match',
317
344
  advisor: advisorLabel,
318
345
  advisor_profile: d.advisor_profile || null,
319
- message_de: 'Hab leider nichts in deinem Suchbereich gefunden. Probier es mit weniger Filtern oder anderen Begriffen.',
320
- message_en: 'No products found for your search. Try fewer filters or different terms.',
321
- compared: 0,
322
- affiliate_notice: d.disclosure || 'Affiliate disclosure: The links on this page are affiliate links. byclaw.io receives a commission from the retailer when you make a purchase. No additional cost to you.',
346
+ message: d.message || '',
347
+ compared: d.compared || 0,
348
+ disclosure: d.disclosure || null,
323
349
  ...(d.note ? { note: d.note } : {}),
324
350
  }, null, 2) });
325
351
  }
326
352
  else {
353
+ const hasPick = !!d.picked;
327
354
  content.push({ type: 'text', text: JSON.stringify({
328
- type: 'product_recommendation',
355
+ type: d.status || 'product_found',
329
356
  advisor: advisorLabel,
330
357
  advisor_profile: d.advisor_profile || null,
331
- recommendation: {
332
- title: d.picked?.title || '',
333
- price: d.picked?.price ?? '',
334
- brand: d.picked?.brand || '',
335
- merchant: d.picked?.merchant || '',
336
- url: d.picked?.affiliate_url || '',
337
- review: d.review || d.reasoning || '',
338
- },
358
+ picked: hasPick ? {
359
+ title: d.picked.title || '',
360
+ price: d.picked.price ?? null,
361
+ brand: d.picked.brand || '',
362
+ merchant: d.picked.merchant || '',
363
+ affiliate_url: d.picked.affiliate_url || '',
364
+ image_url: imageUrlOf(d.picked),
365
+ } : null,
339
366
  alternatives: (d.alternatives || []).slice(0, 3).map((a) => ({
340
367
  title: a.title,
341
368
  price: a.price,
342
369
  brand: a.brand || '',
343
370
  category: a.category || '',
344
371
  merchant: a.merchant || '',
345
- url: a.affiliate_url || '',
372
+ affiliate_url: a.affiliate_url || '',
373
+ image_url: imageUrlOf(a),
346
374
  })),
347
375
  compared: d.compared || 0,
348
- reasoning: d.reasoning || '',
349
- affiliate_notice: d.disclosure || 'Affiliate disclosure: The links on this page are affiliate links. byclaw.io receives a commission from the retailer when you make a purchase. No additional cost to you.',
376
+ reasoning: hasPick && d.reasoning ? d.reasoning : null,
377
+ review: hasPick && d.review ? d.review : null,
378
+ disclosure: d.disclosure || null,
350
379
  ...(d.note ? { note: d.note } : {}),
351
380
  }, null, 2) });
352
381
  }
@@ -384,6 +413,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
384
413
  else
385
414
  params.set('limit', '10');
386
415
  const result = await apiCall(`/api/products/search?${params}`);
416
+ // Guarantee every result row has a usable `image_url` string so
417
+ // markdown-only clients (Claude Desktop) can render `![title](url)`
418
+ // without first checking for null. The backend usually populates
419
+ // image_url already; this just backstops empty/missing rows.
420
+ if (result?.ok && result?.data) {
421
+ if (Array.isArray(result.data.products)) {
422
+ result.data.products = result.data.products.map((p) => ({ ...p, image_url: imageUrlOf(p) }));
423
+ }
424
+ else if (Array.isArray(result.data)) {
425
+ result.data = result.data.map((p) => ({ ...p, image_url: imageUrlOf(p) }));
426
+ }
427
+ }
387
428
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
388
429
  }
389
430
  case 'leave_review': {
@@ -441,16 +482,13 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
441
482
  return { content: [{ type: 'text', text: JSON.stringify({ type: 'web_search_error', error: result?.error || 'Unknown error', raw: result }, null, 2) }] };
442
483
  }
443
484
  const d = result.data;
444
- // Clarification responses have different fields than product responses
485
+ // Clarification responses
445
486
  if (d.status === 'clarification_needed') {
446
487
  return { content: [{ type: 'text', text: JSON.stringify({
447
488
  type: 'clarification_needed',
448
- endpoint: '/api/agents/shop',
449
489
  advisor: d.advisor_profile?.name || d.agent?.personality || null,
450
490
  advisor_profile: d.advisor_profile || null,
451
491
  message: d.message || null,
452
- message_de: d.message_de || null,
453
- message_en: d.message_en || null,
454
492
  suggested_next: d.suggested_next || [],
455
493
  context_summary: d.context_summary || null,
456
494
  disclosure: d.disclosure || null,
@@ -461,27 +499,29 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
461
499
  if (d.status === 'gift_found' && d.gift_picks) {
462
500
  return { content: [{ type: 'text', text: JSON.stringify({
463
501
  type: 'gift_found',
464
- endpoint: '/api/agents/shop',
465
502
  advisor: d.advisor_profile?.name || d.agent?.personality || null,
466
503
  advisor_profile: d.advisor_profile || null,
467
504
  message: d.message || null,
468
- gift_picks: d.gift_picks.map((g) => ({ title: g.title, price: g.price, brand: g.brand, reasoning: g.reasoning || null })),
505
+ gift_picks: d.gift_picks.map((g) => ({ title: g.title, price: g.price, brand: g.brand, reasoning: g.reasoning || null, image_url: imageUrlOf(g) })),
469
506
  gift_intent: d.gift_intent || null,
470
507
  disclosure: d.disclosure || null,
471
508
  note: d.note || null,
472
509
  }, null, 2) }] };
473
510
  }
511
+ // product_found / no_match / fallback
512
+ // F2: review and reasoning only when picked exists AND fields are present
513
+ // (no fallback to d.message — that produced bit-identical strings on no_match)
514
+ const hasPick = !!d.picked;
474
515
  return { content: [{ type: 'text', text: JSON.stringify({
475
- type: d.status || 'web_search_result',
476
- endpoint: '/api/agents/shop',
516
+ type: d.status || 'no_match',
477
517
  advisor: d.advisor_profile?.name || d.agent?.personality || null,
478
518
  advisor_profile: d.advisor_profile || null,
479
519
  message: d.message || null,
480
- picked: d.picked ? { title: d.picked.title, price: d.picked.price, brand: d.picked.brand, affiliate_url: d.picked.affiliate_url } : null,
481
- alternatives: (d.alternatives || []).map((a) => ({ title: a.title, price: a.price, brand: a.brand })),
520
+ picked: hasPick ? { title: d.picked.title, price: d.picked.price, brand: d.picked.brand, affiliate_url: d.picked.affiliate_url, image_url: imageUrlOf(d.picked) } : null,
521
+ alternatives: (d.alternatives || []).map((a) => ({ title: a.title, price: a.price, brand: a.brand, image_url: imageUrlOf(a) })),
482
522
  compared: d.compared || 0,
483
- review: d.review || d.message || null,
484
- reasoning: d.reasoning || d.message || null,
523
+ review: hasPick && d.review ? d.review : null,
524
+ reasoning: hasPick && d.reasoning ? d.reasoning : null,
485
525
  disclosure: d.disclosure || null,
486
526
  note: d.note || null,
487
527
  }, null, 2) }] };
@@ -498,6 +538,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
498
538
  async function main() {
499
539
  const transport = new stdio_js_1.StdioServerTransport();
500
540
  await server.connect(transport);
501
- console.error('byclaw MCP server v0.4.0 running on stdio');
541
+ console.error('byclaw MCP server v0.4.11 running on stdio');
502
542
  }
503
543
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "byclaw-mcp",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
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": {