byclaw-mcp 0.4.10 → 0.4.12

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 +57 -15
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,15 +4,42 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
5
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
6
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ // Single source of truth for the version string. Used in serverInfo,
10
+ // startup banner, User-Agent, and as a [byclaw-mcp vX.Y.Z] prefix on
11
+ // every tool description so MCP clients (and the model running inside
12
+ // them) can see at a glance which version they're talking to without
13
+ // peeking at the MCP-internal serverInfo handshake metadata.
14
+ const SERVER_VERSION = (() => {
15
+ try {
16
+ const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf8'));
17
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
18
+ }
19
+ catch {
20
+ return '0.0.0';
21
+ }
22
+ })();
23
+ const TOOL_PREFIX = `[byclaw-mcp v${SERVER_VERSION}] `;
7
24
  const API_BASE = process.env.BYCLAW_API_URL || 'https://byclaw.io';
8
25
  const API_KEY = process.env.BYCLAW_API_KEY || '';
26
+ // Brand fallback when the catalog row has no image. Clients (Claude Desktop
27
+ // in particular) currently can't render MCP image content blocks, so they
28
+ // rely on `image_url` strings in the JSON to emit `![title](url)` markdown.
29
+ // A string fallback keeps the markdown valid even when an upstream feed
30
+ // row drops its image — better a brand logo than a broken image tag.
31
+ const IMAGE_FALLBACK = `${API_BASE}/icon-512.png`;
32
+ function imageUrlOf(p) {
33
+ const url = typeof p?.image_url === 'string' ? p.image_url.trim() : '';
34
+ return url.startsWith('https://') || url.startsWith('http://') ? url : IMAGE_FALLBACK;
35
+ }
9
36
  async function fetchImageBase64(url) {
10
37
  if (!url || url.includes('picsum.photos'))
11
38
  return null;
12
39
  try {
13
40
  const res = await fetch(url, {
14
41
  signal: AbortSignal.timeout(8000),
15
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; byclaw-mcp/0.4.0)' },
42
+ headers: { 'User-Agent': `Mozilla/5.0 (compatible; byclaw-mcp/${SERVER_VERSION})` },
16
43
  redirect: 'follow',
17
44
  });
18
45
  if (!res.ok)
@@ -40,7 +67,7 @@ async function apiCall(path, options = {}) {
40
67
  });
41
68
  return res.json();
42
69
  }
43
- const server = new index_js_1.Server({ name: 'byclaw', version: '0.4.10' }, { capabilities: { tools: {}, prompts: {} } });
70
+ const server = new index_js_1.Server({ name: 'byclaw', version: SERVER_VERSION }, { capabilities: { tools: {}, prompts: {} } });
44
71
  // Prompts
45
72
  server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
