@usewhisper/mcp-server 1.4.0 → 2.1.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 +104 -18
  2. package/dist/server.js +535 -121
  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,52 @@ 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
+ OpenCode project config generation:
162
+
163
+ ```text
164
+ whisper-context-mcp scope --project my-project --source github --client opencode
165
+ ```
166
+
167
+ Write the generated config to a file:
108
168
 
109
- ```bash
110
- whisper-context-mcp scope --project my-project --source github --client vscode --write "$HOME/.config/Code/User/mcp.json"
169
+ ```text
170
+ whisper-context-mcp scope --project my-project --source github --client vscode --write /absolute/path/to/mcp.json
111
171
  ```
112
172
 
113
173
  ## Breaking Rename Migration
114
174
 
115
175
  Print full map:
116
176
 
117
- ```bash
177
+ ```text
118
178
  whisper-context-mcp --print-tool-map
119
179
  ```
120
180
 
121
181
  This release removes legacy un-namespaced tool names.
122
182
 
183
+ ## Contract Metadata
184
+
185
+ Public API metadata is available at:
186
+
187
+ ```text
188
+ GET /v1/contracts/meta
189
+ ```
190
+
191
+ It exposes:
192
+
193
+ - current contract version
194
+ - active surfaces (`http`, `sdk`, `mcp`)
195
+ - approved primary MCP verbs
196
+ - migration window and removal policy
197
+ - deprecated HTTP routes with replacement hints
198
+
199
+ It does not expose internal security checklist fields.
200
+
123
201
  ## Security Defaults
124
202
 
125
203
  Local ingest (`index.local_scan_ingest` and `type=local`) enforces:
@@ -129,18 +207,26 @@ Local ingest (`index.local_scan_ingest` and `type=local`) enforces:
129
207
 
130
208
  ## 30-Second Demo
131
209
 
132
- One-command wizard + production MCP connectors + scoped config:
210
+ One command:
133
211
 
134
- ```bash
212
+ ```text
135
213
  npx whisper-wizard
