@oomkapwn/enquire-mcp 2.5.0 → 2.6.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/CHANGELOG.md +62 -0
- package/README.md +22 -3
- package/dist/http-transport.d.ts +92 -0
- package/dist/http-transport.d.ts.map +1 -0
- package/dist/http-transport.js +384 -0
- package/dist/http-transport.js.map +1 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +162 -44
- package/dist/index.js.map +1 -1
- package/docs/api.md +2 -0
- package/docs/http-transport.md +305 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { FtsIndex } from "./fts5.js";
|
|
4
|
+
import { Vault } from "./vault.js";
|
|
5
|
+
import { VaultWatcher } from "./watcher.js";
|
|
6
|
+
export interface ServeOptions {
|
|
3
7
|
vault: string;
|
|
4
8
|
enableWrite?: boolean;
|
|
5
9
|
maxFileBytes?: string;
|
|
@@ -17,7 +21,47 @@ interface ServeOptions {
|
|
|
17
21
|
diagnosticSearchTools?: boolean;
|
|
18
22
|
}
|
|
19
23
|
declare function main(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Heavyweight resources shared across every MCP-server instance: the vault
|
|
26
|
+
* (parsed-note cache + privacy filter), the FTS5 index handle, the optional
|
|
27
|
+
* filesystem watcher. v2.6.0 split this out so the HTTP transport can spin up
|
|
28
|
+
* a fresh `McpServer` per session over the SAME vault/index — opening the
|
|
29
|
+
* SQLite handle once and reusing it across thousands of remote-MCP calls.
|
|
30
|
+
*
|
|
31
|
+
* `warningTracker` is a single-fire latch for the `--disabled-tools` /
|
|
32
|
+
* `--enabled-tools` typo warnings: stdio prints them once at boot; HTTP
|
|
33
|
+
* prints them on the first session build, then never again.
|
|
34
|
+
*/
|
|
35
|
+
export interface ServerDeps {
|
|
36
|
+
vault: Vault;
|
|
37
|
+
ftsIndex: FtsIndex | null;
|
|
38
|
+
watcher: VaultWatcher | null;
|
|
39
|
+
disabledTools: Set<string>;
|
|
40
|
+
enabledTools: Set<string>;
|
|
41
|
+
warningTracker: {
|
|
42
|
+
printed: boolean;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One-time bootstrap of the heavy deps (vault open + FTS5 sync + watcher).
|
|
47
|
+
* Idempotent on a per-call basis but NOT designed to be called multiple
|
|
48
|
+
* times in one process — the FTS5 sync would double-index. Stdio + HTTP
|
|
49
|
+
* each call this exactly once at startup.
|
|
50
|
+
*/
|
|
51
|
+
export declare function prepareServerDeps(opts: ServeOptions): Promise<ServerDeps>;
|
|
52
|
+
/**
|
|
53
|
+
* Build a fresh `McpServer` over already-prepared deps. Cheap (just
|
|
54
|
+
* registers tool handlers — no I/O, no SQLite open). Stdio calls this once;
|
|
55
|
+
* HTTP calls it per session.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildMcpServer(deps: ServerDeps, opts: ServeOptions): McpServer;
|
|
20
58
|
declare function startServer(opts: ServeOptions): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Shared "ready" banner used by stdio + HTTP startup paths so the runtime
|
|
61
|
+
* configuration summary is identical regardless of transport. Transport
|
|
62
|
+
* suffix is appended by the caller.
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatReadyBanner(deps: ServerDeps): string;
|
|
21
65
|
declare function parsePositiveInt(raw: string, flag: string): number;
|
|
22
66
|
export { main, parsePositiveInt, startServer };
|
|
23
67
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAMtF,OAAO,EAAkC,QAAQ,EAAE,MAAM,WAAW,CAAC;AAsCrE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAW5C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAcD,iBAAe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA0SnC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,cAAc,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,CA8C/E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,GAAG,SAAS,CAqF9E;AAED,iBAAe,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoD5D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAY1D;AAotDD,iBAAS,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM3D;AAsCD,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { chunkContent, defaultIndexFile, FtsIndex } from "./fts5.js";
|
|
|
12
12
|
import { appendToNote, archiveNote, chatThreadAppend, chatThreadRead, contextPack, createNote, dataviewQuery, embeddingsSearch, findPath, findSimilar, frontmatterGet, frontmatterSearch, frontmatterSet, getBacklinks, getNoteNeighbors, getOpenQuestions, getOutboundLinks, getRecentEdits, getUnresolvedWikilinks, getVaultStats, lintWiki, listCanvases, listNotes, listTags, openInUi, paperAudit, readCanvas, readNote, renameNote, replaceInNotes, resolveWikilink, searchHybrid, searchText, semanticSearch, validateNoteProposal } from "./tools.js";
|
|
13
13
|
import { Vault } from "./vault.js";
|
|
14
14
|
import { VaultWatcher } from "./watcher.js";
|
|
15
|
-
const VERSION = "2.
|
|
15
|
+
const VERSION = "2.6.0";
|
|
16
16
|
/** Default location for the persistent embedding index, alongside .fts5.db. */
|
|
17
17
|
function embedDbPath(vaultRoot) {
|
|
18
18
|
// Match the FTS5 location convention by stripping the .fts5.db extension
|
|
@@ -46,6 +46,79 @@ async function main() {
|
|
|
46
46
|
.action(async (opts) => {
|
|
47
47
|
await startServer(opts);
|
|
48
48
|
});
|
|
49
|
+
// v2.6.0 — remote-MCP HTTP transport. Mirrors `serve` flags + adds HTTP
|
|
50
|
+
// surface (bearer auth, rate-limit, CORS). See docs/http-transport.md.
|
|
51
|
+
program
|
|
52
|
+
.command("serve-http")
|
|
53
|
+
.description("Start the MCP server over HTTP (Streamable HTTP transport). For remote-MCP use with claude.ai web, ChatGPT, Cursor HTTP mode, mobile clients. Requires --bearer-token (or --bearer-token-env). Bind to 127.0.0.1 by default — front with Tailscale Funnel / Cloudflare Tunnel for remote access.")
|
|
54
|
+
.requiredOption("--vault <path>", "Path to the Obsidian vault root")
|
|
55
|
+
.option("--port <n>", "TCP port (default 3000)", "3000")
|
|
56
|
+
.option("--host <host>", "Bind host (default 127.0.0.1 — explicit because 0.0.0.0 must be opt-in for remote-MCP)", "127.0.0.1")
|
|
57
|
+
.option("--bearer-token <token>", "Bearer token clients must present in the Authorization header. Generate with `enquire-mcp gen-token`. Required.")
|
|
58
|
+
.option("--bearer-token-env <name>", "Read the bearer token from this env var instead of --bearer-token (cleaner for systemd / .env / process listings). Either flag is required.")
|
|
59
|
+
.option("--mcp-path <path>", "URL path for the MCP endpoint (default /mcp)", "/mcp")
|
|
60
|
+
.option("--rate-limit <n>", "Max requests per minute per bearer token (default 120). Pass 0 to disable.", "120")
|
|
61
|
+
.option("--cors-origin <origin...>", "CORS allowlist (repeatable). Default empty — no Access-Control-Allow-Origin sent. Use '*' as a single entry to allow any origin (not compatible with credentialed Bearer requests; you almost always want explicit origins like https://claude.ai).")
|
|
62
|
+
.option("--health-path <path>", "URL path for the unauthenticated health probe (default /health)", "/health")
|
|
63
|
+
.option("--enable-write", "Enable the write tools (gated identically to stdio mode). Off by default.")
|
|
64
|
+
.option("--max-file-bytes <n>", "Max bytes for any single file read/write (default 5MB)")
|
|
65
|
+
.option("--cache-size <n>", "Max parsed-note cache entries (default 1024)")
|
|
66
|
+
.option("--persistent-cache", "Persist parsed-note cache to disk so cold starts skip re-parsing")
|
|
67
|
+
.option("--cache-file <path>", "Override the persistent-cache file location")
|
|
68
|
+
.option("--persistent-index", "Maintain a SQLite FTS5 inverted index for sub-100ms BM25-ranked search")
|
|
69
|
+
.option("--index-file <path>", "Override the FTS5 index file location")
|
|
70
|
+
.option("--tokenize <mode>", "FTS5 tokenize mode: 'unicode61' (default) or 'trigram'")
|
|
71
|
+
.option("--exclude-glob <pattern...>", "Privacy denylist (same semantics as `serve`).")
|
|
72
|
+
.option("--read-paths <pattern...>", "Privacy allowlist (same semantics as `serve`).")
|
|
73
|
+
.option("--watch", "Watch the vault for .md changes and refresh indexes incrementally.")
|
|
74
|
+
.option("--disabled-tools <name...>", "Skip registration of specific tools by name.")
|
|
75
|
+
.option("--enabled-tools <name...>", "Strict allowlist — when set, ONLY listed tools register.")
|
|
76
|
+
.option("--diagnostic-search-tools", "Register the four single-ranker search tools alongside obsidian_search.")
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
const tokenFromArg = typeof opts.bearerToken === "string" ? opts.bearerToken.trim() : "";
|
|
79
|
+
const tokenFromEnv = typeof opts.bearerTokenEnv === "string" ? (process.env[opts.bearerTokenEnv] ?? "").trim() : "";
|
|
80
|
+
const bearerToken = tokenFromArg.length > 0 ? tokenFromArg : tokenFromEnv;
|
|
81
|
+
if (!bearerToken) {
|
|
82
|
+
process.stderr.write("enquire serve-http: --bearer-token (or --bearer-token-env <name>) is required.\n" +
|
|
83
|
+
" Generate one with: enquire-mcp gen-token\n");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
// --port accepts 0 as "kernel-assigned ephemeral" — useful for tests
|
|
87
|
+
// and for scenarios where the user binds via a tunnel and doesn't
|
|
88
|
+
// care which local port. So we use a non-negative-integer check
|
|
89
|
+
// here, NOT parsePositiveInt (which would reject 0).
|
|
90
|
+
const portNum = Number(opts.port ?? "3000");
|
|
91
|
+
if (!Number.isFinite(portNum) || !Number.isInteger(portNum) || portNum < 0 || portNum > 65535) {
|
|
92
|
+
throw new Error(`--port must be an integer in [0, 65535]; got "${opts.port}"`);
|
|
93
|
+
}
|
|
94
|
+
const httpOpts = {
|
|
95
|
+
...opts,
|
|
96
|
+
port: portNum,
|
|
97
|
+
host: opts.host ?? "127.0.0.1",
|
|
98
|
+
bearerToken,
|
|
99
|
+
mcpPath: opts.mcpPath ?? "/mcp",
|
|
100
|
+
rateLimitPerMinute: opts.rateLimit !== undefined ? Number(opts.rateLimit) : 120,
|
|
101
|
+
corsOrigins: opts.corsOrigin ?? [],
|
|
102
|
+
healthPath: opts.healthPath ?? "/health"
|
|
103
|
+
};
|
|
104
|
+
if (!Number.isFinite(httpOpts.rateLimitPerMinute) ||
|
|
105
|
+
httpOpts.rateLimitPerMinute < 0 ||
|
|
106
|
+
!Number.isInteger(httpOpts.rateLimitPerMinute)) {
|
|
107
|
+
throw new Error(`--rate-limit must be a non-negative integer; got "${opts.rateLimit}"`);
|
|
108
|
+
}
|
|
109
|
+
const { startHttpServer } = await import("./http-transport.js");
|
|
110
|
+
await startHttpServer(httpOpts);
|
|
111
|
+
});
|
|
112
|
+
// v2.6.0 — convenience helper. Same as `node -e
|
|
113
|
+
// 'console.log(require("crypto").randomBytes(32).toString("base64url"))'`
|
|
114
|
+
// but discoverable in --help.
|
|
115
|
+
program
|
|
116
|
+
.command("gen-token")
|
|
117
|
+
.description("Generate a fresh 32-byte base64url bearer token suitable for `serve-http --bearer-token`.")
|
|
118
|
+
.action(async () => {
|
|
119
|
+
const { generateBearerToken } = await import("./http-transport.js");
|
|
120
|
+
process.stdout.write(`${generateBearerToken()}\n`);
|
|
121
|
+
});
|
|
49
122
|
program
|
|
50
123
|
.command("clear-cache")
|
|
51
124
|
.description("Delete the persistent-cache file for a given vault")
|
|
@@ -169,7 +242,13 @@ async function main() {
|
|
|
169
242
|
});
|
|
170
243
|
await program.parseAsync(process.argv);
|
|
171
244
|
}
|
|
172
|
-
|
|
245
|
+
/**
|
|
246
|
+
* One-time bootstrap of the heavy deps (vault open + FTS5 sync + watcher).
|
|
247
|
+
* Idempotent on a per-call basis but NOT designed to be called multiple
|
|
248
|
+
* times in one process — the FTS5 sync would double-index. Stdio + HTTP
|
|
249
|
+
* each call this exactly once at startup.
|
|
250
|
+
*/
|
|
251
|
+
export async function prepareServerDeps(opts) {
|
|
173
252
|
const vault = new Vault(opts.vault, {
|
|
174
253
|
enableWrite: !!opts.enableWrite,
|
|
175
254
|
maxFileBytes: opts.maxFileBytes !== undefined ? parsePositiveInt(opts.maxFileBytes, "--max-file-bytes") : undefined,
|
|
@@ -198,6 +277,28 @@ async function startServer(opts) {
|
|
|
198
277
|
throw err;
|
|
199
278
|
}
|
|
200
279
|
}
|
|
280
|
+
// Optional watcher — only when --watch is passed. Starts AFTER the initial
|
|
281
|
+
// FTS5 sync so we don't double-index files during boot.
|
|
282
|
+
let watcher = null;
|
|
283
|
+
if (opts.watch) {
|
|
284
|
+
watcher = new VaultWatcher({ vault, ftsIndex });
|
|
285
|
+
await watcher.start();
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
vault,
|
|
289
|
+
ftsIndex,
|
|
290
|
+
watcher,
|
|
291
|
+
disabledTools: new Set(opts.disabledTools ?? []),
|
|
292
|
+
enabledTools: new Set(opts.enabledTools ?? []),
|
|
293
|
+
warningTracker: { printed: false }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Build a fresh `McpServer` over already-prepared deps. Cheap (just
|
|
298
|
+
* registers tool handlers — no I/O, no SQLite open). Stdio calls this once;
|
|
299
|
+
* HTTP calls it per session.
|
|
300
|
+
*/
|
|
301
|
+
export function buildMcpServer(deps, opts) {
|
|
201
302
|
const server = new McpServer({
|
|
202
303
|
name: "enquire",
|
|
203
304
|
version: VERSION
|
|
@@ -217,62 +318,75 @@ async function startServer(opts) {
|
|
|
217
318
|
// unknown — typo or stale doc reference. Pre-fix, a typo in
|
|
218
319
|
// `--disabled-tools obsidan_search` (note the missing `i`) silently
|
|
219
320
|
// disabled nothing; now we log a warning so the user can correct it.
|
|
220
|
-
const disabledTools = new Set(opts.disabledTools ?? []);
|
|
221
|
-
const enabledTools = new Set(opts.enabledTools ?? []);
|
|
222
321
|
const usedDisabled = new Set();
|
|
223
322
|
const usedEnabled = new Set();
|
|
224
323
|
const registeredNames = new Set();
|
|
225
|
-
|
|
324
|
+
// v2.6.0: only print skip-logging on the first build (stdio: once at boot;
|
|
325
|
+
// HTTP: once on first session). Subsequent HTTP sessions reuse the same
|
|
326
|
+
// gating decisions silently — no need to spam logs per request.
|
|
327
|
+
const verbose = !deps.warningTracker.printed;
|
|
328
|
+
if (deps.disabledTools.size > 0 || deps.enabledTools.size > 0) {
|
|
226
329
|
const origRegisterTool = server.registerTool.bind(server);
|
|
227
330
|
server.registerTool = (name, ...rest) => {
|
|
228
331
|
registeredNames.add(name);
|
|
229
|
-
if (enabledTools.size > 0) {
|
|
230
|
-
if (enabledTools.has(name)) {
|
|
332
|
+
if (deps.enabledTools.size > 0) {
|
|
333
|
+
if (deps.enabledTools.has(name)) {
|
|
231
334
|
usedEnabled.add(name);
|
|
232
335
|
}
|
|
233
336
|
else {
|
|
234
|
-
|
|
337
|
+
if (verbose)
|
|
338
|
+
process.stderr.write(`enquire: skipping tool ${name} (not in --enabled-tools allowlist)\n`);
|
|
235
339
|
return undefined;
|
|
236
340
|
}
|
|
237
341
|
}
|
|
238
|
-
if (disabledTools.has(name)) {
|
|
342
|
+
if (deps.disabledTools.has(name)) {
|
|
239
343
|
usedDisabled.add(name);
|
|
240
|
-
|
|
344
|
+
if (verbose)
|
|
345
|
+
process.stderr.write(`enquire: skipping tool ${name} (disabled by --disabled-tools)\n`);
|
|
241
346
|
return undefined;
|
|
242
347
|
}
|
|
243
348
|
return origRegisterTool(name, ...rest);
|
|
244
349
|
};
|
|
245
350
|
}
|
|
246
|
-
registerReadTools(server, vault, ftsIndex, opts.diagnosticSearchTools ?? false);
|
|
247
|
-
if (vault.writeEnabled)
|
|
248
|
-
registerWriteTools(server, vault);
|
|
249
|
-
if (ftsIndex && opts.diagnosticSearchTools)
|
|
250
|
-
registerFtsTools(server, ftsIndex, vault);
|
|
251
|
-
registerResources(server, vault);
|
|
252
|
-
if (ftsIndex)
|
|
253
|
-
registerChunkResource(server, ftsIndex, vault);
|
|
351
|
+
registerReadTools(server, deps.vault, deps.ftsIndex, opts.diagnosticSearchTools ?? false);
|
|
352
|
+
if (deps.vault.writeEnabled)
|
|
353
|
+
registerWriteTools(server, deps.vault);
|
|
354
|
+
if (deps.ftsIndex && opts.diagnosticSearchTools)
|
|
355
|
+
registerFtsTools(server, deps.ftsIndex, deps.vault);
|
|
356
|
+
registerResources(server, deps.vault);
|
|
357
|
+
if (deps.ftsIndex)
|
|
358
|
+
registerChunkResource(server, deps.ftsIndex, deps.vault);
|
|
254
359
|
registerPrompts(server);
|
|
255
360
|
// v2.0.0-beta.1: warn on unknown names AFTER all tools are registered.
|
|
256
361
|
// We can't validate at parse time because the canonical list depends on
|
|
257
362
|
// runtime config (e.g. --persistent-index gates obsidian_full_text_search,
|
|
258
363
|
// --enable-write gates the 5 write tools). So we wait until everything is
|
|
259
364
|
// registered, then diff the user's lists against what was actually seen.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
365
|
+
if (verbose) {
|
|
366
|
+
for (const name of deps.disabledTools) {
|
|
367
|
+
if (!usedDisabled.has(name)) {
|
|
368
|
+
const hint = registeredNames.has(name)
|
|
369
|
+
? "" // shouldn't happen — would have been used
|
|
370
|
+
: ` (no such tool registered; check spelling; available: ${[...registeredNames].sort().join(", ")})`;
|
|
371
|
+
process.stderr.write(`enquire: warning — --disabled-tools "${name}" did not match any tool${hint}\n`);
|
|
372
|
+
}
|
|
266
373
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
:
|
|
273
|
-
|
|
374
|
+
for (const name of deps.enabledTools) {
|
|
375
|
+
if (!usedEnabled.has(name)) {
|
|
376
|
+
const hint = registeredNames.has(name)
|
|
377
|
+
? ""
|
|
378
|
+
: ` (no such tool; check spelling; available: ${[...registeredNames].sort().join(", ")})`;
|
|
379
|
+
process.stderr.write(`enquire: warning — --enabled-tools "${name}" did not match any tool${hint}\n`);
|
|
380
|
+
}
|
|
274
381
|
}
|
|
382
|
+
deps.warningTracker.printed = true;
|
|
275
383
|
}
|
|
384
|
+
return server;
|
|
385
|
+
}
|
|
386
|
+
async function startServer(opts) {
|
|
387
|
+
const deps = await prepareServerDeps(opts);
|
|
388
|
+
const { vault, ftsIndex, watcher } = deps;
|
|
389
|
+
const server = buildMcpServer(deps, opts);
|
|
276
390
|
const transport = new StdioServerTransport();
|
|
277
391
|
await server.connect(transport);
|
|
278
392
|
if (vault.persistentCacheEnabled) {
|
|
@@ -305,12 +419,7 @@ async function startServer(opts) {
|
|
|
305
419
|
void flush();
|
|
306
420
|
});
|
|
307
421
|
}
|
|
308
|
-
|
|
309
|
-
// FTS5 sync so we don't double-index files during boot.
|
|
310
|
-
let watcher = null;
|
|
311
|
-
if (opts.watch) {
|
|
312
|
-
watcher = new VaultWatcher({ vault, ftsIndex });
|
|
313
|
-
await watcher.start();
|
|
422
|
+
if (watcher) {
|
|
314
423
|
const closeWatcher = () => {
|
|
315
424
|
void watcher?.close();
|
|
316
425
|
};
|
|
@@ -318,6 +427,21 @@ async function startServer(opts) {
|
|
|
318
427
|
process.once("SIGTERM", closeWatcher);
|
|
319
428
|
process.on("beforeExit", closeWatcher);
|
|
320
429
|
}
|
|
430
|
+
process.stderr.write(`${formatReadyBanner(deps)} (transport=stdio)\n`);
|
|
431
|
+
if (ftsIndex) {
|
|
432
|
+
const closeFts = () => ftsIndex?.close();
|
|
433
|
+
process.once("SIGINT", closeFts);
|
|
434
|
+
process.once("SIGTERM", closeFts);
|
|
435
|
+
process.on("beforeExit", closeFts);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Shared "ready" banner used by stdio + HTTP startup paths so the runtime
|
|
440
|
+
* configuration summary is identical regardless of transport. Transport
|
|
441
|
+
* suffix is appended by the caller.
|
|
442
|
+
*/
|
|
443
|
+
export function formatReadyBanner(deps) {
|
|
444
|
+
const { vault, ftsIndex, watcher, disabledTools, enabledTools } = deps;
|
|
321
445
|
const writeMode = vault.writeEnabled ? "WRITE-ENABLED" : "read-only";
|
|
322
446
|
const cacheMode = vault.persistentCacheEnabled ? `, persistent-cache=${vault.cacheFile}` : "";
|
|
323
447
|
const ftsMode = ftsIndex ? `, fts5-index (${ftsIndex.totalFiles()} files / ${ftsIndex.totalChunks()} chunks)` : "";
|
|
@@ -327,13 +451,7 @@ async function startServer(opts) {
|
|
|
327
451
|
const watchMode = watcher ? ", watch=on" : "";
|
|
328
452
|
const disabledMode = disabledTools.size > 0 ? `, disabled-tools=${disabledTools.size}` : "";
|
|
329
453
|
const enabledMode = enabledTools.size > 0 ? `, enabled-tools=${enabledTools.size}` : "";
|
|
330
|
-
|
|
331
|
-
if (ftsIndex) {
|
|
332
|
-
const closeFts = () => ftsIndex?.close();
|
|
333
|
-
process.once("SIGINT", closeFts);
|
|
334
|
-
process.once("SIGTERM", closeFts);
|
|
335
|
-
process.on("beforeExit", closeFts);
|
|
336
|
-
}
|
|
454
|
+
return `enquire ${VERSION} ready (${writeMode}, vault=${vault.root}${cacheMode}${ftsMode}${privacyMode}${watchMode}${disabledMode}${enabledMode})`;
|
|
337
455
|
}
|
|
338
456
|
// v2.0 alpha — sync the persistent embedding index. Same incremental-rebuild
|
|
339
457
|
// pattern as syncFtsIndex (mtime tracked in source_state); we only re-embed
|