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.
- package/dist/index.js +83 -43
- 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 `` 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.
|
|
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.
|
|
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:
|
|
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: ``. 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
|
|
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: ``. 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 `` 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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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: '
|
|
355
|
+
type: d.status || 'product_found',
|
|
329
356
|
advisor: advisorLabel,
|
|
330
357
|
advisor_profile: d.advisor_profile || null,
|
|
331
|
-
|
|
332
|
-
title: d.picked
|
|
333
|
-
price: d.picked
|
|
334
|
-
brand: d.picked
|
|
335
|
-
merchant: d.picked
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
372
|
+
affiliate_url: a.affiliate_url || '',
|
|
373
|
+
image_url: imageUrlOf(a),
|
|
346
374
|
})),
|
|
347
375
|
compared: d.compared || 0,
|
|
348
|
-
reasoning: d.reasoning
|
|
349
|
-
|
|
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 ``
|
|
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
|
|
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 || '
|
|
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:
|
|
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
|
|
484
|
-
reasoning: d.reasoning
|
|
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.
|
|
541
|
+
console.error('byclaw MCP server v0.4.11 running on stdio');
|
|
502
542
|
}
|
|
503
543
|
main().catch(console.error);
|
package/package.json
CHANGED