46
73
  prompts: [{
@@ -51,7 +78,7 @@ server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
51
78
  server.setRequestHandler(types_js_1.GetPromptRequestSchema, async () => ({
52
79
  messages: [{
53
80
  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.' },
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.' },
55
82
  }],
56
83
  }));
57
84
  // ═══════════════════════════════════════
@@ -62,7 +89,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
62
89
  // ── Core: one-shot shopping ──
63
90
  {
64
91
  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.',
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.',
66
93
  inputSchema: {
67
94
  type: 'object',
68
95
  properties: {
@@ -78,7 +105,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
78
105
  // ── Optional: browse & explore ──
79
106
  {
80
107
  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.',
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.',
82
109
  inputSchema: {
83
110
  type: 'object',
84
111
  properties: {
@@ -93,7 +120,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
93
120
  },
94
121
  {
95
122
  name: 'leave_review',
96
- description: 'Write a product review. Only call if the user explicitly asks to leave a review. Only de and en allowed.',
123
+ description: TOOL_PREFIX + 'Write a product review. Only call if the user explicitly asks to leave a review. Only de and en allowed.',
97
124
  inputSchema: {
98
125
  type: 'object',
99
126
  properties: {
@@ -109,17 +136,17 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
109
136
  // ── Discovery ──
110
137
  {
111
138
  name: 'list_categories',
112
- description: 'Browse all available product categories.',
139
+ description: TOOL_PREFIX + 'Browse all available product categories.',
113
140
  inputSchema: { type: 'object', properties: {} },
114
141
  },
115
142
  {
116
143
  name: 'get_trending',
117
- description: 'Get trending products that agents are buying right now.',
144
+ description: TOOL_PREFIX + 'Get trending products that agents are buying right now.',
118
145
  inputSchema: { type: 'object', properties: {} },
119
146
  },
120
147
  {
121
148
  name: 'get_product_reviews',
122
- description: 'Get agent reviews for a product.',
149
+ description: TOOL_PREFIX + 'Get agent reviews for a product.',
123
150
  inputSchema: {
124
151
  type: 'object',
125
152
  properties: {
@@ -129,13 +156,13 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
129
156
  },
130
157
  {
131
158
  name: 'get_platform_stats',
132
- description: 'Get platform statistics: products compared, reviews written, recommendations sent.',
159
+ description: TOOL_PREFIX + 'Get platform statistics: products compared, reviews written, recommendations sent.',
133
160
  inputSchema: { type: 'object', properties: {} },
134
161
  },
135
162
  // ── QA / Testing ──
136
163
  {
137
164
  name: 'test_web_search',
138
- description: 'Test the web agent search endpoint (same as dashboard search). Use for QA to verify web search behavior vs MCP shop behavior.',
165
+ description: TOOL_PREFIX + 'Test the web agent search endpoint (same as dashboard search). Use for QA to verify web search behavior vs MCP shop behavior.',
139
166
  inputSchema: {
140
167
  type: 'object',
141
168
  properties: {
@@ -280,6 +307,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
280
307
  merchant: p.merchant || '',
281
308
  reasoning: p.reasoning || null,
282
309
  affiliate_url: p.affiliate_url || '',
310
+ image_url: imageUrlOf(p),
283
311
  })),
284
312
  gift_intent: {
285
313
  recipient: intent.recipient || '',
@@ -350,6 +378,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
350
378
  brand: d.picked.brand || '',
351
379
  merchant: d.picked.merchant || '',
352
380
  affiliate_url: d.picked.affiliate_url || '',
381
+ image_url: imageUrlOf(d.picked),
353
382
  } : null,
354
383
  alternatives: (d.alternatives || []).slice(0, 3).map((a) => ({
355
384
  title: a.title,
@@ -358,6 +387,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
358
387
  category: a.category || '',
359
388
  merchant: a.merchant || '',
360
389
  affiliate_url: a.affiliate_url || '',
390
+ image_url: imageUrlOf(a),
361
391
  })),
362
392
  compared: d.compared || 0,
363
393
  reasoning: hasPick && d.reasoning ? d.reasoning : null,
@@ -400,6 +430,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
400
430
  else
401
431
  params.set('limit', '10');
402
432
  const result = await apiCall(`/api/products/search?${params}`);
433
+ // Guarantee every result row has a usable `image_url` string so
434
+ // markdown-only clients (Claude Desktop) can render `![title](url)`
435
+ // without first checking for null. The backend usually populates
436
+ // image_url already; this just backstops empty/missing rows.
437
+ if (result?.ok && result?.data) {
438
+ if (Array.isArray(result.data.products)) {
439
+ result.data.products = result.data.products.map((p) => ({ ...p, image_url: imageUrlOf(p) }));
440
+ }
441
+ else if (Array.isArray(result.data)) {
442
+ result.data = result.data.map((p) => ({ ...p, image_url: imageUrlOf(p) }));
443
+ }
444
+ }
403
445
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
404
446
  }
405
447
  case 'leave_review': {
@@ -477,7 +519,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
477
519
  advisor: d.advisor_profile?.name || d.agent?.personality || null,
478
520
  advisor_profile: d.advisor_profile || null,
479
521
  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 })),
522
+ 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
523
  gift_intent: d.gift_intent || null,
482
524
  disclosure: d.disclosure || null,
483
525
  note: d.note || null,
@@ -492,8 +534,8 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
492
534
  advisor: d.advisor_profile?.name || d.agent?.personality || null,
493
535
  advisor_profile: d.advisor_profile || null,
494
536
  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 })),
537
+ 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,
538
+ alternatives: (d.alternatives || []).map((a) => ({ title: a.title, price: a.price, brand: a.brand, image_url: imageUrlOf(a) })),
497
539
  compared: d.compared || 0,
498
540
  review: hasPick && d.review ? d.review : null,
499
541
  reasoning: hasPick && d.reasoning ? d.reasoning : null,
@@ -513,6 +555,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
513
555
  async function main() {
514
556
  const transport = new stdio_js_1.StdioServerTransport();
515
557
  await server.connect(transport);
516
- console.error('byclaw MCP server v0.4.0 running on stdio');
558
+ console.error(`byclaw MCP server v${SERVER_VERSION} running on stdio`);
517
559
  }
518
560
  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.12",
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": {