@usewhisper/mcp-server 2.5.0 → 2.7.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.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/dist/server.js +494 -93
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -12,6 +12,13 @@ Whisper MCP is the universal context bridge for coding agents. It connects Claud
12
12
  - Scoped MCP config generator: `whisper-context-mcp scope ...`
13
13
  - Tool migration helper: `whisper-context-mcp --print-tool-map`
14
14
 
15
+ ## What's New in v2.6
16
+
17
+ - Automatic project, user, and session scope defaults for MCP retrieval and memory operations.
18
+ - `context.query` can use the automatic agent runtime by default when the caller has not requested manual-only filters.
19
+ - URL and domain-style retrieval requests now isolate matching sources better in shared projects.
20
+ - MCP memory operations now default to pending/session-aware reads and writes when compatible.
21
+
15
22
  ## Install
16
23
 
17
24
  ```text
@@ -132,6 +139,16 @@ All valid `search` calls return the same top-level payload shape:
132
139
  }
133
140
  ```
134
141
 
142
+ ### Automatic Defaults
143
+
144
+ When compatible with the caller’s requested options, Whisper MCP now behaves like an implicit context layer instead of a raw tool database:
145
+
146
+ - `context.query` auto-attaches default project, user, and session scope
147
+ - compatible retrieval calls use automatic runtime-backed pre-retrieval
148
+ - memory writes and session ingest calls attach stable default scope automatically
149
+
150
+ If the caller explicitly requests manual query controls such as `chunk_types`, explicit graph traversal, or other narrow filters, MCP preserves those manual semantics instead of forcing automatic mode.
151
+
135
152
  ## Source Contract (`context.add_source`)
136
153
 
137
154
  Input:
