@usewhisper/mcp-server 1.4.0 → 2.0.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 +88 -18
  2. package/dist/server.js +383 -98
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -14,13 +14,18 @@ Whisper MCP is the universal context bridge for coding agents. It connects Claud
14
14
 
15
15
  ## Install
16
16
 
17
- ```bash
17
+ ```text
18
18
  npm i -g @usewhisper/mcp-server
19
19
  ```
20
20
 
21
+ ## Terminal vs MCP
22
+
23
+ Use the terminal commands only to install the server, print migration info, or generate client config.
24
+ Once the server is running, agents should call MCP tools like `search`, `remember`, `learn`, and `share_context`, not shell commands.
25
+
21
26
  ## Required Environment
22
27
 
23
- - `WHISPER_API_KEY` (required)
28
+ - `WHISPER_API_KEY` (required, use your `wsk_*` API key)
24
29
  - `WHISPER_PROJECT` (optional default)
25
30
  - `WHISPER_BASE_URL` (optional, defaults to `https://context.usewhisper.dev`)
26
31
  - `WHISPER_MCP_MODE` (optional: `remote|local|auto`, default `remote`)
@@ -28,6 +33,22 @@ npm i -g @usewhisper/mcp-server
28
33
  - `SLACK_BOT_TOKEN` (required for Slack connector runs)
29
34
  - `SLACK_CHANNEL_ID` (required for Slack connector runs)
30
35
 
36
+ Example values:
37
+
38
+ ```text
39
+ WHISPER_API_KEY=wsk_your_api_key_here
40
+ WHISPER_PROJECT=my-project
41
+ WHISPER_BASE_URL=https://context.usewhisper.dev
42
+ ```
43
+
44
+ Example shell setup:
45
+
46
+ ```text
47
+ export WHISPER_API_KEY="wsk_your_api_key_here"
48
+ export WHISPER_PROJECT="my-project"
49
+ export WHISPER_BASE_URL="https://context.usewhisper.dev"
50
+ ```
51
+
31
52
  ## Connector Status + Credentials
32
53
 
33
54
  | Connector | Status | Minimum required config/creds |
@@ -65,21 +86,52 @@ npm i -g @usewhisper/mcp-server
65
86
  22. `code.search_text`
66
87
  23. `code.search_semantic`
67
88
 
68
- ## Agent-Friendly Aliases
89
+ ## Agent-Friendly Primary Verbs
69
90
 
70
- These aliases sit on top of the canonical namespaced tools and are easier for coding agents to pick correctly:
91
+ These verbs are the primary MCP interface for agents. Namespaced tools remain available as compatibility and advanced surfaces.
71
92
 
72
- - `search` -> `context.query`
93
+ - `search` -> exact fetch by `id`, semantic retrieval by `query`, or hybrid retrieval when both are present
73
94
  - `search_code` -> `code.search_semantic`
74
95
  - `grep` -> `code.search_text`
75
96
  - `read` -> local file read with optional line ranges
76
97
  - `explore` -> local repository tree browsing
77
98
  - `research` -> `research.oracle`
78
- - `index` -> source add or workspace refresh
79
99
  - `remember` -> `memory.add`
80
- - `recall` -> `memory.search`
100
+ - `record` -> `memory.ingest_conversation`
101
+ - `learn` -> `context.add_text | context.add_source | context.add_document`
81
102
  - `share_context` -> `context.share`
82
103
 
104
+ `index` remains available as an advanced/admin compatibility tool for indexing jobs and workspace refresh operations.
105
+
106
+ ### `search` behavior
107
+
108
+ `search` is the only primary retrieval verb.
109
+
110
+ - `search({ id })` returns an exact memory fetch
111
+ - `search({ query })` returns semantic retrieval
112
+ - `search({ id, query })` returns the exact memory plus related retrieval context
113
+ - `search({})` is invalid and returns `{ "success": false, "error": { "code": "invalid_request", "message": "..." } }`
114
+
115
+ All primary verbs return structured JSON text payloads. Success payloads begin with `"success": true`; failures use the shared `{ success: false, error: { code, message } }` envelope.
116
+
117
+ All valid `search` calls return the same top-level payload shape:
118
+
119
+ ```json
120
+ {
121
+ "success": true,
122
+ "mode": "exact | semantic | hybrid",
123
+ "query": "string | null",
124
+ "id": "string | null",
125
+ "exact_memory": {},
126
+ "context": "string",
127
+ "results": [],
128
+ "count": 0,
129
+ "degraded_mode": false,
130
+ "degraded_reason": null,
131
+ "warnings": []
132
+ }
133
+ ```
134
+
83
135
  ## Source Contract (`context.add_source`)
84
136
 
