@usewhisper/mcp-server 0.5.0 → 1.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 +101 -196
  2. package/dist/server.js +612 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,198 +1,103 @@
1
1
  # @usewhisper/mcp-server
2
2
 
3
- Model Context Protocol server for [Whisper Context API](https://usewhisper.dev) - Connect Claude Desktop to your knowledge base.
4
-
5
- Whisper makes code agents refuse unsupported claims about your codebase by combining retrieval, claim verification, and evidence-locked answering.
6
-
7
- **Version 0.2.0** - Now with 15 tools including SOTA Memory System, Oracle Research Mode, and Cost Optimization!
8
-
9
- ## What is MCP?
10
-
11
- The Model Context Protocol (MCP) allows Claude Desktop to connect directly to external knowledge sources and tools. This server gives Claude Desktop access to your Whisper Context projects, enabling seamless RAG-powered conversations with advanced memory and research capabilities.
12
-
13
- ## Installation
14
-
15
- ```bash
16
- npm install -g @usewhisper/mcp-server
17
- ```
18
-
19
- ## Setup
20
-
21
- ### 1. Get Your API Key
22
-
23
- Get your Whisper API key from the [dashboard](https://usewhisper.dev/dashboard).
24
-
25
- ### 2. Configure Claude Desktop
26
-
27
- Add to your Claude Desktop config file:
28
-
29
- **MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
30
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
31
-
32
- ```json
33
- {
34
- "mcpServers": {
35
- "whisper-context": {
36
- "command": "whisper-context-mcp",
37
- "env": {
38
- "WHISPER_API_KEY": "wctx_...",
39
- "WHISPER_PROJECT": "my-project"
40
- }
41
- }
42
- }
43
- }
44
- ```
45
-
46
- **Optional environment variables:**
47
- - `WHISPER_BASE_URL`: Custom API endpoint (defaults to `https://context.usewhisper.dev`)
48
-
49
- ### 3. Restart Claude Desktop
50
-
51
- After configuration, restart Claude Desktop. The MCP server will appear in the bottom-right corner.
52
-
53
- ## Available Tools
54
-
55
- Once connected, Claude Desktop gets access to these tools:
56
-
57
- ### Trust pipeline (recommended first)
58
-
59
- 1. `resolve_workspace`
60
- 2. `repo_index_status`
61
- 3. `get_relevant_context`
62
- 4. `claim_verifier`
63
- 5. `evidence_locked_answer`
64
-
65
- ### query_context
66
- Search your knowledge base with hybrid vector+keyword search, memory inclusion, and knowledge graph traversal.
67
-
68
- ```
69
- Query your docs project for "authentication flow"
70
- ```
71
-
72
- ### add_memory
73
- Store persistent memories (facts, preferences, decisions) across conversations.
74
-
75
- ```
76
- Remember that I prefer TypeScript over JavaScript
77
- ```
78
-
79
- ### search_memories
80
- Recall facts and preferences from previous interactions.
81
-
82
- ```
83
- What programming languages do I prefer?
84
- ```
85
-
86
- ### list_projects
87
- List all available context projects.
88
-
89
- ### list_sources
90
- See all data sources connected to a project (GitHub, Notion, Confluence, etc).
91
-
92
- ### add_context
93
- Add text content directly to your knowledge base.
94
-
95
- ```
96
- Add this API documentation to my docs project
97
- ```
98
-
99
- ### track_conversation
100
- Record conversation messages for context tracking.
101
-
102
- ### get_conversation
103
- Retrieve conversation history for a session.
104
-
105
- ### get_relevant_context
106
- Core retrieval contract with ranked evidence payloads (`path:line` ready).
107
-
108
- ### claim_verifier
109
- Checks a claim and returns `supported | partial | unsupported` with evidence spans.
110
-
111
- ### evidence_locked_answer
112
- Returns a cited answer only when thresholds are met; otherwise returns explicit abstain payload.
113
-
114
- ### forget
115
- Deletes or invalidates memories with immutable audit metadata.
116
-
117
- ### export_context_bundle / import_context_bundle
118
- Round-trip project/workspace context in a portable bundle with checksum verification.
119
-
120
- ### diff_context
121
- Deterministic change view anchored by `session_id`, `timestamp`, or `commit`.
122
-
123
- ## Usage Examples
124
-
125
- Once configured, you can chat with Claude Desktop naturally:
126
-
127
- ```
128
- You: "Query my engineering-docs project for how to deploy to production"
129
- Claude: [Uses query_context tool to search your docs]
130
-
131
- You: "Remember that our staging environment is at staging.example.com"
132
- Claude: [Uses add_memory to store this fact]
133
-
134
- You: "What projects do I have?"
135
- Claude: [Uses list_projects to show all your context projects]
136
- ```
137
-
138
- ## Features
139
-
140
- - **Semantic Search**: Vector embeddings + BM25 hybrid search
141
- - **Conversational Memory**: Persistent memories across sessions
142
- - **Knowledge Graph**: Graph-based context traversal
143
- - **Auto-sync Sources**: GitHub, Notion, Confluence, Slack, and 10+ more
144
- - **Direct Ingestion**: Add content directly from conversations
145
- - **Session Tracking**: Maintains conversation context
146
-
147
- ## Architecture
148
-
149
- The MCP server connects to your Whisper Context API:
150
- - **Whisper Context API** for semantic search, memory, and knowledge graph
151
- - **Stdio Transport** for Claude Desktop communication
152
- - **Zero dependencies** on database or infrastructure
153
-
154
- ## Environment Variables
155
-
156
- | Variable | Required | Description |
157
- |----------|----------|-------------|
158
- | `WHISPER_API_KEY` | Yes | Your Whisper API key (get from dashboard) |
159
- | `WHISPER_PROJECT` | Optional | Default project name/slug for all operations |
160
- | `WHISPER_BASE_URL` | Optional | Custom API endpoint (defaults to https://context.usewhisper.dev) |
161
-
162
- ## Troubleshooting
163
-
164
- ### Server not appearing in Claude Desktop
165
-
166
- 1. Check the config file path is correct
167
- 2. Verify JSON syntax is valid
168
- 3. Restart Claude Desktop completely
169
- 4. Check Claude Desktop logs: `~/Library/Logs/Claude/` (Mac) or `%APPDATA%\Claude\logs\` (Windows)
170
-
171
- ### Connection errors
172
-
173
- - Verify your `WHISPER_API_KEY` is valid (starts with `wctx_`)
174
- - Check that you have network connectivity to the API
175
- - Ensure your API key has the necessary permissions
176
-
177
- ### "Project not found" errors
178
-
179
- - Make sure your project name/slug is correct
180
- - Set `WHISPER_PROJECT` in config for a default project
181
- - Use `list_projects` tool to see all available projects
182
-
183
- ### No results from queries
184
-
185
- - Make sure your project has data sources connected or documents ingested
186
- - Verify the project name/slug matches exactly
187
- - Check that sources have been synced successfully in the dashboard
188
-
189
- ## Links
190
-
191
- - [Documentation](https://docs.usewhisper.dev/mcp)
192
- - [MCP Protocol](https://modelcontextprotocol.io)
193
- - [GitHub](https://github.com/usewhisper/whisper)
194
- - [Website](https://usewhisper.dev)
195
-
196
- ## License
197
-
198
- MIT
3
+ Whisper MCP is the universal context bridge for coding agents. It connects Claude/Cursor/VS Code/Windsurf/Cline/Continue to fresh, indexed context with grounded retrieval and memory.
4
+
5
+ ## What's New (Major)
6
+
7
+ - Canonical namespaced tool surface (breaking rename)
8
+ - Source multiplexer in `context.add_source` for `github|web|pdf|local|slack`
9
+ - Auto-index by default for created sources
10
+ - Runtime modes: `remote` (default), `local`, `auto`
11
+ - Degraded retrieval behavior: lexical fallback with explicit `degraded_mode`
12
+ - Scoped MCP config generator: `whisper-context-mcp scope ...`
13
+ - Tool migration helper: `whisper-context-mcp --print-tool-map`
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm i -g @usewhisper/mcp-server
19
+ ```
20
+
21
+ ## Required Environment
22
+
23
+ - `WHISPER_API_KEY` (required)
24
+ - `WHISPER_PROJECT` (optional default)
25
+ - `WHISPER_BASE_URL` (optional, defaults to `https://context.usewhisper.dev`)
26
+ - `WHISPER_MCP_MODE` (optional: `remote|local|auto`, default `remote`)
27
+ - `WHISPER_LOCAL_ALLOWLIST` (optional comma-separated roots for local ingest)
28
+
29
+ ## Canonical Tools
30
+
31
+ 1. `context.list_projects`
32
+ 2. `context.list_sources`
33
+ 3. `context.add_source`
34
+ 4. `context.source_status`
35
+ 5. `context.query`
36
+ 6. `context.get_relevant`
37
+ 7. `context.claim_verify`
38
+ 8. `context.evidence_answer`
39
+ 9. `context.export_bundle`
40
+ 10. `context.import_bundle`
41
+ 11. `context.diff`
42
+ 12. `context.share`
43
+ 13. `memory.add`
44
+ 14. `memory.search`
45
+ 15. `memory.forget`
46
+ 16. `memory.consolidate`
47
+ 17. `research.oracle`
48
+ 18. `index.workspace_status`
49
+ 19. `index.workspace_run`
50
+ 20. `index.local_scan_ingest`
51
+ 21. `index.autosubscribe_deps`
52
+ 22. `code.search_text`
53
+ 23. `code.search_semantic`
54
+
55
+ ## Source Contract (`context.add_source`)
56
+
57
+ Input:
58
+ - `project?`, `type`, `name?`, `auto_index?` (default `true`), `metadata?`
59
+ - `type=github`: `owner`, `repo`, `branch?`, `paths?`
60
+ - `type=web`: `url`, `crawl_depth?`, `include_paths?`, `exclude_paths?`
61
+ - `type=pdf`: `url?`, `file_path?`
62
+ - `type=local`: `path`, `glob?`, `max_files?`
63
+ - `type=slack`: `workspace_id?`, `channel_ids?`, `since?`
64
+
65
+ Output:
66
+ - `source_id`
67
+ - `status` (`queued|indexing|ready|failed`)
68
+ - `job_id`
69
+ - `index_started`
70
+ - `warnings[]`
71
+
72
+ ## Scoped MCP Generator
73
+
74
+ ```bash
75
+ whisper-context-mcp scope --project my-project --source github --client claude
76
+ ```
77
+
78
+ Optional write:
79
+
80
+ ```bash
81
+ whisper-context-mcp scope --project my-project --source github --client vscode --write "$HOME/.config/Code/User/mcp.json"
82
+ ```
83
+
84
+ ## Breaking Rename Migration
85
+
86
+ Print full map:
87
+
88
+ ```bash
89
+ whisper-context-mcp --print-tool-map
90
+ ```
91
+
92
+ This release removes legacy un-namespaced tool names.
93
+
94
+ ## Security Defaults
95
+
96
+ Local ingest (`index.local_scan_ingest` and `type=local`) enforces:
97
+ - allowlisted roots
98
+ - secret/sensitive path filters (`.env`, `.pem`, `.key`, `.aws`, `.ssh`, build artifacts)
99
+ - content redaction pass for likely secrets
100
+
101
+ ## License
102
+
103
+ MIT
package/dist/server.js CHANGED
@@ -94,6 +94,7 @@ var DEFAULT_TIMEOUTS = {
94
94
  sessionMs: 2500
95
95
  };
96
96
  var DEFAULT_RETRYABLE_STATUS = [408, 429, 500, 502, 503, 504];
97
+ var DEFAULT_API_KEY_ONLY_PREFIXES = ["/v1/memory", "/v1/context/query"];
97
98
  var DEFAULT_RETRY_ATTEMPTS = {
98
99
  search: 3,
99
100
  writeAck: 2,
@@ -210,6 +211,16 @@ var RuntimeClient = class {
210
211
  const maybeWindow = globalThis.window;
211
212
  return maybeWindow && typeof maybeWindow === "object" ? "browser" : "node";
212
213
  }
214
+ apiKeyOnlyPrefixes() {
215
+ const raw = process.env.WHISPER_API_KEY_ONLY_PREFIXES;
216
+ if (!raw || !raw.trim()) return DEFAULT_API_KEY_ONLY_PREFIXES;
217
+ return raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
218
+ }
219
+ shouldAttachApiKeyHeader(endpoint) {
220
+ if (this.sendApiKeyHeader) return true;
221
+ const prefixes = this.apiKeyOnlyPrefixes();
222
+ return prefixes.some((prefix) => endpoint === prefix || endpoint.startsWith(`${prefix}/`));
223
+ }
213
224
  createRequestFingerprint(options) {
214
225
  const normalizedEndpoint = normalizeEndpoint(options.endpoint);
215
226
  const authFingerprint = stableHash(this.apiKey.replace(/^Bearer\s+/i, ""));
@@ -276,6 +287,7 @@ var RuntimeClient = class {
276
287
  const controller = new AbortController();
277
288
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
278
289
  try {
290
+ const attachApiKeyHeader = this.shouldAttachApiKeyHeader(normalizedEndpoint);
279
291
  const response = await fetch(`${this.baseUrl}${normalizedEndpoint}`, {
280
292
  method,
281
293
  signal: controller.signal,
@@ -283,7 +295,7 @@ var RuntimeClient = class {
283
295
  headers: {
284
296
  "Content-Type": "application/json",
285
297
  Authorization: this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`,
286
- ...this.sendApiKeyHeader ? { "X-API-Key": this.apiKey.replace(/^Bearer\s+/i, "") } : {},
298
+ ...attachApiKeyHeader ? { "X-API-Key": this.apiKey.replace(/^Bearer\s+/i, "") } : {},
287
299
  "x-trace-id": traceId,
288
300
  "x-span-id": spanId,
289
301
  "x-sdk-version": this.sdkVersion,
@@ -720,6 +732,69 @@ var WhisperContext = class _WhisperContext {
720
732
  async syncSource(sourceId) {
721
733
  return this.request(`/v1/sources/${sourceId}/sync`, { method: "POST" });
722
734
  }
735
+ async addSourceByType(projectId, params) {
736
+ const resolvedProjectId = await this.resolveProjectId(projectId);
737
+ return this.request(`/v1/projects/${resolvedProjectId}/add_source`, {
738
+ method: "POST",
739
+ body: JSON.stringify(params)
740
+ });
741
+ }
742
+ async getSourceStatus(sourceId) {
743
+ return this.request(`/v1/sources/${sourceId}/status`, { method: "GET" });
744
+ }
745
+ async createCanonicalSource(project, params) {
746
+ const connector_type = params.type === "github" ? "github" : params.type === "web" ? "website" : params.type === "pdf" ? "pdf" : params.type === "local" ? "local-folder" : "slack";
747
+ const config = {};
748
+ if (params.type === "github") {
749
+ if (!params.owner || !params.repo) throw new WhisperError({ code: "REQUEST_FAILED", message: "github source requires owner and repo" });
750
+ config.owner = params.owner;
751
+ config.repo = params.repo;
752
+ if (params.branch) config.branch = params.branch;
753
+ if (params.paths) config.paths = params.paths;
754
+ } else if (params.type === "web") {
755
+ if (!params.url) throw new WhisperError({ code: "REQUEST_FAILED", message: "web source requires url" });
756
+ config.url = params.url;
757
+ if (params.crawl_depth !== void 0) config.crawl_depth = params.crawl_depth;
758
+ if (params.include_paths) config.include_paths = params.include_paths;
759
+ if (params.exclude_paths) config.exclude_paths = params.exclude_paths;
760
+ } else if (params.type === "pdf") {
761
+ if (!params.url && !params.file_path) throw new WhisperError({ code: "REQUEST_FAILED", message: "pdf source requires url or file_path" });
762
+ if (params.url) config.url = params.url;
763
+ if (params.file_path) config.file_path = params.file_path;
764
+ } else if (params.type === "local") {
765
+ if (!params.path) throw new WhisperError({ code: "REQUEST_FAILED", message: "local source requires path" });
766
+ config.path = params.path;
767
+ if (params.glob) config.glob = params.glob;
768
+ if (params.max_files !== void 0) config.max_files = params.max_files;
769
+ } else {
770
+ config.channel_ids = params.channel_ids || [];
771
+ if (params.since) config.since = params.since;
772
+ if (params.workspace_id) config.workspace_id = params.workspace_id;
773
+ if (params.token) config.token = params.token;
774
+ if (params.auth_ref) config.auth_ref = params.auth_ref;
775
+ }
776
+ if (params.metadata) config.metadata = params.metadata;
777
+ config.auto_index = params.auto_index ?? true;
778
+ const created = await this.addSource(project, {
779
+ name: params.name || `${params.type}-source-${Date.now()}`,
780
+ connector_type,
781
+ config
782
+ });
783
+ let status = "queued";
784
+ let jobId = null;
785
+ if (params.auto_index ?? true) {
786
+ const syncRes = await this.syncSource(created.id);
787
+ status = "indexing";
788
+ jobId = String(syncRes?.id || syncRes?.job_id || "");
789
+ }
790
+ return {
791
+ source_id: created.id,
792
+ status,
793
+ job_id: jobId,
794
+ index_started: params.auto_index ?? true,
795
+ warnings: []
796
+ };
797
+ }
723
798
  async ingest(projectId, documents) {
724
799
  const resolvedProjectId = await this.resolveProjectId(projectId);
725
800
  return this.request(`/v1/projects/${resolvedProjectId}/ingest`, {
@@ -1197,8 +1272,11 @@ var WhisperContext = class _WhisperContext {
1197
1272
  };
1198
1273
  sources = {
1199
1274
  add: (projectId, params) => this.addSource(projectId, params),
1275
+ addSource: (projectId, params) => this.addSourceByType(projectId, params),
1200
1276
  sync: (sourceId) => this.syncSource(sourceId),
1201
- syncSource: (sourceId) => this.syncSource(sourceId)
1277
+ syncSource: (sourceId) => this.syncSource(sourceId),
1278
+ status: (sourceId) => this.getSourceStatus(sourceId),
1279
+ getStatus: (sourceId) => this.getSourceStatus(sourceId)
1202
1280
  };
1203
1281
  memory = {
1204
1282
  add: (params) => this.addMemory(params),
@@ -1247,15 +1325,14 @@ var WhisperContext = class _WhisperContext {
1247
1325
  var API_KEY = process.env.WHISPER_API_KEY || "";
1248
1326
  var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
1249
1327
  var BASE_URL = process.env.WHISPER_BASE_URL;
1250
- if (!API_KEY) {
1251
- console.error("Error: WHISPER_API_KEY environment variable is required");
1252
- process.exit(1);
1253
- }
1254
- var whisper = new WhisperContext({
1328
+ var RUNTIME_MODE = (process.env.WHISPER_MCP_MODE || "remote").toLowerCase();
1329
+ var CLI_ARGS = process.argv.slice(2);
1330
+ var IS_MANAGEMENT_ONLY = CLI_ARGS.includes("--print-tool-map") || CLI_ARGS[0] === "scope";
1331
+ var whisper = !IS_MANAGEMENT_ONLY && API_KEY ? new WhisperContext({
1255
1332
  apiKey: API_KEY,
1256
1333
  project: DEFAULT_PROJECT,
1257
1334
  ...BASE_URL && { baseUrl: BASE_URL }
1258
- });
1335
+ }) : null;
1259
1336
  var server = new McpServer({
1260
1337
  name: "whisper-context",
1261
1338
  version: "0.2.8"
@@ -1263,6 +1340,31 @@ var server = new McpServer({
1263
1340
  var STATE_DIR = join(homedir(), ".whisper-mcp");
1264
1341
  var STATE_PATH = join(STATE_DIR, "state.json");
1265
1342
  var AUDIT_LOG_PATH = join(STATE_DIR, "forget-audit.log");
1343
+ var LOCAL_INGEST_MANIFEST_PATH = join(STATE_DIR, "local-ingest-manifest.json");
1344
+ var TOOL_MIGRATION_MAP = [
1345
+ { old: "list_projects", next: "context.list_projects" },
1346
+ { old: "list_sources", next: "context.list_sources" },
1347
+ { old: "add_source", next: "context.add_source" },
1348
+ { old: "source_status", next: "context.source_status" },
1349
+ { old: "query_context", next: "context.query" },
1350
+ { old: "get_relevant_context", next: "context.get_relevant" },
1351
+ { old: "claim_verifier", next: "context.claim_verify" },
1352
+ { old: "evidence_locked_answer", next: "context.evidence_answer" },
1353
+ { old: "export_context_bundle", next: "context.export_bundle" },
1354
+ { old: "import_context_bundle", next: "context.import_bundle" },
1355
+ { old: "diff_context", next: "context.diff" },
1356
+ { old: "share_context", next: "context.share" },
1357
+ { old: "add_memory", next: "memory.add" },
1358
+ { old: "search_memories", next: "memory.search" },
1359
+ { old: "forget", next: "memory.forget" },
1360
+ { old: "consolidate_memories", next: "memory.consolidate" },
1361
+ { old: "oracle_search", next: "research.oracle" },
1362
+ { old: "repo_index_status", next: "index.workspace_status" },
1363
+ { old: "index_workspace", next: "index.workspace_run" },
1364
+ { old: "autosubscribe_dependencies", next: "index.autosubscribe_deps" },
1365
+ { old: "search_files", next: "code.search_text" },
1366
+ { old: "semantic_search_codebase", next: "code.search_semantic" }
1367
+ ];
1266
1368
  function ensureStateDir() {
1267
1369
  if (!existsSync(STATE_DIR)) {
1268
1370
  mkdirSync(STATE_DIR, { recursive: true });
@@ -1286,6 +1388,15 @@ function clamp01(value) {
1286
1388
  return value;
1287
1389
  }
1288
1390
  function renderCitation(ev) {
1391
+ const videoUrl = ev.metadata?.video_url;
1392
+ const tsRaw = ev.metadata?.timestamp_start_ms;
1393
+ const ts = tsRaw ? Number(tsRaw) : NaN;
1394
+ if (videoUrl && Number.isFinite(ts) && ts >= 0) {
1395
+ const totalSeconds = Math.floor(ts / 1e3);
1396
+ const minutes = Math.floor(totalSeconds / 60);
1397
+ const seconds = totalSeconds % 60;
1398
+ return `${videoUrl} @ ${minutes}:${String(seconds).padStart(2, "0")}`;
1399
+ }
1289
1400
  return ev.line_end && ev.line_end !== ev.line_start ? `${ev.path}:${ev.line_start}-${ev.line_end}` : `${ev.path}:${ev.line_start}`;
1290
1401
  }
1291
1402
  function extractLineStart(metadata) {
@@ -1323,7 +1434,11 @@ function toEvidenceRef(source, workspaceId, methodFallback) {
1323
1434
  workspace_id: workspaceId,
1324
1435
  metadata: {
1325
1436
  source: String(source.source || ""),
1326
- document: String(source.document || "")
1437
+ document: String(source.document || ""),
1438
+ video_url: String(metadata.video_url || ""),
1439
+ timestamp_start_ms: String(metadata.timestamp_start_ms ?? ""),
1440
+ timestamp_end_ms: String(metadata.timestamp_end_ms ?? ""),
1441
+ citation: String(metadata.citation || "")
1327
1442
  }
1328
1443
  };
1329
1444
  }
@@ -1383,7 +1498,7 @@ function buildAbstain(args) {
1383
1498
  reason: args.reason,
1384
1499
  message: args.message,
1385
1500
  closest_evidence: args.closest_evidence,
1386
- recommended_next_calls: ["repo_index_status", "index_workspace", "symbol_search", "get_relevant_context"],
1501
+ recommended_next_calls: ["index.workspace_status", "index.workspace_run", "symbol_search", "context.get_relevant"],
1387
1502
  diagnostics: {
1388
1503
  claims_evaluated: args.claims_evaluated,
1389
1504
  evidence_items_found: args.evidence_items_found,
@@ -1437,8 +1552,255 @@ function countCodeFiles(searchPath, maxFiles = 5e3) {
1437
1552
  walk(searchPath);
1438
1553
  return { total, skipped };
1439
1554
  }
1555
+ function toTextResult(payload) {
1556
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1557
+ }
1558
+ function likelyEmbeddingFailure(error) {
1559
+ const message = String(error?.message || error || "").toLowerCase();
1560
+ return message.includes("embedding") || message.includes("vector") || message.includes("timeout") || message.includes("timed out") || message.includes("temporarily unavailable");
1561
+ }
1562
+ async function queryWithDegradedFallback(params) {
1563
+ try {
1564
+ const response = await whisper.query({
1565
+ project: params.project,
1566
+ query: params.query,
1567
+ top_k: params.top_k,
1568
+ include_memories: params.include_memories,
1569
+ include_graph: params.include_graph,
1570
+ hybrid: true,
1571
+ rerank: true
1572
+ });
1573
+ return { response, degraded_mode: false };
1574
+ } catch (error) {
1575
+ if (!likelyEmbeddingFailure(error)) throw error;
1576
+ const response = await whisper.query({
1577
+ project: params.project,
1578
+ query: params.query,
1579
+ top_k: params.top_k,
1580
+ include_memories: params.include_memories,
1581
+ include_graph: false,
1582
+ hybrid: false,
1583
+ rerank: false,
1584
+ vector_weight: 0,
1585
+ bm25_weight: 1
1586
+ });
1587
+ return {
1588
+ response,
1589
+ degraded_mode: true,
1590
+ degraded_reason: "Embedding/graph path unavailable; lexical fallback used.",
1591
+ recommendation: "Check embedding service health, then re-run for full hybrid quality."
1592
+ };
1593
+ }
1594
+ }
1595
+ function getLocalAllowlistRoots() {
1596
+ const fromEnv = (process.env.WHISPER_LOCAL_ALLOWLIST || "").split(",").map((v) => v.trim()).filter(Boolean);
1597
+ if (fromEnv.length > 0) return fromEnv;
1598
+ return [process.cwd()];
1599
+ }
1600
+ function isPathAllowed(targetPath) {
1601
+ const normalized = targetPath.replace(/\\/g, "/").toLowerCase();
1602
+ const allowlist = getLocalAllowlistRoots();
1603
+ const allowed = allowlist.some((root) => normalized.startsWith(root.replace(/\\/g, "/").toLowerCase()));
1604
+ return { allowed, allowlist };
1605
+ }
1606
+ function shouldSkipSensitivePath(filePath) {
1607
+ const p = filePath.replace(/\\/g, "/").toLowerCase();
1608
+ const denySnippets = [
1609
+ "/node_modules/",
1610
+ "/.git/",
1611
+ "/dist/",
1612
+ "/build/",
1613
+ "/.next/",
1614
+ "/.aws/",
1615
+ "/.ssh/",
1616
+ ".pem",
1617
+ ".key",
1618
+ ".env",
1619
+ "credentials"
1620
+ ];
1621
+ return denySnippets.some((s) => p.includes(s));
1622
+ }
1623
+ function redactLikelySecrets(content) {
1624
+ return content.replace(/(api[_-]?key\s*[=:]\s*)[^\s"'`]+/gi, "$1[REDACTED]").replace(/(token\s*[=:]\s*)[^\s"'`]+/gi, "$1[REDACTED]").replace(/(secret\s*[=:]\s*)[^\s"'`]+/gi, "$1[REDACTED]");
1625
+ }
1626
+ function loadIngestManifest() {
1627
+ ensureStateDir();
1628
+ if (!existsSync(LOCAL_INGEST_MANIFEST_PATH)) return {};
1629
+ try {
1630
+ const parsed = JSON.parse(readFileSync(LOCAL_INGEST_MANIFEST_PATH, "utf-8"));
1631
+ return parsed && typeof parsed === "object" ? parsed : {};
1632
+ } catch {
1633
+ return {};
1634
+ }
1635
+ }
1636
+ function saveIngestManifest(manifest) {
1637
+ ensureStateDir();
1638
+ writeFileSync(LOCAL_INGEST_MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf-8");
1639
+ }
1640
+ async function ingestLocalPath(params) {
1641
+ if (RUNTIME_MODE === "remote") {
1642
+ throw new Error("Local ingestion is disabled in remote mode. Set WHISPER_MCP_MODE=auto or local.");
1643
+ }
1644
+ const rootPath = params.path || process.cwd();
1645
+ const gate = isPathAllowed(rootPath);
1646
+ if (!gate.allowed) {
1647
+ throw new Error(`Path not allowed by WHISPER_LOCAL_ALLOWLIST. Allowed roots: ${gate.allowlist.join(", ")}`);
1648
+ }
1649
+ const maxFiles = Math.max(1, params.max_files || 200);
1650
+ const maxBytesPerFile = 512 * 1024;
1651
+ const files = [];
1652
+ function collect(dir) {
1653
+ if (files.length >= maxFiles) return;
1654
+ let entries;
1655
+ try {
1656
+ entries = readdirSync(dir, { withFileTypes: true });
1657
+ } catch {
1658
+ return;
1659
+ }
1660
+ for (const entry of entries) {
1661
+ if (files.length >= maxFiles) return;
1662
+ const full = join(dir, entry.name);
1663
+ if (entry.isDirectory()) {
1664
+ if (shouldSkipSensitivePath(full)) continue;
1665
+ collect(full);
1666
+ } else if (entry.isFile()) {
1667
+ if (shouldSkipSensitivePath(full)) continue;
1668
+ files.push(full);
1669
+ }
1670
+ }
1671
+ }
1672
+ collect(rootPath);
1673
+ const manifest = loadIngestManifest();
1674
+ const workspaceId = getWorkspaceIdForPath(rootPath);
1675
+ if (!manifest[workspaceId]) manifest[workspaceId] = { last_run_at: (/* @__PURE__ */ new Date(0)).toISOString(), files: {} };
1676
+ const docs = [];
1677
+ const skipped = [];
1678
+ for (const fullPath of files) {
1679
+ try {
1680
+ const st = statSync(fullPath);
1681
+ if (st.size > maxBytesPerFile) {
1682
+ skipped.push(`${relative(rootPath, fullPath)} (size>${maxBytesPerFile})`);
1683
+ continue;
1684
+ }
1685
+ const mtime = String(st.mtimeMs);
1686
+ const rel = relative(rootPath, fullPath);
1687
+ if (manifest[workspaceId].files[rel] === mtime) continue;
1688
+ const raw = readFileSync(fullPath, "utf-8");
1689
+ const content = redactLikelySecrets(raw).slice(0, params.chunk_chars || 2e4);
1690
+ docs.push({
1691
+ title: rel,
1692
+ content,
1693
+ file_path: rel,
1694
+ metadata: { source_type: "local", path: rel, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
1695
+ });
1696
+ manifest[workspaceId].files[rel] = mtime;
1697
+ } catch {
1698
+ skipped.push(relative(rootPath, fullPath));
1699
+ }
1700
+ }
1701
+ let ingested = 0;
1702
+ const batchSize = 25;
1703
+ for (let i = 0; i < docs.length; i += batchSize) {
1704
+ const batch = docs.slice(i, i + batchSize);
1705
+ const result = await whisper.ingest(params.project, batch);
1706
+ ingested += Number(result.ingested || batch.length);
1707
+ }
1708
+ manifest[workspaceId].last_run_at = (/* @__PURE__ */ new Date()).toISOString();
1709
+ saveIngestManifest(manifest);
1710
+ appendFileSync(
1711
+ AUDIT_LOG_PATH,
1712
+ `${(/* @__PURE__ */ new Date()).toISOString()} local_ingest workspace=${workspaceId} root_hash=${createHash("sha256").update(rootPath).digest("hex").slice(0, 16)} files=${docs.length}
1713
+ `
1714
+ );
1715
+ return { ingested, scanned: files.length, queued: docs.length, skipped, workspace_id: workspaceId };
1716
+ }
1717
+ async function createSourceByType(params) {
1718
+ const connector_type = params.type === "github" ? "github" : params.type === "web" ? "website" : params.type === "pdf" ? "pdf" : params.type === "local" ? "local-folder" : "slack";
1719
+ const config = {};
1720
+ if (params.type === "github") {
1721
+ if (!params.owner || !params.repo) throw new Error("github source requires owner and repo");
1722
+ config.owner = params.owner;
1723
+ config.repo = params.repo;
1724
+ if (params.branch) config.branch = params.branch;
1725
+ if (params.paths) config.paths = params.paths;
1726
+ } else if (params.type === "web") {
1727
+ if (!params.url) throw new Error("web source requires url");
1728
+ config.url = params.url;
1729
+ if (params.crawl_depth !== void 0) config.crawl_depth = params.crawl_depth;
1730
+ if (params.include_paths) config.include_paths = params.include_paths;
1731
+ if (params.exclude_paths) config.exclude_paths = params.exclude_paths;
1732
+ } else if (params.type === "pdf") {
1733
+ if (!params.url && !params.file_path) throw new Error("pdf source requires url or file_path");
1734
+ if (params.url) config.url = params.url;
1735
+ if (params.file_path) config.file_path = params.file_path;
1736
+ } else if (params.type === "local") {
1737
+ if (!params.path) throw new Error("local source requires path");
1738
+ const ingest = await ingestLocalPath({
1739
+ project: params.project,
1740
+ path: params.path,
1741
+ glob: params.glob,
1742
+ max_files: params.max_files
1743
+ });
1744
+ return {
1745
+ source_id: `local_${ingest.workspace_id}`,
1746
+ status: "ready",
1747
+ job_id: `local_ingest_${Date.now()}`,
1748
+ index_started: true,
1749
+ warnings: ingest.skipped.slice(0, 20),
1750
+ details: ingest
1751
+ };
1752
+ } else if (params.type === "slack") {
1753
+ config.channel_ids = params.channel_ids || [];
1754
+ if (params.since) config.since = params.since;
1755
+ if (params.workspace_id) config.workspace_id = params.workspace_id;
1756
+ if (params.token) config.token = params.token;
1757
+ if (params.auth_ref) config.auth_ref = params.auth_ref;
1758
+ }
1759
+ if (params.metadata) config.metadata = params.metadata;
1760
+ config.auto_index = params.auto_index ?? true;
1761
+ const created = await whisper.addSource(params.project, {
1762
+ name: params.name || `${params.type}-source-${Date.now()}`,
1763
+ connector_type,
1764
+ config
1765
+ });
1766
+ let jobId;
1767
+ let status = "queued";
1768
+ if (params.auto_index ?? true) {
1769
+ const syncRes = await whisper.syncSource(created.id);
1770
+ jobId = String(syncRes?.id || syncRes?.job_id || "");
1771
+ status = "indexing";
1772
+ }
1773
+ return {
1774
+ source_id: created.id,
1775
+ status,
1776
+ job_id: jobId || null,
1777
+ index_started: params.auto_index ?? true,
1778
+ warnings: []
1779
+ };
1780
+ }
1781
+ function scopeConfigJson(project, source, client) {
1782
+ const serverDef = {
1783
+ command: "npx",
1784
+ args: ["-y", "@usewhisper/mcp-server"],
1785
+ env: {
1786
+ WHISPER_API_KEY: "wctx_...",
1787
+ WHISPER_PROJECT: project,
1788
+ WHISPER_SCOPE_SOURCE: source
1789
+ }
1790
+ };
1791
+ if (client === "json") {
1792
+ return JSON.stringify({ mcpServers: { "whisper-context-scoped": serverDef } }, null, 2);
1793
+ }
1794
+ return JSON.stringify({ mcpServers: { "whisper-context-scoped": serverDef } }, null, 2);
1795
+ }
1796
+ function printToolMap() {
1797
+ console.log("Legacy -> canonical MCP tool names:");
1798
+ for (const row of TOOL_MIGRATION_MAP) {
1799
+ console.log(`- ${row.old} => ${row.next}`);
1800
+ }
1801
+ }
1440
1802
  server.tool(
1441
- "resolve_workspace",
1803
+ "index.workspace_resolve",
1442
1804
  "Resolve workspace identity from path + API key and map to a project without mandatory dashboard setup.",
1443
1805
  {
1444
1806
  path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
@@ -1472,7 +1834,7 @@ server.tool(
1472
1834
  }
1473
1835
  );
1474
1836
  server.tool(
1475
- "repo_index_status",
1837
+ "index.workspace_status",
1476
1838
  "Check index freshness, coverage, commit, and pending changes before retrieval/edits.",
1477
1839
  {
1478
1840
  workspace_id: z.string().optional(),
@@ -1506,7 +1868,7 @@ server.tool(
1506
1868
  }
1507
1869
  );
1508
1870
  server.tool(
1509
- "index_workspace",
1871
+ "index.workspace_run",
1510
1872
  "Index workspace in full or incremental mode and update index metadata for freshness checks.",
1511
1873
  {
1512
1874
  workspace_id: z.string().optional(),
@@ -1545,7 +1907,43 @@ server.tool(
1545
1907
  }
1546
1908
  );
1547
1909
  server.tool(
1548
- "get_relevant_context",
1910
+ "index.local_scan_ingest",
1911
+ "Scan a local folder safely (allowlist + secret filters), ingest changed files, and persist incremental manifest.",
1912
+ {
1913
+ project: z.string().optional().describe("Project name or slug"),
1914
+ path: z.string().optional().describe("Local path to ingest. Defaults to current working directory."),
1915
+ glob: z.string().optional().describe("Optional include glob"),
1916
+ max_files: z.number().optional().default(200),
1917
+ chunk_chars: z.number().optional().default(2e4)
1918
+ },
1919
+ async ({ project, path, glob, max_files, chunk_chars }) => {
1920
+ try {
1921
+ const resolvedProject = await resolveProjectRef(project);
1922
+ if (!resolvedProject) {
1923
+ return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
1924
+ }
1925
+ const result = await ingestLocalPath({
1926
+ project: resolvedProject,
1927
+ path: path || process.cwd(),
1928
+ glob,
1929
+ max_files,
1930
+ chunk_chars
1931
+ });
1932
+ return toTextResult({
1933
+ source_id: `local_${result.workspace_id}`,
1934
+ status: "ready",
1935
+ job_id: `local_ingest_${Date.now()}`,
1936
+ index_started: true,
1937
+ warnings: result.skipped.slice(0, 20),
1938
+ details: result
1939
+ });
1940
+ } catch (error) {
1941
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1942
+ }
1943
+ }
1944
+ );
1945
+ server.tool(
1946
+ "context.get_relevant",
1549
1947
  "Core retrieval. Task goes in, ranked context chunks come out with structured evidence (file:line ready).",
1550
1948
  {
1551
1949
  question: z.string().describe("Task/question to retrieve context for"),
@@ -1574,7 +1972,7 @@ server.tool(
1574
1972
  };
1575
1973
  return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
1576
1974
  }
1577
- const response = await whisper.query({
1975
+ const queryResult = await queryWithDegradedFallback({
1578
1976
  project: resolvedProject,
1579
1977
  query: question,
1580
1978
  top_k,
@@ -1583,6 +1981,7 @@ server.tool(
1583
1981
  session_id,
1584
1982
  user_id
1585
1983
  });
1984
+ const response = queryResult.response;
1586
1985
  const evidence = (response.results || []).map((r) => toEvidenceRef(r, workspaceId, "semantic"));
1587
1986
  const payload = {
1588
1987
  question,
@@ -1591,7 +1990,10 @@ server.tool(
1591
1990
  context: response.context || "",
1592
1991
  evidence,
1593
1992
  used_context_ids: (response.results || []).map((r) => String(r.id)),
1594
- latency_ms: response.meta?.latency_ms || 0
1993
+ latency_ms: response.meta?.latency_ms || 0,
1994
+ degraded_mode: queryResult.degraded_mode,
1995
+ degraded_reason: queryResult.degraded_reason,
1996
+ recommendation: queryResult.recommendation
1595
1997
  };
1596
1998
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1597
1999
  } catch (error) {
@@ -1600,7 +2002,7 @@ server.tool(
1600
2002
  }
1601
2003
  );
1602
2004
  server.tool(
1603
- "claim_verifier",
2005
+ "context.claim_verify",
1604
2006
  "Verify whether a claim is supported by retrieved context. Returns supported/partial/unsupported with evidence.",
1605
2007
  {
1606
2008
  claim: z.string().describe("Claim to verify"),
@@ -1653,7 +2055,7 @@ server.tool(
1653
2055
  }
1654
2056
  );
1655
2057
  server.tool(
1656
- "evidence_locked_answer",
2058
+ "context.evidence_answer",
1657
2059
  "Answer a question only when evidence requirements are met. Fails closed with an abstain payload when not verifiable.",
1658
2060
  {
1659
2061
  question: z.string(),
@@ -1767,7 +2169,7 @@ server.tool(
1767
2169
  }
1768
2170
  );
1769
2171
  server.tool(
1770
- "query_context",
2172
+ "context.query",
1771
2173
  "Search your knowledge base for relevant context. Returns packed context ready for LLM consumption. Supports hybrid vector+keyword search, memory inclusion, and knowledge graph traversal.",
1772
2174
  {
1773
2175
  project: z.string().optional().describe("Project name or slug (optional if WHISPER_PROJECT is set)"),
@@ -1782,31 +2184,38 @@ server.tool(
1782
2184
  },
1783
2185
  async ({ project, query, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens }) => {
1784
2186
  try {
1785
- const response = await whisper.query({
1786
- project,
2187
+ const resolvedProject = await resolveProjectRef(project);
2188
+ if (!resolvedProject) {
2189
+ return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
2190
+ }
2191
+ const queryResult = await queryWithDegradedFallback({
2192
+ project: resolvedProject,
1787
2193
  query,
1788
2194
  top_k,
1789
- chunk_types,
1790
2195
  include_memories,
1791
2196
  include_graph,
1792
2197
  user_id,
1793
- session_id,
1794
- max_tokens
2198
+ session_id
1795
2199
  });
2200
+ const response = queryResult.response;
1796
2201
  if (response.results.length === 0) {
1797
2202
  return { content: [{ type: "text", text: "No relevant context found." }] };
1798
2203
  }
1799
2204
  const header = `Found ${response.meta.total} results (${response.meta.latency_ms}ms${response.meta.cache_hit ? ", cached" : ""}):
1800
2205
 
1801
2206
  `;
1802
- return { content: [{ type: "text", text: header + response.context }] };
2207
+ const suffix = queryResult.degraded_mode ? `
2208
+
2209
+ [degraded_mode=true] ${queryResult.degraded_reason}
2210
+ Recommendation: ${queryResult.recommendation}` : "";
2211
+ return { content: [{ type: "text", text: header + response.context + suffix }] };
1803
2212
  } catch (error) {
1804
2213
  return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1805
2214
  }
1806
2215
  }
1807
2216
  );
1808
2217
  server.tool(
1809
- "add_memory",
2218
+ "memory.add",
1810
2219
  "Store a memory (fact, preference, decision) that persists across conversations. Memories can be scoped to a user, session, or agent.",
1811
2220
  {
1812
2221
  project: z.string().optional().describe("Project name or slug"),
@@ -1835,7 +2244,7 @@ server.tool(
1835
2244
  }
1836
2245
  );
1837
2246
  server.tool(
1838
- "search_memories",
2247
+ "memory.search",
1839
2248
  "Search stored memories by semantic similarity. Recall facts, preferences, past decisions from previous interactions.",
1840
2249
  {
1841
2250
  project: z.string().optional().describe("Project name or slug"),
@@ -1867,7 +2276,7 @@ ${r.content}`).join("\n\n");
1867
2276
  }
1868
2277
  );
1869
2278
  server.tool(
1870
- "list_projects",
2279
+ "context.list_projects",
1871
2280
  "List all available context projects.",
1872
2281
  {},
1873
2282
  async () => {
@@ -1881,7 +2290,7 @@ server.tool(
1881
2290
  }
1882
2291
  );
1883
2292
  server.tool(
1884
- "list_sources",
2293
+ "context.list_sources",
1885
2294
  "List all data sources connected to a project.",
1886
2295
  { project: z.string().optional().describe("Project name or slug") },
1887
2296
  async ({ project }) => {
@@ -1896,7 +2305,85 @@ server.tool(
1896
2305
  }
1897
2306
  );
1898
2307
  server.tool(
1899
- "add_context",
2308
+ "context.add_source",
2309
+ "Add a source to a project with normalized source contract and auto-index by default.",
2310
+ {
2311
+ project: z.string().optional().describe("Project name or slug"),
2312
+ type: z.enum(["github", "web", "pdf", "local", "slack"]).default("github"),
2313
+ name: z.string().optional(),
2314
+ auto_index: z.boolean().optional().default(true),
2315
+ metadata: z.record(z.string()).optional(),
2316
+ owner: z.string().optional(),
2317
+ repo: z.string().optional(),
2318
+ branch: z.string().optional(),
2319
+ paths: z.array(z.string()).optional(),
2320
+ url: z.string().url().optional(),
2321
+ crawl_depth: z.number().optional(),
2322
+ include_paths: z.array(z.string()).optional(),
2323
+ exclude_paths: z.array(z.string()).optional(),
2324
+ file_path: z.string().optional(),
2325
+ path: z.string().optional(),
2326
+ glob: z.string().optional(),
2327
+ max_files: z.number().optional(),
2328
+ workspace_id: z.string().optional(),
2329
+ channel_ids: z.array(z.string()).optional(),
2330
+ since: z.string().optional(),
2331
+ token: z.string().optional(),
2332
+ auth_ref: z.string().optional()
2333
+ },
2334
+ async (input) => {
2335
+ try {
2336
+ const resolvedProject = await resolveProjectRef(input.project);
2337
+ if (!resolvedProject) {
2338
+ return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
2339
+ }
2340
+ const result = await createSourceByType({
2341
+ project: resolvedProject,
2342
+ type: input.type,
2343
+ name: input.name,
2344
+ auto_index: input.auto_index,
2345
+ metadata: input.metadata,
2346
+ owner: input.owner,
2347
+ repo: input.repo,
2348
+ branch: input.branch,
2349
+ paths: input.paths,
2350
+ url: input.url,
2351
+ crawl_depth: input.crawl_depth,
2352
+ include_paths: input.include_paths,
2353
+ exclude_paths: input.exclude_paths,
2354
+ file_path: input.file_path,
2355
+ path: input.path,
2356
+ glob: input.glob,
2357
+ max_files: input.max_files,
2358
+ workspace_id: input.workspace_id,
2359
+ channel_ids: input.channel_ids,
2360
+ since: input.since,
2361
+ token: input.token,
2362
+ auth_ref: input.auth_ref
2363
+ });
2364
+ return toTextResult(result);
2365
+ } catch (error) {
2366
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2367
+ }
2368
+ }
2369
+ );
2370
+ server.tool(
2371
+ "context.source_status",
2372
+ "Get status and stage/progress details for a source sync job.",
2373
+ {
2374
+ source_id: z.string().describe("Source id")
2375
+ },
2376
+ async ({ source_id }) => {
2377
+ try {
2378
+ const result = await whisper.getSourceStatus(source_id);
2379
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2380
+ } catch (error) {
2381
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2382
+ }
2383
+ }
2384
+ );
2385
+ server.tool(
2386
+ "context.add_text",
1900
2387
  "Add text content to a project's knowledge base.",
1901
2388
  {
1902
2389
  project: z.string().optional().describe("Project name or slug"),
@@ -1917,7 +2404,56 @@ server.tool(
1917
2404
  }
1918
2405
  );
1919
2406
  server.tool(
1920
- "memory_search_sota",
2407
+ "context.add_document",
2408
+ "Ingest a document into project knowledge. Supports plain text and video URLs.",
2409
+ {
2410
+ project: z.string().optional().describe("Project name or slug"),
2411
+ source_type: z.enum(["text", "video"]).default("text"),
2412
+ title: z.string().optional().describe("Title for text documents"),
2413
+ content: z.string().optional().describe("Text document content"),
2414
+ url: z.string().url().optional().describe("Video URL when source_type=video"),
2415
+ auto_sync: z.boolean().optional().default(true),
2416
+ tags: z.array(z.string()).optional(),
2417
+ platform: z.enum(["youtube", "loom", "generic"]).optional(),
2418
+ language: z.string().optional()
2419
+ },
2420
+ async ({ project, source_type, title, content, url, auto_sync, tags, platform, language }) => {
2421
+ try {
2422
+ const resolvedProject = await resolveProjectRef(project);
2423
+ if (!resolvedProject) {
2424
+ return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
2425
+ }
2426
+ if (source_type === "video") {
2427
+ if (!url) {
2428
+ return { content: [{ type: "text", text: "Error: url is required when source_type=video." }] };
2429
+ }
2430
+ const result = await whisper.addSourceByType(resolvedProject, {
2431
+ type: "video",
2432
+ url,
2433
+ auto_sync,
2434
+ tags,
2435
+ platform,
2436
+ language
2437
+ });
2438
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2439
+ }
2440
+ if (!content?.trim()) {
2441
+ return { content: [{ type: "text", text: "Error: content is required when source_type=text." }] };
2442
+ }
2443
+ await whisper.addContext({
2444
+ project: resolvedProject,
2445
+ title: title || "Document",
2446
+ content,
2447
+ metadata: { source: "mcp:add_document", tags: tags || [] }
2448
+ });
2449
+ return { content: [{ type: "text", text: `Indexed "${title || "Document"}" (${content.length} chars).` }] };
2450
+ } catch (error) {
2451
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2452
+ }
2453
+ }
2454
+ );
2455
+ server.tool(
2456
+ "memory.search_sota",
1921
2457
  "SOTA memory search with temporal reasoning and relation graphs. Searches memories with support for temporal queries ('what did I say yesterday?'), type filtering, and knowledge graph traversal.",
1922
2458
  {
1923
2459
  project: z.string().optional().describe("Project name or slug"),
@@ -1962,7 +2498,7 @@ server.tool(
1962
2498
  }
1963
2499
  );
1964
2500
  server.tool(
1965
- "ingest_conversation",
2501
+ "memory.ingest_conversation",
1966
2502
  "Extract memories from a conversation session. Automatically handles disambiguation, temporal grounding, and relation detection.",
1967
2503
  {
1968
2504
  project: z.string().optional().describe("Project name or slug"),
@@ -1998,7 +2534,7 @@ server.tool(
1998
2534
  }
1999
2535
  );
2000
2536
  server.tool(
2001
- "oracle_search",
2537
+ "research.oracle",
2002
2538
  "Oracle Research Mode - Tree-guided document navigation with multi-step reasoning. More precise than standard search, especially for bleeding-edge features.",
2003
2539
  {
2004
2540
  project: z.string().optional().describe("Project name or slug"),
@@ -2046,7 +2582,7 @@ ${r.content.slice(0, 200)}...`
2046
2582
  }
2047
2583
  );
2048
2584
  server.tool(
2049
- "autosubscribe_dependencies",
2585
+ "index.autosubscribe_deps",
2050
2586
  "Automatically index a project's dependencies (package.json, requirements.txt, etc.). Resolves docs URLs and indexes documentation.",
2051
2587
  {
2052
2588
  project: z.string().optional().describe("Project name or slug"),
@@ -2079,7 +2615,7 @@ server.tool(
2079
2615
  }
2080
2616
  );
2081
2617
  server.tool(
2082
- "share_context",
2618
+ "context.share",
2083
2619
  "Create a shareable snapshot of a conversation with memories. Returns a URL that can be shared or resumed later.",
2084
2620
  {
2085
2621
  project: z.string().optional().describe("Project name or slug"),
@@ -2113,7 +2649,7 @@ Share URL: ${result.share_url}`
2113
2649
  }
2114
2650
  );
2115
2651
  server.tool(
2116
- "consolidate_memories",
2652
+ "memory.consolidate",
2117
2653
  "Find and merge duplicate memories to reduce bloat. Uses vector similarity + LLM merging.",
2118
2654
  {
2119
2655
  project: z.string().optional().describe("Project name or slug"),
@@ -2156,7 +2692,7 @@ Run without dry_run to merge.`
2156
2692
  }
2157
2693
  );
2158
2694
  server.tool(
2159
- "get_cost_summary",
2695
+ "context.cost_summary",
2160
2696
  "Get cost tracking summary showing spending by model and task. Includes savings vs always-Opus.",
2161
2697
  {
2162
2698
  project: z.string().optional().describe("Project name or slug (optional for org-wide)"),
@@ -2199,7 +2735,7 @@ server.tool(
2199
2735
  }
2200
2736
  );
2201
2737
  server.tool(
2202
- "forget",
2738
+ "memory.forget",
2203
2739
  "Delete or invalidate memories with immutable audit logging.",
2204
2740
  {
2205
2741
  workspace_id: z.string().optional(),
@@ -2311,7 +2847,7 @@ server.tool(
2311
2847
  }
2312
2848
  );
2313
2849
  server.tool(
2314
- "export_context_bundle",
2850
+ "context.export_bundle",
2315
2851
  "Export project/workspace memory and context to a portable bundle with checksum.",
2316
2852
  {
2317
2853
  workspace_id: z.string().optional(),
@@ -2363,7 +2899,7 @@ server.tool(
2363
2899
  }
2364
2900
  );
2365
2901
  server.tool(
2366
- "import_context_bundle",
2902
+ "context.import_bundle",
2367
2903
  "Import a portable context bundle with merge/replace modes and checksum verification.",
2368
2904
  {
2369
2905
  workspace_id: z.string().optional(),
@@ -2465,7 +3001,7 @@ server.tool(
2465
3001
  }
2466
3002
  );
2467
3003
  server.tool(
2468
- "diff_context",
3004
+ "context.diff",
2469
3005
  "Return deterministic context changes from an explicit anchor (session_id, timestamp, or commit).",
2470
3006
  {
2471
3007
  workspace_id: z.string().optional(),
@@ -2556,7 +3092,7 @@ function extractSignature(filePath, content) {
2556
3092
  return signature.join("\n").slice(0, 2e3);
2557
3093
  }
2558
3094
  server.tool(
2559
- "semantic_search_codebase",
3095
+ "code.search_semantic",
2560
3096
  "Semantically search a local codebase without pre-indexing. Unlike grep/ripgrep, this understands meaning \u2014 so 'find authentication logic' finds auth code even if it doesn't literally say 'auth'. Uses vector embeddings via the Whisper API. Perfect for exploring unfamiliar codebases.",
2561
3097
  {
2562
3098
  query: z.string().describe("Natural language description of what you're looking for. E.g. 'authentication and session management', 'database connection pooling', 'error handling middleware'"),
@@ -2673,7 +3209,7 @@ function* walkDir(dir, fileTypes) {
2673
3209
  }
2674
3210
  }
2675
3211
  server.tool(
2676
- "search_files",
3212
+ "code.search_text",
2677
3213
  "Search files and content in a local directory without requiring pre-indexing. Uses ripgrep when available, falls back to Node.js. Great for finding files, functions, patterns, or any text across a codebase instantly.",
2678
3214
  {
2679
3215
  query: z.string().describe("What to search for \u2014 natural language keyword, function name, pattern, etc."),
@@ -2812,7 +3348,7 @@ server.tool(
2812
3348
  }
2813
3349
  );
2814
3350
  server.tool(
2815
- "semantic_search",
3351
+ "code.semantic_documents",
2816
3352
  "Semantic vector search over provided documents. Uses embeddings to find semantically similar content. Perfect for AI code search, finding similar functions, or searching by meaning rather than keywords.",
2817
3353
  {
2818
3354
  query: z.string().describe("What to search for semantically (e.g. 'authentication logic', 'database connection')"),
@@ -2856,6 +3392,38 @@ server.tool(
2856
3392
  }
2857
3393
  );
2858
3394
  async function main() {
3395
+ const args = process.argv.slice(2);
3396
+ if (args.includes("--print-tool-map")) {
3397
+ printToolMap();
3398
+ return;
3399
+ }
3400
+ if (args[0] === "scope") {
3401
+ const readArg = (name) => {
3402
+ const idx = args.indexOf(name);
3403
+ if (idx === -1) return void 0;
3404
+ return args[idx + 1];
3405
+ };
3406
+ const project = readArg("--project") || DEFAULT_PROJECT || "my-project";
3407
+ const source = readArg("--source") || "source-or-type";
3408
+ const client = readArg("--client") || "json";
3409
+ const outPath = readArg("--write");
3410
+ const rendered = scopeConfigJson(project, source, client);
3411
+ if (outPath) {
3412
+ const backup = existsSync(outPath) ? `${outPath}.bak-${Date.now()}` : void 0;
3413
+ if (backup) writeFileSync(backup, readFileSync(outPath, "utf-8"), "utf-8");
3414
+ writeFileSync(outPath, `${rendered}
3415
+ `, "utf-8");
3416
+ console.log(JSON.stringify({ ok: true, path: outPath, backup: backup || null, client }, null, 2));
3417
+ return;
3418
+ }
3419
+ console.log(rendered);
3420
+ return;
3421
+ }
3422
+ if (!API_KEY && !IS_MANAGEMENT_ONLY) {
3423
+ console.error("Error: WHISPER_API_KEY environment variable is required");
3424
+ process.exit(1);
3425
+ }
3426
+ console.error("[whisper-context-mcp] Breaking change: canonical namespaced tool names are active. Run with --print-tool-map for migration mapping.");
2859
3427
  const transport = new StdioServerTransport();
2860
3428
  await server.connect(transport);
2861
3429
  console.error("Whisper Context MCP server running on stdio");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usewhisper/mcp-server",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "scripts": {
5
5
  "build": "tsup ../src/mcp/server.ts --format esm --out-dir dist",
6
6
  "prepublishOnly": "npm run build"