@clarify-labs/cli 0.10.3 → 0.11.1
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/output/cli/commands/mcp.d.ts +27 -0
- package/output/cli/mcp/orama-loader.d.ts +56 -0
- package/output/cli/mcp/search-server.d.ts +34 -0
- package/output/cli/program.d.ts +0 -1
- package/output/cli.cjs +294 -13
- package/output/cli.cjs.map +1 -1
- package/output/cli.js +295 -14
- package/output/cli.js.map +1 -1
- package/output/core/config/config-schema.d.ts +138 -253
- package/output/core/config/config.d.ts +2 -1
- package/output/core/config/options.d.ts +0 -8
- package/output/core/config/user-config.d.ts +4 -3
- package/output/core/plugin/hooks.d.ts +0 -1
- package/output/core/project/project-info.d.ts +3 -3
- package/output/core/runtime/html-template.d.ts +0 -6
- package/output/core/runtime/ssg.d.ts +1 -1
- package/output/core/runtime/virtual-modules.d.ts +1 -5
- package/output/{engine-BDdPszs2.cjs → engine-Cbn_coqg.cjs} +569 -170
- package/output/engine-Cbn_coqg.cjs.map +1 -0
- package/output/{engine-CyFtr8Pz.js → engine-CxnKEKv8.js} +558 -171
- package/output/engine-CxnKEKv8.js.map +1 -0
- package/output/index.cjs +1 -1
- package/output/index.js +1 -1
- package/output/parsers/routes/routes.d.ts +7 -11
- package/output/parsers/routes/routes.test-utils.d.ts +2 -2
- package/output/plugins/content-artifacts/artifacts.d.ts +0 -2
- package/output/plugins/mcp-search/index.d.ts +15 -0
- package/output/plugins/mcp-search/mcp-config.d.ts +72 -0
- package/output/plugins/mcp-search/orama-index.d.ts +153 -0
- package/output/plugins/openapi/virtual-modules.d.ts +0 -1
- package/output/plugins/page-search/index.d.ts +2 -0
- package/output/plugins/source-links/source-links.d.ts +3 -3
- package/output/types.d.ts +64 -33
- package/package.json +6 -4
- package/output/engine-BDdPszs2.cjs.map +0 -1
- package/output/engine-CyFtr8Pz.js.map +0 -1
- package/output/plugins/search-index/index.d.ts +0 -2
- /package/output/plugins/{search-index → page-search}/search-language.d.ts +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpSiteConfig } from '../../plugins/mcp-search/mcp-config.js';
|
|
2
|
+
export type McpCliOptions = {
|
|
3
|
+
/** Skip the on-disk index cache; always fetch fresh. */
|
|
4
|
+
noCache?: boolean;
|
|
5
|
+
/** Optional logger for diagnostics (defaults to stderr). */
|
|
6
|
+
log?: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Start a local MCP server (stdio transport) that exposes full-text search
|
|
10
|
+
* over one or more deployed Clarify sites' Orama indices.
|
|
11
|
+
*
|
|
12
|
+
* For each site:
|
|
13
|
+
* 1. Fetch `<site>/mcp.json` to learn the index path, default locale, and
|
|
14
|
+
* document count. This is a tiny file and avoids hardcoding any
|
|
15
|
+
* assumptions about where the index lives on the site.
|
|
16
|
+
* 2. Use the descriptor to fetch the binary `.msp` index (cached locally
|
|
17
|
+
* keyed by site origin + document count + locale fingerprint) and
|
|
18
|
+
* deserialize it into an in-memory Orama db with the correct tokenizer.
|
|
19
|
+
*
|
|
20
|
+
* Then register the `search_docs` tool, which searches across all loaded
|
|
21
|
+
* sites and merges results (each hit annotated with its source site), and
|
|
22
|
+
* serve over stdio.
|
|
23
|
+
*
|
|
24
|
+
* All logs go to stderr - stdout is reserved for the JSON-RPC protocol.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runMcp(siteUrls: string[], options?: McpCliOptions): Promise<void>;
|
|
27
|
+
export declare function fetchMcpConfig(siteUrl: string, fetchImpl: typeof fetch, log: (m: string) => void): Promise<McpSiteConfig>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { McpSiteConfig } from '../../plugins/mcp-search/mcp-config.js';
|
|
2
|
+
import { McpSearchResult } from '../../plugins/mcp-search/orama-index.js';
|
|
3
|
+
/**
|
|
4
|
+
* A ready-to-search Orama index loaded for the MCP server.
|
|
5
|
+
*
|
|
6
|
+
* The underlying db is fully in-memory after load, so `search` is synchronous
|
|
7
|
+
* to the index. `dispose` drops the reference so the buffer can be GC'd; it's
|
|
8
|
+
* safe to call multiple times.
|
|
9
|
+
*/
|
|
10
|
+
export type LoadedSearchIndex = {
|
|
11
|
+
/** Search the index. Locale filter is optional. */
|
|
12
|
+
search: (params: {
|
|
13
|
+
query: string;
|
|
14
|
+
locale?: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
}) => McpSearchResult;
|
|
17
|
+
/** Release the in-memory index. Safe to call multiple times. */
|
|
18
|
+
dispose: () => void;
|
|
19
|
+
};
|
|
20
|
+
/** Re-export so callers can name the result type without reaching into orama-index. */
|
|
21
|
+
export type { McpSearchResult as SearchResult } from '../../plugins/mcp-search/orama-index.js';
|
|
22
|
+
export type OramaLoaderOptions = {
|
|
23
|
+
/**
|
|
24
|
+
* Root directory for the on-disk cache. Defaults to `~/.clarify/mcp-cache`.
|
|
25
|
+
* Each site gets a subdirectory keyed by a stable hash of its origin + path.
|
|
26
|
+
*/
|
|
27
|
+
cacheDir?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Bypass the cache and always fetch fresh. Implies purging the cached copy
|
|
30
|
+
* for this site on a successful refresh.
|
|
31
|
+
*/
|
|
32
|
+
noCache?: boolean;
|
|
33
|
+
/** Optional fetch override (testing). */
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
/** Optional logger (testing). Defaults to no-op. */
|
|
36
|
+
log?: (message: string) => void;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Load the Orama binary index for a remote site and return a ready-to-search
|
|
40
|
+
* handle.
|
|
41
|
+
*
|
|
42
|
+
* The `.msp` index is a single binary file (msgpack-serialized Orama `save()`
|
|
43
|
+
* output). It is cached on disk so repeated MCP starts don't re-download.
|
|
44
|
+
* Cache validity is keyed by the document count and locale list recorded in
|
|
45
|
+
* `mcp.json`; if either changes, the cached index is stale and refetched.
|
|
46
|
+
*
|
|
47
|
+
* The caller must supply the same `defaultLocale` recorded in `mcp.json` so
|
|
48
|
+
* the tokenizer matches the one used at build time. A mismatch would not
|
|
49
|
+
* corrupt data but would cause CJK query terms to be tokenized incorrectly.
|
|
50
|
+
*/
|
|
51
|
+
export declare function loadSearchIndex(siteUrl: string, config: McpSiteConfig, options?: OramaLoaderOptions): Promise<LoadedSearchIndex>;
|
|
52
|
+
/**
|
|
53
|
+
* Remove the on-disk cache for a site. Useful for `--no-cache` cleanup or for
|
|
54
|
+
* tests. No-op if the site has no cache directory.
|
|
55
|
+
*/
|
|
56
|
+
export declare function purgeSiteCache(siteUrl: string, cacheDir?: string): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { McpSiteConfig } from '../../plugins/mcp-search/mcp-config.js';
|
|
3
|
+
import { LoadedSearchIndex } from './orama-loader.js';
|
|
4
|
+
export type SearchServerOptions = {
|
|
5
|
+
/** Optional logger routed to stderr (stdout is the JSON-RPC channel). */
|
|
6
|
+
log?: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A site loaded for the MCP server: its descriptor, ready-to-search index,
|
|
10
|
+
* and the origin URL the index was fetched from (used to resolve relative
|
|
11
|
+
* result paths and to label results by source site).
|
|
12
|
+
*/
|
|
13
|
+
export type LoadedMcpSite = {
|
|
14
|
+
siteUrl: string;
|
|
15
|
+
config: McpSiteConfig;
|
|
16
|
+
index: LoadedSearchIndex;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Build the MCP server exposing site search over one or more Clarify sites.
|
|
20
|
+
*
|
|
21
|
+
* Tools registered:
|
|
22
|
+
* - `search_docs`: full-text search across all loaded sites, with each
|
|
23
|
+
* result annotated by its source site.
|
|
24
|
+
*
|
|
25
|
+
* Results are serialized to a compact, LLM-friendly shape: title, URL, site,
|
|
26
|
+
* excerpt, score, and locale. Full page content is intentionally omitted to
|
|
27
|
+
* keep responses within token budgets; clients can fetch the URL themselves.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createMcpSearchServer(sites: LoadedMcpSite[], options?: SearchServerOptions): McpServer;
|
|
30
|
+
/**
|
|
31
|
+
* Run the search server over stdio until the client disconnects or the process
|
|
32
|
+
* is signaled. Resolves on clean shutdown.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runMcpSearchServer(sites: LoadedMcpSite[], options?: SearchServerOptions): Promise<void>;
|
package/output/cli/program.d.ts
CHANGED
package/output/cli.cjs
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_engine = require("./engine-
|
|
2
|
+
const require_engine = require("./engine-Cbn_coqg.cjs");
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
let node_fs_promises = require("node:fs/promises");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
let vite = require("vite");
|
|
7
|
+
let zod = require("zod");
|
|
7
8
|
let node_url = require("node:url");
|
|
9
|
+
let node_os = require("node:os");
|
|
8
10
|
let node_module = require("node:module");
|
|
9
11
|
let cac = require("cac");
|
|
10
12
|
let _clarify_labs_templates = require("@clarify-labs/templates");
|
|
11
13
|
let node_child_process = require("node:child_process");
|
|
14
|
+
let node_crypto = require("node:crypto");
|
|
15
|
+
let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
16
|
+
let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
12
17
|
//#region source/core/runtime/log.ts
|
|
13
18
|
var LOG_FILE_NAME = ".clarify.log";
|
|
14
19
|
var DEFAULT_LOG_ENTRIES = 50;
|
|
@@ -462,6 +467,285 @@ function runInit(options, force, template, install = false) {
|
|
|
462
467
|
console.log(` ${suggestedRun}`);
|
|
463
468
|
}
|
|
464
469
|
//#endregion
|
|
470
|
+
//#region source/cli/mcp/orama-loader.ts
|
|
471
|
+
var DEFAULT_CACHE_DIR = () => (0, node_path.join)((0, node_os.homedir)(), ".clarify", "mcp-cache");
|
|
472
|
+
var FETCH_TIMEOUT_MS = 6e4;
|
|
473
|
+
/** Stable per-site cache key. Two URLs with the same origin+path share a cache. */
|
|
474
|
+
function siteCacheKey(siteUrl) {
|
|
475
|
+
let url;
|
|
476
|
+
try {
|
|
477
|
+
url = new URL(siteUrl);
|
|
478
|
+
} catch {
|
|
479
|
+
throw new Error(`[clarify] Invalid site URL: ${siteUrl}`);
|
|
480
|
+
}
|
|
481
|
+
const normalized = `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
482
|
+
return (0, node_crypto.createHash)("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
483
|
+
}
|
|
484
|
+
async function fetchBufferWithTimeout(url, fetchImpl, log) {
|
|
485
|
+
const controller = new AbortController();
|
|
486
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
487
|
+
try {
|
|
488
|
+
log(`Fetching ${url}`);
|
|
489
|
+
const res = await fetchImpl(url, {
|
|
490
|
+
signal: controller.signal,
|
|
491
|
+
redirect: "follow"
|
|
492
|
+
});
|
|
493
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} fetching ${url}`);
|
|
494
|
+
const buf = await res.arrayBuffer();
|
|
495
|
+
return new Uint8Array(buf);
|
|
496
|
+
} finally {
|
|
497
|
+
clearTimeout(timer);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Load the Orama binary index for a remote site and return a ready-to-search
|
|
502
|
+
* handle.
|
|
503
|
+
*
|
|
504
|
+
* The `.msp` index is a single binary file (msgpack-serialized Orama `save()`
|
|
505
|
+
* output). It is cached on disk so repeated MCP starts don't re-download.
|
|
506
|
+
* Cache validity is keyed by the document count and locale list recorded in
|
|
507
|
+
* `mcp.json`; if either changes, the cached index is stale and refetched.
|
|
508
|
+
*
|
|
509
|
+
* The caller must supply the same `defaultLocale` recorded in `mcp.json` so
|
|
510
|
+
* the tokenizer matches the one used at build time. A mismatch would not
|
|
511
|
+
* corrupt data but would cause CJK query terms to be tokenized incorrectly.
|
|
512
|
+
*/
|
|
513
|
+
async function loadSearchIndex(siteUrl, config, options = {}) {
|
|
514
|
+
const log = options.log ?? (() => {});
|
|
515
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
516
|
+
const siteCacheDir = (0, node_path.join)(options.cacheDir ?? DEFAULT_CACHE_DIR(), siteCacheKey(siteUrl));
|
|
517
|
+
const indexCachePath = (0, node_path.join)(siteCacheDir, "mcp-search.msp");
|
|
518
|
+
const manifestPath = (0, node_path.join)(siteCacheDir, "manifest.json");
|
|
519
|
+
const base = new URL(siteUrl).href;
|
|
520
|
+
log(`Site base resolved to ${base}`);
|
|
521
|
+
const search = config.capabilities.search;
|
|
522
|
+
if (!search) throw new Error(`[clarify] mcp.json for ${siteUrl} has no search capability.`);
|
|
523
|
+
const indexUrl = new URL(search.indexPath, base).href;
|
|
524
|
+
const fingerprint = localeFingerprint(config);
|
|
525
|
+
let useCache = !options.noCache;
|
|
526
|
+
if (useCache) try {
|
|
527
|
+
const manifest = JSON.parse(await (0, node_fs_promises.readFile)(manifestPath, "utf8"));
|
|
528
|
+
if (manifest.documentCount !== search.documentCount || manifest.fingerprint !== fingerprint) {
|
|
529
|
+
log("Cache stale (document count or locales changed); refetching");
|
|
530
|
+
useCache = false;
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
useCache = false;
|
|
534
|
+
}
|
|
535
|
+
let buffer;
|
|
536
|
+
if (useCache) try {
|
|
537
|
+
buffer = await (0, node_fs_promises.readFile)(indexCachePath);
|
|
538
|
+
log("Using cached mcp-search.msp");
|
|
539
|
+
} catch {}
|
|
540
|
+
if (buffer === void 0) {
|
|
541
|
+
const fetched = await fetchBufferWithTimeout(indexUrl, fetchImpl, log);
|
|
542
|
+
buffer = fetched;
|
|
543
|
+
try {
|
|
544
|
+
await (0, node_fs_promises.mkdir)(siteCacheDir, { recursive: true });
|
|
545
|
+
await (0, node_fs_promises.writeFile)(indexCachePath, fetched);
|
|
546
|
+
await (0, node_fs_promises.writeFile)(manifestPath, JSON.stringify({
|
|
547
|
+
siteUrl: base,
|
|
548
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
549
|
+
documentCount: search.documentCount,
|
|
550
|
+
fingerprint
|
|
551
|
+
}, null, 2), "utf8");
|
|
552
|
+
log(`Cached mcp-search.msp to ${indexCachePath}`);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
log(`Warning: failed to cache index: ${err instanceof Error ? err.message : String(err)}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const defaultLocale = search.defaultLocale;
|
|
558
|
+
const db = require_engine.deserializeSearchIndex(buffer, defaultLocale);
|
|
559
|
+
let disposed = false;
|
|
560
|
+
return {
|
|
561
|
+
search: ({ query, locale, limit }) => {
|
|
562
|
+
if (disposed) throw new Error("[clarify] Search index already disposed.");
|
|
563
|
+
return require_engine.searchMcpIndex(db, {
|
|
564
|
+
query,
|
|
565
|
+
locale,
|
|
566
|
+
limit
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
dispose: () => {
|
|
570
|
+
disposed = true;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Build a stable fingerprint of the indexed locales so the cache can detect
|
|
576
|
+
* locale-set changes even when the document count happens to stay the same.
|
|
577
|
+
*/
|
|
578
|
+
function localeFingerprint(config) {
|
|
579
|
+
return (0, node_crypto.createHash)("sha256").update((config.capabilities.search?.locales ?? []).slice().sort().join("|")).digest("hex").slice(0, 16);
|
|
580
|
+
}
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region source/cli/mcp/search-server.ts
|
|
583
|
+
/**
|
|
584
|
+
* Build the MCP server exposing site search over one or more Clarify sites.
|
|
585
|
+
*
|
|
586
|
+
* Tools registered:
|
|
587
|
+
* - `search_docs`: full-text search across all loaded sites, with each
|
|
588
|
+
* result annotated by its source site.
|
|
589
|
+
*
|
|
590
|
+
* Results are serialized to a compact, LLM-friendly shape: title, URL, site,
|
|
591
|
+
* excerpt, score, and locale. Full page content is intentionally omitted to
|
|
592
|
+
* keep responses within token budgets; clients can fetch the URL themselves.
|
|
593
|
+
*/
|
|
594
|
+
function createMcpSearchServer(sites, options = {}) {
|
|
595
|
+
const log = options.log ?? (() => {});
|
|
596
|
+
const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
|
|
597
|
+
name: "clarify-search",
|
|
598
|
+
version: "1.0.0"
|
|
599
|
+
});
|
|
600
|
+
if (sites.length === 0) throw new Error("[clarify] createMcpSearchServer requires at least one loaded site.");
|
|
601
|
+
const totalDocs = sites.reduce((sum, site) => sum + (site.config.capabilities.search?.documentCount ?? 0), 0);
|
|
602
|
+
const localeSet = /* @__PURE__ */ new Set();
|
|
603
|
+
for (const site of sites) for (const locale of site.config.capabilities.search?.locales ?? []) localeSet.add(locale);
|
|
604
|
+
const localeList = [...localeSet];
|
|
605
|
+
const siteTitles = sites.map((site) => site.config.site.title ?? "site").filter((title, i, arr) => arr.indexOf(title) === i);
|
|
606
|
+
const siteHint = siteTitles.length === 1 ? `"${siteTitles[0]}"` : `${sites.length} sites (${siteTitles.join(", ")})`;
|
|
607
|
+
const localeHint = localeList.length > 0 ? ` Available locales: ${localeList.join(", ")}.` : "";
|
|
608
|
+
const defaultLocale = sites[0].config.capabilities.search?.defaultLocale;
|
|
609
|
+
const defaultLocaleHint = defaultLocale ? ` Defaults to "${defaultLocale}".` : "";
|
|
610
|
+
server.registerTool("search_docs", {
|
|
611
|
+
title: "Search documentation",
|
|
612
|
+
description: [
|
|
613
|
+
`Full-text search over ${siteHint} documentation index.`,
|
|
614
|
+
`Returns ranked results with excerpts, URLs, and source site. The index contains ${totalDocs} pages across ${localeList.length} locale(s).`,
|
|
615
|
+
localeHint,
|
|
616
|
+
defaultLocaleHint,
|
|
617
|
+
"Pass a `locale` filter to restrict results to a specific language."
|
|
618
|
+
].filter(Boolean).join(" "),
|
|
619
|
+
inputSchema: {
|
|
620
|
+
query: zod.z.string().min(1).describe("Search query. CJK text is word-segmented automatically; Latin text is split on whitespace/punctuation."),
|
|
621
|
+
locale: zod.z.string().optional().describe(`Filter results to a single locale. One of: ${localeList.join(", ")}.`),
|
|
622
|
+
limit: zod.z.number().int().min(1).max(50).optional().describe("Maximum results to return. Default 10, max 50.")
|
|
623
|
+
}
|
|
624
|
+
}, async ({ query, locale, limit }) => {
|
|
625
|
+
log(`search_docs query=${JSON.stringify(query)} locale=${JSON.stringify(locale)} limit=${limit ?? 10}`);
|
|
626
|
+
const allHits = [];
|
|
627
|
+
let errorResult;
|
|
628
|
+
for (const site of sites) try {
|
|
629
|
+
const result = site.index.search({
|
|
630
|
+
query,
|
|
631
|
+
locale,
|
|
632
|
+
limit
|
|
633
|
+
});
|
|
634
|
+
for (const hit of result.hits) allHits.push({
|
|
635
|
+
...hit,
|
|
636
|
+
site: site.siteUrl
|
|
637
|
+
});
|
|
638
|
+
} catch (err) {
|
|
639
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
640
|
+
log(`search failed for ${site.siteUrl}: ${message}`);
|
|
641
|
+
errorResult = { text: `Search failed for ${site.siteUrl}: ${message}` };
|
|
642
|
+
}
|
|
643
|
+
if (allHits.length === 0 && errorResult) return {
|
|
644
|
+
isError: true,
|
|
645
|
+
content: [{
|
|
646
|
+
type: "text",
|
|
647
|
+
text: errorResult.text
|
|
648
|
+
}]
|
|
649
|
+
};
|
|
650
|
+
allHits.sort((a, b) => b.score - a.score);
|
|
651
|
+
const capped = limit ? allHits.slice(0, limit) : allHits.slice(0, 10);
|
|
652
|
+
return { content: [{
|
|
653
|
+
type: "text",
|
|
654
|
+
text: [[
|
|
655
|
+
`Found ${allHits.length} result(s) across ${totalDocs} indexed pages from ${sites.length} site(s).`,
|
|
656
|
+
`Showing top ${capped.length}.`,
|
|
657
|
+
""
|
|
658
|
+
].join("\n"), ...capped.map((hit, i) => formatResultText(hit, i))].join("\n---\n")
|
|
659
|
+
}] };
|
|
660
|
+
});
|
|
661
|
+
return server;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Run the search server over stdio until the client disconnects or the process
|
|
665
|
+
* is signaled. Resolves on clean shutdown.
|
|
666
|
+
*/
|
|
667
|
+
async function runMcpSearchServer(sites, options = {}) {
|
|
668
|
+
const log = options.log ?? (() => {});
|
|
669
|
+
const server = createMcpSearchServer(sites, options);
|
|
670
|
+
const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
|
|
671
|
+
transport.onclose = () => {
|
|
672
|
+
log("MCP transport closed; disposing search indices");
|
|
673
|
+
for (const site of sites) site.index.dispose();
|
|
674
|
+
};
|
|
675
|
+
await server.connect(transport);
|
|
676
|
+
log(`MCP search server listening on stdio (${sites.length} site(s))`);
|
|
677
|
+
}
|
|
678
|
+
function formatResultText(hit, index) {
|
|
679
|
+
const lines = [
|
|
680
|
+
`## ${index + 1}. ${hit.title}`,
|
|
681
|
+
`URL: ${hit.path}`,
|
|
682
|
+
`Site: ${hit.site}`,
|
|
683
|
+
`Score: ${hit.score}`
|
|
684
|
+
];
|
|
685
|
+
if (hit.locale) lines.push(`Locale: ${hit.locale}`);
|
|
686
|
+
if (hit.excerpt) lines.push("", hit.excerpt);
|
|
687
|
+
if (hit.description) lines.push("", `Description: ${hit.description}`);
|
|
688
|
+
if (hit.keywords?.length > 0) lines.push("", `Keywords: ${hit.keywords.join(", ")}`);
|
|
689
|
+
return lines.join("\n");
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region source/cli/commands/mcp.ts
|
|
693
|
+
/**
|
|
694
|
+
* Start a local MCP server (stdio transport) that exposes full-text search
|
|
695
|
+
* over one or more deployed Clarify sites' Orama indices.
|
|
696
|
+
*
|
|
697
|
+
* For each site:
|
|
698
|
+
* 1. Fetch `<site>/mcp.json` to learn the index path, default locale, and
|
|
699
|
+
* document count. This is a tiny file and avoids hardcoding any
|
|
700
|
+
* assumptions about where the index lives on the site.
|
|
701
|
+
* 2. Use the descriptor to fetch the binary `.msp` index (cached locally
|
|
702
|
+
* keyed by site origin + document count + locale fingerprint) and
|
|
703
|
+
* deserialize it into an in-memory Orama db with the correct tokenizer.
|
|
704
|
+
*
|
|
705
|
+
* Then register the `search_docs` tool, which searches across all loaded
|
|
706
|
+
* sites and merges results (each hit annotated with its source site), and
|
|
707
|
+
* serve over stdio.
|
|
708
|
+
*
|
|
709
|
+
* All logs go to stderr - stdout is reserved for the JSON-RPC protocol.
|
|
710
|
+
*/
|
|
711
|
+
async function runMcp(siteUrls, options = {}) {
|
|
712
|
+
if (siteUrls.length === 0) throw new Error("[clarify] At least one site URL is required. Usage: clarify mcp <site> [site...]");
|
|
713
|
+
const log = options.log ?? stderrLog;
|
|
714
|
+
const sites = [];
|
|
715
|
+
for (const siteUrl of siteUrls) {
|
|
716
|
+
const config = await fetchMcpConfig(siteUrl, fetch, log);
|
|
717
|
+
const search = config.capabilities.search;
|
|
718
|
+
if (!search) throw new Error(`[clarify] mcp.json for ${siteUrl} has no search capability. Run a build with search enabled.`);
|
|
719
|
+
log(`Loaded mcp.json for ${siteUrl}: ${search.documentCount} doc(s), ${search.locales.length} locale(s)`);
|
|
720
|
+
const index = await loadSearchIndex(siteUrl, config, {
|
|
721
|
+
noCache: options.noCache,
|
|
722
|
+
fetchImpl: fetch,
|
|
723
|
+
log
|
|
724
|
+
});
|
|
725
|
+
sites.push({
|
|
726
|
+
siteUrl,
|
|
727
|
+
config,
|
|
728
|
+
index
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
await runMcpSearchServer(sites, { log });
|
|
732
|
+
}
|
|
733
|
+
function stderrLog(message) {
|
|
734
|
+
process.stderr.write(`[clarify] ${message}\n`);
|
|
735
|
+
}
|
|
736
|
+
async function fetchMcpConfig(siteUrl, fetchImpl, log) {
|
|
737
|
+
const configUrl = new URL("mcp.json", new URL(siteUrl).href).href;
|
|
738
|
+
log(`Fetching ${configUrl}`);
|
|
739
|
+
const res = await fetchImpl(configUrl, { redirect: "follow" });
|
|
740
|
+
if (!res.ok) throw new Error(`[clarify] Failed to fetch mcp.json from ${configUrl} (HTTP ${res.status}). Ensure the site was built with a recent version of Clarify that emits mcp.json.`);
|
|
741
|
+
const config = await res.json();
|
|
742
|
+
if (config.version !== 3) throw new Error(`[clarify] Unsupported mcp.json schema version ${config.version}. Please update the clarify CLI.`);
|
|
743
|
+
const search = config.capabilities.search;
|
|
744
|
+
if (!search || search.documentCount === 0) throw new Error(`[clarify] mcp.json at ${configUrl} has no indexed documents. Run a build with search enabled.`);
|
|
745
|
+
if (!search.indexPath) throw new Error(`[clarify] mcp.json at ${configUrl} is missing capabilities.search.indexPath.`);
|
|
746
|
+
return config;
|
|
747
|
+
}
|
|
748
|
+
//#endregion
|
|
465
749
|
//#region source/cli/options.ts
|
|
466
750
|
function resolvePort(port) {
|
|
467
751
|
if (port === void 0) return void 0;
|
|
@@ -484,32 +768,29 @@ function resolveCliOptions(options) {
|
|
|
484
768
|
function withBuildOptions(command) {
|
|
485
769
|
return command.option("--content <dir>", "Content directory relative to root");
|
|
486
770
|
}
|
|
487
|
-
function resolveInitOptions(directory, options) {
|
|
488
|
-
return resolveOptions({
|
|
489
|
-
...options,
|
|
490
|
-
root: directory
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
771
|
function withSharedOptions(command) {
|
|
494
772
|
return command.option("--root <dir>", "Project root directory").option("--content <dir>", "Content directory relative to root").option("--output <dir>", "Build output directory relative to root");
|
|
495
773
|
}
|
|
496
|
-
function resolveOptions(options) {
|
|
497
|
-
return resolveCliOptions(options);
|
|
498
|
-
}
|
|
499
774
|
function createCli() {
|
|
500
775
|
const cli = (0, cac.cac)("clarify");
|
|
501
776
|
cli.version(require_engine.cliPackageVersion).help();
|
|
502
777
|
withSharedOptions(cli.command("dev", "Start the local documentation server")).option("--host [host]", "Dev server host").option("--port <port>", "Dev server port").option("--open [path]", "Open the dev server in a browser").action(async (options) => {
|
|
503
|
-
await runDev(
|
|
778
|
+
await runDev(resolveCliOptions(options));
|
|
504
779
|
});
|
|
505
780
|
withSharedOptions(cli.command("build", "Build the static documentation site")).action(async (options) => {
|
|
506
|
-
await runBuild(
|
|
781
|
+
await runBuild(resolveCliOptions(options));
|
|
507
782
|
});
|
|
508
783
|
withSharedOptions(cli.command("check", "Validate the documentation project")).option("--strict", "Treat warnings as failures").option("--format <format>", "Output format: text or json").action(async (options) => {
|
|
509
784
|
await runCheck(options);
|
|
510
785
|
});
|
|
511
786
|
withBuildOptions(cli.command("init [directory]", "Create a Clarify project scaffold")).option("--force", "Overwrite files created by init").option("--template <name>", "Template to use: minimal, standard, or complete").option("--install", "Install dependencies after init").action((directory, options) => {
|
|
512
|
-
runInit(
|
|
787
|
+
runInit(resolveCliOptions({
|
|
788
|
+
...options,
|
|
789
|
+
root: directory
|
|
790
|
+
}), options.force === true, options.template, options.install === true);
|
|
791
|
+
});
|
|
792
|
+
cli.command("mcp <...site>", "Start a local MCP server exposing full-text search over one or more deployed sites").option("--no-cache", "Skip the local search index cache and always fetch fresh").action(async (sites, options) => {
|
|
793
|
+
await runMcp(sites, { noCache: options.noCache === true });
|
|
513
794
|
});
|
|
514
795
|
return cli;
|
|
515
796
|
}
|