85
137
  Input:
@@ -100,26 +152,46 @@ Output:
100
152
 
101
153
  ## Scoped MCP Generator
102
154
 
103
- ```bash
155
+ Print config to stdout:
156
+
157
+ ```text
104
158
  whisper-context-mcp scope --project my-project --source github --client claude
105
159
  ```
106
160
 
107
- Optional write:
161
+ Write the generated config to a file:
108
162
 
109
- ```bash
110
- whisper-context-mcp scope --project my-project --source github --client vscode --write "$HOME/.config/Code/User/mcp.json"
163
+ ```text
164
+ whisper-context-mcp scope --project my-project --source github --client vscode --write /absolute/path/to/mcp.json
111
165
  ```
112
166
 
113
167
  ## Breaking Rename Migration
114
168
 
115
169
  Print full map:
116
170
 
117
- ```bash
171
+ ```text
118
172
  whisper-context-mcp --print-tool-map
119
173
  ```
120
174
 
121
175
  This release removes legacy un-namespaced tool names.
122
176
 
177
+ ## Contract Metadata
178
+
179
+ Public API metadata is available at:
180
+
181
+ ```text
182
+ GET /v1/contracts/meta
183
+ ```
184
+
185
+ It exposes:
186
+
187
+ - current contract version
188
+ - active surfaces (`http`, `sdk`, `mcp`)
189
+ - approved primary MCP verbs
190
+ - migration window and removal policy
191
+ - deprecated HTTP routes with replacement hints
192
+
193
+ It does not expose internal security checklist fields.
194
+
123
195
  ## Security Defaults
124
196
 
125
197
  Local ingest (`index.local_scan_ingest` and `type=local`) enforces:
@@ -129,18 +201,16 @@ Local ingest (`index.local_scan_ingest` and `type=local`) enforces:
129
201
 
130
202
  ## 30-Second Demo
131
203
 
132
- One-command wizard + production MCP connectors + scoped config:
204
+ One command:
133
205
 
134
- ```bash
206
+ ```text
135
207
  npx whisper-wizard
136
208
  ```
137
209
 
138
210
  Flow:
139
211
  1. Run wizard and complete auth/project setup.
140
- 2. Add a source with MCP:
141
- `context.add_source` (for example GitHub/Web/PDF/Slack/Local/Video).
142
- 3. Ask a grounded question:
143
- `context.query` or `context.evidence_answer` for citation-locked output.
212
+ 2. Add a source with MCP: `context.add_source`
213
+ 3. Ask a grounded question: `context.query` or `context.evidence_answer`
144
214
 
145
215
  ## License
146
216
 
package/dist/server.js CHANGED
@@ -488,7 +488,7 @@ var WhisperContext = class _WhisperContext {
488
488
  });
489
489
  warnDeprecatedOnce(
490
490
  "whisper_context_class",
491
- "[Whisper SDK] WhisperContext remains supported in v2 but is legacy. Prefer WhisperClient for runtime features (queue/cache/session/diagnostics)."
491
+ "[Whisper SDK] WhisperContext is deprecated in v3 and scheduled for removal in v4. Prefer WhisperClient for runtime features and future contract compatibility."
492
492
  );
493
493
  }
494
494
  withProject(project) {
@@ -1090,6 +1090,16 @@ var WhisperContext = class _WhisperContext {
1090
1090
  }
1091
1091
  });
1092
1092
  }
1093
+ async getMemory(memoryId) {
1094
+ try {
1095
+ return await this.request(`/v1/memory/${memoryId}`);
1096
+ } catch (error) {
1097
+ if (!this.isEndpointNotFoundError(error)) {
1098
+ throw error;
1099
+ }
1100
+ return this.request(`/v1/memories/${memoryId}`);
1101
+ }
1102
+ }
1093
1103
  async getMemoryVersions(memoryId) {
1094
1104
  return this.request(`/v1/memory/${memoryId}/versions`);
1095
1105
  }
@@ -1288,6 +1298,7 @@ var WhisperContext = class _WhisperContext {
1288
1298
  ingestSession: (params) => this.ingestSession(params),
1289
1299
  getSessionMemories: (params) => this.getSessionMemories(params),
1290
1300
  getUserProfile: (params) => this.getUserProfile(params),
1301
+ get: (memoryId) => this.getMemory(memoryId),
1291
1302
  getVersions: (memoryId) => this.getMemoryVersions(memoryId),
1292
1303
  update: (memoryId, params) => this.updateMemory(memoryId, params),
1293
1304
  delete: (memoryId) => this.deleteMemory(memoryId),
@@ -1321,6 +1332,75 @@ var WhisperContext = class _WhisperContext {
1321
1332
  };
1322
1333
  };
