@dpesch/mantisbt-mcp-server 1.8.3 → 1.9.0
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/CHANGELOG.md +21 -0
- package/README.de.md +3 -3
- package/README.md +3 -3
- package/dist/date-filter.js +55 -0
- package/dist/search/highlight.js +63 -0
- package/dist/search/store.js +4 -0
- package/dist/search/tools.js +65 -4
- package/dist/tools/config.js +23 -8
- package/dist/tools/issues.js +123 -18
- package/dist/tools/notes.js +1 -1
- package/docs/cookbook.de.md +64 -7
- package/docs/cookbook.md +64 -7
- package/docs/examples.de.md +12 -0
- package/docs/examples.md +12 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/tests/fixtures/get_issue.json +22 -0
- package/tests/helpers/search-mocks.ts +29 -6
- package/tests/search/highlight.test.ts +129 -0
- package/tests/search/tools.test.ts +258 -0
- package/tests/tools/issues.test.ts +446 -4
- package/tests/utils/date-filter.test.ts +169 -0
|
@@ -332,6 +332,100 @@ describe('get_search_index_status – with stored total', () => {
|
|
|
332
332
|
// get_search_index_status – edge cases
|
|
333
333
|
// ---------------------------------------------------------------------------
|
|
334
334
|
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// search_issues – date filters
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
describe('search_issues – updated_after filter (no select, uses store metadata)', () => {
|
|
340
|
+
it('returns only results whose store metadata updated_at is after the threshold', async () => {
|
|
341
|
+
const store = makeMockStore({
|
|
342
|
+
items: [
|
|
343
|
+
{ id: 1, score: 0.9, updated_at: '2026-03-26T00:00:00Z' }, // pass
|
|
344
|
+
{ id: 2, score: 0.8, updated_at: '2026-03-23T00:00:00Z' }, // fail
|
|
345
|
+
{ id: 3, score: 0.7, updated_at: '2026-03-25T12:00:00Z' }, // pass
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
349
|
+
|
|
350
|
+
const result = await mockServer.callTool('search_issues', {
|
|
351
|
+
query: 'test',
|
|
352
|
+
top_n: 10,
|
|
353
|
+
updated_after: '2026-03-25T00:00:00Z',
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(result.isError).toBeUndefined();
|
|
357
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<{ id: number }>;
|
|
358
|
+
expect(parsed.map(r => r.id)).toEqual([1, 3]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('makes no API calls when filtering via store metadata (no select)', async () => {
|
|
362
|
+
const store = makeMockStore({
|
|
363
|
+
items: [{ id: 1, updated_at: '2026-03-26T00:00:00Z' }],
|
|
364
|
+
});
|
|
365
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
366
|
+
|
|
367
|
+
await mockServer.callTool('search_issues', {
|
|
368
|
+
query: 'test',
|
|
369
|
+
top_n: 5,
|
|
370
|
+
updated_after: '2026-03-25T00:00:00Z',
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('excludes results with no updated_at in store metadata', async () => {
|
|
377
|
+
const store = makeMockStore({
|
|
378
|
+
items: [
|
|
379
|
+
{ id: 1, score: 0.9 }, // no updated_at → excluded
|
|
380
|
+
{ id: 2, score: 0.8, updated_at: '2026-03-26T00:00:00Z' }, // pass
|
|
381
|
+
],
|
|
382
|
+
});
|
|
383
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
384
|
+
|
|
385
|
+
const result = await mockServer.callTool('search_issues', {
|
|
386
|
+
query: 'test',
|
|
387
|
+
top_n: 10,
|
|
388
|
+
updated_after: '2026-03-25T00:00:00Z',
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<{ id: number }>;
|
|
392
|
+
expect(parsed.map(r => r.id)).toEqual([2]);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('search_issues – updated_after filter (with select, uses fetched issue data)', () => {
|
|
397
|
+
it('returns only results whose fetched updated_at is after the threshold', async () => {
|
|
398
|
+
const store = makeMockStore({
|
|
399
|
+
items: [
|
|
400
|
+
{ id: 1, score: 0.9 },
|
|
401
|
+
{ id: 2, score: 0.8 },
|
|
402
|
+
],
|
|
403
|
+
});
|
|
404
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
405
|
+
|
|
406
|
+
vi.mocked(fetch)
|
|
407
|
+
.mockResolvedValueOnce(makeResponse(200, JSON.stringify({
|
|
408
|
+
issues: [{ id: 1, summary: 'Recent bug', updated_at: '2026-03-26T00:00:00Z' }],
|
|
409
|
+
})))
|
|
410
|
+
.mockResolvedValueOnce(makeResponse(200, JSON.stringify({
|
|
411
|
+
issues: [{ id: 2, summary: 'Old bug', updated_at: '2026-03-20T00:00:00Z' }],
|
|
412
|
+
})));
|
|
413
|
+
|
|
414
|
+
const result = await mockServer.callTool('search_issues', {
|
|
415
|
+
query: 'test',
|
|
416
|
+
top_n: 10,
|
|
417
|
+
select: 'summary',
|
|
418
|
+
updated_after: '2026-03-25T00:00:00Z',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(result.isError).toBeUndefined();
|
|
422
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<{ id: number; summary: string }>;
|
|
423
|
+
expect(parsed).toHaveLength(1);
|
|
424
|
+
expect(parsed[0]!.id).toBe(1);
|
|
425
|
+
expect(parsed[0]!.summary).toBe('Recent bug');
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
335
429
|
describe('get_search_index_status – edge cases', () => {
|
|
336
430
|
it('returns 0 % when stored total is 0', async () => {
|
|
337
431
|
const store = makeMockStore({ itemCount: 0, lastKnownTotal: 0 });
|
|
@@ -372,3 +466,167 @@ describe('get_search_index_status – edge cases', () => {
|
|
|
372
466
|
expect(parsed.summary).toContain('total unknown');
|
|
373
467
|
});
|
|
374
468
|
});
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// search_issues – highlight parameter
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
describe('search_issues – highlight: true (no select, uses store metadata)', () => {
|
|
475
|
+
it('adds highlights field with bolded terms from store metadata summary', async () => {
|
|
476
|
+
const store = makeMockStore({
|
|
477
|
+
items: [{ id: 1, score: 0.9 }],
|
|
478
|
+
});
|
|
479
|
+
vi.mocked(store.getItem).mockResolvedValue({
|
|
480
|
+
id: 1,
|
|
481
|
+
vector: [],
|
|
482
|
+
metadata: { summary: 'Login error occurred', description: 'The login fails with error code 500.' },
|
|
483
|
+
});
|
|
484
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
485
|
+
|
|
486
|
+
const result = await mockServer.callTool('search_issues', {
|
|
487
|
+
query: 'login error',
|
|
488
|
+
top_n: 1,
|
|
489
|
+
highlight: true,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
expect(result.isError).toBeUndefined();
|
|
493
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<Record<string, unknown>>;
|
|
494
|
+
expect(parsed[0]).toHaveProperty('highlights');
|
|
495
|
+
const highlights = parsed[0]!['highlights'] as Record<string, string>;
|
|
496
|
+
expect(highlights['summary']).toContain('**');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('omits highlights field when no query terms match', async () => {
|
|
500
|
+
const store = makeMockStore({
|
|
501
|
+
items: [{ id: 1, score: 0.9 }],
|
|
502
|
+
});
|
|
503
|
+
vi.mocked(store.getItem).mockResolvedValue({
|
|
504
|
+
id: 1,
|
|
505
|
+
vector: [],
|
|
506
|
+
metadata: { summary: 'Unrelated issue', description: undefined },
|
|
507
|
+
});
|
|
508
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
509
|
+
|
|
510
|
+
const result = await mockServer.callTool('search_issues', {
|
|
511
|
+
query: 'xyzzy',
|
|
512
|
+
top_n: 1,
|
|
513
|
+
highlight: true,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<Record<string, unknown>>;
|
|
517
|
+
expect(parsed[0]).not.toHaveProperty('highlights');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('does not call store.getItem for highlighting when highlight is false', async () => {
|
|
521
|
+
const store = makeMockStore({ itemCount: 2 });
|
|
522
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
523
|
+
|
|
524
|
+
await mockServer.callTool('search_issues', { query: 'login', top_n: 2 });
|
|
525
|
+
|
|
526
|
+
// getItem should NOT have been called (no date filter, no highlight)
|
|
527
|
+
expect(store.getItem).not.toHaveBeenCalled();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('still returns id and score alongside highlights', async () => {
|
|
531
|
+
const store = makeMockStore({
|
|
532
|
+
items: [{ id: 42, score: 0.85 }],
|
|
533
|
+
});
|
|
534
|
+
vi.mocked(store.getItem).mockResolvedValue({
|
|
535
|
+
id: 42,
|
|
536
|
+
vector: [],
|
|
537
|
+
metadata: { summary: 'Login timeout issue' },
|
|
538
|
+
});
|
|
539
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
540
|
+
|
|
541
|
+
const result = await mockServer.callTool('search_issues', {
|
|
542
|
+
query: 'login',
|
|
543
|
+
top_n: 1,
|
|
544
|
+
highlight: true,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<Record<string, unknown>>;
|
|
548
|
+
expect(parsed[0]).toHaveProperty('id', 42);
|
|
549
|
+
expect(parsed[0]).toHaveProperty('score');
|
|
550
|
+
expect(parsed[0]).toHaveProperty('highlights');
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe('search_issues – highlight: true (with select)', () => {
|
|
555
|
+
it('highlights summary from fetched issue when summary is in select', async () => {
|
|
556
|
+
const store = makeMockStore({ itemCount: 1 });
|
|
557
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
558
|
+
|
|
559
|
+
vi.mocked(fetch).mockResolvedValueOnce(
|
|
560
|
+
makeResponse(200, JSON.stringify({
|
|
561
|
+
issues: [{ id: 1, summary: 'Login error in dashboard', status: { id: 10, name: 'new' } }],
|
|
562
|
+
}))
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const result = await mockServer.callTool('search_issues', {
|
|
566
|
+
query: 'login error',
|
|
567
|
+
top_n: 1,
|
|
568
|
+
select: 'summary,status',
|
|
569
|
+
highlight: true,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(result.isError).toBeUndefined();
|
|
573
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<Record<string, unknown>>;
|
|
574
|
+
expect(parsed[0]).toHaveProperty('highlights');
|
|
575
|
+
const highlights = parsed[0]!['highlights'] as Record<string, string>;
|
|
576
|
+
expect(highlights['summary']).toContain('**Login**');
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('falls back to store metadata for highlighting when API fetch fails', async () => {
|
|
580
|
+
const store = makeMockStore({ items: [{ id: 1, score: 0.9 }] });
|
|
581
|
+
vi.mocked(store.getItem).mockResolvedValue({
|
|
582
|
+
id: 1,
|
|
583
|
+
vector: [],
|
|
584
|
+
metadata: { summary: 'Login crash issue' },
|
|
585
|
+
});
|
|
586
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
587
|
+
|
|
588
|
+
vi.mocked(fetch).mockResolvedValueOnce(makeResponse(500, 'Server Error'));
|
|
589
|
+
|
|
590
|
+
const result = await mockServer.callTool('search_issues', {
|
|
591
|
+
query: 'login',
|
|
592
|
+
top_n: 1,
|
|
593
|
+
select: 'summary',
|
|
594
|
+
highlight: true,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Falls back to {id, score} but we still try to add highlights from store metadata
|
|
598
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<Record<string, unknown>>;
|
|
599
|
+
expect(parsed[0]).toHaveProperty('id', 1);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe('search_issues – highlight: true combined with date filter', () => {
|
|
604
|
+
it('returns highlights only for date-filtered results (no select)', async () => {
|
|
605
|
+
const store = makeMockStore({
|
|
606
|
+
items: [
|
|
607
|
+
{ id: 1, score: 0.9, updated_at: '2026-03-26T00:00:00Z' }, // passes filter
|
|
608
|
+
{ id: 2, score: 0.8, updated_at: '2026-03-20T00:00:00Z' }, // filtered out
|
|
609
|
+
],
|
|
610
|
+
});
|
|
611
|
+
vi.mocked(store.getItem).mockImplementation(async (id: number) => ({
|
|
612
|
+
id,
|
|
613
|
+
vector: [],
|
|
614
|
+
metadata: {
|
|
615
|
+
summary: id === 1 ? 'Login crash issue' : 'Old login bug',
|
|
616
|
+
updated_at: id === 1 ? '2026-03-26T00:00:00Z' : '2026-03-20T00:00:00Z',
|
|
617
|
+
},
|
|
618
|
+
}));
|
|
619
|
+
registerSearchTools(mockServer as never, client, store, embedder);
|
|
620
|
+
|
|
621
|
+
const result = await mockServer.callTool('search_issues', {
|
|
622
|
+
query: 'login',
|
|
623
|
+
top_n: 10,
|
|
624
|
+
highlight: true,
|
|
625
|
+
updated_after: '2026-03-25T00:00:00Z',
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const parsed = JSON.parse(result.content[0]!.text) as Array<{ id: number }>;
|
|
629
|
+
expect(parsed).toHaveLength(1);
|
|
630
|
+
expect(parsed[0]!.id).toBe(1);
|
|
631
|
+
});
|
|
632
|
+
});
|