byclaw-mcp 0.4.6 → 0.4.8
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 +160 -10
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({
|
|
|
51
51
|
server.setRequestHandler(types_js_1.GetPromptRequestSchema, async () => ({
|
|
52
52
|
messages: [{
|
|
53
53
|
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
|
|
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.' },
|
|
55
55
|
}],
|
|
56
56
|
}));
|
|
57
57
|
// ═══════════════════════════════════════
|
|
@@ -153,16 +153,95 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
|
153
153
|
// TOOL HANDLERS
|
|
154
154
|
// ═══════════════════════════════════════
|
|
155
155
|
let reviewLeftThisSession = false;
|
|
156
|
+
// ═══ Session state for follow-up queries ═══
|
|
157
|
+
// originalQuery = the first real product search (category anchor, never overwritten by modifiers)
|
|
158
|
+
// query = the effective query sent last (may be a reconstructed follow-up)
|
|
159
|
+
let lastShopContext = null;
|
|
160
|
+
// Patterns that indicate a follow-up to the previous search (no standalone product intent)
|
|
161
|
+
const FOLLOWUP_PATTERNS = [
|
|
162
|
+
// DE
|
|
163
|
+
/^(hast du |gibt es |gibt's |zeig mir |zeig |noch |was anderes|was billigeres|was teureres)/i,
|
|
164
|
+
/^(günstiger|teurer|billiger|andere|alternative|statt dessen|weiteres|mehr davon)/i,
|
|
165
|
+
/^(das erste|den ersten|die erste|das zweite|das letzte|das nehme ich|das nehm ich)/i,
|
|
166
|
+
/^(in größe|in farbe|in schwarz|in weiß|in blau|in rot|in grün)/i,
|
|
167
|
+
/^(nochmal|nochmals|lieber|besser|schlechter|kleiner|größer)/i,
|
|
168
|
+
// EN
|
|
169
|
+
/^(show me|anything else|something else|something cheaper|something better)/i,
|
|
170
|
+
/^(instead|cheaper|more expensive|different|other options|alternatives)/i,
|
|
171
|
+
/^(the first|the second|the last|i'll take|in size|in color|in black|in white)/i,
|
|
172
|
+
/^(do you have|is there|can you|what about|how about|any other)/i,
|
|
173
|
+
// Short contextual (only match if very short — <30 chars)
|
|
174
|
+
];
|
|
175
|
+
// Patterns that should go to the clarification flow, NOT follow-up reconstruction
|
|
176
|
+
const CLARIFICATION_PATTERNS = [
|
|
177
|
+
/^(hi|hallo|hey|hello|moin|servus|grüß)/i,
|
|
178
|
+
/^(hilf|help|ich weiß nicht|i don't know|bin unsicher|not sure)/i,
|
|
179
|
+
/^(erzähl|tell me more|was meinst|what do you mean|ich verstehe)/i,
|
|
180
|
+
/^(was |irgendwas|irgendwelche|something|anything)$/i,
|
|
181
|
+
/^(danke|thanks|ok|okay|ja|nein|yes|no)$/i,
|
|
182
|
+
];
|
|
183
|
+
function isFollowUp(q) {
|
|
184
|
+
if (!lastShopContext)
|
|
185
|
+
return false;
|
|
186
|
+
const trimmed = q.trim();
|
|
187
|
+
// Clarification queries should NOT be reconstructed as follow-ups
|
|
188
|
+
if (CLARIFICATION_PATTERNS.some(p => p.test(trimmed)))
|
|
189
|
+
return false;
|
|
190
|
+
// Very short queries (<30 chars) without a product noun are likely follow-ups
|
|
191
|
+
if (trimmed.length < 30 && !/\b(kopfhörer|headphones|schuhe|shoes|tasche|bag|uhr|watch|laptop|phone|tablet|kamera|camera|rucksack|jacke|jacket)\b/i.test(trimmed)) {
|
|
192
|
+
if (FOLLOWUP_PATTERNS.some(p => p.test(trimmed)))
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
// Longer queries: only match if they start with a clear follow-up pattern
|
|
196
|
+
if (trimmed.length <= 80 && FOLLOWUP_PATTERNS.some(p => p.test(trimmed)))
|
|
197
|
+
return true;
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
function buildFollowUpQuery(q) {
|
|
201
|
+
if (!lastShopContext)
|
|
202
|
+
return q;
|
|
203
|
+
const trimmed = q.trim().toLowerCase();
|
|
204
|
+
// "das erste nehmen" / "the first one" → return the picked product directly
|
|
205
|
+
if (/das erste|den ersten|die erste|the first|i'll take|das nehm/i.test(trimmed)) {
|
|
206
|
+
return lastShopContext.picked;
|
|
207
|
+
}
|
|
208
|
+
// "in Größe 42" / "in size 42" → can't fulfill (no size data), let API handle
|
|
209
|
+
if (/in größe|in size|in farbe|in color/i.test(trimmed)) {
|
|
210
|
+
return `${lastShopContext.picked} ${q}`;
|
|
211
|
+
}
|
|
212
|
+
// Price modifiers → use ORIGINAL category anchor + price constraint
|
|
213
|
+
const anchor = lastShopContext.originalQuery;
|
|
214
|
+
if (/günstiger|billiger|cheaper|was billigeres|noch günstiger/i.test(trimmed)) {
|
|
215
|
+
return `${anchor} unter ${Math.floor(lastShopContext.price * 0.8)}€`;
|
|
216
|
+
}
|
|
217
|
+
if (/teurer|more expensive|was teureres/i.test(trimmed)) {
|
|
218
|
+
return `${anchor} über ${Math.ceil(lastShopContext.price * 1.3)}€`;
|
|
219
|
+
}
|
|
220
|
+
// Generic "something else" / "andere" → use ORIGINAL category anchor
|
|
221
|
+
return `${anchor} — ${q}`;
|
|
222
|
+
}
|
|
156
223
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
157
224
|
const { name, arguments: args } = request.params;
|
|
158
225
|
try {
|
|
159
226
|
switch (name) {
|
|
160
227
|
case 'shop': {
|
|
228
|
+
// Follow-up detection: reconstruct query with context from last search
|
|
229
|
+
let effectiveQuery = String(args?.query || '');
|
|
230
|
+
const wasFollowUp = isFollowUp(effectiveQuery);
|
|
231
|
+
if (wasFollowUp) {
|
|
232
|
+
effectiveQuery = buildFollowUpQuery(effectiveQuery);
|
|
233
|
+
console.error(`[mcp] Follow-up detected: "${args?.query}" → "${effectiveQuery}"`);
|
|
234
|
+
}
|
|
161
235
|
const result = await apiCall('/api/mcp/shop', {
|
|
162
236
|
method: 'POST',
|
|
163
237
|
body: JSON.stringify({
|
|
164
|
-
query:
|
|
238
|
+
query: effectiveQuery,
|
|
165
239
|
max_price: args?.max_price,
|
|
240
|
+
previous_context: lastShopContext ? {
|
|
241
|
+
original_query: lastShopContext.originalQuery,
|
|
242
|
+
picked: lastShopContext.picked,
|
|
243
|
+
price: lastShopContext.price,
|
|
244
|
+
} : undefined,
|
|
166
245
|
category: args?.category,
|
|
167
246
|
brand: args?.brand,
|
|
168
247
|
language: args?.language,
|
|
@@ -177,16 +256,45 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
177
256
|
// ── GIFT MODE ──
|
|
178
257
|
if (d.status === 'gift_found' && Array.isArray(d.gift_picks) && d.gift_picks.length > 0) {
|
|
179
258
|
const intent = d.gift_intent || {};
|
|
259
|
+
// Track session for follow-ups (keep originalQuery stable across chains)
|
|
260
|
+
lastShopContext = {
|
|
261
|
+
originalQuery: wasFollowUp ? (lastShopContext?.originalQuery || String(args?.query || '')) : String(args?.query || ''),
|
|
262
|
+
query: effectiveQuery,
|
|
263
|
+
picked: d.gift_picks[0]?.title || '',
|
|
264
|
+
price: d.gift_picks[0]?.price || 0,
|
|
265
|
+
alternatives: d.gift_picks.slice(1).map((p) => p.title),
|
|
266
|
+
};
|
|
180
267
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
181
|
-
type: '
|
|
182
|
-
|
|
183
|
-
|
|
268
|
+
type: 'gift_found',
|
|
269
|
+
advisor: advisorLabel,
|
|
270
|
+
advisor_profile: d.advisor_profile || null,
|
|
271
|
+
gift_picks: d.gift_picks.map((p) => ({
|
|
184
272
|
title: p.title,
|
|
185
273
|
price: p.price,
|
|
274
|
+
brand: p.brand || '',
|
|
186
275
|
merchant: p.merchant || '',
|
|
187
276
|
reasoning: p.reasoning || '',
|
|
188
277
|
url: p.affiliate_url || '',
|
|
189
278
|
})),
|
|
279
|
+
gift_intent: {
|
|
280
|
+
recipient: intent.recipient || '',
|
|
281
|
+
summary: intent.summary || '',
|
|
282
|
+
},
|
|
283
|
+
affiliate_notice: disclosure,
|
|
284
|
+
...(d.note ? { note: d.note } : {}),
|
|
285
|
+
}, null, 2) }] };
|
|
286
|
+
}
|
|
287
|
+
// ── CLARIFICATION MODE ──
|
|
288
|
+
if (d.status === 'clarification_needed') {
|
|
289
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
290
|
+
type: 'clarification_needed',
|
|
291
|
+
advisor: advisorLabel,
|
|
292
|
+
advisor_profile: d.advisor_profile || null,
|
|
293
|
+
message: d.message || '',
|
|
294
|
+
suggested_next: d.suggested_next || [],
|
|
295
|
+
context_summary: d.context_summary || null,
|
|
296
|
+
affiliate_notice: disclosure,
|
|
297
|
+
...(d.note ? { note: d.note } : {}),
|
|
190
298
|
}, null, 2) }] };
|
|
191
299
|
}
|
|
192
300
|
// ── NORMAL / FOLLOW-UP MODE ──
|
|
@@ -203,7 +311,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
203
311
|
content.push({ type: 'image', data: allImages[0].data, mimeType: allImages[0].mimeType });
|
|
204
312
|
}
|
|
205
313
|
// Add structured data — handle no_match vs product_recommendation
|
|
206
|
-
if (d.
|
|
314
|
+
if (d.status === 'no_match') {
|
|
207
315
|
content.push({ type: 'text', text: JSON.stringify({
|
|
208
316
|
type: 'no_match',
|
|
209
317
|
advisor: advisorLabel,
|
|
@@ -247,6 +355,16 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
247
355
|
if (img)
|
|
248
356
|
content.push({ type: 'image', data: img.data, mimeType: img.mimeType });
|
|
249
357
|
});
|
|
358
|
+
// Track session for follow-ups (only on product_found, not no_match)
|
|
359
|
+
if (d.status !== 'no_match' && d.picked) {
|
|
360
|
+
lastShopContext = {
|
|
361
|
+
originalQuery: wasFollowUp ? (lastShopContext?.originalQuery || String(args?.query || '')) : String(args?.query || ''),
|
|
362
|
+
query: effectiveQuery,
|
|
363
|
+
picked: d.picked.title || '',
|
|
364
|
+
price: d.picked.price || 0,
|
|
365
|
+
alternatives: (d.alternatives || []).map((a) => a.title),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
250
368
|
return { content };
|
|
251
369
|
}
|
|
252
370
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
@@ -316,22 +434,54 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
316
434
|
max_price: args?.max_price,
|
|
317
435
|
brand: args?.brand,
|
|
318
436
|
language: args?.language,
|
|
319
|
-
source: '
|
|
437
|
+
source: 'test',
|
|
320
438
|
}),
|
|
321
439
|
});
|
|
322
440
|
if (!result?.ok) {
|
|
323
441
|
return { content: [{ type: 'text', text: JSON.stringify({ type: 'web_search_error', error: result?.error || 'Unknown error', raw: result }, null, 2) }] };
|
|
324
442
|
}
|
|
325
443
|
const d = result.data;
|
|
444
|
+
// Clarification responses have different fields than product responses
|
|
445
|
+
if (d.status === 'clarification_needed') {
|
|
446
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
447
|
+
type: 'clarification_needed',
|
|
448
|
+
endpoint: '/api/agents/shop',
|
|
449
|
+
advisor: d.advisor_profile?.name || d.agent?.personality || null,
|
|
450
|
+
advisor_profile: d.advisor_profile || null,
|
|
451
|
+
message: d.message || null,
|
|
452
|
+
message_de: d.message_de || null,
|
|
453
|
+
message_en: d.message_en || null,
|
|
454
|
+
suggested_next: d.suggested_next || [],
|
|
455
|
+
context_summary: d.context_summary || null,
|
|
456
|
+
disclosure: d.disclosure || null,
|
|
457
|
+
note: d.note || null,
|
|
458
|
+
}, null, 2) }] };
|
|
459
|
+
}
|
|
460
|
+
// Gift responses
|
|
461
|
+
if (d.status === 'gift_found' && d.gift_picks) {
|
|
462
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
463
|
+
type: 'gift_found',
|
|
464
|
+
endpoint: '/api/agents/shop',
|
|
465
|
+
advisor: d.advisor_profile?.name || d.agent?.personality || null,
|
|
466
|
+
advisor_profile: d.advisor_profile || null,
|
|
467
|
+
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 })),
|
|
469
|
+
gift_intent: d.gift_intent || null,
|
|
470
|
+
disclosure: d.disclosure || null,
|
|
471
|
+
note: d.note || null,
|
|
472
|
+
}, null, 2) }] };
|
|
473
|
+
}
|
|
326
474
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
327
475
|
type: d.status || 'web_search_result',
|
|
328
476
|
endpoint: '/api/agents/shop',
|
|
477
|
+
advisor: d.advisor_profile?.name || d.agent?.personality || null,
|
|
478
|
+
advisor_profile: d.advisor_profile || null,
|
|
479
|
+
message: d.message || null,
|
|
329
480
|
picked: d.picked ? { title: d.picked.title, price: d.picked.price, brand: d.picked.brand, affiliate_url: d.picked.affiliate_url } : null,
|
|
330
481
|
alternatives: (d.alternatives || []).map((a) => ({ title: a.title, price: a.price, brand: a.brand })),
|
|
331
482
|
compared: d.compared || 0,
|
|
332
|
-
review: d.review || null,
|
|
333
|
-
reasoning: d.reasoning || null,
|
|
334
|
-
gift_picks: d.gift_picks ? d.gift_picks.map((g) => ({ title: g.title, price: g.price, brand: g.brand })) : undefined,
|
|
483
|
+
review: d.review || d.message || null,
|
|
484
|
+
reasoning: d.reasoning || d.message || null,
|
|
335
485
|
disclosure: d.disclosure || null,
|
|
336
486
|
note: d.note || null,
|
|
337
487
|
}, null, 2) }] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "byclaw-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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": {
|
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
"dev": "tsx src/index.ts",
|
|
12
12
|
"prepublishOnly": "npm run build"
|
|
13
13
|
},
|
|
14
|
-
"keywords": [
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"ai-shopping",
|
|
18
|
+
"claude",
|
|
19
|
+
"byclaw"
|
|
20
|
+
],
|
|
15
21
|
"author": "NJUDEV S.L.",
|
|
16
22
|
"license": "MIT",
|
|
17
23
|
"repository": {
|