1323
1334
 
1335
+ // ../src/mcp/search-payload.mjs
1336
+ function normalizeExactMemory(memory) {
1337
+ if (!memory) return null;
1338
+ return {
1339
+ id: memory.id ? String(memory.id) : null,
1340
+ type: memory.type ? String(memory.type) : memory.memoryType ? String(memory.memoryType) : null,
1341
+ content: String(memory.content || ""),
1342
+ user_id: memory.user_id ? String(memory.user_id) : memory.userId ? String(memory.userId) : null,
1343
+ session_id: memory.session_id ? String(memory.session_id) : memory.sessionId ? String(memory.sessionId) : null,
1344
+ updated_at: memory.updated_at ? String(memory.updated_at) : memory.updatedAt ? String(memory.updatedAt) : null,
1345
+ metadata: memory.metadata && typeof memory.metadata === "object" && !Array.isArray(memory.metadata) ? memory.metadata : null
1346
+ };
1347
+ }
1348
+ function normalizeSearchResults(results) {
1349
+ return (results || []).map((result) => {
1350
+ const candidate = result?.memory ? result.memory : result;
1351
+ const similarity = result?.similarity;
1352
+ return {
1353
+ id: candidate?.id ? String(candidate.id) : null,
1354
+ content: String(candidate?.content || result?.content || ""),
1355
+ score: typeof similarity === "number" ? similarity : typeof result?.score === "number" ? result.score : similarity != null ? Number(similarity) : result?.score != null ? Number(result.score) : null,
1356
+ source: result?.source ? String(result.source) : result?.chunk ? "memory" : null,
1357
+ document: result?.document ? String(result.document) : result?.chunk?.id ? String(result.chunk.id) : null,
1358
+ metadata: result?.metadata && typeof result.metadata === "object" && !Array.isArray(result.metadata) ? result.metadata : candidate?.metadata && typeof candidate.metadata === "object" && !Array.isArray(candidate.metadata) ? candidate.metadata : null,
1359
+ memory_type: candidate?.type ? String(candidate.type) : candidate?.memory_type ? String(candidate.memory_type) : null
1360
+ };
1361
+ });
1362
+ }
1363
+ function normalizeCanonicalResults(input) {
1364
+ if (Array.isArray(input?.results)) return input.results;
1365
+ if (Array.isArray(input?.memories)) return input.memories;
1366
+ return [];
1367
+ }
1368
+ function buildPrimaryToolSuccess(payload) {
1369
+ return {
1370
+ success: true,
1371
+ ...payload
1372
+ };
1373
+ }
1374
+ function buildPrimaryToolError(message, options = {}) {
1375
+ return {
1376
+ success: false,
1377
+ error: {
1378
+ code: options.code ?? "tool_error",
1379
+ message
1380
+ }
1381
+ };
1382
+ }
1383
+ function buildMcpSearchPayload(input) {
1384
+ const normalizedResults = normalizeSearchResults(input.results);
1385
+ return buildPrimaryToolSuccess({
1386
+ mode: input.mode,
1387
+ query: input.query ?? null,
1388
+ id: input.id ?? null,
1389
+ exact_memory: normalizeExactMemory(input.exactMemory),
1390
+ context: input.context ?? "",
1391
+ results: normalizedResults,
1392
+ count: normalizedResults.length,
1393
+ degraded_mode: Boolean(input.degradedMode),
1394
+ degraded_reason: input.degradedReason ?? null,
1395
+ warnings: input.warnings || []
1396
+ });
1397
+ }
1398
+ function buildMcpSearchError(message, options = {}) {
1399
+ return buildPrimaryToolError(message, {
1400
+ code: options.code ?? "invalid_request"
1401
+ });
1402
+ }
1403
+
1324
1404
  // ../src/mcp/server.ts
1325
1405
  var API_KEY = process.env.WHISPER_API_KEY || "";
1326
1406
  var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
@@ -1366,7 +1446,7 @@ var TOOL_MIGRATION_MAP = [
1366
1446
  { old: "semantic_search_codebase", next: "code.search_semantic" }
1367
1447
  ];