136
214
  ```
137
215
 
138
216
  Flow:
139
217
  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.
218
+ 2. Add a source with MCP: `context.add_source`
219
+ 3. Ask a grounded question: `context.query` or `context.evidence_answer`
220
+ 4. If you selected OpenCode, wizard also updates `opencode.json` and creates `.opencode/plugins/whisper-context.ts`
221
+
222
+ ## OpenCode Plugin Notes
223
+
224
+ When using OpenCode, the plugin runtime adds two OpenCode-only helper tools:
225
+
226
+ - `whisper_status`
227
+ - `whisper_flush_session`
228
+
229
+ These are plugin tools, not MCP tools. Continue using Whisper MCP for retrieval and persistence verbs.
144
230
 
145
231
  ## License
146
232
 
package/dist/server.js CHANGED
@@ -144,6 +144,7 @@ var RuntimeClient = class {
144
144
  diagnostics;
145
145
  inFlight = /* @__PURE__ */ new Map();
146
146
  sendApiKeyHeader;
147
+ fetchImpl;
147
148
  constructor(options, diagnostics) {
148
149
  if (!options.apiKey) {
149
150
  throw new RuntimeClientError({
@@ -168,6 +169,7 @@ var RuntimeClient = class {
168
169
  ...options.timeouts || {}
169
170
  };
170
171
  this.sendApiKeyHeader = process.env.WHISPER_SEND_X_API_KEY === "1";
172
+ this.fetchImpl = options.fetchImpl || fetch;
171
173
  this.diagnostics = diagnostics || new DiagnosticsStore(1e3);
172
174
  }
173
175
  getDiagnosticsStore() {
@@ -288,7 +290,7 @@ var RuntimeClient = class {
288
290
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
289
291
  try {
290
292
  const attachApiKeyHeader = this.shouldAttachApiKeyHeader(normalizedEndpoint);
291
- const response = await fetch(`${this.baseUrl}${normalizedEndpoint}`, {
293
+ const response = await this.fetchImpl(`${this.baseUrl}${normalizedEndpoint}`, {
292
294
  method,
293
295
  signal: controller.signal,
294
296
  keepalive: method !== "GET",
@@ -488,7 +490,7 @@ var WhisperContext = class _WhisperContext {
488
490
  });
489
491
  warnDeprecatedOnce(
490
492
  "whisper_context_class",
491
- "[Whisper SDK] WhisperContext remains supported in v2 but is legacy. Prefer WhisperClient for runtime features (queue/cache/session/diagnostics)."
493
+ "[Whisper SDK] WhisperContext is deprecated in v3 and scheduled for removal in v4. Prefer WhisperClient for runtime features and future contract compatibility."
492
494
  );
493
495
  }
494
496
  withProject(project) {
@@ -774,6 +776,9 @@ var WhisperContext = class _WhisperContext {
774
776
  if (params.auth_ref) config.auth_ref = params.auth_ref;
775
777
  }
776
778
  if (params.metadata) config.metadata = params.metadata;
779
+ if (params.ingestion_profile) config.ingestion_profile = params.ingestion_profile;
780
+ if (params.strategy_override) config.strategy_override = params.strategy_override;
781
+ if (params.profile_config) config.profile_config = params.profile_config;
777
782
  config.auto_index = params.auto_index ?? true;
778
783
  const created = await this.addSource(project, {
779
784
  name: params.name || `${params.type}-source-${Date.now()}`,
@@ -858,12 +863,37 @@ var WhisperContext = class _WhisperContext {
858
863
  write_mode: params.write_mode
859
864
  })
860
865
  });
861
- const id2 = direct?.memory?.id || direct?.id || direct?.memory_id || direct?.job_id;
866
+ const mode = direct?.mode === "async" ? "async" : direct?.mode === "sync" ? "sync" : void 0;
867
+ const memoryId = direct?.memory?.id || direct?.memory_id || (mode !== "async" ? direct?.id : void 0);
868
+ const jobId = direct?.job_id || (mode === "async" ? direct?.id : void 0);
869
+ const id2 = memoryId || jobId || "";
862
870
  if (id2) {
863
- return { id: id2, success: true, path: "sota", fallback_used: false };
871
+ return {
872
+ id: id2,
873
+ success: true,
874
+ path: "sota",
875
+ fallback_used: false,
876
+ mode,
877
+ ...memoryId ? { memory_id: memoryId } : {},
878
+ ...jobId ? { job_id: jobId } : {},
879
+ ...direct?.status_url ? { status_url: direct.status_url } : {},
880
+ ...direct?.accepted_at ? { accepted_at: direct.accepted_at } : {},
881
+ ...direct?.visibility_sla_ms ? { visibility_sla_ms: direct.visibility_sla_ms } : {},
882
+ ...direct?.pending_visibility !== void 0 ? { pending_visibility: Boolean(direct.pending_visibility) } : {}
883
+ };
864
884
  }
865
885
  if (direct?.success === true) {
866
- return { id: "", success: true, path: "sota", fallback_used: false };
886
+ return {
887
+ id: "",
888
+ success: true,
889
+ path: "sota",
890
+ fallback_used: false,
891
+ mode,
892
+ ...direct?.status_url ? { status_url: direct.status_url } : {},
893
+ ...direct?.accepted_at ? { accepted_at: direct.accepted_at } : {},
894
+ ...direct?.visibility_sla_ms ? { visibility_sla_ms: direct.visibility_sla_ms } : {},
895
+ ...direct?.pending_visibility !== void 0 ? { pending_visibility: Boolean(direct.pending_visibility) } : {}
896
+ };
867
897
  }
868
898
  } catch (error) {
869
899
  if (params.allow_legacy_fallback === false) {
@@ -891,7 +921,14 @@ var WhisperContext = class _WhisperContext {
891
921
  message: "Memory create succeeded but no memory id was returned by the API"
892
922
  });
893
923
  }
894
- return { id, success: true, path: "legacy", fallback_used: true };
924
+ return {
925
+ id,
926
+ success: true,
927
+ path: "legacy",
928
+ fallback_used: true,
929
+ mode: "sync",
930
+ memory_id: id
931
+ };
895
932
  });
896
933
  }
897
934
  async addMemoriesBulk(params) {
@@ -1090,6 +1127,16 @@ var WhisperContext = class _WhisperContext {
1090
1127
  }
1091
1128
  });
1092
1129
  }
1130
+ async getMemory(memoryId) {
1131
+ try {
1132
+ return await this.request(`/v1/memory/${memoryId}`);
1133
+ } catch (error) {
1134
+ if (!this.isEndpointNotFoundError(error)) {
1135
+ throw error;
1136
+ }
1137
+ return this.request(`/v1/memories/${memoryId}`);
1138
+ }
1139
+ }
1093
1140
  async getMemoryVersions(memoryId) {
1094
1141
  return this.request(`/v1/memory/${memoryId}/versions`);
1095
1142
  }
@@ -1288,6 +1335,7 @@ var WhisperContext = class _WhisperContext {
1288
1335
  ingestSession: (params) => this.ingestSession(params),
1289
1336
  getSessionMemories: (params) => this.getSessionMemories(params),
1290
1337
  getUserProfile: (params) => this.getUserProfile(params),
1338
+ get: (memoryId) => this.getMemory(memoryId),
1291
1339
  getVersions: (memoryId) => this.getMemoryVersions(memoryId),
1292
1340
  update: (memoryId, params) => this.updateMemory(memoryId, params),
1293
1341
  delete: (memoryId) => this.deleteMemory(memoryId),
@@ -1321,6 +1369,75 @@ var WhisperContext = class _WhisperContext {
1321
1369
  };
1322
1370
  };
1323
1371
 
1372
+ // ../src/mcp/search-payload.mjs
1373
+ function normalizeExactMemory(memory) {
1374
+ if (!memory) return null;
1375
+ return {
1376
+ id: memory.id ? String(memory.id) : null,
1377
+ type: memory.type ? String(memory.type) : memory.memoryType ? String(memory.memoryType) : null,
1378
+ content: String(memory.content || ""),
1379
+ user_id: memory.user_id ? String(memory.user_id) : memory.userId ? String(memory.userId) : null,
1380
+ session_id: memory.session_id ? String(memory.session_id) : memory.sessionId ? String(memory.sessionId) : null,
1381
+ updated_at: memory.updated_at ? String(memory.updated_at) : memory.updatedAt ? String(memory.updatedAt) : null,
1382
+ metadata: memory.metadata && typeof memory.metadata === "object" && !Array.isArray(memory.metadata) ? memory.metadata : null
1383
+ };
1384
+ }
1385
+ function normalizeSearchResults(results) {
1386
+ return (results || []).map((result) => {
1387
+ const candidate = result?.memory ? result.memory : result;
1388
+ const similarity = result?.similarity;
1389
+ return {
1390
+ id: candidate?.id ? String(candidate.id) : null,
1391
+ content: String(candidate?.content || result?.content || ""),
1392
+ score: typeof similarity === "number" ? similarity : typeof result?.score === "number" ? result.score : similarity != null ? Number(similarity) : result?.score != null ? Number(result.score) : null,
1393
+ source: result?.source ? String(result.source) : result?.chunk ? "memory" : null,
1394
+ document: result?.document ? String(result.document) : result?.chunk?.id ? String(result.chunk.id) : null,
1395
+ 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,
1396
+ memory_type: candidate?.type ? String(candidate.type) : candidate?.memory_type ? String(candidate.memory_type) : null
1397
+ };
1398
+ });
1399
+ }
1400
+ function normalizeCanonicalResults(input) {
1401
+ if (Array.isArray(input?.results)) return input.results;
1402
+ if (Array.isArray(input?.memories)) return input.memories;
1403
+ return [];
1404
+ }
1405
+ function buildPrimaryToolSuccess(payload) {
1406
+ return {
1407
+ success: true,
1408
+ ...payload
1409
+ };
1410
+ }
1411
+ function buildPrimaryToolError(message, options = {}) {
1412
+ return {
1413
+ success: false,
1414
+ error: {
1415
+ code: options.code ?? "tool_error",
1416
+ message
1417
+ }
1418
+ };
1419
+ }
1420
+ function buildMcpSearchPayload(input) {
1421
+ const normalizedResults = normalizeSearchResults(input.results);
1422
+ return buildPrimaryToolSuccess({
1423
+ mode: input.mode,
1424
+ query: input.query ?? null,
1425
+ id: input.id ?? null,
1426
+ exact_memory: normalizeExactMemory(input.exactMemory),
1427
+ context: input.context ?? "",
1428
+ results: normalizedResults,
1429
+ count: normalizedResults.length,
1430
+ degraded_mode: Boolean(input.degradedMode),
1431
+ degraded_reason: input.degradedReason ?? null,
1432
+ warnings: input.warnings || []
1433
+ });
1434
+ }
1435
+ function buildMcpSearchError(message, options = {}) {
1436
+ return buildPrimaryToolError(message, {
1437
+ code: options.code ?? "invalid_request"
1438
+ });
1439
+ }
1440
+
1324
1441
  // ../src/mcp/server.ts
1325
1442
  var API_KEY = process.env.WHISPER_API_KEY || "";
1326
1443
  var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
@@ -1328,15 +1445,25 @@ var BASE_URL = process.env.WHISPER_BASE_URL;
1328
1445
  var RUNTIME_MODE = (process.env.WHISPER_MCP_MODE || "remote").toLowerCase();
1329
1446
  var CLI_ARGS = process.argv.slice(2);
1330
1447
  var IS_MANAGEMENT_ONLY = CLI_ARGS.includes("--print-tool-map") || CLI_ARGS[0] === "scope";
1331
- var whisper = !IS_MANAGEMENT_ONLY && API_KEY ? new WhisperContext({
1332
- apiKey: API_KEY,
1333
- project: DEFAULT_PROJECT,
1334
- ...BASE_URL && { baseUrl: BASE_URL }
1335
- }) : null;
1448
+ function createWhisperMcpClient(options) {
1449
+ const apiKey = options?.apiKey ?? API_KEY;
1450
+ if (!apiKey || IS_MANAGEMENT_ONLY) {
1451
+ return null;
1452
+ }
1453
+ return new WhisperContext({
1454
+ apiKey,
1455
+ project: options?.project ?? DEFAULT_PROJECT,
1456
+ ...(options?.baseUrl ?? BASE_URL) && { baseUrl: options?.baseUrl ?? BASE_URL }
1457
+ });
1458
+ }
1459
+ var whisper = createWhisperMcpClient();
1336
1460
  var server = new McpServer({
1337
1461
  name: "whisper-context",
1338
1462
  version: "0.2.8"
1339
1463
  });
1464
+ function createMcpServer() {
1465
+ return server;
1466
+ }
1340
1467
  var STATE_DIR = join(homedir(), ".whisper-mcp");
1341
1468
  var STATE_PATH = join(STATE_DIR, "state.json");
1342
1469
  var AUDIT_LOG_PATH = join(STATE_DIR, "forget-audit.log");
@@ -1366,7 +1493,7 @@ var TOOL_MIGRATION_MAP = [
1366
1493
  { old: "semantic_search_codebase", next: "code.search_semantic" }
1367
1494
  ];
1368
1495
  var ALIAS_TOOL_MAP = [
1369
- { alias: "search", target: "context.query" },
1496
+ { alias: "search", target: "context.query | memory.get" },
1370
1497
  { alias: "search_code", target: "code.search_semantic" },
1371
1498
  { alias: "grep", target: "code.search_text" },
1372
1499
  { alias: "read", target: "local.file_read" },
@@ -1374,7 +1501,8 @@ var ALIAS_TOOL_MAP = [
1374
1501
  { alias: "research", target: "research.oracle" },
1375
1502
  { alias: "index", target: "context.add_source | index.workspace_run" },
1376
1503
  { alias: "remember", target: "memory.add" },
1377
- { alias: "recall", target: "memory.search" },
1504
+ { alias: "record", target: "memory.ingest_conversation" },
1505
+ { alias: "learn", target: "context.add_text | context.add_source | context.add_document" },
1378
1506
  { alias: "share_context", target: "context.share" }
1379
1507
  ];
1380
1508
  function ensureStateDir() {
@@ -1567,6 +1695,25 @@ function countCodeFiles(searchPath, maxFiles = 5e3) {
1567
1695
  function toTextResult(payload) {
1568
1696
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1569
1697
  }
1698
+ function primaryToolSuccess(payload) {
1699
+ return toTextResult(buildPrimaryToolSuccess(payload));
1700
+ }
1701
+ function primaryToolError(message, code = "tool_error") {
1702
+ return toTextResult(buildPrimaryToolError(message, { code }));
1703
+ }
1704
+ function formatCanonicalMemoryResults(rawResults) {
1705
+ const results = normalizeCanonicalResults(rawResults);
1706
+ return results.map((result) => {
1707
+ const memory = result?.memory || result;
1708
+ return {
1709
+ id: memory?.id ? String(memory.id) : null,
1710
+ content: String(memory?.content || result?.content || ""),
1711
+ memory_type: memory?.type ? String(memory.type) : memory?.memory_type ? String(memory.memory_type) : null,
1712
+ similarity: typeof result?.similarity === "number" ? result.similarity : typeof result?.score === "number" ? result.score : null,
1713
+ metadata: memory?.metadata && typeof memory.metadata === "object" && !Array.isArray(memory.metadata) ? memory.metadata : null
1714
+ };
1715
+ });
1716
+ }
1570
1717
  function likelyEmbeddingFailure(error) {
1571
1718
  const message = String(error?.message || error || "").toLowerCase();
1572
1719
  return message.includes("embedding") || message.includes("vector") || message.includes("timeout") || message.includes("timed out") || message.includes("temporarily unavailable");
@@ -1777,6 +1924,9 @@ async function createSourceByType(params) {
1777
1924
  if (params.max_chunks !== void 0) config.max_chunks = params.max_chunks;
1778
1925
  }
1779
1926
  if (params.metadata) config.metadata = params.metadata;
1927
+ if (params.ingestion_profile) config.ingestion_profile = params.ingestion_profile;
1928
+ if (params.strategy_override) config.strategy_override = params.strategy_override;
1929
+ if (params.profile_config) config.profile_config = params.profile_config;
1780
1930
  config.auto_index = params.auto_index ?? true;
1781
1931
  const created = await whisper.addSource(params.project, {
1782
1932
  name: params.name || `${params.type}-source-${Date.now()}`,
@@ -1798,7 +1948,109 @@ async function createSourceByType(params) {
1798
1948
  warnings: []
1799
1949
  };
1800
1950
  }
1801
- function scopeConfigJson(project, source, client) {
1951
+ function normalizeRecordMessages(input) {
1952
+ if (Array.isArray(input.messages) && input.messages.length > 0) {
1953
+ return input.messages.map((message) => ({
1954
+ role: message.role || "user",
1955
+ content: message.content,
1956
+ timestamp: message.timestamp || (/* @__PURE__ */ new Date()).toISOString()
1957
+ }));
1958
+ }
1959
+ if (!input.content) {
1960
+ throw new Error("Provide messages[] or content.");
1961
+ }
1962
+ return [{
1963
+ role: input.role || "user",
1964
+ content: input.content,
1965
+ timestamp: input.timestamp || (/* @__PURE__ */ new Date()).toISOString()
1966
+ }];
1967
+ }
1968
+ async function learnFromInput(input) {
1969
+ const resolvedProject = await resolveProjectRef(input.project);
1970
+ if (!resolvedProject) {
1971
+ throw new Error("No project resolved. Set WHISPER_PROJECT or provide project.");
1972
+ }
1973
+ if (input.content) {
1974
+ const mergedMetadata = {
1975
+ ...input.metadata || {},
1976
+ ...input.ingestion_profile ? { ingestion_profile: input.ingestion_profile } : {},
1977
+ ...input.strategy_override ? { strategy_override: input.strategy_override } : {},
1978
+ ...input.profile_config ? { profile_config: input.profile_config } : {}
1979
+ };
1980
+ const result = await whisper.addContext({
1981
+ project: resolvedProject,
1982
+ content: input.content,
1983
+ title: input.title || "Learned Context",
1984
+ metadata: mergedMetadata
1985
+ });
1986
+ return {
1987
+ mode: "text",
1988
+ project: resolvedProject,
1989
+ ingested: result.ingested
1990
+ };
1991
+ }
1992
+ if (input.owner && input.repo) {
1993
+ return createSourceByType({
1994
+ project: resolvedProject,
1995
+ type: "github",
1996
+ owner: input.owner,
1997
+ repo: input.repo,
1998
+ branch: input.branch,
1999
+ name: input.name,
2000
+ auto_index: true,
2001
+ metadata: input.metadata,
2002
+ ingestion_profile: input.ingestion_profile,
2003
+ strategy_override: input.strategy_override,
2004
+ profile_config: input.profile_config
2005
+ });
2006
+ }
2007
+ if (input.path) {
2008
+ return createSourceByType({
2009
+ project: resolvedProject,
2010
+ type: "local",
2011
+ path: input.path,
2012
+ glob: input.glob,
2013
+ max_files: input.max_files,
2014
+ name: input.name,
2015
+ metadata: input.metadata,
2016
+ ingestion_profile: input.ingestion_profile,
2017
+ strategy_override: input.strategy_override,
2018
+ profile_config: input.profile_config
2019
+ });
2020
+ }
2021
+ if (input.file_path) {
2022
+ return createSourceByType({
2023
+ project: resolvedProject,
2024
+ type: "pdf",
2025
+ file_path: input.file_path,
2026
+ name: input.name,
2027
+ metadata: input.metadata,
2028
+ ingestion_profile: input.ingestion_profile,
2029
+ strategy_override: input.strategy_override,
2030
+ profile_config: input.profile_config
2031
+ });
2032
+ }
2033
+ if (input.url) {
2034
+ return createSourceByType({
2035
+ project: resolvedProject,
2036
+ type: input.url.endsWith(".pdf") ? "pdf" : "web",
2037
+ url: input.url,
2038
+ name: input.name,
2039
+ metadata: input.metadata,
2040
+ ingestion_profile: input.ingestion_profile,
2041
+ strategy_override: input.strategy_override,
2042
+ profile_config: input.profile_config,
2043
+ crawl_depth: input.crawl_depth,
2044
+ channel_ids: input.channel_ids,
2045
+ token: input.token,
2046
+ workspace_id: input.workspace_id,
2047
+ since: input.since,
2048
+ auth_ref: input.auth_ref
2049
+ });
2050
+ }
2051
+ throw new Error("Provide content, owner+repo, path, file_path, or url.");
2052
+ }
2053
+ function renderScopedMcpConfig(project, source, client) {
1802
2054
  const serverDef = {
1803
2055
  command: "npx",
1804
2056
  args: ["-y", "@usewhisper/mcp-server"],
@@ -2261,7 +2513,12 @@ server.tool(
2261
2513
  agent_id,
2262
2514
  importance
2263
2515
  });
2264
- return { content: [{ type: "text", text: `Memory stored (id: ${result.id}, type: ${memory_type}).` }] };
2516
+ const memoryId = result?.memory_id || result.id;
2517
+ const jobId = result?.job_id;
2518
+ const mode = result?.mode;
2519
+ const typeLabel = memory_type || "factual";
2520
+ const text = mode === "async" || jobId ? `Memory queued (job_id: ${jobId || result.id}, type: ${typeLabel}).` : `Memory stored (id: ${memoryId}, type: ${typeLabel}).`;
2521
+ return { content: [{ type: "text", text }] };
2265
2522
  } catch (error) {
2266
2523
  return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2267
2524
  }
@@ -2288,14 +2545,15 @@ server.tool(
2288
2545
  top_k,
2289
2546
  memory_types
2290
2547
  });
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 }] };
2548
+ const normalizedResults = formatCanonicalMemoryResults(results);
2549
+ return primaryToolSuccess({
2550
+ tool: "memory.search",
2551
+ query,
2552
+ results: normalizedResults,
2553
+ count: normalizedResults.length
2554
+ });
2297
2555
  } catch (error) {
2298
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2556
+ return primaryToolError(error.message);
2299
2557
  }
2300
2558
  }
2301
2559
  );
@@ -2337,6 +2595,9 @@ server.tool(
2337
2595
  name: z.string().optional(),
2338
2596
  auto_index: z.boolean().optional().default(true),
2339
2597
  metadata: z.record(z.string()).optional(),
2598
+ ingestion_profile: z.enum(["auto", "repo", "web_docs", "pdf_layout", "video_transcript", "plain_text"]).optional(),
2599
+ strategy_override: z.enum(["fixed", "recursive", "semantic", "hierarchical", "adaptive"]).optional(),
2600
+ profile_config: z.record(z.any()).optional(),
2340
2601
  owner: z.string().optional(),
2341
2602
  repo: z.string().optional(),
2342
2603
  branch: z.string().optional(),
@@ -2372,6 +2633,9 @@ server.tool(
2372
2633
  name: input.name,
2373
2634
  auto_index: input.auto_index,
2374
2635
  metadata: input.metadata,
2636
+ ingestion_profile: input.ingestion_profile,
2637
+ strategy_override: input.strategy_override,
2638
+ profile_config: input.profile_config,
2375
2639
  owner: input.owner,
2376
2640
  repo: input.repo,
2377
2641
  branch: input.branch,
@@ -2422,14 +2686,22 @@ server.tool(
2422
2686
  {
2423
2687
  project: z.string().optional().describe("Project name or slug"),
2424
2688
  title: z.string().describe("Title for this content"),
2425
- content: z.string().describe("The text content to index")
2689
+ content: z.string().describe("The text content to index"),
2690
+ ingestion_profile: z.enum(["auto", "repo", "web_docs", "pdf_layout", "video_transcript", "plain_text"]).optional(),
2691
+ strategy_override: z.enum(["fixed", "recursive", "semantic", "hierarchical", "adaptive"]).optional(),
2692
+ profile_config: z.record(z.any()).optional()
2426
2693
  },
2427
- async ({ project, title, content }) => {
2694
+ async ({ project, title, content, ingestion_profile, strategy_override, profile_config }) => {
2428
2695
  try {
2429
2696
  await whisper.addContext({
2430
2697
  project,
2431
2698
  title,
2432
- content
2699
+ content,
2700
+ metadata: {
2701
+ ...ingestion_profile ? { ingestion_profile } : {},
2702
+ ...strategy_override ? { strategy_override } : {},
2703
+ ...profile_config ? { profile_config } : {}
2704
+ }
2433
2705
  });
2434
2706
  return { content: [{ type: "text", text: `Indexed "${title}" (${content.length} chars).` }] };
2435
2707
  } catch (error) {
@@ -2449,9 +2721,12 @@ server.tool(
2449
2721
  auto_sync: z.boolean().optional().default(true),
2450
2722
  tags: z.array(z.string()).optional(),
2451
2723
  platform: z.enum(["youtube", "loom", "generic"]).optional(),
2452
- language: z.string().optional()
2724
+ language: z.string().optional(),
2725
+ ingestion_profile: z.enum(["auto", "repo", "web_docs", "pdf_layout", "video_transcript", "plain_text"]).optional(),
2726
+ strategy_override: z.enum(["fixed", "recursive", "semantic", "hierarchical", "adaptive"]).optional(),
2727
+ profile_config: z.record(z.any()).optional()
2453
2728
  },
2454
- async ({ project, source_type, title, content, url, auto_sync, tags, platform, language }) => {
2729
+ async ({ project, source_type, title, content, url, auto_sync, tags, platform, language, ingestion_profile, strategy_override, profile_config }) => {
2455
2730
  try {
2456
2731
  const resolvedProject = await resolveProjectRef(project);
2457
2732
  if (!resolvedProject) {
@@ -2467,7 +2742,10 @@ server.tool(
2467
2742
  auto_sync,
2468
2743
  tags,
2469
2744
  platform,
2470
- language
2745
+ language,
2746
+ ingestion_profile,
2747
+ strategy_override,
2748
+ profile_config
2471
2749
  });
2472
2750
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2473
2751
  }
@@ -2478,7 +2756,13 @@ server.tool(
2478
2756
  project: resolvedProject,
2479
2757
  title: title || "Document",
2480
2758
  content,
2481
- metadata: { source: "mcp:add_document", tags: tags || [] }
2759
+ metadata: {
2760
+ source: "mcp:add_document",
2761
+ tags: tags || [],
2762
+ ...ingestion_profile ? { ingestion_profile } : {},
2763
+ ...strategy_override ? { strategy_override } : {},
2764
+ ...profile_config ? { profile_config } : {}
2765
+ }
2482
2766
  });
2483
2767
  return { content: [{ type: "text", text: `Indexed "${title || "Document"}" (${content.length} chars).` }] };
2484
2768
  } catch (error) {
@@ -2511,23 +2795,17 @@ server.tool(
2511
2795
  top_k,
2512
2796
  include_relations
2513
2797
  });
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 }] };
2798
+ const normalizedResults = formatCanonicalMemoryResults(results);
2799
+ return primaryToolSuccess({
2800
+ tool: "memory.search_sota",
2801
+ query,
2802
+ question_date: question_date || null,
2803
+ include_relations,
2804
+ results: normalizedResults,
2805
+ count: normalizedResults.length
2806
+ });
2529
2807
  } catch (error) {
2530
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2808
+ return primaryToolError(error.message);
2531
2809
  }
2532
2810
  }
2533
2811
  );
@@ -2546,16 +2824,21 @@ server.tool(
2546
2824
  },
2547
2825
  async ({ project, session_id, user_id, messages }) => {
2548
2826
  try {
2827
+ const normalizedMessages = messages.map((message) => ({
2828
+ role: message.role,
2829
+ content: message.content,
2830
+ timestamp: message.timestamp
2831
+ }));
2549
2832
  const result = await whisper.ingestSession({
2550
2833
  project,
2551
2834
  session_id,
2552
2835
  user_id,
2553
- messages
2836
+ messages: normalizedMessages
2554
2837
  });
2555
2838
  return {
2556
2839
  content: [{
2557
2840
  type: "text",
2558
- text: `Processed ${messages.length} messages:
2841
+ text: `Processed ${normalizedMessages.length} messages:
2559
2842
  - Created ${result.memories_created} memories
2560
2843
  - Detected ${result.relations_created} relations
2561
2844
  - Updated ${result.memories_invalidated} outdated memories` + (result.errors && result.errors.length > 0 ? `
@@ -3460,25 +3743,50 @@ server.tool(
3460
3743
  );
3461
3744
  server.tool(
3462
3745
  "search",
3463
- "Search indexed code, docs, and connected sources with one obvious verb. Use this first for most retrieval tasks.",
3746
+ "Search retrievable context by query, exact id, or both. Use `id` for exact fetch and `query` for semantic retrieval.",
3464
3747
  {
3465
3748
  project: z.string().optional().describe("Project name or slug"),
3466
- query: z.string().describe("What you want to find"),
3749
+ query: z.string().optional().describe("Semantic retrieval query"),
3750
+ id: z.string().optional().describe("Exact memory id to fetch"),
3467
3751
  top_k: z.number().optional().default(10),
3468
3752
  include_memories: z.boolean().optional().default(false),
3469
3753
  include_graph: z.boolean().optional().default(false),
3470
3754
  user_id: z.string().optional(),
3471
3755
  session_id: z.string().optional()
3472
3756
  },
3473
- async ({ project, query, top_k, include_memories, include_graph, user_id, session_id }) => {
3757
+ async ({ project, query, id, top_k, include_memories, include_graph, user_id, session_id }) => {
3474
3758
  try {
3759
+ if (!id && !query) {
3760
+ return toTextResult(buildMcpSearchError("Provide query, id, or both."));
3761
+ }
3762
+ let exactMemory;
3763
+ if (id) {
3764
+ const memoryResult = await whisper.getMemory(id);
3765
+ exactMemory = memoryResult?.memory || memoryResult;
3766
+ }
3767
+ if (!query) {
3768
+ return toTextResult(buildMcpSearchPayload({
3769
+ mode: "exact",
3770
+ id,
3771
+ exactMemory
3772
+ }));
3773
+ }
3475
3774
  const resolvedProject = await resolveProjectRef(project);
3476
3775
  if (!resolvedProject) {
3477
- return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
3776
+ if (exactMemory) {
3777
+ return toTextResult(buildMcpSearchPayload({
3778
+ mode: "hybrid",
3779
+ query,
3780
+ id,
3781
+ exactMemory,
3782
+ warnings: ["Project not resolved, so semantic context was skipped."]
3783
+ }));
3784
+ }
3785
+ return toTextResult(buildMcpSearchError("No project resolved. Set WHISPER_PROJECT or pass project."));
3478
3786
  }
3479
3787
  const queryResult = await queryWithDegradedFallback({
3480
3788
  project: resolvedProject,
3481
- query,
3789
+ query: query || exactMemory?.content || "",
3482
3790
  top_k,
3483
3791
  include_memories,
3484
3792
  include_graph,
@@ -3486,15 +3794,18 @@ server.tool(
3486
3794
  session_id
3487
3795
  });
3488
3796
  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 }] };
3797
+ return toTextResult(buildMcpSearchPayload({
3798
+ mode: exactMemory ? "hybrid" : "semantic",
3799
+ query,
3800
+ id,
3801
+ exactMemory,
3802
+ context: response.context,
3803
+ results: response.results,
3804
+ degradedMode: queryResult.degraded_mode,
3805
+ degradedReason: queryResult.degraded_reason
3806
+ }));
3496
3807
  } catch (error) {
3497
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3808
+ return primaryToolError(error.message);
3498
3809
  }
3499
3810
  }
3500
3811
  );
@@ -3534,7 +3845,13 @@ server.tool(
3534
3845
  }
3535
3846
  collect(rootPath);
3536
3847
  if (files.length === 0) {
3537
- return { content: [{ type: "text", text: `No code files found in ${rootPath}` }] };
3848
+ return primaryToolSuccess({
3849
+ tool: "search_code",
3850
+ query,
3851
+ path: rootPath,
3852
+ results: [],
3853
+ count: 0
3854
+ });
3538
3855
  }
3539
3856
  const documents = [];
3540
3857
  for (const filePath of files) {
@@ -3555,13 +3872,23 @@ server.tool(
3555
3872
  threshold: threshold ?? 0.2
3556
3873
  });
3557
3874
  if (!response.results?.length) {
3558
- return { content: [{ type: "text", text: `No semantically relevant files found for "${query}".` }] };
3875
+ return primaryToolSuccess({
3876
+ tool: "search_code",
3877
+ query,
3878
+ path: rootPath,
3879
+ results: [],
3880
+ count: 0
3881
+ });
3559
3882
  }
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") }] };
3883
+ return primaryToolSuccess({
3884
+ tool: "search_code",
3885
+ query,
3886
+ path: rootPath,
3887
+ results: response.results,
3888
+ count: response.results.length
3889
+ });
3563
3890
  } catch (error) {
3564
- return { content: [{ type: "text", text: `Semantic search failed: ${error.message}` }] };
3891
+ return primaryToolError(`Semantic search failed: ${error.message}`);
3565
3892
  }
3566
3893
  }
3567
3894
  );
@@ -3585,9 +3912,9 @@ server.tool(
3585
3912
  const stat = statSync(filePath);
3586
3913
  if (stat.size > 512 * 1024) continue;
3587
3914
  const text = readFileSync(filePath, "utf-8");
3588
- const lines2 = text.split("\n");
3915
+ const lines = text.split("\n");
3589
3916
  const matches = [];
3590
- lines2.forEach((line, index) => {
3917
+ lines.forEach((line, index) => {
3591
3918
  regex.lastIndex = 0;
3592
3919
  if (regex.test(line)) {
3593
3920
  matches.push({ line: index + 1, content: line.trimEnd() });
@@ -3600,14 +3927,21 @@ server.tool(
3600
3927
  }
3601
3928
  }
3602
3929
  if (!results.length) {
3603
- return { content: [{ type: "text", text: `No matches found for "${query}" in ${rootPath}` }] };
3930
+ return primaryToolSuccess({
3931
+ tool: "grep",
3932
+ query,
3933
+ path: rootPath,
3934
+ results: [],
3935
+ count: 0
3936
+ });
3604
3937
  }
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") }] };
3938
+ return primaryToolSuccess({
3939
+ tool: "grep",
3940
+ query,
3941
+ path: rootPath,
3942
+ results,
3943
+ count: results.length
3944
+ });
3611
3945
  }
3612
3946
  );
3613
3947
  server.tool(
@@ -3623,11 +3957,17 @@ server.tool(
3623
3957
  const fullPath = path.includes(":") || path.startsWith("/") ? path : join(process.cwd(), path);
3624
3958
  const stats = statSync(fullPath);
3625
3959
  if (!stats.isFile()) {
3626
- return { content: [{ type: "text", text: `Error: ${path} is not a file.` }] };
3960
+ return primaryToolError(`${path} is not a file.`, "invalid_request");
3627
3961
  }
3628
- return { content: [{ type: "text", text: readFileWindow(fullPath, start_line, end_line) }] };
3962
+ return primaryToolSuccess({
3963
+ tool: "read",
3964
+ path: fullPath,
3965
+ start_line,
3966
+ end_line,
3967
+ content: readFileWindow(fullPath, start_line, end_line)
3968
+ });
3629
3969
  } catch (error) {
3630
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3970
+ return primaryToolError(error.message);
3631
3971
  }
3632
3972
  }
3633
3973
  );
@@ -3644,11 +3984,21 @@ server.tool(
3644
3984
  const rootPath = path || process.cwd();
3645
3985
  const tree = listTree(rootPath, max_depth, max_entries);
3646
3986
  if (!tree.length) {
3647
- return { content: [{ type: "text", text: `No visible files found in ${rootPath}` }] };
3987
+ return primaryToolSuccess({
3988
+ tool: "explore",
3989
+ path: rootPath,
3990
+ entries: [],
3991
+ count: 0
3992
+ });
3648
3993
  }
3649
- return { content: [{ type: "text", text: [`TREE ${rootPath}`, ...tree].join("\n") }] };
3994
+ return primaryToolSuccess({
3995
+ tool: "explore",
3996
+ path: rootPath,
3997
+ entries: tree,
3998
+ count: tree.length
3999
+ });
3650
4000
  } catch (error) {
3651
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
4001
+ return primaryToolError(error.message);
3652
4002
  }
3653
4003
  }
3654
4004
  );
@@ -3665,17 +4015,16 @@ server.tool(
3665
4015
  async ({ project, query, mode, max_results, max_steps }) => {
3666
4016
  try {
3667
4017
  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 }] };
4018
+ return primaryToolSuccess({
4019
+ tool: "research",
4020
+ mode,
4021
+ query,
4022
+ answer: results.answer || null,
4023
+ results: results.results || [],
4024
+ count: Array.isArray(results.results) ? results.results.length : 0
4025
+ });
3677
4026
  } catch (error) {
3678
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
4027
+ return primaryToolError(error.message);
3679
4028
  }
3680
4029
  }
3681
4030
  );
@@ -3771,34 +4120,95 @@ server.tool(
3771
4120
  async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
3772
4121
  try {
3773
4122
  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}).` }] };
