byclaw-mcp 0.4.10 → 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 +34 -9
  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.10' }, { 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: {
@@ -280,6 +290,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
280
290
  merchant: p.merchant || '',
281
291
  reasoning: p.reasoning || null,
282
292
  affiliate_url: p.affiliate_url || '',
293
+ image_url: imageUrlOf(p),
283
294
  })),
284
295
  gift_intent: {
285
296
  recipient: intent.recipient || '',
@@ -350,6 +361,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
350
361
  brand: d.picked.brand || '',
351
362
  merchant: d.picked.merchant || '',
352
363
  affiliate_url: d.picked.affiliate_url || '',
364
+ image_url: imageUrlOf(d.picked),
353
365
  } : null,
354
366
  alternatives: (d.alternatives || []).slice(0, 3).map((a) => ({
355
367
  title: a.title,
@@ -358,6 +370,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
358
370
  category: a.category || '',
359
371
  merchant: a.merchant || '',
360
372
  affiliate_url: a.affiliate_url || '',
373
+ image_url: imageUrlOf(a),
361
374
  })),
362
375
  compared: d.compared || 0,
363
376
  reasoning: hasPick && d.reasoning ? d.reasoning : null,
@@ -400,6 +413,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
400
413
  else
401
414
  params.set('limit', '10');
402
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
+ }
403
428
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
404
429
  }
405
430
  case 'leave_review': {
@@ -477,7 +502,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
477
502
  advisor: d.advisor_profile?.name || d.agent?.personality || null,
478
503
  advisor_profile: d.advisor_profile || null,
479
504
  message: d.message || null,
480
- 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) })),
481
506
  gift_intent: d.gift_intent || null,
482
507
  disclosure: d.disclosure || null,
483
508
  note: d.note || null,
@@ -492,8 +517,8 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
492
517
  advisor: d.advisor_profile?.name || d.agent?.personality || null,
493
518
  advisor_profile: d.advisor_profile || null,
494
519
  message: d.message || null,
495
- picked: hasPick ? { title: d.picked.title, price: d.picked.price, brand: d.picked.brand, affiliate_url: d.picked.affiliate_url } : null,
496
- 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) })),
497
522
  compared: d.compared || 0,
498
523
  review: hasPick && d.review ? d.review : null,
499
524
  reasoning: hasPick && d.reasoning ? d.reasoning : null,
@@ -513,6 +538,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
513
538
  async function main() {
514
539
  const transport = new stdio_js_1.StdioServerTransport();
515
540
  await server.connect(transport);
516
- console.error('byclaw MCP server v0.4.0 running on stdio');
541
+ console.error('byclaw MCP server v0.4.11 running on stdio');
517
542
  }
518
543
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "byclaw-mcp",
3
- "version": "0.4.10",
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": {