1368
1448
  var ALIAS_TOOL_MAP = [
1369
- { alias: "search", target: "context.query" },
1449
+ { alias: "search", target: "context.query | memory.get" },
1370
1450
  { alias: "search_code", target: "code.search_semantic" },
1371
1451
  { alias: "grep", target: "code.search_text" },
1372
1452
  { alias: "read", target: "local.file_read" },
@@ -1374,7 +1454,8 @@ var ALIAS_TOOL_MAP = [
1374
1454
  { alias: "research", target: "research.oracle" },
1375
1455
  { alias: "index", target: "context.add_source | index.workspace_run" },
1376
1456
  { alias: "remember", target: "memory.add" },
1377
- { alias: "recall", target: "memory.search" },
1457
+ { alias: "record", target: "memory.ingest_conversation" },
1458
+ { alias: "learn", target: "context.add_text | context.add_source | context.add_document" },
1378
1459
  { alias: "share_context", target: "context.share" }
1379
1460
  ];
1380
1461
  function ensureStateDir() {
@@ -1567,6 +1648,25 @@ function countCodeFiles(searchPath, maxFiles = 5e3) {
1567
1648
  function toTextResult(payload) {
1568
1649
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1569
1650
  }
1651
+ function primaryToolSuccess(payload) {
1652
+ return toTextResult(buildPrimaryToolSuccess(payload));
1653
+ }
1654
+ function primaryToolError(message, code = "tool_error") {
1655
+ return toTextResult(buildPrimaryToolError(message, { code }));
1656
+ }
1657
+ function formatCanonicalMemoryResults(rawResults) {
1658
+ const results = normalizeCanonicalResults(rawResults);
1659
+ return results.map((result) => {
1660
+ const memory = result?.memory || result;
1661
+ return {
1662
+ id: memory?.id ? String(memory.id) : null,
1663
+ content: String(memory?.content || result?.content || ""),
1664
+ memory_type: memory?.type ? String(memory.type) : memory?.memory_type ? String(memory.memory_type) : null,
1665
+ similarity: typeof result?.similarity === "number" ? result.similarity : typeof result?.score === "number" ? result.score : null,
1666
+ metadata: memory?.metadata && typeof memory.metadata === "object" && !Array.isArray(memory.metadata) ? memory.metadata : null
1667
+ };
1668
+ });
1669
+ }
1570
1670
  function likelyEmbeddingFailure(error) {
1571
1671
  const message = String(error?.message || error || "").toLowerCase();
1572
1672
  return message.includes("embedding") || message.includes("vector") || message.includes("timeout") || message.includes("timed out") || message.includes("temporarily unavailable");
@@ -1798,6 +1898,90 @@ async function createSourceByType(params) {
1798
1898
  warnings: []
1799
1899
  };
1800
1900
  }
1901
+ function normalizeRecordMessages(input) {
1902
+ if (Array.isArray(input.messages) && input.messages.length > 0) {
1903
+ return input.messages.map((message) => ({
1904
+ role: message.role || "user",
1905
+ content: message.content,
1906
+ timestamp: message.timestamp || (/* @__PURE__ */ new Date()).toISOString()
1907
+ }));
1908
+ }
1909
+ if (!input.content) {
1910
+ throw new Error("Provide messages[] or content.");
1911
+ }
1912
+ return [{
1913
+ role: input.role || "user",
1914
+ content: input.content,
1915
+ timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString()
1916
+ }];
1917
+ }
1918
+ async function learnFromInput(input) {
1919
+ const resolvedProject = await resolveProjectRef(input.project);
1920
+ if (!resolvedProject) {
1921
+ throw new Error("No project resolved. Set WHISPER_PROJECT or provide project.");
1922
+ }
1923
+ if (input.content) {
1924
+ const result = await whisper.addContext({
1925
+ project: resolvedProject,
1926
+ content: input.content,
1927
+ title: input.title || "Learned Context",
1928
+ metadata: input.metadata
1929
+ });
1930
+ return {
1931
+ mode: "text",
1932
+ project: resolvedProject,
1933
+ ingested: result.ingested
1934
+ };
1935
+ }
1936
+ if (input.owner && input.repo) {
1937
+ return createSourceByType({
1938
+ project: resolvedProject,
1939
+ type: "github",
1940
+ owner: input.owner,
1941
+ repo: input.repo,
1942
+ branch: input.branch,
1943
+ name: input.name,
1944
+ auto_index: true,
1945
+ metadata: input.metadata
1946
+ });
1947
+ }
1948
+ if (input.path) {
1949
+ return createSourceByType({
1950
+ project: resolvedProject,
1951
+ type: "local",
1952
+ path: input.path,
1953
+ glob: input.glob,
1954
+ max_files: input.max_files,
1955
+ name: input.name,
1956
+ metadata: input.metadata
1957
+ });
1958
+ }
1959
+ if (input.file_path) {
1960
+ return createSourceByType({
1961
+ project: resolvedProject,
1962
+ type: "pdf",
1963
+ file_path: input.file_path,
1964
+ name: input.name,
1965
+ metadata: input.metadata
1966
+ });
1967
+ }
1968
+ if (input.url) {
1969
+ return createSourceByType({
1970
+ project: resolvedProject,
1971
+ type: input.url.endsWith(".pdf") ? "pdf" : "web",
1972
+ url: input.url,
1973
+ name: input.name,
1974
+ metadata: input.metadata,
1975
+ crawl_depth: input.crawl_depth,
1976
+ channel_ids: input.channel_ids,
1977
+ token: input.token,
1978
+ workspace_id: input.workspace_id,
1979
+ since: input.since,
1980
+ auth_ref: input.auth_ref
1981
+ });
1982
+ }
1983
+ throw new Error("Provide content, owner+repo, path, file_path, or url.");
1984
+ }
1801
1985
  function scopeConfigJson(project, source, client) {
1802
1986
  const serverDef = {
1803
1987
  command: "npx",
@@ -2288,14 +2472,15 @@ server.tool(
2288
2472
  top_k,
2289
2473
  memory_types
2290
2474
  });
2291
- if (!results.memories || results.memories.length === 0) {
2292
- return { content: [{ type: "text", text: "No memories found." }] };
2293
- }
2294
- const text = results.memories.map((r, i) => `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
2295
- ${r.content}`).join("\n\n");
2296
- return { content: [{ type: "text", text }] };
2475
+ const normalizedResults = formatCanonicalMemoryResults(results);
2476
+ return primaryToolSuccess({
2477
+ tool: "memory.search",
2478
+ query,
2479
+ results: normalizedResults,
2480
+ count: normalizedResults.length
2481
+ });
2297
2482
  } catch (error) {
2298
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2483
+ return primaryToolError(error.message);
2299
2484
  }
2300
2485
  }
2301
2486
  );
@@ -2511,23 +2696,17 @@ server.tool(
2511
2696
  top_k,
2512
2697
  include_relations
2513
2698
  });