4123
+ const memoryId = result?.memory_id || (result.mode === "sync" ? result.id : null);
4124
+ const jobId = result?.job_id || (result.mode === "async" ? result.id : null);
4125
+ return primaryToolSuccess({
4126
+ tool: "remember",
4127
+ id: memoryId || jobId || null,
4128
+ memory_id: memoryId,
4129
+ job_id: jobId,
4130
+ mode: result?.mode || null,
4131
+ memory_type,
4132
+ stored: result.success === true,
4133
+ queued: result?.mode === "async" || Boolean(jobId)
4134
+ });
3775
4135
  } catch (error) {
3776
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
4136
+ return primaryToolError(error.message);
3777
4137
  }
3778
4138
  }
3779
4139
  );
3780
4140
  server.tool(
3781
- "recall",
3782
- "Recall facts, decisions, and preferences from previous sessions or prior work.",
4141
+ "record",
4142
+ "Record what just happened in a session or conversation. Use this for temporal capture, not durable preference storage.",
3783
4143
  {
3784
4144
  project: z.string().optional(),
3785
- query: z.string().describe("What to recall"),
4145
+ session_id: z.string().describe("Session to record into"),
3786
4146
  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()
4147
+ messages: z.array(z.object({
4148
+ role: z.string().optional(),
4149
+ content: z.string(),
4150
+ timestamp: z.string().optional()
4151
+ })).optional(),
4152
+ role: z.string().optional(),
4153
+ content: z.string().optional(),
4154
+ timestamp: z.string().optional()
3790
4155
  },
3791
- async ({ project, query, user_id, session_id, top_k, memory_types }) => {
4156
+ async ({ project, session_id, user_id, messages, role, content, timestamp }) => {
3792
4157
  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 }] };