package/dist/server.js CHANGED
@@ -1365,6 +1365,29 @@ ${lines.join("\n")}`;
1365
1365
  function compactWhitespace(value) {
1366
1366
  return value.replace(/\s+/g, " ").trim();
1367
1367
  }
1368
+ function normalizeSummary(value) {
1369
+ return compactWhitespace(String(value || "").toLowerCase());
1370
+ }
1371
+ function tokenize(value) {
1372
+ return normalizeSummary(value).split(/[^a-z0-9_./-]+/i).map((token) => token.trim()).filter(Boolean);
1373
+ }
1374
+ function jaccardOverlap(left, right) {
1375
+ const leftTokens = new Set(tokenize(left));
1376
+ const rightTokens = new Set(tokenize(right));
1377
+ if (leftTokens.size === 0 || rightTokens.size === 0) return 0;
1378
+ let intersection = 0;
1379
+ for (const token of leftTokens) {
1380
+ if (rightTokens.has(token)) intersection += 1;
1381
+ }
1382
+ const union = (/* @__PURE__ */ new Set([...leftTokens, ...rightTokens])).size;
1383
+ return union > 0 ? intersection / union : 0;
1384
+ }
1385
+ function clamp01(value) {
1386
+ if (!Number.isFinite(value)) return 0;
1387
+ if (value < 0) return 0;
1388
+ if (value > 1) return 1;
1389
+ return value;
1390
+ }
1368
1391
  function withTimeout(promise, timeoutMs) {
1369
1392
  return new Promise((resolve, reject) => {
1370
1393
  const timeout = setTimeout(() => {
@@ -1398,39 +1421,76 @@ function extractTimestamp(metadata) {
1398
1421
  }
1399
1422
  return 0;
1400
1423
  }
1401
- function salienceBoost(metadata) {
1402
- const value = metadata?.salience;
1403
- if (value === "high") return 0.12;
1404
- if (value === "medium") return 0.06;
1405
- return 0;
1406
- }
1424
+ var DEFAULT_RANK_WEIGHTS = {
1425
+ focusedPassBonus: 0.2,
1426
+ sourceMatchBonus: 0.18,
1427
+ touchedFileBonus: 0.12,
1428
+ clientMatchBonus: 0.1,
1429
+ highSalienceBonus: 0.12,
1430
+ mediumSalienceBonus: 0.06,
1431
+ staleBroadPenalty: -0.1,
1432
+ unrelatedClientPenalty: -0.18,
1433
+ lowSaliencePenalty: -0.12
1434
+ };
1435
+ var DEFAULT_SOURCE_ACTIVITY = {
1436
+ maxTurns: 10,
1437
+ maxIdleMs: 30 * 60 * 1e3,
1438
+ decayAfterTurns: 5,
1439
+ decayAfterIdleMs: 15 * 60 * 1e3,
1440
+ evictOnTaskSwitch: true
1441
+ };
1407
1442
  var WhisperAgentRuntime = class {
1408
1443
  constructor(args) {
1409
1444
  this.args = args;
1410
1445
  this.bindingStore = createBindingStore(args.options.bindingStorePath);
1411
- this.topK = args.options.topK ?? 6;
1446
+ const retrieval = args.options.retrieval || {};
1447
+ this.focusedTopK = retrieval.focusedTopK ?? args.options.topK ?? 6;
1448
+ this.broadTopK = retrieval.broadTopK ?? Math.max(args.options.topK ?? 6, 10);
1412
1449
  this.maxTokens = args.options.maxTokens ?? 4e3;
1413
1450
  this.targetRetrievalMs = args.options.targetRetrievalMs ?? 2500;
1414
1451
  this.hardRetrievalTimeoutMs = args.options.hardRetrievalTimeoutMs ?? 4e3;
1415
1452
  this.recentWorkLimit = args.options.recentWorkLimit ?? 40;
1416
1453
  this.baseContext = args.baseContext;
1417
1454
  this.clientName = args.baseContext.clientName || "whisper-agent-runtime";
1455
+ this.minFocusedResults = retrieval.minFocusedResults ?? 3;
1456
+ this.minFocusedTopScore = retrieval.minFocusedTopScore ?? 0.55;
1457
+ this.minProjectScore = retrieval.minProjectScore ?? 0.5;
1458
+ this.minMemoryScore = retrieval.minMemoryScore ?? 0.6;
1459
+ this.rankWeights = { ...DEFAULT_RANK_WEIGHTS, ...retrieval.rankWeights || {} };
1460
+ this.sourceActivityOptions = { ...DEFAULT_SOURCE_ACTIVITY, ...retrieval.sourceActivity || {} };
1418
1461
  }
1419
1462
  bindingStore;
1420
- topK;
1463
+ focusedTopK;
1464
+ broadTopK;
1421
1465
  maxTokens;
1422
1466
  targetRetrievalMs;
1423
1467
  hardRetrievalTimeoutMs;
1424
1468
  recentWorkLimit;
1425
1469
  baseContext;
1426
1470
  clientName;
1471
+ minFocusedResults;
1472
+ minFocusedTopScore;
1473
+ minProjectScore;
1474
+ minMemoryScore;
1475
+ rankWeights;
1476
+ sourceActivityOptions;
1427
1477
  bindings = null;
1428
1478
  touchedFiles = [];
1429
1479
  recentWork = [];
1480
+ recentSourceActivity = [];
1430
1481
  bufferedLowSalience = [];
1431
1482
  lastPreparedTurn = null;
1432
1483
  mergedCount = 0;
1433
1484
  droppedCount = 0;
1485
+ focusedPassHits = 0;
1486
+ fallbackTriggers = 0;
1487
+ floorDroppedCount = 0;
1488
+ injectedItemCount = 0;
1489
+ sourceScopedTurns = 0;
1490
+ broadScopedTurns = 0;
1491
+ totalTurns = 0;
1492
+ currentTurn = 0;
1493
+ lastTaskSummary = "";
1434
1494
  lastScope = {};
1435
1495
  async getBindings() {
1436
1496
  if (!this.bindings) {
@@ -1448,6 +1508,64 @@ var WhisperAgentRuntime = class {
1448
1508
  pushWorkEvent(event) {
1449
1509
  this.recentWork = [...this.recentWork, event].slice(-this.recentWorkLimit);
1450
1510
  }
1511
+ noteSourceActivity(sourceIds) {
1512
+ const now = Date.now();
1513
+ for (const sourceId of [...new Set((sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))]) {
1514
+ this.recentSourceActivity = [
1515
+ ...this.recentSourceActivity.filter((entry) => entry.sourceId !== sourceId),
1516
+ { sourceId, turn: this.currentTurn, at: now }
1517
+ ].slice(-24);
1518
+ }
1519
+ }
1520
+ refreshTaskSummary(taskSummary) {
1521
+ const next = normalizeSummary(taskSummary);
1522
+ if (!next) return;
1523
+ if (this.sourceActivityOptions.evictOnTaskSwitch && this.lastTaskSummary && this.lastTaskSummary !== next && jaccardOverlap(this.lastTaskSummary, next) < 0.6) {
1524
+ this.recentSourceActivity = [];
1525
+ }
1526
+ this.lastTaskSummary = next;
1527
+ }
1528
+ activeSourceIds() {
1529
+ const now = Date.now();
1530
+ const active = /* @__PURE__ */ new Map();
1531
+ const maxTurns = this.sourceActivityOptions.maxTurns;
1532
+ const maxIdleMs = this.sourceActivityOptions.maxIdleMs;
1533
+ const decayAfterTurns = this.sourceActivityOptions.decayAfterTurns;
1534
+ const decayAfterIdleMs = this.sourceActivityOptions.decayAfterIdleMs;
1535
+ const fresh = [];
1536
+ for (const entry of this.recentSourceActivity) {
1537
+ const turnDelta = this.currentTurn - entry.turn;
1538
+ const idleDelta = now - entry.at;
1539
+ if (turnDelta > maxTurns || idleDelta > maxIdleMs) continue;
1540
+ fresh.push(entry);
1541
+ let weight = 1;
1542
+ if (turnDelta > decayAfterTurns || idleDelta > decayAfterIdleMs) {
1543
+ weight = 0.5;
1544
+ }
1545
+ const current = active.get(entry.sourceId) || 0;
1546
+ active.set(entry.sourceId, Math.max(current, weight));
1547
+ }
1548
+ this.recentSourceActivity = fresh.slice(-24);
1549
+ return [...active.entries()].sort((left, right) => right[1] - left[1]).map(([sourceId]) => sourceId).slice(0, 4);
1550
+ }
1551
+ focusedScope(input) {
1552
+ const sourceIds = this.activeSourceIds();
1553
+ const fileHints = [...new Set([
1554
+ ...input.touchedFiles || [],
1555
+ ...this.touchedFiles,
1556
+ ...this.recentWork.flatMap((event) => event.filePaths || [])
1557
+ ].map((value) => String(value || "").trim()).filter(Boolean))].slice(-4);
1558
+ return {
1559
+ sourceIds,
1560
+ fileHints,
1561
+ clientName: this.clientName || void 0
1562
+ };
1563
+ }
1564
+ exactFileMetadataFilter(fileHints) {
1565
+ const exact = fileHints.find((value) => /[\\/]/.test(value));
1566
+ if (!exact) return void 0;
1567
+ return { filePath: exact };
1568
+ }
1451
1569
  makeTaskFrameQuery(input) {
1452
1570
  const task = compactWhitespace(input.taskSummary || "");
1453
1571
  const salient = this.recentWork.filter((event) => event.salience === "high").slice(-3).map((event) => `${event.kind}: ${event.summary}`);
@@ -1524,23 +1642,29 @@ var WhisperAgentRuntime = class {
1524
1642
  };
1525
1643
  }
1526
1644
  }
1527
- contextItems(result, sourceQuery) {
1645
+ contextItems(result, sourceQuery, pass) {
1646
+ const sourceScope = result.meta?.source_scope;
1647
+ if (sourceScope?.mode === "auto" || sourceScope?.mode === "explicit") {
1648
+ this.noteSourceActivity(sourceScope.source_ids || []);
1649
+ }
1528
1650
  return (result.results || []).map((item) => ({
1529
1651
  id: item.id,
1530
1652
  content: item.content,
1531
1653
  type: "project",
1532
1654
  score: item.score ?? 0,
1533
1655
  sourceQuery,
1656
+ pass,
1534
1657
  metadata: item.metadata || {}
1535
1658
  }));
1536
1659
  }
1537
- memoryItems(result, sourceQuery) {
1660
+ memoryItems(result, sourceQuery, pass) {
1538
1661
  return (result.results || []).map((item, index) => ({
1539
1662
  id: item.memory?.id || item.chunk?.id || `${sourceQuery}_memory_${index}`,
1540
1663
  content: item.chunk?.content || item.memory?.content || "",
1541
1664
  type: "memory",
1542
1665
  score: item.similarity ?? 0,
1543
1666
  sourceQuery,
1667
+ pass,
1544
1668
  metadata: {
1545
1669
  ...item.chunk?.metadata || {},
1546
1670
  ...item.memory?.temporal || {},
@@ -1548,22 +1672,99 @@ var WhisperAgentRuntime = class {
1548
1672
  }
1549
1673
  })).filter((item) => item.content);
1550
1674
  }
1551
- rerank(items) {
1675
+ stableItemKey(item) {
1676
+ const metadata = item.metadata || {};
1677
+ const sourceId = String(metadata.source_id || "");
1678
+ const documentId = String(metadata.document_id || metadata.documentId || "");
1679
+ const chunkId = String(metadata.chunk_id || metadata.chunkId || item.id || "");
1680
+ return stableHash(`${sourceId}|${documentId}|${chunkId}|${item.content.slice(0, 256)}`);
1681
+ }
1682
+ metadataStrings(item) {
1683
+ const metadata = item.metadata || {};
1684
+ return [
1685
+ metadata.filePath,
1686
+ metadata.file_path,
1687
+ metadata.path,
1688
+ metadata.section_path,
1689
+ metadata.parent_section_path,
1690
+ metadata.web_url,
1691
+ metadata.url
1692
+ ].map((value) => String(value || "").toLowerCase()).filter(Boolean);
1693
+ }
1694
+ hasSourceMatch(item, scope) {
1695
+ const sourceId = String(item.metadata?.source_id || "");
1696
+ return Boolean(sourceId && scope.sourceIds.includes(sourceId));
1697
+ }
1698
+ hasFileMatch(item, scope) {
1699
+ if (scope.fileHints.length === 0) return false;
1700
+ const metadata = this.metadataStrings(item);
1701
+ const lowerHints = scope.fileHints.map((hint) => hint.toLowerCase());
1702
+ return lowerHints.some((hint) => {
1703
+ const base = pathBase(hint).toLowerCase();
1704
+ return metadata.some((value) => value.includes(hint) || value.endsWith(base));
1705
+ });
1706
+ }
1707
+ hasClientMatch(item, scope) {
1708
+ const itemClient = String(item.metadata?.client_name || "");
1709
+ return Boolean(scope.clientName && itemClient && itemClient === scope.clientName);
1710
+ }
1711
+ salienceAdjustment(item) {
1712
+ const salience = item.metadata?.salience;
1713
+ if (salience === "high") return this.rankWeights.highSalienceBonus;
1714
+ if (salience === "medium") return this.rankWeights.mediumSalienceBonus;
1715
+ if (salience === "low") return this.rankWeights.lowSaliencePenalty;
1716
+ return 0;
1717
+ }
1718
+ narrowFocusedMemories(items, scope) {
1719
+ const hasSignals = scope.sourceIds.length > 0 || scope.fileHints.length > 0 || Boolean(scope.clientName);
1720
+ if (!hasSignals) return items;
1721
+ const narrowed = items.filter((item) => {
1722
+ const matchesClient = this.hasClientMatch(item, scope);
1723
+ const matchesFile = this.hasFileMatch(item, scope);
1724
+ const matchesSource = this.hasSourceMatch(item, scope);
1725
+ const salience = item.metadata?.salience;
1726
+ if (scope.clientName && item.metadata?.client_name && !matchesClient) {
1727
+ return false;
1728
+ }
1729
+ if (salience === "low" && !matchesFile && !matchesSource) {
1730
+ return false;
1731
+ }
1732
+ return matchesClient || matchesFile || matchesSource || !scope.clientName;
1733
+ });
1734
+ return narrowed.length > 0 ? narrowed : items;
1735
+ }
1736
+ applyRelevanceFloor(items) {
1737
+ const filtered = items.filter(
1738
+ (item) => item.type === "project" ? item.score >= this.minProjectScore : item.score >= this.minMemoryScore
1739
+ );
1740
+ return { items: filtered, dropped: Math.max(0, items.length - filtered.length) };
1741
+ }
1742
+ rerank(items, scope) {
1552
1743
  const deduped = /* @__PURE__ */ new Map();
1553
1744
  for (const item of items) {
1554
- const key = `${item.id}:${item.content.toLowerCase()}`;
1745
+ const key = this.stableItemKey(item);
1555
1746
  const recency = extractTimestamp(item.metadata) > 0 ? 0.04 : 0;
1556
1747
  const queryBonus = item.sourceQuery === "primary" ? 0.08 : item.sourceQuery === "task_frame" ? 0.04 : 0.03;
1748
+ const sourceMatch = this.hasSourceMatch(item, scope);
1749
+ const fileMatch = this.hasFileMatch(item, scope);
1750
+ const clientMatch = this.hasClientMatch(item, scope);
1751
+ const broadPenalty = item.pass === "broad" && !sourceMatch && !fileMatch && !clientMatch ? this.rankWeights.staleBroadPenalty : 0;
1752
+ const clientPenalty = scope.clientName && item.metadata?.client_name && !clientMatch ? this.rankWeights.unrelatedClientPenalty : 0;
1557
1753
  const next = {
1558
1754
  ...item,
1559
- score: item.score + queryBonus + salienceBoost(item.metadata) + recency
1755
+ score: clamp01(
1756
+ item.score + queryBonus + recency + (item.pass === "focused" ? this.rankWeights.focusedPassBonus : 0) + (sourceMatch ? this.rankWeights.sourceMatchBonus : 0) + (fileMatch ? this.rankWeights.touchedFileBonus : 0) + (clientMatch ? this.rankWeights.clientMatchBonus : 0) + this.salienceAdjustment(item) + broadPenalty + clientPenalty
1757
+ )
1560
1758
  };
1561
1759
  const existing = deduped.get(key);
1562
1760
  if (!existing || next.score > existing.score) {
1563
1761
  deduped.set(key, next);
1564
1762
  }
1565
1763
  }
1566
- return [...deduped.values()].sort((left, right) => right.score - left.score);
1764
+ return {
1765
+ items: [...deduped.values()].sort((left, right) => right.score - left.score),
1766
+ dedupedCount: Math.max(0, items.length - deduped.size)
1767
+ };
1567
1768
  }
1568
1769
  buildContext(items) {
1569
1770
  const maxChars = this.maxTokens * 4;
@@ -1602,7 +1803,7 @@ ${lines.join("\n")}`;
1602
1803
  this.runBranch("project_rules", () => this.args.adapter.query({
1603
1804
  project: scope.project,
1604
1805
  query: "project rules instructions constraints conventions open threads",
1605
- top_k: this.topK,
1806
+ top_k: this.focusedTopK,
1606
1807
  include_memories: false,
1607
1808
  user_id: scope.userId,
1608
1809
  session_id: scope.sessionId,
@@ -1620,7 +1821,7 @@ ${lines.join("\n")}`;
1620
1821
  continue;
1621
1822
  }
1622
1823
  if (branch.name === "project_rules") {
1623
- items.push(...this.contextItems(branch.value, "bootstrap"));
1824
+ items.push(...this.contextItems(branch.value, "bootstrap", "bootstrap"));
1624
1825
  continue;
1625
1826
  }
1626
1827
  const records = branch.value.memories || [];
@@ -1630,10 +1831,12 @@ ${lines.join("\n")}`;
1630
1831
  type: "memory",
1631
1832
  score: 0.4,
1632
1833
  sourceQuery: "bootstrap",
1834
+ pass: "bootstrap",
1633
1835
  metadata: memory
1634
1836
  })).filter((item) => item.content));
