byclaw-mcp 0.4.5 → 0.4.7

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 +198 -5
  2. package/package.json +8 -2
package/dist/index.js CHANGED
@@ -132,22 +132,116 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
132
132
  description: 'Get platform statistics: products compared, reviews written, recommendations sent.',
133
133
  inputSchema: { type: 'object', properties: {} },
134
134
  },
135
+ // ── QA / Testing ──
136
+ {
137
+ 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.',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ query: { type: 'string', description: 'Search query' },
143
+ max_price: { type: 'number', description: 'Maximum price in EUR (optional)' },
144
+ brand: { type: 'string', description: 'Brand filter (optional)' },
145
+ language: { type: 'string', description: 'Language: de, en (optional)' },
146
+ },
147
+ required: ['query'],
148
+ },
149
+ },
135
150
  ],
136
151
  }));
137
152
  // ═══════════════════════════════════════
138
153
  // TOOL HANDLERS
139
154
  // ═══════════════════════════════════════
140
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
+ }
141
223
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
142
224
  const { name, arguments: args } = request.params;
143
225
  try {
144
226
  switch (name) {
145
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
+ }
146
235
  const result = await apiCall('/api/mcp/shop', {
147
236
  method: 'POST',
148
237
  body: JSON.stringify({
149
- query: args?.query,
238
+ query: effectiveQuery,
150
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,
151
245
  category: args?.category,
152
246
  brand: args?.brand,
153
247
  language: args?.language,
@@ -162,16 +256,45 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
162
256
  // ── GIFT MODE ──
163
257
  if (d.status === 'gift_found' && Array.isArray(d.gift_picks) && d.gift_picks.length > 0) {
164
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
+ };
165
267
  return { content: [{ type: 'text', text: JSON.stringify({
166
- type: 'gift_recommendation',
167
- recipient: intent.summary || '',
168
- 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) => ({
169
272
  title: p.title,
170
273
  price: p.price,
274
+ brand: p.brand || '',
171
275
  merchant: p.merchant || '',
172
276
  reasoning: p.reasoning || '',
173
277
  url: p.affiliate_url || '',
174
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 } : {}),
175
298
  }, null, 2) }] };
176
299
  }
177
300
  // ── NORMAL / FOLLOW-UP MODE ──
@@ -188,7 +311,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
188
311
  content.push({ type: 'image', data: allImages[0].data, mimeType: allImages[0].mimeType });
189
312
  }
190
313
  // Add structured data — handle no_match vs product_recommendation
191
- if (d.type === 'no_match') {
314
+ if (d.status === 'no_match') {
192
315
  content.push({ type: 'text', text: JSON.stringify({
193
316
  type: 'no_match',
194
317
  advisor: advisorLabel,
@@ -232,6 +355,16 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
232
355
  if (img)
233
356
  content.push({ type: 'image', data: img.data, mimeType: img.mimeType });
234
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
+ }
235
368
  return { content };
236
369
  }
237
370
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
@@ -293,6 +426,66 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
293
426
  const result = await apiCall('/api/stats');
294
427
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
295
428
  }
429
+ case 'test_web_search': {
430
+ const result = await apiCall('/api/agents/shop', {
431
+ method: 'POST',
432
+ body: JSON.stringify({
433
+ query: args?.query,
434
+ max_price: args?.max_price,
435
+ brand: args?.brand,
436
+ language: args?.language,
437
+ source: 'test',
438
+ }),
439
+ });
440
+ if (!result?.ok) {
441
+ return { content: [{ type: 'text', text: JSON.stringify({ type: 'web_search_error', error: result?.error || 'Unknown error', raw: result }, null, 2) }] };
442
+ }
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
+ }
474
+ return { content: [{ type: 'text', text: JSON.stringify({
475
+ type: d.status || 'web_search_result',
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,
480
+ picked: d.picked ? { title: d.picked.title, price: d.picked.price, brand: d.picked.brand, affiliate_url: d.picked.affiliate_url } : null,
481
+ alternatives: (d.alternatives || []).map((a) => ({ title: a.title, price: a.price, brand: a.brand })),
482
+ compared: d.compared || 0,
483
+ review: d.review || d.message || null,
484
+ reasoning: d.reasoning || d.message || null,
485
+ disclosure: d.disclosure || null,
486
+ note: d.note || null,
487
+ }, null, 2) }] };
488
+ }
296
489
  default:
297
490
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
298
491
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "byclaw-mcp",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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": {