2514
- if (!results.memories || results.memories.length === 0) {
2515
- return { content: [{ type: "text", text: "No memories found." }] };
2516
- }
2517
- const text = results.memories.map((r, i) => {
2518
- let line = `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
2519
- `;
2520
- line += ` ${r.content}
2521
- `;
2522
- if (r.event_date) {
2523
- line += ` Event: ${new Date(r.event_date).toISOString().split("T")[0]}
2524
- `;
2525
- }
2526
- return line;
2527
- }).join("\n");
2528
- return { content: [{ type: "text", text }] };
2699
+ const normalizedResults = formatCanonicalMemoryResults(results);
2700
+ return primaryToolSuccess({
2701
+ tool: "memory.search_sota",
2702
+ query,
2703
+ question_date: question_date || null,
2704
+ include_relations,
2705
+ results: normalizedResults,
2706
+ count: normalizedResults.length
2707
+ });
2529
2708
  } catch (error) {
2530
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2709
+ return primaryToolError(error.message);
2531
2710
  }
2532
2711
  }
2533
2712
  );
@@ -3460,25 +3639,50 @@ server.tool(
3460
3639
  );
3461
3640
  server.tool(
3462
3641
  "search",
3463
- "Search indexed code, docs, and connected sources with one obvious verb. Use this first for most retrieval tasks.",
3642
+ "Search retrievable context by query, exact id, or both. Use `id` for exact fetch and `query` for semantic retrieval.",
3464
3643
  {
3465
3644
  project: z.string().optional().describe("Project name or slug"),
3466
- query: z.string().describe("What you want to find"),
3645
+ query: z.string().optional().describe("Semantic retrieval query"),
3646
+ id: z.string().optional().describe("Exact memory id to fetch"),
3467
3647
  top_k: z.number().optional().default(10),
3468
3648
  include_memories: z.boolean().optional().default(false),
3469
3649
  include_graph: z.boolean().optional().default(false),
3470
3650
  user_id: z.string().optional(),
3471
3651
  session_id: z.string().optional()
3472
3652
  },
3473
- async ({ project, query, top_k, include_memories, include_graph, user_id, session_id }) => {
3653
+ async ({ project, query, id, top_k, include_memories, include_graph, user_id, session_id }) => {
3474
3654
  try {
3655
+ if (!id && !query) {
3656
+ return toTextResult(buildMcpSearchError("Provide query, id, or both."));
3657
+ }
3658
+ let exactMemory;
3659
+ if (id) {
3660
+ const memoryResult = await whisper.getMemory(id);
3661
+ exactMemory = memoryResult?.memory || memoryResult;
3662
+ }
3663
+ if (!query) {
3664
+ return toTextResult(buildMcpSearchPayload({
3665
+ mode: "exact",
3666
+ id,
3667
+ exactMemory
3668
+ }));
3669
+ }
3475
3670
  const resolvedProject = await resolveProjectRef(project);
3476
3671
  if (!resolvedProject) {
3477
- return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
3672
+ if (exactMemory) {
3673
+ return toTextResult(buildMcpSearchPayload({
3674
+ mode: "hybrid",
3675
+ query,
3676
+ id,
3677
+ exactMemory,
3678
+ warnings: ["Project not resolved, so semantic context was skipped."]
3679
+ }));
3680
+ }
3681
+ return toTextResult(buildMcpSearchError("No project resolved. Set WHISPER_PROJECT or pass project."));
3478
3682
  }
3479
3683
  const queryResult = await queryWithDegradedFallback({
3480
3684
  project: resolvedProject,
3481
- query,
3685
+ query: query || exactMemory?.content || "",
3482
3686
  top_k,
3483
3687
  include_memories,
3484
3688
  include_graph,
@@ -3486,15 +3690,18 @@ server.tool(
3486
3690
  session_id
3487
3691
  });
3488
3692
  const response = queryResult.response;
3489
- if (!response.results?.length) {
3490
- return { content: [{ type: "text", text: `No relevant results found for "${query}".` }] };
3491
- }
3492
- const suffix = queryResult.degraded_mode ? `
3493
-
3494
- [degraded_mode=true] ${queryResult.degraded_reason}` : "";
3495
- return { content: [{ type: "text", text: response.context + suffix }] };
3693
+ return toTextResult(buildMcpSearchPayload({
3694
+ mode: exactMemory ? "hybrid" : "semantic",
3695
+ query,
3696
+ id,
3697
+ exactMemory,
3698
+ context: response.context,
3699
+ results: response.results,
3700
+ degradedMode: queryResult.degraded_mode,
3701
+ degradedReason: queryResult.degraded_reason
3702
+ }));
3496
3703
  } catch (error) {
3497
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3704
+ return primaryToolError(error.message);
3498
3705
  }
3499
3706
  }
3500
3707
  );
@@ -3534,7 +3741,13 @@ server.tool(
3534
3741
  }
3535
3742
  collect(rootPath);
3536
3743
  if (files.length === 0) {
3537
- return { content: [{ type: "text", text: `No code files found in ${rootPath}` }] };
3744
+ return primaryToolSuccess({
3745
+ tool: "search_code",
3746
+ query,
3747
+ path: rootPath,
3748
+ results: [],
3749
+ count: 0
3750
+ });
3538
3751
  }
3539
3752
  const documents = [];
3540
3753
  for (const filePath of files) {
@@ -3555,13 +3768,23 @@ server.tool(
3555
3768
  threshold: threshold ?? 0.2
3556
3769
  });
3557
3770
  if (!response.results?.length) {
3558
- return { content: [{ type: "text", text: `No semantically relevant files found for "${query}".` }] };
3771
+ return primaryToolSuccess({
3772
+ tool: "search_code",
3773
+ query,
3774
+ path: rootPath,
3775
+ results: [],
3776
+ count: 0
3777
+ });
3559
3778
  }
3560
- const lines = response.results.map((result) => `${result.id} (score: ${result.score})${result.snippet ? `
3561
- ${result.snippet}` : ""}`);
3562
- return { content: [{ type: "text", text: lines.join("\n\n") }] };
3779
+ return primaryToolSuccess({
3780
+ tool: "search_code",
3781
+ query,
3782
+ path: rootPath,
3783
+ results: response.results,
3784
+ count: response.results.length
3785
+ });
3563
3786
  } catch (error) {
3564
- return { content: [{ type: "text", text: `Semantic search failed: ${error.message}` }] };
3787
+ return primaryToolError(`Semantic search failed: ${error.message}`);
3565
3788
  }
3566
3789
  }
