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.
- package/dist/index.js +57 -15
- 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 `` 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':
|
|
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:
|
|
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:
|
|
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: ``. 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
|
|
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: ``. 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 `` 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 ``
|
|
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(
|
|
558
|
+
console.error(`byclaw MCP server v${SERVER_VERSION} running on stdio`);
|
|
517
559
|
}
|
|
518
560
|
main().catch(console.error);
|
package/package.json
CHANGED