4158
+ const normalizedMessages = normalizeRecordMessages({
4159
+ messages: messages?.map((message) => ({
4160
+ role: message.role,
4161
+ content: message.content,
4162
+ timestamp: message.timestamp
4163
+ })),
4164
+ role,
4165
+ content,
4166
+ timestamp
4167
+ });
4168
+ const result = await whisper.ingestSession({ project, session_id, user_id, messages: normalizedMessages });
4169
+ return primaryToolSuccess({
4170
+ tool: "record",
4171
+ session_id,
4172
+ messages_recorded: normalizedMessages.length,
4173
+ memories_created: result.memories_created,
4174
+ relations_created: result.relations_created
4175
+ });
3800
4176
  } catch (error) {
3801
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
4177
+ return primaryToolError(error.message);
4178
+ }
4179
+ }
4180
+ );
4181
+ server.tool(
4182
+ "learn",
4183
+ "Learn content or connect a source so it becomes retrievable later. Use this for docs, URLs, repos, files, or local paths.",
4184
+ {
4185
+ project: z.string().optional(),
4186
+ content: z.string().optional().describe("Inline text content to ingest"),
4187
+ title: z.string().optional().describe("Title for inline content"),
4188
+ url: z.string().optional().describe("URL to learn from"),
4189
+ owner: z.string().optional().describe("GitHub owner"),
4190
+ repo: z.string().optional().describe("GitHub repository"),
4191
+ branch: z.string().optional(),
4192
+ path: z.string().optional().describe("Local path to learn from"),
4193
+ file_path: z.string().optional().describe("Single file path to learn from"),
4194
+ name: z.string().optional().describe("Optional source name"),
4195
+ metadata: z.record(z.string()).optional(),
4196
+ ingestion_profile: z.enum(["auto", "repo", "web_docs", "pdf_layout", "video_transcript", "plain_text"]).optional(),
4197
+ strategy_override: z.enum(["fixed", "recursive", "semantic", "hierarchical", "adaptive"]).optional(),
4198
+ profile_config: z.record(z.any()).optional(),
4199
+ max_files: z.number().optional(),
4200
+ glob: z.string().optional(),
4201
+ crawl_depth: z.number().optional()
4202
+ },
4203
+ async (input) => {
4204
+ try {
4205
+ const result = await learnFromInput(input);
4206
+ return primaryToolSuccess({
4207
+ tool: "learn",
4208
+ ...result
4209
+ });
4210
+ } catch (error) {
4211
+ return primaryToolError(error.message);
3802
4212
  }
3803
4213
  }
3804
4214
  );
