@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.
- package/README.md +101 -196
- package/dist/server.js +612 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,198 +1,103 @@
|
|
|
1
1
|
# @usewhisper/mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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: ["
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
1786
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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");
|