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.
- package/dist/index.js +34 -9
- 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: {
|
|
@@ -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 ``
|
|
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.
|
|
541
|
+
console.error('byclaw MCP server v0.4.11 running on stdio');
|
|
517
542
|
}
|
|
518
543
|
main().catch(console.error);
|
package/package.json
CHANGED