3567
3790
  );
@@ -3585,9 +3808,9 @@ server.tool(
3585
3808
  const stat = statSync(filePath);
3586
3809
  if (stat.size > 512 * 1024) continue;
3587
3810
  const text = readFileSync(filePath, "utf-8");
3588
- const lines2 = text.split("\n");
3811
+ const lines = text.split("\n");
3589
3812
  const matches = [];
3590
- lines2.forEach((line, index) => {
3813
+ lines.forEach((line, index) => {
3591
3814
  regex.lastIndex = 0;
3592
3815
  if (regex.test(line)) {
3593
3816
  matches.push({ line: index + 1, content: line.trimEnd() });
@@ -3600,14 +3823,21 @@ server.tool(
3600
3823
  }
3601
3824
  }
3602
3825
  if (!results.length) {
3603
- return { content: [{ type: "text", text: `No matches found for "${query}" in ${rootPath}` }] };
3826
+ return primaryToolSuccess({
3827
+ tool: "grep",
3828
+ query,
3829
+ path: rootPath,
3830
+ results: [],
3831
+ count: 0
3832
+ });
3604
3833
  }
3605
- const lines = results.flatMap((result) => [
3606
- `FILE ${result.file}`,
3607
- ...result.matches.map((match) => `L${match.line}: ${match.content}`),
3608
- ""
3609
- ]);
3610
- return { content: [{ type: "text", text: lines.join("\n") }] };
3834
+ return primaryToolSuccess({
3835
+ tool: "grep",
3836
+ query,
3837
+ path: rootPath,
3838
+ results,
3839
+ count: results.length
3840
+ });
3611
3841
  }
3612
3842
  );
3613
3843
  server.tool(
@@ -3623,11 +3853,17 @@ server.tool(
3623
3853
  const fullPath = path.includes(":") || path.startsWith("/") ? path : join(process.cwd(), path);
3624
3854
  const stats = statSync(fullPath);
3625
3855
  if (!stats.isFile()) {
3626
- return { content: [{ type: "text", text: `Error: ${path} is not a file.` }] };
3856
+ return primaryToolError(`${path} is not a file.`, "invalid_request");
3627
3857
  }
3628
- return { content: [{ type: "text", text: readFileWindow(fullPath, start_line, end_line) }] };
3858
+ return primaryToolSuccess({
3859
+ tool: "read",
3860
+ path: fullPath,
3861
+ start_line,
3862
+ end_line,
3863
+ content: readFileWindow(fullPath, start_line, end_line)
3864
+ });
3629
3865
  } catch (error) {
3630
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3866
+ return primaryToolError(error.message);
3631
3867
  }
3632
3868
  }
3633
3869
  );
@@ -3644,11 +3880,21 @@ server.tool(
3644
3880
  const rootPath = path || process.cwd();
3645
3881
  const tree = listTree(rootPath, max_depth, max_entries);
3646
3882
  if (!tree.length) {
3647
- return { content: [{ type: "text", text: `No visible files found in ${rootPath}` }] };
3883
+ return primaryToolSuccess({
3884
+ tool: "explore",
3885
+ path: rootPath,
3886
+ entries: [],
3887
+ count: 0
3888
+ });
3648
3889
  }
3649
- return { content: [{ type: "text", text: [`TREE ${rootPath}`, ...tree].join("\n") }] };
3890
+ return primaryToolSuccess({
3891
+ tool: "explore",
3892
+ path: rootPath,
3893
+ entries: tree,
3894
+ count: tree.length
3895
+ });
3650
3896
  } catch (error) {
3651
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3897
+ return primaryToolError(error.message);
3652
3898
  }
3653
3899
  }
3654
3900
  );