@@ -3814,18 +4224,15 @@ server.tool(
3814
4224
  async ({ project, session_id, title, expiry_days }) => {
3815
4225
  try {
3816
4226
  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
- };
4227
+ return primaryToolSuccess({
4228
+ tool: "share_context",
4229
+ share_id: result.share_id,
4230
+ share_url: result.share_url,
4231
+ expires_at: result.expires_at || null,
4232
+ title: result.title || title || null
4233
+ });
3827
4234
  } catch (error) {
3828
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
4235
+ return primaryToolError(error.message);
3829
4236
  }
3830
4237
  }
3831
4238
  );
@@ -3845,7 +4252,7 @@ async function main() {
3845
4252
  const source = readArg("--source") || "source-or-type";
3846
4253
  const client = readArg("--client") || "json";
3847
4254
  const outPath = readArg("--write");
3848
- const rendered = scopeConfigJson(project, source, client);
4255
+ const rendered = renderScopedMcpConfig(project, source, client);
3849
4256
  if (outPath) {
3850
4257
  const backup = existsSync(outPath) ? `${outPath}.bak-${Date.now()}` : void 0;
3851
4258
  if (backup) writeFileSync(backup, readFileSync(outPath, "utf-8"), "utf-8");
@@ -3866,4 +4273,11 @@ async function main() {
3866
4273
  await server.connect(transport);
3867
4274
  console.error("Whisper Context MCP server running on stdio");
3868
4275
  }
3869
- main().catch(console.error);
4276
+ if (process.argv[1] && /server\.(mjs|cjs|js|ts)$/.test(process.argv[1])) {
4277
+ main().catch(console.error);
4278
+ }
4279
+ export {
4280
+ createMcpServer,
4281
+ createWhisperMcpClient,
4282
+ renderScopedMcpConfig
4283
+ };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@usewhisper/mcp-server",
3
- "version": "1.4.0",
3
+ "version": "2.1.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"