1635
1837
  }
1636
- const ranked = this.rerank(items).slice(0, this.topK * 2);
1838
+ const reranked = this.rerank(items, { sourceIds: [], fileHints: [], clientName: this.clientName });
1839
+ const ranked = reranked.items.slice(0, this.broadTopK * 2);
1637
1840
  const prepared = {
1638
1841
  scope,
1639
1842
  retrieval: {
@@ -1645,7 +1848,14 @@ ${lines.join("\n")}`;
1645
1848
  durationMs: Date.now() - startedAt,
1646
1849
  targetBudgetMs: this.targetRetrievalMs,
1647
1850
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
1648
- branchStatus
1851
+ branchStatus,
1852
+ focusedScopeApplied: false,
1853
+ focusedSourceIds: [],
1854
+ focusedFileHints: [],
1855
+ clientScoped: false,
1856
+ fallbackUsed: false,
1857
+ droppedBelowFloor: 0,
1858
+ dedupedCount: reranked.dedupedCount
1649
1859
  },
1650
1860
  context: this.buildContext(ranked),
1651
1861
  items: ranked
@@ -1654,100 +1864,195 @@ ${lines.join("\n")}`;
1654
1864
  return prepared;
1655
1865
  }
1656
1866
  async beforeTurn(input, context = {}) {
1867
+ this.currentTurn += 1;
1657
1868
  this.pushTouchedFiles(input.touchedFiles);
1869
+ this.refreshTaskSummary(input.taskSummary);
1658
1870
  const { scope, warning } = await this.resolveScope(context);
1659
1871
  const primaryQuery = compactWhitespace(input.userMessage);
1660
1872
  const taskFrameQuery = this.makeTaskFrameQuery(input);
1873
+ const focusedScope = this.focusedScope(input);
1874
+ const focusedMetadataFilter = this.exactFileMetadataFilter(focusedScope.fileHints);
1875
+ const focusedScopeApplied = focusedScope.sourceIds.length > 0 || focusedScope.fileHints.length > 0 || Boolean(focusedScope.clientName);
1661
1876
  const warnings = warning ? [warning] : [];
1662
1877
  const startedAt = Date.now();
1663
- const branches = await Promise.all([
1664
- this.runBranch("context_primary", () => this.args.adapter.query({
1878
+ const branchStatus = {};
1879
+ const collectFromBranches = (branches, pass) => {
1880
+ const collected = [];
1881
+ let okCount = 0;
1882
+ for (const branch of branches) {
1883
+ branchStatus[branch.name] = branch.status;
1884
+ if (branch.status !== "ok") {
1885
+ if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1886
+ continue;
1887
+ }
1888
+ okCount += 1;
1889
+ if (branch.name.startsWith("context")) {
1890
+ collected.push(...this.contextItems(
1891
+ branch.value,
1892
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1893
+ pass
1894
+ ));
1895
+ } else {
1896
+ const memoryItems = this.memoryItems(
1897
+ branch.value,
1898
+ branch.name.includes("task_frame") ? "task_frame" : "primary",
1899
+ pass
1900
+ );
1901
+ collected.push(...pass === "focused" ? this.narrowFocusedMemories(memoryItems, focusedScope) : memoryItems);
1902
+ }
1903
+ }
1904
+ return { collected, okCount };
1905
+ };
1906
+ const focusedBranches = await Promise.all([
1907
+ this.runBranch("context_primary_focused", () => this.args.adapter.query({
1665
1908
  project: scope.project,
1666
1909
  query: primaryQuery,
1667
- top_k: this.topK,
1910
+ top_k: this.focusedTopK,
1668
1911
  include_memories: false,
1669
1912
  user_id: scope.userId,
1670
1913
  session_id: scope.sessionId,
1914
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1915
+ metadata_filter: focusedMetadataFilter,
1671
1916
  max_tokens: this.maxTokens,
1672
1917
  compress: true,
1673
1918
  compression_strategy: "adaptive"
1674
1919
  })),
1675
- this.runBranch("memory_primary", () => this.args.adapter.searchMemories({
1920
+ this.runBranch("memory_primary_focused", () => this.args.adapter.searchMemories({
1676
1921
  project: scope.project,
1677
1922
  query: primaryQuery,
1678
1923
  user_id: scope.userId,
1679
1924
  session_id: scope.sessionId,
1680
- top_k: this.topK,
1925
+ top_k: this.focusedTopK,
1681
1926
  include_pending: true,
1682
1927
  profile: "balanced"
1683
1928
  })),
1684
- taskFrameQuery ? this.runBranch("context_task_frame", () => this.args.adapter.query({
1929
+ taskFrameQuery ? this.runBranch("context_task_frame_focused", () => this.args.adapter.query({
1685
1930
  project: scope.project,
1686
1931
  query: taskFrameQuery,
1687
- top_k: this.topK,
1932
+ top_k: this.focusedTopK,
1688
1933
  include_memories: false,
1689
1934
  user_id: scope.userId,
1690
1935
  session_id: scope.sessionId,
1936
+ source_ids: focusedScope.sourceIds.length > 0 ? focusedScope.sourceIds : void 0,
1937
+ metadata_filter: focusedMetadataFilter,
1691
1938
  max_tokens: this.maxTokens,
1692
1939
  compress: true,
1693
1940
  compression_strategy: "adaptive"
1694
- })) : Promise.resolve({
1695
- name: "context_task_frame",
1696
- status: "skipped",
1697
- durationMs: 0
1698
- }),
1699
- taskFrameQuery ? this.runBranch("memory_task_frame", () => this.args.adapter.searchMemories({
1941
+ })) : Promise.resolve({ name: "context_task_frame_focused", status: "skipped", durationMs: 0 }),
1942
+ taskFrameQuery ? this.runBranch("memory_task_frame_focused", () => this.args.adapter.searchMemories({
1700
1943
  project: scope.project,
1701
1944
  query: taskFrameQuery,
1702
1945
  user_id: scope.userId,
1703
1946
  session_id: scope.sessionId,
1704
- top_k: this.topK,
1947
+ top_k: this.focusedTopK,
1705
1948
  include_pending: true,
1706
1949
  profile: "balanced"
1707
- })) : Promise.resolve({
1708
- name: "memory_task_frame",
1709
- status: "skipped",
1710
- durationMs: 0
1711
- })
1950
+ })) : Promise.resolve({ name: "memory_task_frame_focused", status: "skipped", durationMs: 0 })
1712
1951
  ]);
1713
- const branchStatus = {};
1714
- const collected = [];
1715
- let okCount = 0;
1716
- for (const branch of branches) {
1952
+ const focusedCollected = collectFromBranches(focusedBranches, "focused");
1953
+ const focusedRanked = this.rerank(focusedCollected.collected, focusedScope);
1954
+ const focusedFloored = this.applyRelevanceFloor(focusedRanked.items);
1955
+ let allCollected = [...focusedFloored.items];
1956
+ let totalOkCount = focusedCollected.okCount;
1957
+ let dedupedCount = focusedRanked.dedupedCount;
1958
+ let droppedBelowFloor = focusedFloored.dropped;
1959
+ const focusedTopScore = focusedFloored.items[0]?.score ?? 0;
1960
+ const fallbackUsed = focusedFloored.items.length < this.minFocusedResults || focusedTopScore < this.minFocusedTopScore;
1961
+ if (focusedScopeApplied) {
1962
+ this.sourceScopedTurns += 1;
1963
+ }
1964
+ if (!fallbackUsed) {
1965
+ this.focusedPassHits += 1;
1966
+ }
1967
+ const broadBranches = fallbackUsed ? await Promise.all([
1968
+ this.runBranch("context_primary_broad", () => this.args.adapter.query({
1969
+ project: scope.project,
1970
+ query: primaryQuery,
1971
+ top_k: this.broadTopK,
1972
+ include_memories: false,
1973
+ user_id: scope.userId,
1974
+ session_id: scope.sessionId,
1975
+ max_tokens: this.maxTokens,
1976
+ compress: true,
1977
+ compression_strategy: "adaptive"
1978
+ })),
1979
+ this.runBranch("memory_primary_broad", () => this.args.adapter.searchMemories({
1980
+ project: scope.project,
1981
+ query: primaryQuery,
1982
+ user_id: scope.userId,
1983
+ session_id: scope.sessionId,
1984
+ top_k: this.broadTopK,
1985
+ include_pending: true,
1986
+ profile: "balanced"
1987
+ })),
1988
+ taskFrameQuery ? this.runBranch("context_task_frame_broad", () => this.args.adapter.query({
1989
+ project: scope.project,
1990
+ query: taskFrameQuery,
1991
+ top_k: this.broadTopK,
1992
+ include_memories: false,
1993
+ user_id: scope.userId,
1994
+ session_id: scope.sessionId,
1995
+ max_tokens: this.maxTokens,
1996
+ compress: true,
1997
+ compression_strategy: "adaptive"
1998
+ })) : Promise.resolve({ name: "context_task_frame_broad", status: "skipped", durationMs: 0 }),
1999
+ taskFrameQuery ? this.runBranch("memory_task_frame_broad", () => this.args.adapter.searchMemories({
2000
+ project: scope.project,
2001
+ query: taskFrameQuery,
2002
+ user_id: scope.userId,
2003
+ session_id: scope.sessionId,
2004
+ top_k: this.broadTopK,
2005
+ include_pending: true,
2006
+ profile: "balanced"
2007
+ })) : Promise.resolve({ name: "memory_task_frame_broad", status: "skipped", durationMs: 0 })
2008
+ ]) : [
2009
+ { name: "context_primary_broad", status: "skipped", durationMs: 0 },
2010
+ { name: "memory_primary_broad", status: "skipped", durationMs: 0 },
2011
+ { name: "context_task_frame_broad", status: "skipped", durationMs: 0 },
2012
+ { name: "memory_task_frame_broad", status: "skipped", durationMs: 0 }
2013
+ ];
2014
+ const broadCollected = collectFromBranches(broadBranches, "broad");
2015
+ totalOkCount += broadCollected.okCount;
2016
+ if (fallbackUsed) {
2017
+ this.fallbackTriggers += 1;
2018
+ this.broadScopedTurns += 1;
2019
+ allCollected = [...allCollected, ...broadCollected.collected];
2020
+ }
2021
+ const ranked = this.rerank(allCollected, focusedScope);
2022
+ dedupedCount += ranked.dedupedCount;
2023
+ const floored = this.applyRelevanceFloor(ranked.items);
2024
+ droppedBelowFloor += floored.dropped;
2025
+ this.floorDroppedCount += droppedBelowFloor;
2026
+ this.droppedCount += droppedBelowFloor;
2027
+ const finalItems = floored.items.slice(0, this.broadTopK);
2028
+ this.injectedItemCount += finalItems.length;
2029
+ this.totalTurns += 1;
2030
+ const executedBranches = [...focusedBranches, ...broadBranches].filter((branch) => branch.status !== "skipped");
2031
+ for (const branch of [...focusedBranches, ...broadBranches]) {
1717
2032
  branchStatus[branch.name] = branch.status;
1718
- if (branch.status !== "ok") {
1719
- if (branch.status !== "skipped" && branch.reason) warnings.push(`${branch.name}:${branch.reason}`);
1720
- continue;
1721
- }
1722
- okCount += 1;
1723
- if (branch.name.startsWith("context")) {
1724
- collected.push(...this.contextItems(
1725
- branch.value,
1726
- branch.name.includes("task_frame") ? "task_frame" : "primary"
1727
- ));
1728
- } else {
1729
- collected.push(...this.memoryItems(
1730
- branch.value,
1731
- branch.name.includes("task_frame") ? "task_frame" : "primary"
1732
- ));
1733
- }
1734
2033
  }
1735
- const ranked = this.rerank(collected).slice(0, this.topK * 2);
1736
2034
  const prepared = {
1737
2035
  scope,
1738
2036
  retrieval: {
1739
2037
  primaryQuery,
1740
2038
  taskFrameQuery,
1741
2039
  warnings,
1742
- degraded: okCount < branches.filter((branch) => branch.status !== "skipped").length,
1743
- degradedReason: okCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
2040
+ degraded: totalOkCount < executedBranches.length,
2041
+ degradedReason: totalOkCount === 0 ? "all_retrieval_failed" : warnings.length > 0 ? "partial_retrieval_failed" : void 0,
1744
2042
  durationMs: Date.now() - startedAt,
1745
2043
  targetBudgetMs: this.targetRetrievalMs,
1746
2044
  hardTimeoutMs: this.hardRetrievalTimeoutMs,
1747
- branchStatus
2045
+ branchStatus,
2046
+ focusedScopeApplied,
2047
+ focusedSourceIds: focusedScope.sourceIds,
2048
+ focusedFileHints: focusedScope.fileHints.map((value) => pathBase(value)),
2049
+ clientScoped: Boolean(focusedScope.clientName),
2050
+ fallbackUsed,
2051
+ droppedBelowFloor,
2052
+ dedupedCount
1748
2053
  },
1749
- context: this.buildContext(ranked),
1750
- items: ranked
2054
+ context: this.buildContext(finalItems),
2055
+ items: finalItems
1751
2056
  };
1752
2057
  this.lastPreparedTurn = prepared.retrieval;
1753
2058
  return prepared;
@@ -1842,7 +2147,14 @@ ${lines.join("\n")}`;
1842
2147
  counters: {
1843
2148
  mergedCount: this.mergedCount,
1844
2149
  droppedCount: this.droppedCount,
1845
- bufferedLowSalience: this.bufferedLowSalience.length
2150
+ bufferedLowSalience: this.bufferedLowSalience.length,
2151
+ focusedPassHits: this.focusedPassHits,
2152
+ fallbackTriggers: this.fallbackTriggers,
2153
+ floorDroppedCount: this.floorDroppedCount,
2154
+ injectedItemCount: this.injectedItemCount,
2155
+ sourceScopedTurns: this.sourceScopedTurns,
2156
+ broadScopedTurns: this.broadScopedTurns,
2157
+ totalTurns: this.totalTurns
1846
2158
  }
1847
2159
  };
1848
2160
  }
@@ -3421,9 +3733,10 @@ function createWhisperMcpRuntimeClient(options) {
3421
3733
  }
3422
3734
  var whisper = createWhisperMcpClient();
3423
3735
  var runtimeClient = createWhisperMcpRuntimeClient();
3736
+ var runtimeSessions = /* @__PURE__ */ new Map();
3424
3737
  var server = new McpServer({
3425
3738
  name: "whisper-context",
3426
- version: "0.2.8"
3739
+ version: "0.3.0"
3427
3740
  });
3428
3741
  function createMcpServer() {
3429
3742
  return server;
@@ -3485,7 +3798,7 @@ function getWorkspaceIdForPath(path, workspaceId) {
3485
3798
  const seed = `${path}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12)}`;
3486
3799
  return createHash("sha256").update(seed).digest("hex").slice(0, 20);
3487
3800
  }
3488
- function clamp01(value) {
3801
+ function clamp012(value) {
3489
3802
  if (Number.isNaN(value)) return 0;
3490
3803
  if (value < 0) return 0;
3491
3804
  if (value > 1) return 1;
@@ -3531,7 +3844,7 @@ function toEvidenceRef(source, workspaceId, methodFallback) {
3531
3844
  line_start: lineStart,
3532
3845
  ...lineEnd ? { line_end: lineEnd } : {},
3533
3846
  snippet: String(source.content || metadata.snippet || "").slice(0, 500),
3534
- score: clamp01(Number(source.score ?? metadata.score ?? 0)),
3847
+ score: clamp012(Number(source.score ?? metadata.score ?? 0)),
3535
3848
  retrieval_method: retrievalMethod,
3536
3849
  indexed_at: String(metadata.indexed_at || (/* @__PURE__ */ new Date()).toISOString()),
3537
3850
  ...metadata.commit ? { commit: String(metadata.commit) } : {},
@@ -3625,18 +3938,55 @@ async function prepareAutomaticQuery(params) {
3625
3938
  throw new Error("Whisper runtime client unavailable.");
3626
3939
  }
3627
3940
  const scope = resolveMcpScope(params);
3628
- const runtime = runtimeClient.createAgentRuntime({
3629
- project: params.project,
3630
- userId: scope.userId,
3631
- sessionId: scope.sessionId,
3632
- workspacePath: scope.workspacePath,
3633
- topK: params.top_k,
3634
- clientName: "whisper-mcp"
3635
- });
3941
+ const key = [
3942
+ params.project || DEFAULT_PROJECT || "",
3943
+ scope.userId,
3944
+ scope.sessionId,
3945
+ scope.workspacePath || process.cwd(),
3946
+ String(params.top_k || 10)
3947
+ ].join("|");
3948
+ let runtime = runtimeSessions.get(key);
3949
+ if (!runtime) {
3950
+ runtime = runtimeClient.createAgentRuntime({
3951
+ project: params.project,
3952
+ userId: scope.userId,
3953
+ sessionId: scope.sessionId,
3954
+ workspacePath: scope.workspacePath,
3955
+ topK: params.top_k,
3956
+ clientName: "whisper-mcp"
3957
+ });
3958
+ runtimeSessions.set(key, runtime);
3959
+ }
3636
3960
  return runtime.beforeTurn({
3637
3961
  userMessage: params.query
3638
3962
  });
3639
3963
  }
3964
+ function noteAutomaticSourceActivity(params) {
3965
+ if (!runtimeClient) return;
3966
+ const sourceIds = [...new Set((params.sourceIds || []).map((value) => String(value || "").trim()).filter(Boolean))];
3967
+ if (sourceIds.length === 0) return;
3968
+ const scope = resolveMcpScope(params);
3969
+ const key = [
3970
+ params.project || DEFAULT_PROJECT || "",
3971
+ scope.userId,
3972
+ scope.sessionId,
3973
+ scope.workspacePath || process.cwd(),
3974
+ String(params.top_k || 10)
3975
+ ].join("|");
3976
+ let runtime = runtimeSessions.get(key);
3977
+ if (!runtime) {
3978
+ runtime = runtimeClient.createAgentRuntime({
3979
+ project: params.project,
3980
+ userId: scope.userId,
3981
+ sessionId: scope.sessionId,
3982
+ workspacePath: scope.workspacePath,
3983
+ topK: params.top_k,
3984
+ clientName: "whisper-mcp"
3985
+ });
3986
+ runtimeSessions.set(key, runtime);
3987
+ }
3988
+ runtime.noteSourceActivity(sourceIds);
3989
+ }
3640
3990
  function buildAbstain(args) {
3641
3991
  return {
3642
3992
  status: "abstained",
@@ -4476,29 +4826,67 @@ server.tool(
4476
4826
  }
4477
4827
  const automaticMode = include_memories !== false && include_graph !== true && !(chunk_types && chunk_types.length > 0) && max_tokens === void 0 && runtimeClient;
4478
4828
  if (automaticMode) {
4479
- const prepared = await prepareAutomaticQuery({
4480
- project: resolvedProject,
4481
- query,
4482
- top_k,
4483
- user_id,
4484
- session_id
4485
- });
4486
- if (!prepared.items.length) {
4487
- return { content: [{ type: "text", text: "No relevant context found." }] };
4488
- }
4489
- const warnings = prepared.retrieval.warnings.length ? `
4829
+ try {
4830
+ const prepared = await prepareAutomaticQuery({
4831
+ project: resolvedProject,
4832
+ query,
4833
+ top_k,
4834
+ user_id,
4835
+ session_id
4836
+ });
4837
+ if (!prepared.items.length) {
4838
+ return { content: [{ type: "text", text: "No relevant context found." }] };
4839
+ }
4840
+ const warnings = prepared.retrieval.warnings.length ? `
4490
4841
 
4491
4842
  [automatic_runtime]
4492
4843
  ${prepared.retrieval.warnings.join("\n")}` : "";
4493
- const scope2 = `project=${prepared.scope.project} user=${prepared.scope.userId} session=${prepared.scope.sessionId}`;
4494
- return {
4495
- content: [{
4496
- type: "text",
4497
- text: `Found ${prepared.items.length} runtime-ranked items (${prepared.retrieval.durationMs}ms, ${scope2}):
4844
+ const diagnostics = [
4845
+ `focused_scope=${prepared.retrieval.focusedScopeApplied}`,
4846
+ `fallback_used=${prepared.retrieval.fallbackUsed}`,
4847
+ `deduped=${prepared.retrieval.dedupedCount}`,
4848
+ `dropped_below_floor=${prepared.retrieval.droppedBelowFloor}`
4849
+ ].join(" ");
4850
+ const scope2 = `project=${prepared.scope.project} user=${prepared.scope.userId} session=${prepared.scope.sessionId}`;
4851
+ return {
4852
+ content: [{
4853
+ type: "text",
4854
+ text: `Found ${prepared.items.length} runtime-ranked items (${prepared.retrieval.durationMs}ms, ${scope2}, ${diagnostics}):
4498
4855
 
4499
4856
  ${prepared.context}${warnings}`
4500
- }]
4501
- };
4857
+ }]
4858
+ };
4859
+ } catch (error) {
4860
+ const automaticWarning = `Automatic runtime unavailable: ${error.message}. Falling back to broad/manual query path.`;
4861
+ const queryResult2 = await queryWithDegradedFallback({
4862
+ project: resolvedProject,
4863
+ query,
4864
+ top_k,
4865
+ include_memories: include_memories === true,
4866
+ include_graph,
4867
+ user_id: user_id || resolveMcpScope({ user_id }).userId,
4868
+ session_id: session_id || resolveMcpScope({ session_id }).sessionId
4869
+ });
4870
+ const response2 = queryResult2.response;
4871
+ if (response2.results.length === 0) {
4872
+ return { content: [{ type: "text", text: `No relevant context found.
4873
+
4874
+ [automatic_runtime]
4875
+ ${automaticWarning}` }] };
4876
+ }
4877
+ const scope2 = resolveMcpScope({ user_id, session_id });
4878
+ const header2 = `Found ${response2.meta.total} results (${response2.meta.latency_ms}ms${response2.meta.cache_hit ? ", cached" : ""}, project=${resolvedProject}, user=${scope2.userId}, session=${scope2.sessionId}):
4879
+
4880
+ `;
4881
+ const suffix2 = queryResult2.degraded_mode ? `
4882
+
4883
+ [degraded_mode=true] ${queryResult2.degraded_reason}
4884
+ Recommendation: ${queryResult2.recommendation}` : "";
4885
+ return { content: [{ type: "text", text: `${header2}${response2.context}
4886
+
4887
+ [automatic_runtime]
4888
+ ${automaticWarning}${suffix2}` }] };
4889
+ }
4502
4890
  }
4503
4891
  const queryResult = await queryWithDegradedFallback({
4504
4892
  project: resolvedProject,
@@ -4722,6 +5110,10 @@ server.tool(
4722
5110
  max_duration_minutes: input.max_duration_minutes,
4723
5111
  max_chunks: input.max_chunks
4724
5112
  });
5113
+ noteAutomaticSourceActivity({
5114
+ project: resolvedProject,
5115
+ sourceIds: [result.source_id]
5116
+ });
4725
5117
  return toTextResult(result);
4726
5118
  } catch (error) {
4727
5119
  return { content: [{ type: "text", text: `Error: ${error.message}` }] };
@@ -4810,6 +5202,11 @@ server.tool(
4810
5202
  strategy_override,
4811
5203
  profile_config
4812
5204
  });
5205
+ const sourceId = result?.source_id || result?.id;
5206
+ noteAutomaticSourceActivity({
5207
+ project: resolvedProject,
5208
+ sourceIds: sourceId ? [String(sourceId)] : []
5209
+ });
4813
5210
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
4814
5211
  }
4815
5212
  if (!content?.trim()) {
@@ -6244,6 +6641,10 @@ server.tool(
6244
6641
  max_chunks: input.max_chunks,
6245
6642
  auto_index: true
6246
6643
  });
6644
+ noteAutomaticSourceActivity({
6645
+ project: resolvedProject,
6646
+ sourceIds: [result.source_id]
6647
+ });
6247
6648
  return toTextResult(result);
6248
6649
  } catch (error) {
6249
6650
  return { content: [{ type: "text", text: `Error: ${error.message}` }] };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@usewhisper/mcp-server",
3
- "version": "2.5.0",
4
- "whisperContractVersion": "2026.03.09",
3
+ "version": "2.7.0",
4
+ "whisperContractVersion": "2026.03.10",
5
5
  "scripts": {
6
6
  "build": "tsup ../src/mcp/server.ts --format esm --out-dir dist",
7
7
  "prepublishOnly": "npm run build"