@@ -3665,17 +3911,16 @@ server.tool(
3665
3911
  async ({ project, query, mode, max_results, max_steps }) => {
3666
3912
  try {
3667
3913
  const results = await whisper.oracleSearch({ project, query, mode, max_results, max_steps });
3668
- if (mode === "research" && results.answer) {
3669
- return { content: [{ type: "text", text: results.answer }] };
3670
- }
3671
- if (!results.results?.length) {
3672
- return { content: [{ type: "text", text: "No research results found." }] };
3673
- }
3674
- const text = results.results.map((r, i) => `${i + 1}. ${r.path || r.source}
3675
- ${String(r.content || "").slice(0, 200)}...`).join("\n\n");
3676
- return { content: [{ type: "text", text }] };
3914
+ return primaryToolSuccess({
3915
+ tool: "research",
3916
+ mode,
3917
+ query,
3918
+ answer: results.answer || null,
3919
+ results: results.results || [],
3920
+ count: Array.isArray(results.results) ? results.results.length : 0
3921
+ });
3677
3922
  } catch (error) {
3678
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3923
+ return primaryToolError(error.message);
3679
3924
  }
3680
3925
  }
3681
3926
  );
@@ -3771,34 +4016,77 @@ server.tool(
3771
4016
  async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
3772
4017
  try {
3773
4018
  const result = await whisper.addMemory({ project, content, memory_type, user_id, session_id, agent_id, importance });
3774
- return { content: [{ type: "text", text: `Memory stored (id: ${result.id}, type: ${memory_type}).` }] };
4019
+ return primaryToolSuccess({
4020
+ tool: "remember",
4021
+ id: result.id || null,
4022
+ memory_type,
4023
+ stored: result.success === true
4024
+ });
3775
4025
  } catch (error) {
3776
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
4026
+ return primaryToolError(error.message);
3777
4027
  }
3778
4028
  }
3779
4029
  );
