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.
Files changed (2) hide show
  1. package/dist/index.js +160 -10
  2. 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. It returns structured JSON with the top recommendation (title, price, merchant, review) and alternatives. Present the results clearly: show the recommended product, its price, the advisor\'s reasoning, and link to the merchant. byclaw.io is operated by NJUDEV S.L., a European company.' },
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: args?.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: 'gift_recommendation',
182
- recipient: intent.summary || '',
183
- picks: d.gift_picks.map((p) => ({
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.type === 'no_match') {
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: 'web',
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.6",
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": ["mcp", "model-context-protocol", "ai-shopping", "claude", "byclaw"],
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": {