@usewhisper/mcp-server 0.5.0 → 1.3.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 +145 -196
- package/dist/server.js +1050 -44
- package/package.json +1 -1
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,43 @@ 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
|
+
];
|
|
1368
|
+
var ALIAS_TOOL_MAP = [
|
|
1369
|
+
{ alias: "search", target: "context.query" },
|
|
1370
|
+
{ alias: "search_code", target: "code.search_semantic" },
|
|
1371
|
+
{ alias: "grep", target: "code.search_text" },
|
|
1372
|
+
{ alias: "read", target: "local.file_read" },
|
|
1373
|
+
{ alias: "explore", target: "local.tree" },
|
|
1374
|
+
{ alias: "research", target: "research.oracle" },
|
|
1375
|
+
{ alias: "index", target: "context.add_source | index.workspace_run" },
|
|
1376
|
+
{ alias: "remember", target: "memory.add" },
|
|
1377
|
+
{ alias: "recall", target: "memory.search" },
|
|
1378
|
+
{ alias: "share_context", target: "context.share" }
|
|
1379
|
+
];
|
|
1266
1380
|
function ensureStateDir() {
|
|
1267
1381
|
if (!existsSync(STATE_DIR)) {
|
|
1268
1382
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
@@ -1286,6 +1400,15 @@ function clamp01(value) {
|
|
|
1286
1400
|
return value;
|
|
1287
1401
|
}
|
|
1288
1402
|
function renderCitation(ev) {
|
|
1403
|
+
const videoUrl = ev.metadata?.video_url;
|
|
1404
|
+
const tsRaw = ev.metadata?.timestamp_start_ms;
|
|
1405
|
+
const ts = tsRaw ? Number(tsRaw) : NaN;
|
|
1406
|
+
if (videoUrl && Number.isFinite(ts) && ts >= 0) {
|
|
1407
|
+
const totalSeconds = Math.floor(ts / 1e3);
|
|
1408
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
1409
|
+
const seconds = totalSeconds % 60;
|
|
1410
|
+
return `${videoUrl} @ ${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
1411
|
+
}
|
|
1289
1412
|
return ev.line_end && ev.line_end !== ev.line_start ? `${ev.path}:${ev.line_start}-${ev.line_end}` : `${ev.path}:${ev.line_start}`;
|
|
1290
1413
|
}
|
|
1291
1414
|
function extractLineStart(metadata) {
|
|
@@ -1323,7 +1446,11 @@ function toEvidenceRef(source, workspaceId, methodFallback) {
|
|
|
1323
1446
|
workspace_id: workspaceId,
|
|
1324
1447
|
metadata: {
|
|
1325
1448
|
source: String(source.source || ""),
|
|
1326
|
-
document: String(source.document || "")
|
|
1449
|
+
document: String(source.document || ""),
|
|
1450
|
+
video_url: String(metadata.video_url || ""),
|
|
1451
|
+
timestamp_start_ms: String(metadata.timestamp_start_ms ?? ""),
|
|
1452
|
+
timestamp_end_ms: String(metadata.timestamp_end_ms ?? ""),
|
|
1453
|
+
citation: String(metadata.citation || "")
|
|
1327
1454
|
}
|
|
1328
1455
|
};
|
|
1329
1456
|
}
|
|
@@ -1383,7 +1510,7 @@ function buildAbstain(args) {
|
|
|
1383
1510
|
reason: args.reason,
|
|
1384
1511
|
message: args.message,
|
|
1385
1512
|
closest_evidence: args.closest_evidence,
|
|
1386
|
-
recommended_next_calls: ["
|
|
1513
|
+
recommended_next_calls: ["index.workspace_status", "index.workspace_run", "symbol_search", "context.get_relevant"],
|
|
1387
1514
|
diagnostics: {
|
|
1388
1515
|
claims_evaluated: args.claims_evaluated,
|
|
1389
1516
|
evidence_items_found: args.evidence_items_found,
|
|
@@ -1437,8 +1564,267 @@ function countCodeFiles(searchPath, maxFiles = 5e3) {
|
|
|
1437
1564
|
walk(searchPath);
|
|
1438
1565
|
return { total, skipped };
|
|
1439
1566
|
}
|
|
1567
|
+
function toTextResult(payload) {
|
|
1568
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
1569
|
+
}
|
|
1570
|
+
function likelyEmbeddingFailure(error) {
|
|
1571
|
+
const message = String(error?.message || error || "").toLowerCase();
|
|
1572
|
+
return message.includes("embedding") || message.includes("vector") || message.includes("timeout") || message.includes("timed out") || message.includes("temporarily unavailable");
|
|
1573
|
+
}
|
|
1574
|
+
async function queryWithDegradedFallback(params) {
|
|
1575
|
+
try {
|
|
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: params.include_graph,
|
|
1582
|
+
hybrid: true,
|
|
1583
|
+
rerank: true
|
|
1584
|
+
});
|
|
1585
|
+
return { response, degraded_mode: false };
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
if (!likelyEmbeddingFailure(error)) throw error;
|
|
1588
|
+
const response = await whisper.query({
|
|
1589
|
+
project: params.project,
|
|
1590
|
+
query: params.query,
|
|
1591
|
+
top_k: params.top_k,
|
|
1592
|
+
include_memories: params.include_memories,
|
|
1593
|
+
include_graph: false,
|
|
1594
|
+
hybrid: false,
|
|
1595
|
+
rerank: false,
|
|
1596
|
+
vector_weight: 0,
|
|
1597
|
+
bm25_weight: 1
|
|
1598
|
+
});
|
|
1599
|
+
return {
|
|
1600
|
+
response,
|
|
1601
|
+
degraded_mode: true,
|
|
1602
|
+
degraded_reason: "Embedding/graph path unavailable; lexical fallback used.",
|
|
1603
|
+
recommendation: "Check embedding service health, then re-run for full hybrid quality."
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function getLocalAllowlistRoots() {
|
|
1608
|
+
const fromEnv = (process.env.WHISPER_LOCAL_ALLOWLIST || "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
1609
|
+
if (fromEnv.length > 0) return fromEnv;
|
|
1610
|
+
return [process.cwd()];
|
|
1611
|
+
}
|
|
1612
|
+
function isPathAllowed(targetPath) {
|
|
1613
|
+
const normalized = targetPath.replace(/\\/g, "/").toLowerCase();
|
|
1614
|
+
const allowlist = getLocalAllowlistRoots();
|
|
1615
|
+
const allowed = allowlist.some((root) => normalized.startsWith(root.replace(/\\/g, "/").toLowerCase()));
|
|
1616
|
+
return { allowed, allowlist };
|
|
1617
|
+
}
|
|
1618
|
+
function shouldSkipSensitivePath(filePath) {
|
|
1619
|
+
const p = filePath.replace(/\\/g, "/").toLowerCase();
|
|
1620
|
+
const denySnippets = [
|
|
1621
|
+
"/node_modules/",
|
|
1622
|
+
"/.git/",
|
|
1623
|
+
"/dist/",
|
|
1624
|
+
"/build/",
|
|
1625
|
+
"/.next/",
|
|
1626
|
+
"/.aws/",
|
|
1627
|
+
"/.ssh/",
|
|
1628
|
+
".pem",
|
|
1629
|
+
".key",
|
|
1630
|
+
".env",
|
|
1631
|
+
"credentials"
|
|
1632
|
+
];
|
|
1633
|
+
return denySnippets.some((s) => p.includes(s));
|
|
1634
|
+
}
|
|
1635
|
+
function redactLikelySecrets(content) {
|
|
1636
|
+
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]");
|
|
1637
|
+
}
|
|
1638
|
+
function loadIngestManifest() {
|
|
1639
|
+
ensureStateDir();
|
|
1640
|
+
if (!existsSync(LOCAL_INGEST_MANIFEST_PATH)) return {};
|
|
1641
|
+
try {
|
|
1642
|
+
const parsed = JSON.parse(readFileSync(LOCAL_INGEST_MANIFEST_PATH, "utf-8"));
|
|
1643
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1644
|
+
} catch {
|
|
1645
|
+
return {};
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function saveIngestManifest(manifest) {
|
|
1649
|
+
ensureStateDir();
|
|
1650
|
+
writeFileSync(LOCAL_INGEST_MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf-8");
|
|
1651
|
+
}
|
|
1652
|
+
async function ingestLocalPath(params) {
|
|
1653
|
+
if (RUNTIME_MODE === "remote") {
|
|
1654
|
+
throw new Error("Local ingestion is disabled in remote mode. Set WHISPER_MCP_MODE=auto or local.");
|
|
1655
|
+
}
|
|
1656
|
+
const rootPath = params.path || process.cwd();
|
|
1657
|
+
const gate = isPathAllowed(rootPath);
|
|
1658
|
+
if (!gate.allowed) {
|
|
1659
|
+
throw new Error(`Path not allowed by WHISPER_LOCAL_ALLOWLIST. Allowed roots: ${gate.allowlist.join(", ")}`);
|
|
1660
|
+
}
|
|
1661
|
+
const maxFiles = Math.max(1, params.max_files || 200);
|
|
1662
|
+
const maxBytesPerFile = 512 * 1024;
|
|
1663
|
+
const files = [];
|
|
1664
|
+
function collect(dir) {
|
|
1665
|
+
if (files.length >= maxFiles) return;
|
|
1666
|
+
let entries;
|
|
1667
|
+
try {
|
|
1668
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1669
|
+
} catch {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
for (const entry of entries) {
|
|
1673
|
+
if (files.length >= maxFiles) return;
|
|
1674
|
+
const full = join(dir, entry.name);
|
|
1675
|
+
if (entry.isDirectory()) {
|
|
1676
|
+
if (shouldSkipSensitivePath(full)) continue;
|
|
1677
|
+
collect(full);
|
|
1678
|
+
} else if (entry.isFile()) {
|
|
1679
|
+
if (shouldSkipSensitivePath(full)) continue;
|
|
1680
|
+
files.push(full);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
collect(rootPath);
|
|
1685
|
+
const manifest = loadIngestManifest();
|
|
1686
|
+
const workspaceId = getWorkspaceIdForPath(rootPath);
|
|
1687
|
+
if (!manifest[workspaceId]) manifest[workspaceId] = { last_run_at: (/* @__PURE__ */ new Date(0)).toISOString(), files: {} };
|
|
1688
|
+
const docs = [];
|
|
1689
|
+
const skipped = [];
|
|
1690
|
+
for (const fullPath of files) {
|
|
1691
|
+
try {
|
|
1692
|
+
const st = statSync(fullPath);
|
|
1693
|
+
if (st.size > maxBytesPerFile) {
|
|
1694
|
+
skipped.push(`${relative(rootPath, fullPath)} (size>${maxBytesPerFile})`);
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
const mtime = String(st.mtimeMs);
|
|
1698
|
+
const rel = relative(rootPath, fullPath);
|
|
1699
|
+
if (manifest[workspaceId].files[rel] === mtime) continue;
|
|
1700
|
+
const raw = readFileSync(fullPath, "utf-8");
|
|
1701
|
+
const content = redactLikelySecrets(raw).slice(0, params.chunk_chars || 2e4);
|
|
1702
|
+
docs.push({
|
|
1703
|
+
title: rel,
|
|
1704
|
+
content,
|
|
1705
|
+
file_path: rel,
|
|
1706
|
+
metadata: { source_type: "local", path: rel, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1707
|
+
});
|
|
1708
|
+
manifest[workspaceId].files[rel] = mtime;
|
|
1709
|
+
} catch {
|
|
1710
|
+
skipped.push(relative(rootPath, fullPath));
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
let ingested = 0;
|
|
1714
|
+
const batchSize = 25;
|
|
1715
|
+
for (let i = 0; i < docs.length; i += batchSize) {
|
|
1716
|
+
const batch = docs.slice(i, i + batchSize);
|
|
1717
|
+
const result = await whisper.ingest(params.project, batch);
|
|
1718
|
+
ingested += Number(result.ingested || batch.length);
|
|
1719
|
+
}
|
|
1720
|
+
manifest[workspaceId].last_run_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1721
|
+
saveIngestManifest(manifest);
|
|
1722
|
+
appendFileSync(
|
|
1723
|
+
AUDIT_LOG_PATH,
|
|
1724
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} local_ingest workspace=${workspaceId} root_hash=${createHash("sha256").update(rootPath).digest("hex").slice(0, 16)} files=${docs.length}
|
|
1725
|
+
`
|
|
1726
|
+
);
|
|
1727
|
+
return { ingested, scanned: files.length, queued: docs.length, skipped, workspace_id: workspaceId };
|
|
1728
|
+
}
|
|
1729
|
+
async function createSourceByType(params) {
|
|
1730
|
+
const connector_type = params.type === "github" ? "github" : params.type === "web" ? "website" : params.type === "pdf" ? "pdf" : params.type === "local" ? "local-folder" : params.type === "slack" ? "slack" : "video";
|
|
1731
|
+
const config = {};
|
|
1732
|
+
if (params.type === "github") {
|
|
1733
|
+
if (!params.owner || !params.repo) throw new Error("github source requires owner and repo");
|
|
1734
|
+
config.owner = params.owner;
|
|
1735
|
+
config.repo = params.repo;
|
|
1736
|
+
if (params.branch) config.branch = params.branch;
|
|
1737
|
+
if (params.paths) config.paths = params.paths;
|
|
1738
|
+
} else if (params.type === "web") {
|
|
1739
|
+
if (!params.url) throw new Error("web source requires url");
|
|
1740
|
+
config.url = params.url;
|
|
1741
|
+
if (params.crawl_depth !== void 0) config.crawl_depth = params.crawl_depth;
|
|
1742
|
+
if (params.include_paths) config.include_paths = params.include_paths;
|
|
1743
|
+
if (params.exclude_paths) config.exclude_paths = params.exclude_paths;
|
|
1744
|
+
} else if (params.type === "pdf") {
|
|
1745
|
+
if (!params.url && !params.file_path) throw new Error("pdf source requires url or file_path");
|
|
1746
|
+
if (params.url) config.url = params.url;
|
|
1747
|
+
if (params.file_path) config.file_path = params.file_path;
|
|
1748
|
+
} else if (params.type === "local") {
|
|
1749
|
+
if (!params.path) throw new Error("local source requires path");
|
|
1750
|
+
const ingest = await ingestLocalPath({
|
|
1751
|
+
project: params.project,
|
|
1752
|
+
path: params.path,
|
|
1753
|
+
glob: params.glob,
|
|
1754
|
+
max_files: params.max_files
|
|
1755
|
+
});
|
|
1756
|
+
return {
|
|
1757
|
+
source_id: `local_${ingest.workspace_id}`,
|
|
1758
|
+
status: "ready",
|
|
1759
|
+
job_id: `local_ingest_${Date.now()}`,
|
|
1760
|
+
index_started: true,
|
|
1761
|
+
warnings: ingest.skipped.slice(0, 20),
|
|
1762
|
+
details: ingest
|
|
1763
|
+
};
|
|
1764
|
+
} else if (params.type === "slack") {
|
|
1765
|
+
config.channel_ids = params.channel_ids || [];
|
|
1766
|
+
if (params.since) config.since = params.since;
|
|
1767
|
+
if (params.workspace_id) config.workspace_id = params.workspace_id;
|
|
1768
|
+
if (params.token) config.token = params.token;
|
|
1769
|
+
if (params.auth_ref) config.auth_ref = params.auth_ref;
|
|
1770
|
+
} else if (params.type === "video") {
|
|
1771
|
+
if (!params.url) throw new Error("video source requires url");
|
|
1772
|
+
config.url = params.url;
|
|
1773
|
+
if (params.platform) config.platform = params.platform;
|
|
1774
|
+
if (params.language) config.language = params.language;
|
|
1775
|
+
if (params.allow_stt_fallback !== void 0) config.allow_stt_fallback = params.allow_stt_fallback;
|
|
1776
|
+
if (params.max_duration_minutes !== void 0) config.max_duration_minutes = params.max_duration_minutes;
|
|
1777
|
+
if (params.max_chunks !== void 0) config.max_chunks = params.max_chunks;
|
|
1778
|
+
}
|
|
1779
|
+
if (params.metadata) config.metadata = params.metadata;
|
|
1780
|
+
config.auto_index = params.auto_index ?? true;
|
|
1781
|
+
const created = await whisper.addSource(params.project, {
|
|
1782
|
+
name: params.name || `${params.type}-source-${Date.now()}`,
|
|
1783
|
+
connector_type,
|
|
1784
|
+
config
|
|
1785
|
+
});
|
|
1786
|
+
let jobId;
|
|
1787
|
+
let status = "queued";
|
|
1788
|
+
if (params.auto_index ?? true) {
|
|
1789
|
+
const syncRes = await whisper.syncSource(created.id);
|
|
1790
|
+
jobId = String(syncRes?.id || syncRes?.job_id || "");
|
|
1791
|
+
status = "indexing";
|
|
1792
|
+
}
|
|
1793
|
+
return {
|
|
1794
|
+
source_id: created.id,
|
|
1795
|
+
status,
|
|
1796
|
+
job_id: jobId || null,
|
|
1797
|
+
index_started: params.auto_index ?? true,
|
|
1798
|
+
warnings: []
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
function scopeConfigJson(project, source, client) {
|
|
1802
|
+
const serverDef = {
|
|
1803
|
+
command: "npx",
|
|
1804
|
+
args: ["-y", "@usewhisper/mcp-server"],
|
|
1805
|
+
env: {
|
|
1806
|
+
WHISPER_API_KEY: "wctx_...",
|
|
1807
|
+
WHISPER_PROJECT: project,
|
|
1808
|
+
WHISPER_SCOPE_SOURCE: source
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
if (client === "json") {
|
|
1812
|
+
return JSON.stringify({ mcpServers: { "whisper-context-scoped": serverDef } }, null, 2);
|
|
1813
|
+
}
|
|
1814
|
+
return JSON.stringify({ mcpServers: { "whisper-context-scoped": serverDef } }, null, 2);
|
|
1815
|
+
}
|
|
1816
|
+
function printToolMap() {
|
|
1817
|
+
console.log("Legacy -> canonical MCP tool names:");
|
|
1818
|
+
for (const row of TOOL_MIGRATION_MAP) {
|
|
1819
|
+
console.log(`- ${row.old} => ${row.next}`);
|
|
1820
|
+
}
|
|
1821
|
+
console.log("\nAgent-friendly aliases:");
|
|
1822
|
+
for (const row of ALIAS_TOOL_MAP) {
|
|
1823
|
+
console.log(`- ${row.alias} => ${row.target}`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1440
1826
|
server.tool(
|
|
1441
|
-
"
|
|
1827
|
+
"index.workspace_resolve",
|
|
1442
1828
|
"Resolve workspace identity from path + API key and map to a project without mandatory dashboard setup.",
|
|
1443
1829
|
{
|
|
1444
1830
|
path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
|
|
@@ -1472,7 +1858,7 @@ server.tool(
|
|
|
1472
1858
|
}
|
|
1473
1859
|
);
|
|
1474
1860
|
server.tool(
|
|
1475
|
-
"
|
|
1861
|
+
"index.workspace_status",
|
|
1476
1862
|
"Check index freshness, coverage, commit, and pending changes before retrieval/edits.",
|
|
1477
1863
|
{
|
|
1478
1864
|
workspace_id: z.string().optional(),
|
|
@@ -1506,7 +1892,7 @@ server.tool(
|
|
|
1506
1892
|
}
|
|
1507
1893
|
);
|
|
1508
1894
|
server.tool(
|
|
1509
|
-
"
|
|
1895
|
+
"index.workspace_run",
|
|
1510
1896
|
"Index workspace in full or incremental mode and update index metadata for freshness checks.",
|
|
1511
1897
|
{
|
|
1512
1898
|
workspace_id: z.string().optional(),
|
|
@@ -1545,7 +1931,43 @@ server.tool(
|
|
|
1545
1931
|
}
|
|
1546
1932
|
);
|
|
1547
1933
|
server.tool(
|
|
1548
|
-
"
|
|
1934
|
+
"index.local_scan_ingest",
|
|
1935
|
+
"Scan a local folder safely (allowlist + secret filters), ingest changed files, and persist incremental manifest.",
|
|
1936
|
+
{
|
|
1937
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
1938
|
+
path: z.string().optional().describe("Local path to ingest. Defaults to current working directory."),
|
|
1939
|
+
glob: z.string().optional().describe("Optional include glob"),
|
|
1940
|
+
max_files: z.number().optional().default(200),
|
|
1941
|
+
chunk_chars: z.number().optional().default(2e4)
|
|
1942
|
+
},
|
|
1943
|
+
async ({ project, path, glob, max_files, chunk_chars }) => {
|
|
1944
|
+
try {
|
|
1945
|
+
const resolvedProject = await resolveProjectRef(project);
|
|
1946
|
+
if (!resolvedProject) {
|
|
1947
|
+
return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
|
|
1948
|
+
}
|
|
1949
|
+
const result = await ingestLocalPath({
|
|
1950
|
+
project: resolvedProject,
|
|
1951
|
+
path: path || process.cwd(),
|
|
1952
|
+
glob,
|
|
1953
|
+
max_files,
|
|
1954
|
+
chunk_chars
|
|
1955
|
+
});
|
|
1956
|
+
return toTextResult({
|
|
1957
|
+
source_id: `local_${result.workspace_id}`,
|
|
1958
|
+
status: "ready",
|
|
1959
|
+
job_id: `local_ingest_${Date.now()}`,
|
|
1960
|
+
index_started: true,
|
|
1961
|
+
warnings: result.skipped.slice(0, 20),
|
|
1962
|
+
details: result
|
|
1963
|
+
});
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
);
|
|
1969
|
+
server.tool(
|
|
1970
|
+
"context.get_relevant",
|
|
1549
1971
|
"Core retrieval. Task goes in, ranked context chunks come out with structured evidence (file:line ready).",
|
|
1550
1972
|
{
|
|
1551
1973
|
question: z.string().describe("Task/question to retrieve context for"),
|
|
@@ -1574,7 +1996,7 @@ server.tool(
|
|
|
1574
1996
|
};
|
|
1575
1997
|
return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
|
|
1576
1998
|
}
|
|
1577
|
-
const
|
|
1999
|
+
const queryResult = await queryWithDegradedFallback({
|
|
1578
2000
|
project: resolvedProject,
|
|
1579
2001
|
query: question,
|
|
1580
2002
|
top_k,
|
|
@@ -1583,6 +2005,7 @@ server.tool(
|
|
|
1583
2005
|
session_id,
|
|
1584
2006
|
user_id
|
|
1585
2007
|
});
|
|
2008
|
+
const response = queryResult.response;
|
|
1586
2009
|
const evidence = (response.results || []).map((r) => toEvidenceRef(r, workspaceId, "semantic"));
|
|
1587
2010
|
const payload = {
|
|
1588
2011
|
question,
|
|
@@ -1591,7 +2014,10 @@ server.tool(
|
|
|
1591
2014
|
context: response.context || "",
|
|
1592
2015
|
evidence,
|
|
1593
2016
|
used_context_ids: (response.results || []).map((r) => String(r.id)),
|
|
1594
|
-
latency_ms: response.meta?.latency_ms || 0
|
|
2017
|
+
latency_ms: response.meta?.latency_ms || 0,
|
|
2018
|
+
degraded_mode: queryResult.degraded_mode,
|
|
2019
|
+
degraded_reason: queryResult.degraded_reason,
|
|
2020
|
+
recommendation: queryResult.recommendation
|
|
1595
2021
|
};
|
|
1596
2022
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
1597
2023
|
} catch (error) {
|
|
@@ -1600,7 +2026,7 @@ server.tool(
|
|
|
1600
2026
|
}
|
|
1601
2027
|
);
|
|
1602
2028
|
server.tool(
|
|
1603
|
-
"
|
|
2029
|
+
"context.claim_verify",
|
|
1604
2030
|
"Verify whether a claim is supported by retrieved context. Returns supported/partial/unsupported with evidence.",
|
|
1605
2031
|
{
|
|
1606
2032
|
claim: z.string().describe("Claim to verify"),
|
|
@@ -1653,7 +2079,7 @@ server.tool(
|
|
|
1653
2079
|
}
|
|
1654
2080
|
);
|
|
1655
2081
|
server.tool(
|
|
1656
|
-
"
|
|
2082
|
+
"context.evidence_answer",
|
|
1657
2083
|
"Answer a question only when evidence requirements are met. Fails closed with an abstain payload when not verifiable.",
|
|
1658
2084
|
{
|
|
1659
2085
|
question: z.string(),
|
|
@@ -1767,7 +2193,7 @@ server.tool(
|
|
|
1767
2193
|
}
|
|
1768
2194
|
);
|
|
1769
2195
|
server.tool(
|
|
1770
|
-
"
|
|
2196
|
+
"context.query",
|
|
1771
2197
|
"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
2198
|
{
|
|
1773
2199
|
project: z.string().optional().describe("Project name or slug (optional if WHISPER_PROJECT is set)"),
|
|
@@ -1782,31 +2208,38 @@ server.tool(
|
|
|
1782
2208
|
},
|
|
1783
2209
|
async ({ project, query, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens }) => {
|
|
1784
2210
|
try {
|
|
1785
|
-
const
|
|
1786
|
-
|
|
2211
|
+
const resolvedProject = await resolveProjectRef(project);
|
|
2212
|
+
if (!resolvedProject) {
|
|
2213
|
+
return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
|
|
2214
|
+
}
|
|
2215
|
+
const queryResult = await queryWithDegradedFallback({
|
|
2216
|
+
project: resolvedProject,
|
|
1787
2217
|
query,
|
|
1788
2218
|
top_k,
|
|
1789
|
-
chunk_types,
|
|
1790
2219
|
include_memories,
|
|
1791
2220
|
include_graph,
|
|
1792
2221
|
user_id,
|
|
1793
|
-
session_id
|
|
1794
|
-
max_tokens
|
|
2222
|
+
session_id
|
|
1795
2223
|
});
|
|
2224
|
+
const response = queryResult.response;
|
|
1796
2225
|
if (response.results.length === 0) {
|
|
1797
2226
|
return { content: [{ type: "text", text: "No relevant context found." }] };
|
|
1798
2227
|
}
|
|
1799
2228
|
const header = `Found ${response.meta.total} results (${response.meta.latency_ms}ms${response.meta.cache_hit ? ", cached" : ""}):
|
|
1800
2229
|
|
|
1801
2230
|
`;
|
|
1802
|
-
|
|
2231
|
+
const suffix = queryResult.degraded_mode ? `
|
|
2232
|
+
|
|
2233
|
+
[degraded_mode=true] ${queryResult.degraded_reason}
|
|
2234
|
+
Recommendation: ${queryResult.recommendation}` : "";
|
|
2235
|
+
return { content: [{ type: "text", text: header + response.context + suffix }] };
|
|
1803
2236
|
} catch (error) {
|
|
1804
2237
|
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
1805
2238
|
}
|
|
1806
2239
|
}
|
|
1807
2240
|
);
|
|
1808
2241
|
server.tool(
|
|
1809
|
-
"
|
|
2242
|
+
"memory.add",
|
|
1810
2243
|
"Store a memory (fact, preference, decision) that persists across conversations. Memories can be scoped to a user, session, or agent.",
|
|
1811
2244
|
{
|
|
1812
2245
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -1835,7 +2268,7 @@ server.tool(
|
|
|
1835
2268
|
}
|
|
1836
2269
|
);
|
|
1837
2270
|
server.tool(
|
|
1838
|
-
"
|
|
2271
|
+
"memory.search",
|
|
1839
2272
|
"Search stored memories by semantic similarity. Recall facts, preferences, past decisions from previous interactions.",
|
|
1840
2273
|
{
|
|
1841
2274
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -1867,7 +2300,7 @@ ${r.content}`).join("\n\n");
|
|
|
1867
2300
|
}
|
|
1868
2301
|
);
|
|
1869
2302
|
server.tool(
|
|
1870
|
-
"list_projects",
|
|
2303
|
+
"context.list_projects",
|
|
1871
2304
|
"List all available context projects.",
|
|
1872
2305
|
{},
|
|
1873
2306
|
async () => {
|
|
@@ -1881,7 +2314,7 @@ server.tool(
|
|
|
1881
2314
|
}
|
|
1882
2315
|
);
|
|
1883
2316
|
server.tool(
|
|
1884
|
-
"list_sources",
|
|
2317
|
+
"context.list_sources",
|
|
1885
2318
|
"List all data sources connected to a project.",
|
|
1886
2319
|
{ project: z.string().optional().describe("Project name or slug") },
|
|
1887
2320
|
async ({ project }) => {
|
|
@@ -1896,7 +2329,95 @@ server.tool(
|
|
|
1896
2329
|
}
|
|
1897
2330
|
);
|
|
1898
2331
|
server.tool(
|
|
1899
|
-
"
|
|
2332
|
+
"context.add_source",
|
|
2333
|
+
"Add a source to a project with normalized source contract and auto-index by default.",
|
|
2334
|
+
{
|
|
2335
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
2336
|
+
type: z.enum(["github", "web", "pdf", "local", "slack", "video"]).default("github"),
|
|
2337
|
+
name: z.string().optional(),
|
|
2338
|
+
auto_index: z.boolean().optional().default(true),
|
|
2339
|
+
metadata: z.record(z.string()).optional(),
|
|
2340
|
+
owner: z.string().optional(),
|
|
2341
|
+
repo: z.string().optional(),
|
|
2342
|
+
branch: z.string().optional(),
|
|
2343
|
+
paths: z.array(z.string()).optional(),
|
|
2344
|
+
url: z.string().url().optional(),
|
|
2345
|
+
crawl_depth: z.number().optional(),
|
|
2346
|
+
include_paths: z.array(z.string()).optional(),
|
|
2347
|
+
exclude_paths: z.array(z.string()).optional(),
|
|
2348
|
+
file_path: z.string().optional(),
|
|
2349
|
+
path: z.string().optional(),
|
|
2350
|
+
glob: z.string().optional(),
|
|
2351
|
+
max_files: z.number().optional(),
|
|
2352
|
+
workspace_id: z.string().optional(),
|
|
2353
|
+
channel_ids: z.array(z.string()).optional(),
|
|
2354
|
+
since: z.string().optional(),
|
|
2355
|
+
token: z.string().optional(),
|
|
2356
|
+
auth_ref: z.string().optional(),
|
|
2357
|
+
platform: z.enum(["youtube", "loom", "generic"]).optional(),
|
|
2358
|
+
language: z.string().optional(),
|
|
2359
|
+
allow_stt_fallback: z.boolean().optional(),
|
|
2360
|
+
max_duration_minutes: z.number().optional(),
|
|
2361
|
+
max_chunks: z.number().optional()
|
|
2362
|
+
},
|
|
2363
|
+
async (input) => {
|
|
2364
|
+
try {
|
|
2365
|
+
const resolvedProject = await resolveProjectRef(input.project);
|
|
2366
|
+
if (!resolvedProject) {
|
|
2367
|
+
return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
|
|
2368
|
+
}
|
|
2369
|
+
const result = await createSourceByType({
|
|
2370
|
+
project: resolvedProject,
|
|
2371
|
+
type: input.type,
|
|
2372
|
+
name: input.name,
|
|
2373
|
+
auto_index: input.auto_index,
|
|
2374
|
+
metadata: input.metadata,
|
|
2375
|
+
owner: input.owner,
|
|
2376
|
+
repo: input.repo,
|
|
2377
|
+
branch: input.branch,
|
|
2378
|
+
paths: input.paths,
|
|
2379
|
+
url: input.url,
|
|
2380
|
+
crawl_depth: input.crawl_depth,
|
|
2381
|
+
include_paths: input.include_paths,
|
|
2382
|
+
exclude_paths: input.exclude_paths,
|
|
2383
|
+
file_path: input.file_path,
|
|
2384
|
+
path: input.path,
|
|
2385
|
+
glob: input.glob,
|
|
2386
|
+
max_files: input.max_files,
|
|
2387
|
+
workspace_id: input.workspace_id,
|
|
2388
|
+
channel_ids: input.channel_ids,
|
|
2389
|
+
since: input.since,
|
|
2390
|
+
token: input.token,
|
|
2391
|
+
auth_ref: input.auth_ref,
|
|
2392
|
+
platform: input.platform,
|
|
2393
|
+
language: input.language,
|
|
2394
|
+
allow_stt_fallback: input.allow_stt_fallback,
|
|
2395
|
+
max_duration_minutes: input.max_duration_minutes,
|
|
2396
|
+
max_chunks: input.max_chunks
|
|
2397
|
+
});
|
|
2398
|
+
return toTextResult(result);
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
);
|
|
2404
|
+
server.tool(
|
|
2405
|
+
"context.source_status",
|
|
2406
|
+
"Get status and stage/progress details for a source sync job.",
|
|
2407
|
+
{
|
|
2408
|
+
source_id: z.string().describe("Source id")
|
|
2409
|
+
},
|
|
2410
|
+
async ({ source_id }) => {
|
|
2411
|
+
try {
|
|
2412
|
+
const result = await whisper.getSourceStatus(source_id);
|
|
2413
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
);
|
|
2419
|
+
server.tool(
|
|
2420
|
+
"context.add_text",
|
|
1900
2421
|
"Add text content to a project's knowledge base.",
|
|
1901
2422
|
{
|
|
1902
2423
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -1917,7 +2438,56 @@ server.tool(
|
|
|
1917
2438
|
}
|
|
1918
2439
|
);
|
|
1919
2440
|
server.tool(
|
|
1920
|
-
"
|
|
2441
|
+
"context.add_document",
|
|
2442
|
+
"Ingest a document into project knowledge. Supports plain text and video URLs.",
|
|
2443
|
+
{
|
|
2444
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
2445
|
+
source_type: z.enum(["text", "video"]).default("text"),
|
|
2446
|
+
title: z.string().optional().describe("Title for text documents"),
|
|
2447
|
+
content: z.string().optional().describe("Text document content"),
|
|
2448
|
+
url: z.string().url().optional().describe("Video URL when source_type=video"),
|
|
2449
|
+
auto_sync: z.boolean().optional().default(true),
|
|
2450
|
+
tags: z.array(z.string()).optional(),
|
|
2451
|
+
platform: z.enum(["youtube", "loom", "generic"]).optional(),
|
|
2452
|
+
language: z.string().optional()
|
|
2453
|
+
},
|
|
2454
|
+
async ({ project, source_type, title, content, url, auto_sync, tags, platform, language }) => {
|
|
2455
|
+
try {
|
|
2456
|
+
const resolvedProject = await resolveProjectRef(project);
|
|
2457
|
+
if (!resolvedProject) {
|
|
2458
|
+
return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
|
|
2459
|
+
}
|
|
2460
|
+
if (source_type === "video") {
|
|
2461
|
+
if (!url) {
|
|
2462
|
+
return { content: [{ type: "text", text: "Error: url is required when source_type=video." }] };
|
|
2463
|
+
}
|
|
2464
|
+
const result = await whisper.addSourceByType(resolvedProject, {
|
|
2465
|
+
type: "video",
|
|
2466
|
+
url,
|
|
2467
|
+
auto_sync,
|
|
2468
|
+
tags,
|
|
2469
|
+
platform,
|
|
2470
|
+
language
|
|
2471
|
+
});
|
|
2472
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2473
|
+
}
|
|
2474
|
+
if (!content?.trim()) {
|
|
2475
|
+
return { content: [{ type: "text", text: "Error: content is required when source_type=text." }] };
|
|
2476
|
+
}
|
|
2477
|
+
await whisper.addContext({
|
|
2478
|
+
project: resolvedProject,
|
|
2479
|
+
title: title || "Document",
|
|
2480
|
+
content,
|
|
2481
|
+
metadata: { source: "mcp:add_document", tags: tags || [] }
|
|
2482
|
+
});
|
|
2483
|
+
return { content: [{ type: "text", text: `Indexed "${title || "Document"}" (${content.length} chars).` }] };
|
|
2484
|
+
} catch (error) {
|
|
2485
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
);
|
|
2489
|
+
server.tool(
|
|
2490
|
+
"memory.search_sota",
|
|
1921
2491
|
"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
2492
|
{
|
|
1923
2493
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -1962,7 +2532,7 @@ server.tool(
|
|
|
1962
2532
|
}
|
|
1963
2533
|
);
|
|
1964
2534
|
server.tool(
|
|
1965
|
-
"ingest_conversation",
|
|
2535
|
+
"memory.ingest_conversation",
|
|
1966
2536
|
"Extract memories from a conversation session. Automatically handles disambiguation, temporal grounding, and relation detection.",
|
|
1967
2537
|
{
|
|
1968
2538
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -1998,7 +2568,7 @@ server.tool(
|
|
|
1998
2568
|
}
|
|
1999
2569
|
);
|
|
2000
2570
|
server.tool(
|
|
2001
|
-
"
|
|
2571
|
+
"research.oracle",
|
|
2002
2572
|
"Oracle Research Mode - Tree-guided document navigation with multi-step reasoning. More precise than standard search, especially for bleeding-edge features.",
|
|
2003
2573
|
{
|
|
2004
2574
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -2046,7 +2616,7 @@ ${r.content.slice(0, 200)}...`
|
|
|
2046
2616
|
}
|
|
2047
2617
|
);
|
|
2048
2618
|
server.tool(
|
|
2049
|
-
"
|
|
2619
|
+
"index.autosubscribe_deps",
|
|
2050
2620
|
"Automatically index a project's dependencies (package.json, requirements.txt, etc.). Resolves docs URLs and indexes documentation.",
|
|
2051
2621
|
{
|
|
2052
2622
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -2079,7 +2649,7 @@ server.tool(
|
|
|
2079
2649
|
}
|
|
2080
2650
|
);
|
|
2081
2651
|
server.tool(
|
|
2082
|
-
"
|
|
2652
|
+
"context.share",
|
|
2083
2653
|
"Create a shareable snapshot of a conversation with memories. Returns a URL that can be shared or resumed later.",
|
|
2084
2654
|
{
|
|
2085
2655
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -2113,7 +2683,7 @@ Share URL: ${result.share_url}`
|
|
|
2113
2683
|
}
|
|
2114
2684
|
);
|
|
2115
2685
|
server.tool(
|
|
2116
|
-
"
|
|
2686
|
+
"memory.consolidate",
|
|
2117
2687
|
"Find and merge duplicate memories to reduce bloat. Uses vector similarity + LLM merging.",
|
|
2118
2688
|
{
|
|
2119
2689
|
project: z.string().optional().describe("Project name or slug"),
|
|
@@ -2156,7 +2726,7 @@ Run without dry_run to merge.`
|
|
|
2156
2726
|
}
|
|
2157
2727
|
);
|
|
2158
2728
|
server.tool(
|
|
2159
|
-
"
|
|
2729
|
+
"context.cost_summary",
|
|
2160
2730
|
"Get cost tracking summary showing spending by model and task. Includes savings vs always-Opus.",
|
|
2161
2731
|
{
|
|
2162
2732
|
project: z.string().optional().describe("Project name or slug (optional for org-wide)"),
|
|
@@ -2199,7 +2769,7 @@ server.tool(
|
|
|
2199
2769
|
}
|
|
2200
2770
|
);
|
|
2201
2771
|
server.tool(
|
|
2202
|
-
"forget",
|
|
2772
|
+
"memory.forget",
|
|
2203
2773
|
"Delete or invalidate memories with immutable audit logging.",
|
|
2204
2774
|
{
|
|
2205
2775
|
workspace_id: z.string().optional(),
|
|
@@ -2311,7 +2881,7 @@ server.tool(
|
|
|
2311
2881
|
}
|
|
2312
2882
|
);
|
|
2313
2883
|
server.tool(
|
|
2314
|
-
"
|
|
2884
|
+
"context.export_bundle",
|
|
2315
2885
|
"Export project/workspace memory and context to a portable bundle with checksum.",
|
|
2316
2886
|
{
|
|
2317
2887
|
workspace_id: z.string().optional(),
|
|
@@ -2363,7 +2933,7 @@ server.tool(
|
|
|
2363
2933
|
}
|
|
2364
2934
|
);
|
|
2365
2935
|
server.tool(
|
|
2366
|
-
"
|
|
2936
|
+
"context.import_bundle",
|
|
2367
2937
|
"Import a portable context bundle with merge/replace modes and checksum verification.",
|
|
2368
2938
|
{
|
|
2369
2939
|
workspace_id: z.string().optional(),
|
|
@@ -2465,7 +3035,7 @@ server.tool(
|
|
|
2465
3035
|
}
|
|
2466
3036
|
);
|
|
2467
3037
|
server.tool(
|
|
2468
|
-
"
|
|
3038
|
+
"context.diff",
|
|
2469
3039
|
"Return deterministic context changes from an explicit anchor (session_id, timestamp, or commit).",
|
|
2470
3040
|
{
|
|
2471
3041
|
workspace_id: z.string().optional(),
|
|
@@ -2556,7 +3126,7 @@ function extractSignature(filePath, content) {
|
|
|
2556
3126
|
return signature.join("\n").slice(0, 2e3);
|
|
2557
3127
|
}
|
|
2558
3128
|
server.tool(
|
|
2559
|
-
"
|
|
3129
|
+
"code.search_semantic",
|
|
2560
3130
|
"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
3131
|
{
|
|
2562
3132
|
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'"),
|
|
@@ -2672,8 +3242,41 @@ function* walkDir(dir, fileTypes) {
|
|
|
2672
3242
|
}
|
|
2673
3243
|
}
|
|
2674
3244
|
}
|
|
3245
|
+
function listTree(rootPath, maxDepth = 3, maxEntries = 200) {
|
|
3246
|
+
const lines = [];
|
|
3247
|
+
let seen = 0;
|
|
3248
|
+
function walk(dir, depth) {
|
|
3249
|
+
if (seen >= maxEntries || depth > maxDepth) return;
|
|
3250
|
+
let entries;
|
|
3251
|
+
try {
|
|
3252
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3253
|
+
} catch {
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
const filtered = entries.filter((entry) => !SKIP_DIRS.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
3257
|
+
for (const entry of filtered) {
|
|
3258
|
+
if (seen >= maxEntries) return;
|
|
3259
|
+
const indent = " ".repeat(depth);
|
|
3260
|
+
lines.push(`${indent}${entry.isDirectory() ? "d" : "f"} ${entry.name}`);
|
|
3261
|
+
seen += 1;
|
|
3262
|
+
if (entry.isDirectory()) {
|
|
3263
|
+
walk(join(dir, entry.name), depth + 1);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
walk(rootPath, 0);
|
|
3268
|
+
return lines;
|
|
3269
|
+
}
|
|
3270
|
+
function readFileWindow(filePath, startLine = 1, endLine = 200) {
|
|
3271
|
+
const content = readFileSync(filePath, "utf-8");
|
|
3272
|
+
const lines = content.split("\n");
|
|
3273
|
+
const safeStart = Math.max(1, startLine);
|
|
3274
|
+
const safeEnd = Math.max(safeStart, endLine);
|
|
3275
|
+
const excerpt = lines.slice(safeStart - 1, safeEnd).map((line, index) => `${safeStart + index}: ${line}`).join("\n");
|
|
3276
|
+
return excerpt || "(empty file)";
|
|
3277
|
+
}
|
|
2675
3278
|
server.tool(
|
|
2676
|
-
"
|
|
3279
|
+
"code.search_text",
|
|
2677
3280
|
"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
3281
|
{
|
|
2679
3282
|
query: z.string().describe("What to search for \u2014 natural language keyword, function name, pattern, etc."),
|
|
@@ -2812,7 +3415,7 @@ server.tool(
|
|
|
2812
3415
|
}
|
|
2813
3416
|
);
|
|
2814
3417
|
server.tool(
|
|
2815
|
-
"
|
|
3418
|
+
"code.semantic_documents",
|
|
2816
3419
|
"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
3420
|
{
|
|
2818
3421
|
query: z.string().describe("What to search for semantically (e.g. 'authentication logic', 'database connection')"),
|
|
@@ -2855,7 +3458,410 @@ server.tool(
|
|
|
2855
3458
|
}
|
|
2856
3459
|
}
|
|
2857
3460
|
);
|
|
3461
|
+
server.tool(
|
|
3462
|
+
"search",
|
|
3463
|
+
"Search indexed code, docs, and connected sources with one obvious verb. Use this first for most retrieval tasks.",
|
|
3464
|
+
{
|
|
3465
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
3466
|
+
query: z.string().describe("What you want to find"),
|
|
3467
|
+
top_k: z.number().optional().default(10),
|
|
3468
|
+
include_memories: z.boolean().optional().default(false),
|
|
3469
|
+
include_graph: z.boolean().optional().default(false),
|
|
3470
|
+
user_id: z.string().optional(),
|
|
3471
|
+
session_id: z.string().optional()
|
|
3472
|
+
},
|
|
3473
|
+
async ({ project, query, top_k, include_memories, include_graph, user_id, session_id }) => {
|
|
3474
|
+
try {
|
|
3475
|
+
const resolvedProject = await resolveProjectRef(project);
|
|
3476
|
+
if (!resolvedProject) {
|
|
3477
|
+
return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
|
|
3478
|
+
}
|
|
3479
|
+
const queryResult = await queryWithDegradedFallback({
|
|
3480
|
+
project: resolvedProject,
|
|
3481
|
+
query,
|
|
3482
|
+
top_k,
|
|
3483
|
+
include_memories,
|
|
3484
|
+
include_graph,
|
|
3485
|
+
user_id,
|
|
3486
|
+
session_id
|
|
3487
|
+
});
|
|
3488
|
+
const response = queryResult.response;
|
|
3489
|
+
if (!response.results?.length) {
|
|
3490
|
+
return { content: [{ type: "text", text: `No relevant results found for "${query}".` }] };
|
|
3491
|
+
}
|
|
3492
|
+
const suffix = queryResult.degraded_mode ? `
|
|
3493
|
+
|
|
3494
|
+
[degraded_mode=true] ${queryResult.degraded_reason}` : "";
|
|
3495
|
+
return { content: [{ type: "text", text: response.context + suffix }] };
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
);
|
|
3501
|
+
server.tool(
|
|
3502
|
+
"search_code",
|
|
3503
|
+
"Semantically search a local codebase by meaning. Use this for questions like 'where is auth handled?' or 'find retry logic'.",
|
|
3504
|
+
{
|
|
3505
|
+
query: z.string().describe("Natural-language code search query"),
|
|
3506
|
+
path: z.string().optional().describe("Codebase root. Defaults to current working directory."),
|
|
3507
|
+
file_types: z.array(z.string()).optional(),
|
|
3508
|
+
top_k: z.number().optional().default(10),
|
|
3509
|
+
threshold: z.number().optional().default(0.2),
|
|
3510
|
+
max_files: z.number().optional().default(150)
|
|
3511
|
+
},
|
|
3512
|
+
async ({ query, path, file_types, top_k, threshold, max_files }) => {
|
|
3513
|
+
const rootPath = path || process.cwd();
|
|
3514
|
+
const allowedExts = file_types ? new Set(file_types) : CODE_EXTENSIONS;
|
|
3515
|
+
const files = [];
|
|
3516
|
+
function collect(dir) {
|
|
3517
|
+
if (files.length >= (max_files ?? 150)) return;
|
|
3518
|
+
let entries;
|
|
3519
|
+
try {
|
|
3520
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3521
|
+
} catch {
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
for (const entry of entries) {
|
|
3525
|
+
if (files.length >= (max_files ?? 150)) break;
|
|
3526
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
3527
|
+
const full = join(dir, entry.name);
|
|
3528
|
+
if (entry.isDirectory()) collect(full);
|
|
3529
|
+
else if (entry.isFile()) {
|
|
3530
|
+
const ext = extname(entry.name).replace(".", "");
|
|
3531
|
+
if (allowedExts.has(ext)) files.push(full);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
collect(rootPath);
|
|
3536
|
+
if (files.length === 0) {
|
|
3537
|
+
return { content: [{ type: "text", text: `No code files found in ${rootPath}` }] };
|
|
3538
|
+
}
|
|
3539
|
+
const documents = [];
|
|
3540
|
+
for (const filePath of files) {
|
|
3541
|
+
try {
|
|
3542
|
+
const stat = statSync(filePath);
|
|
3543
|
+
if (stat.size > 500 * 1024) continue;
|
|
3544
|
+
const content = readFileSync(filePath, "utf-8");
|
|
3545
|
+
const relPath = relative(rootPath, filePath);
|
|
3546
|
+
documents.push({ id: relPath, content: extractSignature(relPath, content) });
|
|
3547
|
+
} catch {
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
try {
|
|
3551
|
+
const response = await whisper.semanticSearch({
|
|
3552
|
+
query,
|
|
3553
|
+
documents,
|
|
3554
|
+
top_k: top_k ?? 10,
|
|
3555
|
+
threshold: threshold ?? 0.2
|
|
3556
|
+
});
|
|
3557
|
+
if (!response.results?.length) {
|
|
3558
|
+
return { content: [{ type: "text", text: `No semantically relevant files found for "${query}".` }] };
|
|
3559
|
+
}
|
|
3560
|
+
const lines = response.results.map((result) => `${result.id} (score: ${result.score})${result.snippet ? `
|
|
3561
|
+
${result.snippet}` : ""}`);
|
|
3562
|
+
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
3563
|
+
} catch (error) {
|
|
3564
|
+
return { content: [{ type: "text", text: `Semantic search failed: ${error.message}` }] };
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
);
|
|
3568
|
+
server.tool(
|
|
3569
|
+
"grep",
|
|
3570
|
+
"Regex or text search across a local codebase. Use this when you know the symbol, string, or pattern you want.",
|
|
3571
|
+
{
|
|
3572
|
+
query: z.string().describe("Text or regex-like pattern to search for"),
|
|
3573
|
+
path: z.string().optional().describe("Search root. Defaults to current working directory."),
|
|
3574
|
+
file_types: z.array(z.string()).optional(),
|
|
3575
|
+
max_results: z.number().optional().default(20),
|
|
3576
|
+
case_sensitive: z.boolean().optional().default(false)
|
|
3577
|
+
},
|
|
3578
|
+
async ({ query, path, file_types, max_results, case_sensitive }) => {
|
|
3579
|
+
const rootPath = path || process.cwd();
|
|
3580
|
+
const results = [];
|
|
3581
|
+
const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), case_sensitive ? "g" : "gi");
|
|
3582
|
+
for (const filePath of walkDir(rootPath, file_types)) {
|
|
3583
|
+
if (results.length >= (max_results ?? 20)) break;
|
|
3584
|
+
try {
|
|
3585
|
+
const stat = statSync(filePath);
|
|
3586
|
+
if (stat.size > 512 * 1024) continue;
|
|
3587
|
+
const text = readFileSync(filePath, "utf-8");
|
|
3588
|
+
const lines2 = text.split("\n");
|
|
3589
|
+
const matches = [];
|
|
3590
|
+
lines2.forEach((line, index) => {
|
|
3591
|
+
regex.lastIndex = 0;
|
|
3592
|
+
if (regex.test(line)) {
|
|
3593
|
+
matches.push({ line: index + 1, content: line.trimEnd() });
|
|
3594
|
+
}
|
|
3595
|
+
});
|
|
3596
|
+
if (matches.length > 0) {
|
|
3597
|
+
results.push({ file: relative(rootPath, filePath), matches: matches.slice(0, 10) });
|
|
3598
|
+
}
|
|
3599
|
+
} catch {
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
if (!results.length) {
|
|
3603
|
+
return { content: [{ type: "text", text: `No matches found for "${query}" in ${rootPath}` }] };
|
|
3604
|
+
}
|
|
3605
|
+
const lines = results.flatMap((result) => [
|
|
3606
|
+
`FILE ${result.file}`,
|
|
3607
|
+
...result.matches.map((match) => `L${match.line}: ${match.content}`),
|
|
3608
|
+
""
|
|
3609
|
+
]);
|
|
3610
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
3611
|
+
}
|
|
3612
|
+
);
|
|
3613
|
+
server.tool(
|
|
3614
|
+
"read",
|
|
3615
|
+
"Read a local file with optional line ranges. Use this after search or grep when you want the actual source.",
|
|
3616
|
+
{
|
|
3617
|
+
path: z.string().describe("Absolute or relative path to the file to read."),
|
|
3618
|
+
start_line: z.number().optional().default(1),
|
|
3619
|
+
end_line: z.number().optional().default(200)
|
|
3620
|
+
},
|
|
3621
|
+
async ({ path, start_line, end_line }) => {
|
|
3622
|
+
try {
|
|
3623
|
+
const fullPath = path.includes(":") || path.startsWith("/") ? path : join(process.cwd(), path);
|
|
3624
|
+
const stats = statSync(fullPath);
|
|
3625
|
+
if (!stats.isFile()) {
|
|
3626
|
+
return { content: [{ type: "text", text: `Error: ${path} is not a file.` }] };
|
|
3627
|
+
}
|
|
3628
|
+
return { content: [{ type: "text", text: readFileWindow(fullPath, start_line, end_line) }] };
|
|
3629
|
+
} catch (error) {
|
|
3630
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
);
|
|
3634
|
+
server.tool(
|
|
3635
|
+
"explore",
|
|
3636
|
+
"Browse a repository tree or directory structure. Use this to orient yourself before reading files.",
|
|
3637
|
+
{
|
|
3638
|
+
path: z.string().optional().describe("Root directory to inspect. Defaults to current working directory."),
|
|
3639
|
+
max_depth: z.number().optional().default(3),
|
|
3640
|
+
max_entries: z.number().optional().default(200)
|
|
3641
|
+
},
|
|
3642
|
+
async ({ path, max_depth, max_entries }) => {
|
|
3643
|
+
try {
|
|
3644
|
+
const rootPath = path || process.cwd();
|
|
3645
|
+
const tree = listTree(rootPath, max_depth, max_entries);
|
|
3646
|
+
if (!tree.length) {
|
|
3647
|
+
return { content: [{ type: "text", text: `No visible files found in ${rootPath}` }] };
|
|
3648
|
+
}
|
|
3649
|
+
return { content: [{ type: "text", text: [`TREE ${rootPath}`, ...tree].join("\n") }] };
|
|
3650
|
+
} catch (error) {
|
|
3651
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
);
|
|
3655
|
+
server.tool(
|
|
3656
|
+
"research",
|
|
3657
|
+
"Run deeper research over indexed sources. Use this when search is not enough and you want synthesis or multi-step investigation.",
|
|
3658
|
+
{
|
|
3659
|
+
project: z.string().optional(),
|
|
3660
|
+
query: z.string().describe("Research question"),
|
|
3661
|
+
mode: z.enum(["search", "research"]).optional().default("research"),
|
|
3662
|
+
max_results: z.number().optional().default(5),
|
|
3663
|
+
max_steps: z.number().optional().default(5)
|
|
3664
|
+
},
|
|
3665
|
+
async ({ project, query, mode, max_results, max_steps }) => {
|
|
3666
|
+
try {
|
|
3667
|
+
const results = await whisper.oracleSearch({ project, query, mode, max_results, max_steps });
|
|
3668
|
+
if (mode === "research" && results.answer) {
|
|
3669
|
+
return { content: [{ type: "text", text: results.answer }] };
|
|
3670
|
+
}
|
|
3671
|
+
if (!results.results?.length) {
|
|
3672
|
+
return { content: [{ type: "text", text: "No research results found." }] };
|
|
3673
|
+
}
|
|
3674
|
+
const text = results.results.map((r, i) => `${i + 1}. ${r.path || r.source}
|
|
3675
|
+
${String(r.content || "").slice(0, 200)}...`).join("\n\n");
|
|
3676
|
+
return { content: [{ type: "text", text }] };
|
|
3677
|
+
} catch (error) {
|
|
3678
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
);
|
|
3682
|
+
server.tool(
|
|
3683
|
+
"index",
|
|
3684
|
+
"Index a new source or refresh a workspace. Use action='source' to add GitHub/web/pdf/local/slack/video. Use action='workspace' to refresh local workspace metadata.",
|
|
3685
|
+
{
|
|
3686
|
+
action: z.enum(["source", "workspace"]).default("source"),
|
|
3687
|
+
project: z.string().optional(),
|
|
3688
|
+
type: z.enum(["github", "web", "pdf", "local", "slack", "video"]).optional(),
|
|
3689
|
+
name: z.string().optional(),
|
|
3690
|
+
owner: z.string().optional(),
|
|
3691
|
+
repo: z.string().optional(),
|
|
3692
|
+
branch: z.string().optional(),
|
|
3693
|
+
url: z.string().optional(),
|
|
3694
|
+
file_path: z.string().optional(),
|
|
3695
|
+
path: z.string().optional(),
|
|
3696
|
+
glob: z.string().optional(),
|
|
3697
|
+
max_files: z.number().optional(),
|
|
3698
|
+
channel_ids: z.array(z.string()).optional(),
|
|
3699
|
+
token: z.string().optional(),
|
|
3700
|
+
workspace_id: z.string().optional(),
|
|
3701
|
+
mode: z.enum(["full", "incremental"]).optional().default("incremental"),
|
|
3702
|
+
platform: z.enum(["youtube", "loom", "generic"]).optional(),
|
|
3703
|
+
language: z.string().optional(),
|
|
3704
|
+
allow_stt_fallback: z.boolean().optional(),
|
|
3705
|
+
max_duration_minutes: z.number().optional(),
|
|
3706
|
+
max_chunks: z.number().optional()
|
|
3707
|
+
},
|
|
3708
|
+
async (input) => {
|
|
3709
|
+
try {
|
|
3710
|
+
if (input.action === "workspace") {
|
|
3711
|
+
const rootPath = input.path || process.cwd();
|
|
3712
|
+
const workspaceId = getWorkspaceIdForPath(rootPath, input.workspace_id);
|
|
3713
|
+
const state = loadState();
|
|
3714
|
+
const workspace = getWorkspaceState(state, workspaceId);
|
|
3715
|
+
const fileStats = countCodeFiles(rootPath, input.max_files || 1500);
|
|
3716
|
+
workspace.index_metadata = {
|
|
3717
|
+
last_indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3718
|
+
last_indexed_commit: getGitHead(rootPath),
|
|
3719
|
+
coverage: input.mode === "full" ? 1 : Math.max(0, Math.min(1, fileStats.total / Math.max(1, input.max_files || 1500)))
|
|
3720
|
+
};
|
|
3721
|
+
saveState(state);
|
|
3722
|
+
return toTextResult({ workspace_id: workspaceId, mode: input.mode, indexed_files: fileStats.total, skipped_files: fileStats.skipped, index_metadata: workspace.index_metadata });
|
|
3723
|
+
}
|
|
3724
|
+
if (!input.type) {
|
|
3725
|
+
return { content: [{ type: "text", text: "Error: type is required when action='source'." }] };
|
|
3726
|
+
}
|
|
3727
|
+
const resolvedProject = await resolveProjectRef(input.project);
|
|
3728
|
+
if (!resolvedProject) {
|
|
3729
|
+
return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
|
|
3730
|
+
}
|
|
3731
|
+
const result = await createSourceByType({
|
|
3732
|
+
project: resolvedProject,
|
|
3733
|
+
type: input.type,
|
|
3734
|
+
name: input.name,
|
|
3735
|
+
owner: input.owner,
|
|
3736
|
+
repo: input.repo,
|
|
3737
|
+
branch: input.branch,
|
|
3738
|
+
url: input.url,
|
|
3739
|
+
file_path: input.file_path,
|
|
3740
|
+
path: input.path,
|
|
3741
|
+
glob: input.glob,
|
|
3742
|
+
max_files: input.max_files,
|
|
3743
|
+
channel_ids: input.channel_ids,
|
|
3744
|
+
token: input.token,
|
|
3745
|
+
workspace_id: input.workspace_id,
|
|
3746
|
+
platform: input.platform,
|
|
3747
|
+
language: input.language,
|
|
3748
|
+
allow_stt_fallback: input.allow_stt_fallback,
|
|
3749
|
+
max_duration_minutes: input.max_duration_minutes,
|
|
3750
|
+
max_chunks: input.max_chunks,
|
|
3751
|
+
auto_index: true
|
|
3752
|
+
});
|
|
3753
|
+
return toTextResult(result);
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
);
|
|
3759
|
+
server.tool(
|
|
3760
|
+
"remember",
|
|
3761
|
+
"Store something the agent should keep across sessions: a fact, decision, preference, or instruction.",
|
|
3762
|
+
{
|
|
3763
|
+
project: z.string().optional(),
|
|
3764
|
+
content: z.string().describe("Memory content"),
|
|
3765
|
+
memory_type: z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"]).optional().default("factual"),
|
|
3766
|
+
user_id: z.string().optional(),
|
|
3767
|
+
session_id: z.string().optional(),
|
|
3768
|
+
agent_id: z.string().optional(),
|
|
3769
|
+
importance: z.number().optional().default(0.5)
|
|
3770
|
+
},
|
|
3771
|
+
async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
|
|
3772
|
+
try {
|
|
3773
|
+
const result = await whisper.addMemory({ project, content, memory_type, user_id, session_id, agent_id, importance });
|
|
3774
|
+
return { content: [{ type: "text", text: `Memory stored (id: ${result.id}, type: ${memory_type}).` }] };
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
);
|
|
3780
|
+
server.tool(
|
|
3781
|
+
"recall",
|
|
3782
|
+
"Recall facts, decisions, and preferences from previous sessions or prior work.",
|
|
3783
|
+
{
|
|
3784
|
+
project: z.string().optional(),
|
|
3785
|
+
query: z.string().describe("What to recall"),
|
|
3786
|
+
user_id: z.string().optional(),
|
|
3787
|
+
session_id: z.string().optional(),
|
|
3788
|
+
top_k: z.number().optional().default(10),
|
|
3789
|
+
memory_types: z.array(z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"])).optional()
|
|
3790
|
+
},
|
|
3791
|
+
async ({ project, query, user_id, session_id, top_k, memory_types }) => {
|
|
3792
|
+
try {
|
|
3793
|
+
const results = await whisper.searchMemoriesSOTA({ project, query, user_id, session_id, top_k, memory_types });
|
|
3794
|
+
if (!results.memories?.length) {
|
|
3795
|
+
return { content: [{ type: "text", text: "No memories found." }] };
|
|
3796
|
+
}
|
|
3797
|
+
const text = results.memories.map((r, i) => `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
|
|
3798
|
+
${r.content}`).join("\n\n");
|
|
3799
|
+
return { content: [{ type: "text", text }] };
|
|
3800
|
+
} catch (error) {
|
|
3801
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
);
|
|
3805
|
+
server.tool(
|
|
3806
|
+
"share_context",
|
|
3807
|
+
"Create a shareable snapshot of a session so another agent or teammate can continue with the same context.",
|
|
3808
|
+
{
|
|
3809
|
+
project: z.string().optional(),
|
|
3810
|
+
session_id: z.string().describe("Session to share"),
|
|
3811
|
+
title: z.string().optional(),
|
|
3812
|
+
expiry_days: z.number().optional().default(30)
|
|
3813
|
+
},
|
|
3814
|
+
async ({ project, session_id, title, expiry_days }) => {
|
|
3815
|
+
try {
|
|
3816
|
+
const result = await whisper.createSharedContext({ project, session_id, title, expiry_days });
|
|
3817
|
+
return {
|
|
3818
|
+
content: [{
|
|
3819
|
+
type: "text",
|
|
3820
|
+
text: `Shared context created:
|
|
3821
|
+
- Share ID: ${result.share_id}
|
|
3822
|
+
- Expires: ${result.expires_at || "Never"}
|
|
3823
|
+
|
|
3824
|
+
Share URL: ${result.share_url}`
|
|
3825
|
+
}]
|
|
3826
|
+
};
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
);
|
|
2858
3832
|
async function main() {
|
|
3833
|
+
const args = process.argv.slice(2);
|
|
3834
|
+
if (args.includes("--print-tool-map")) {
|
|
3835
|
+
printToolMap();
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
if (args[0] === "scope") {
|
|
3839
|
+
const readArg = (name) => {
|
|
3840
|
+
const idx = args.indexOf(name);
|
|
3841
|
+
if (idx === -1) return void 0;
|
|
3842
|
+
return args[idx + 1];
|
|
3843
|
+
};
|
|
3844
|
+
const project = readArg("--project") || DEFAULT_PROJECT || "my-project";
|
|
3845
|
+
const source = readArg("--source") || "source-or-type";
|
|
3846
|
+
const client = readArg("--client") || "json";
|
|
3847
|
+
const outPath = readArg("--write");
|
|
3848
|
+
const rendered = scopeConfigJson(project, source, client);
|
|
3849
|
+
if (outPath) {
|
|
3850
|
+
const backup = existsSync(outPath) ? `${outPath}.bak-${Date.now()}` : void 0;
|
|
3851
|
+
if (backup) writeFileSync(backup, readFileSync(outPath, "utf-8"), "utf-8");
|
|
3852
|
+
writeFileSync(outPath, `${rendered}
|
|
3853
|
+
`, "utf-8");
|
|
3854
|
+
console.log(JSON.stringify({ ok: true, path: outPath, backup: backup || null, client }, null, 2));
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
console.log(rendered);
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
if (!API_KEY && !IS_MANAGEMENT_ONLY) {
|
|
3861
|
+
console.error("Error: WHISPER_API_KEY environment variable is required");
|
|
3862
|
+
process.exit(1);
|
|
3863
|
+
}
|
|
3864
|
+
console.error("[whisper-context-mcp] Breaking change: canonical namespaced tool names are active. Run with --print-tool-map for migration mapping.");
|
|
2859
3865
|
const transport = new StdioServerTransport();
|
|
2860
3866
|
await server.connect(transport);
|
|
2861
3867
|
console.error("Whisper Context MCP server running on stdio");
|