3780
4030
  server.tool(
3781
- "recall",
3782
- "Recall facts, decisions, and preferences from previous sessions or prior work.",
4031
+ "record",
4032
+ "Record what just happened in a session or conversation. Use this for temporal capture, not durable preference storage.",
3783
4033
  {
3784
4034
  project: z.string().optional(),
3785
- query: z.string().describe("What to recall"),
4035
+ session_id: z.string().describe("Session to record into"),
3786
4036
  user_id: z.string().optional(),
3787
- session_id: z.string().optional(),
3788
- top_k: z.number().optional().default(10),
3789
- memory_types: z.array(z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"])).optional()
4037
+ messages: z.array(z.object({
4038
+ role: z.string().optional(),
4039
+ content: z.string(),
4040
+ timestamp: z.string().optional()
4041
+ })).optional(),
4042
+ role: z.string().optional(),
4043
+ content: z.string().optional(),
4044
+ timestamp: z.string().optional()
3790
4045
  },
3791
- async ({ project, query, user_id, session_id, top_k, memory_types }) => {
4046
+ async ({ project, session_id, user_id, messages, role, content, timestamp }) => {
3792
4047
  try {
3793
- const results = await whisper.searchMemoriesSOTA({ project, query, user_id, session_id, top_k, memory_types });
3794
- if (!results.memories?.length) {
3795
- return { content: [{ type: "text", text: "No memories found." }] };
3796
- }
3797
- const text = results.memories.map((r, i) => `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
3798
- ${r.content}`).join("\n\n");
3799
- return { content: [{ type: "text", text }] };
4048
+ const normalizedMessages = normalizeRecordMessages({ messages, role, content, timestamp });
4049
+ const result = await whisper.ingestSession({ project, session_id, user_id, messages: normalizedMessages });
4050
+ return primaryToolSuccess({
4051
+ tool: "record",
4052
+ session_id,
4053
+ messages_recorded: normalizedMessages.length,
4054
+ memories_created: result.memories_created,
4055
+ relations_created: result.relations_created
4056
+ });
3800
4057
  } catch (error) {
3801
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
4058
+ return primaryToolError(error.message);
4059
+ }
4060
+ }
4061
+ );
4062
+ server.tool(
4063
+ "learn",
4064
+ "Learn content or connect a source so it becomes retrievable later. Use this for docs, URLs, repos, files, or local paths.",
4065
+ {
4066
+ project: z.string().optional(),
4067
+ content: z.string().optional().describe("Inline text content to ingest"),
4068
+ title: z.string().optional().describe("Title for inline content"),
4069
+ url: z.string().optional().describe("URL to learn from"),
4070
+ owner: z.string().optional().describe("GitHub owner"),
4071
+ repo: z.string().optional().describe("GitHub repository"),
4072
+ branch: z.string().optional(),
4073
+ path: z.string().optional().describe("Local path to learn from"),
4074
+ file_path: z.string().optional().describe("Single file path to learn from"),
4075
+ name: z.string().optional().describe("Optional source name"),
4076
+ metadata: z.record(z.string()).optional(),
4077
+ max_files: z.number().optional(),
4078
+ glob: z.string().optional(),
4079
+ crawl_depth: z.number().optional()
4080
+ },
4081
+ async (input) => {
4082
+ try {
4083
+ const result = await learnFromInput(input);
4084
+ return primaryToolSuccess({
4085
+ tool: "learn",
4086
+ ...result
4087
+ });
4088
+ } catch (error) {
4089
+ return primaryToolError(error.message);
3802
4090
  }
3803
4091
  }
3804
4092
  );
@@ -3814,18 +4102,15 @@ server.tool(
3814
4102
  async ({ project, session_id, title, expiry_days }) => {
3815
4103
  try {
3816
4104
  const result = await whisper.createSharedContext({ project, session_id, title, expiry_days });
3817
- return {
3818
- content: [{
3819
- type: "text",
3820
- text: `Shared context created:
3821
- - Share ID: ${result.share_id}
3822
- - Expires: ${result.expires_at || "Never"}
3823
-
3824
- Share URL: ${result.share_url}`
3825
- }]
3826
- };
4105
+ return primaryToolSuccess({
4106
+ tool: "share_context",
4107
+ share_id: result.share_id,
4108
+ share_url: result.share_url,
4109
+ expires_at: result.expires_at || null,
4110
+ title: result.title || title || null
4111
+ });
3827
4112
  } catch (error) {
3828
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
4113
+ return primaryToolError(error.message);
3829
4114
  }
3830
4115
  }
3831
4116
  );
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@usewhisper/mcp-server",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
+ "whisperContractVersion": "2026.03.09",
4
5
  "scripts": {
5
6
  "build": "tsup ../src/mcp/server.ts --format esm --out-dir dist",
6
7
  "prepublishOnly": "npm run build"