@looptech-ai/understand-quickly-mcp 0.1.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.
@@ -0,0 +1,36 @@
1
+ import {
2
+ fetchGraph,
3
+ findEntryById,
4
+ loadRegistry,
5
+ resolveRegistrySource,
6
+ } from "../registry.js";
7
+ import type { GetGraphParams } from "../types.js";
8
+
9
+ export async function getGraph(params: GetGraphParams): Promise<unknown> {
10
+ if (!params || typeof params.id !== "string" || params.id.length === 0) {
11
+ throw new Error("`id` is required");
12
+ }
13
+ const registry = await loadRegistry({ source: resolveRegistrySource() });
14
+ const entry = findEntryById(registry, params.id);
15
+ if (!entry) {
16
+ throw new Error(`No registry entry found with id "${params.id}"`);
17
+ }
18
+ return fetchGraph(entry.graph_url);
19
+ }
20
+
21
+ export const getGraphToolDefinition = {
22
+ name: "get_graph",
23
+ description:
24
+ "Fetch and return the parsed knowledge graph JSON for a registry entry by id.",
25
+ inputSchema: {
26
+ type: "object" as const,
27
+ properties: {
28
+ id: {
29
+ type: "string",
30
+ description: "Registry entry id, e.g. \"Lum1104/Understand-Anything\".",
31
+ },
32
+ },
33
+ required: ["id"],
34
+ additionalProperties: false,
35
+ },
36
+ };
@@ -0,0 +1,62 @@
1
+ import {
2
+ filterEntries,
3
+ loadRegistry,
4
+ resolveRegistrySource,
5
+ } from "../registry.js";
6
+ import type {
7
+ ListReposParams,
8
+ RegistryEntry,
9
+ RepoSummary,
10
+ } from "../types.js";
11
+
12
+ function toSummary(entry: RegistryEntry): RepoSummary {
13
+ return {
14
+ id: entry.id,
15
+ format: entry.format,
16
+ description: entry.description,
17
+ status: entry.status,
18
+ tags: entry.tags,
19
+ last_synced: entry.last_synced,
20
+ graph_url: entry.graph_url,
21
+ };
22
+ }
23
+
24
+ export async function listRepos(
25
+ params: ListReposParams = {},
26
+ ): Promise<RepoSummary[]> {
27
+ const registry = await loadRegistry({ source: resolveRegistrySource() });
28
+ const filtered = filterEntries(registry.entries, (entry) => {
29
+ if (params.format && entry.format !== params.format) return false;
30
+ if (params.status && entry.status !== params.status) return false;
31
+ if (params.tag) {
32
+ const tags = entry.tags ?? [];
33
+ if (!tags.includes(params.tag)) return false;
34
+ }
35
+ return true;
36
+ });
37
+ return filtered.map(toSummary);
38
+ }
39
+
40
+ export const listReposToolDefinition = {
41
+ name: "list_repos",
42
+ description:
43
+ "List entries in the understand-quickly registry. Optional filters: `format`, `tag`, `status`.",
44
+ inputSchema: {
45
+ type: "object" as const,
46
+ properties: {
47
+ format: {
48
+ type: "string",
49
+ description: "Exact match on entry.format (e.g. \"understand-anything@1\").",
50
+ },
51
+ tag: {
52
+ type: "string",
53
+ description: "Returns entries whose `tags` array contains this string.",
54
+ },
55
+ status: {
56
+ type: "string",
57
+ description: "Exact match on entry.status (typically \"ok\").",
58
+ },
59
+ },
60
+ additionalProperties: false,
61
+ },
62
+ };
@@ -0,0 +1,239 @@
1
+ import {
2
+ fetchGraph,
3
+ findEntryById,
4
+ loadRegistry,
5
+ loadStats,
6
+ resolveRegistrySource,
7
+ resolveStatsSource,
8
+ } from "../registry.js";
9
+ import type {
10
+ FetchImpl,
11
+ RegistryEntry,
12
+ SearchConceptsParams,
13
+ SearchHit,
14
+ StatsConcept,
15
+ } from "../types.js";
16
+
17
+ // Stub-quality cap on cross-graph searches. A real implementation would either
18
+ // stream results or build a server-side index — for now we just bound the work.
19
+ const CROSS_GRAPH_LIMIT = 5;
20
+ const CONCEPTS_RESULT_CAP = 50;
21
+ const SAMPLES_CAP = 3;
22
+
23
+ interface NodeLike {
24
+ id?: unknown;
25
+ label?: unknown;
26
+ name?: unknown;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ /**
31
+ * Best-effort node enumeration. Different graph formats nest nodes differently
32
+ * (`nodes`, `entities`, `concepts`, …). For the stub we accept any of those
33
+ * common shapes and fall back to walking the top-level object's array values.
34
+ */
35
+ function collectNodes(graph: unknown): NodeLike[] {
36
+ if (!graph || typeof graph !== "object") return [];
37
+ const obj = graph as Record<string, unknown>;
38
+ const candidates = ["nodes", "entities", "concepts", "items"];
39
+ for (const key of candidates) {
40
+ const value = obj[key];
41
+ if (Array.isArray(value)) {
42
+ return value.filter(
43
+ (item): item is NodeLike => !!item && typeof item === "object",
44
+ );
45
+ }
46
+ }
47
+ // Fallback: pull every array value and concat.
48
+ const collected: NodeLike[] = [];
49
+ for (const value of Object.values(obj)) {
50
+ if (Array.isArray(value)) {
51
+ for (const item of value) {
52
+ if (item && typeof item === "object") collected.push(item as NodeLike);
53
+ }
54
+ }
55
+ }
56
+ return collected;
57
+ }
58
+
59
+ function matchNode(node: NodeLike, query: string): SearchHit | undefined {
60
+ const lower = query.toLowerCase();
61
+ const fields: Array<["id" | "label" | "name", unknown]> = [
62
+ ["id", node.id],
63
+ ["label", node.label],
64
+ ["name", node.name],
65
+ ];
66
+ for (const [field, raw] of fields) {
67
+ if (typeof raw === "string" && raw.toLowerCase().includes(lower)) {
68
+ return {
69
+ node_id: typeof node.id === "string" ? node.id : undefined,
70
+ label: typeof node.label === "string" ? node.label : undefined,
71
+ name: typeof node.name === "string" ? node.name : undefined,
72
+ matched_field: field,
73
+ matched_value: raw,
74
+ };
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ async function searchOneGraph(
81
+ graphUrl: string,
82
+ query: string,
83
+ fetchImpl?: FetchImpl,
84
+ ): Promise<SearchHit[]> {
85
+ let graph: unknown;
86
+ try {
87
+ graph = await fetchGraph(graphUrl, fetchImpl);
88
+ } catch (err) {
89
+ // For the stub, swallow per-graph fetch errors so a single 404 does not
90
+ // poison the whole result set.
91
+ return [];
92
+ }
93
+ const nodes = collectNodes(graph);
94
+ const hits: SearchHit[] = [];
95
+ for (const node of nodes) {
96
+ const hit = matchNode(node, query);
97
+ if (hit) hits.push(hit);
98
+ }
99
+ return hits;
100
+ }
101
+
102
+ export interface SearchConceptsResult {
103
+ query: string;
104
+ // Single-graph mode (id given) or fan-out fallback mode.
105
+ results?: Array<{
106
+ id: string;
107
+ graph_url: string;
108
+ hits: SearchHit[];
109
+ }>;
110
+ // stats.json-backed mode (default).
111
+ matches?: Array<{
112
+ term: string;
113
+ count: number;
114
+ entries: number;
115
+ samples: string[];
116
+ }>;
117
+ source: "stats" | "graph" | "fanout";
118
+ truncated?: boolean;
119
+ scanned?: number;
120
+ }
121
+
122
+ export interface SearchConceptsOptions {
123
+ fetchImpl?: FetchImpl;
124
+ registrySource?: string;
125
+ statsSource?: string;
126
+ }
127
+
128
+ async function fanoutSearch(
129
+ query: string,
130
+ fetchImpl: FetchImpl | undefined,
131
+ registrySource: string,
132
+ ): Promise<SearchConceptsResult> {
133
+ const registry = await loadRegistry({ source: registrySource, fetchImpl });
134
+ const okEntries: RegistryEntry[] = registry.entries.filter(
135
+ (entry) => entry.status === "ok",
136
+ );
137
+ const slice = okEntries.slice(0, CROSS_GRAPH_LIMIT);
138
+ const results: NonNullable<SearchConceptsResult["results"]> = [];
139
+ // Sequential to keep the stub gentle on remote hosts.
140
+ for (const entry of slice) {
141
+ const hits = await searchOneGraph(entry.graph_url, query, fetchImpl);
142
+ if (hits.length > 0) {
143
+ results.push({ id: entry.id, graph_url: entry.graph_url, hits });
144
+ }
145
+ }
146
+ return {
147
+ query,
148
+ source: "fanout",
149
+ scanned: slice.length,
150
+ truncated: okEntries.length > slice.length,
151
+ results,
152
+ };
153
+ }
154
+
155
+ function searchStatsConcepts(
156
+ query: string,
157
+ concepts: StatsConcept[],
158
+ ): SearchConceptsResult["matches"] {
159
+ const lower = query.toLowerCase();
160
+ const out: NonNullable<SearchConceptsResult["matches"]> = [];
161
+ for (const c of concepts) {
162
+ if (typeof c?.term !== "string") continue;
163
+ if (!c.term.toLowerCase().includes(lower)) continue;
164
+ out.push({
165
+ term: c.term,
166
+ count: c.entries,
167
+ entries: c.entries,
168
+ samples: Array.isArray(c.samples) ? c.samples.slice(0, SAMPLES_CAP) : [],
169
+ });
170
+ if (out.length >= CONCEPTS_RESULT_CAP) break;
171
+ }
172
+ return out;
173
+ }
174
+
175
+ export async function searchConcepts(
176
+ params: SearchConceptsParams,
177
+ options: SearchConceptsOptions = {},
178
+ ): Promise<SearchConceptsResult> {
179
+ if (!params || typeof params.query !== "string" || params.query.length === 0) {
180
+ throw new Error("`query` is required");
181
+ }
182
+ const registrySource = options.registrySource ?? resolveRegistrySource();
183
+ const statsSource = options.statsSource ?? resolveStatsSource();
184
+ const fetchImpl = options.fetchImpl;
185
+
186
+ // Single-graph mode: keep the legacy fan-out behavior for one specific graph
187
+ // since stats.json is repo-keyed by sample only and is not a substitute.
188
+ if (params.id) {
189
+ const registry = await loadRegistry({ source: registrySource, fetchImpl });
190
+ const entry = findEntryById(registry, params.id);
191
+ if (!entry) {
192
+ throw new Error(`No registry entry found with id "${params.id}"`);
193
+ }
194
+ const hits = await searchOneGraph(entry.graph_url, params.query, fetchImpl);
195
+ return {
196
+ query: params.query,
197
+ source: "graph",
198
+ scanned: 1,
199
+ results: [{ id: entry.id, graph_url: entry.graph_url, hits }],
200
+ };
201
+ }
202
+
203
+ // Default: stats.json-backed concept search. Cheap (one GET, cached 60s).
204
+ try {
205
+ const stats = await loadStats({ source: statsSource, fetchImpl });
206
+ const matches = searchStatsConcepts(params.query, stats.concepts);
207
+ return {
208
+ query: params.query,
209
+ source: "stats",
210
+ matches,
211
+ };
212
+ } catch (err) {
213
+ // Fall through to the legacy fan-out so an outage on stats.json does not
214
+ // break this tool entirely.
215
+ return fanoutSearch(params.query, fetchImpl, registrySource);
216
+ }
217
+ }
218
+
219
+ export const searchConceptsToolDefinition = {
220
+ name: "search_concepts",
221
+ description:
222
+ "Search aggregated concept terms (default: precomputed `stats.json`). Pass `id` to fall back to a single-graph node search. If `stats.json` is unavailable, falls back to a capped cross-graph node fan-out.",
223
+ inputSchema: {
224
+ type: "object" as const,
225
+ properties: {
226
+ query: {
227
+ type: "string",
228
+ description: "Substring to search for (case-insensitive).",
229
+ },
230
+ id: {
231
+ type: "string",
232
+ description:
233
+ "Optional entry id. If given, scopes the search to that single graph (legacy node-level mode).",
234
+ },
235
+ },
236
+ required: ["query"],
237
+ additionalProperties: false,
238
+ },
239
+ };
package/src/types.ts ADDED
@@ -0,0 +1,101 @@
1
+ // Types for the understand-quickly registry. The live schema is owned by the
2
+ // parent project under `schemas/`; we duplicate just the surface this MCP
3
+ // server consumes. Anything we do not use is typed as `unknown` so that we
4
+ // stay forward-compatible with future schema additions.
5
+
6
+ export type RegistryEntryStatus = "ok" | "error" | "pending" | string;
7
+
8
+ export interface RegistryEntry {
9
+ id: string;
10
+ owner?: string;
11
+ repo?: string;
12
+ format: string;
13
+ graph_url: string;
14
+ description?: string;
15
+ status?: RegistryEntryStatus;
16
+ tags?: string[];
17
+ last_sha?: string;
18
+ last_synced?: string;
19
+ source_sha?: string;
20
+ head_sha?: string;
21
+ commits_behind?: number;
22
+ // Forward-compat: any additional fields we have not modelled yet.
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ export interface Registry {
27
+ schema_version: number;
28
+ generated_at: string;
29
+ entries: RegistryEntry[];
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ // Subset of an entry returned by `list_repos`. Mirrors the spec.
34
+ export interface RepoSummary {
35
+ id: string;
36
+ format: string;
37
+ description?: string;
38
+ status?: RegistryEntryStatus;
39
+ tags?: string[];
40
+ last_synced?: string;
41
+ graph_url: string;
42
+ }
43
+
44
+ export interface ListReposParams {
45
+ format?: string;
46
+ tag?: string;
47
+ status?: string;
48
+ }
49
+
50
+ export interface GetGraphParams {
51
+ id: string;
52
+ }
53
+
54
+ export interface SearchConceptsParams {
55
+ query: string;
56
+ id?: string;
57
+ }
58
+
59
+ export interface SearchHit {
60
+ node_id?: string;
61
+ label?: string;
62
+ name?: string;
63
+ matched_field: "id" | "label" | "name";
64
+ matched_value: string;
65
+ }
66
+
67
+ export interface FindGraphForRepoParams {
68
+ id?: string;
69
+ github_url?: string;
70
+ }
71
+
72
+ // Shape of `stats.json` produced by scripts/aggregate.mjs. Only the fields the
73
+ // MCP server consumes are typed; other fields are kept as `unknown`.
74
+ export interface StatsConcept {
75
+ term: string;
76
+ entries: number;
77
+ samples: string[];
78
+ }
79
+
80
+ export interface StatsJson {
81
+ schema_version: number;
82
+ generated_at: string;
83
+ totals?: { entries: number; nodes: number; edges: number };
84
+ kinds?: unknown[];
85
+ languages?: unknown[];
86
+ concepts: StatsConcept[];
87
+ [key: string]: unknown;
88
+ }
89
+
90
+ // `fetch` and `Response` are global in Node 20+, but we keep a narrow alias
91
+ // so that callers can inject a fake during tests without pulling in DOM lib.
92
+ export type FetchImpl = (
93
+ input: string | URL,
94
+ init?: { signal?: AbortSignal },
95
+ ) => Promise<{
96
+ ok: boolean;
97
+ status: number;
98
+ statusText: string;
99
+ json: () => Promise<unknown>;
100
+ text: () => Promise<string>;
101
+ }>;
@@ -0,0 +1,171 @@
1
+ import { describe, it, beforeEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ clearCache,
6
+ filterEntries,
7
+ loadRegistry,
8
+ } from "../src/registry.ts";
9
+ import type { FetchImpl, Registry, RegistryEntry } from "../src/types.ts";
10
+
11
+ const SOURCE = "https://example.invalid/registry.json";
12
+
13
+ function makeRegistry(extras: Partial<Registry> = {}): Registry {
14
+ const entries: RegistryEntry[] = [
15
+ {
16
+ id: "alice/python-graph",
17
+ format: "understand-anything@1",
18
+ graph_url: "https://example.invalid/alice.json",
19
+ status: "ok",
20
+ tags: ["python", "agents"],
21
+ },
22
+ {
23
+ id: "bob/ts-graph",
24
+ format: "understand-anything@1",
25
+ graph_url: "https://example.invalid/bob.json",
26
+ status: "error",
27
+ tags: ["typescript"],
28
+ },
29
+ {
30
+ id: "carol/rust-graph",
31
+ format: "gitnexus@1",
32
+ graph_url: "https://example.invalid/carol.json",
33
+ status: "ok",
34
+ tags: ["rust", "agents"],
35
+ },
36
+ ];
37
+ return {
38
+ schema_version: 1,
39
+ generated_at: "2026-05-07T00:00:00Z",
40
+ entries,
41
+ ...extras,
42
+ };
43
+ }
44
+
45
+ function makeFakeFetch(
46
+ registry: Registry,
47
+ counter: { calls: number },
48
+ overrides: Partial<{ ok: boolean; status: number }> = {},
49
+ ): FetchImpl {
50
+ return async () => {
51
+ counter.calls += 1;
52
+ return {
53
+ ok: overrides.ok ?? true,
54
+ status: overrides.status ?? 200,
55
+ statusText: "OK",
56
+ json: async () => registry,
57
+ text: async () => JSON.stringify(registry),
58
+ };
59
+ };
60
+ }
61
+
62
+ describe("loadRegistry caching", () => {
63
+ beforeEach(() => clearCache());
64
+
65
+ it("fetches once and serves a cache hit on the second call", async () => {
66
+ const counter = { calls: 0 };
67
+ const registry = makeRegistry();
68
+ const fetchImpl = makeFakeFetch(registry, counter);
69
+ const now = () => 1_000_000;
70
+
71
+ const first = await loadRegistry({
72
+ source: SOURCE,
73
+ fetchImpl,
74
+ ttlMs: 60_000,
75
+ now,
76
+ });
77
+ const second = await loadRegistry({
78
+ source: SOURCE,
79
+ fetchImpl,
80
+ ttlMs: 60_000,
81
+ now,
82
+ });
83
+
84
+ assert.equal(counter.calls, 1, "second call should be served from cache");
85
+ assert.equal(first, second, "both loads should return the same object");
86
+ });
87
+
88
+ it("refetches after the cache TTL expires", async () => {
89
+ const counter = { calls: 0 };
90
+ const registry = makeRegistry();
91
+ const fetchImpl = makeFakeFetch(registry, counter);
92
+
93
+ let nowValue = 1_000_000;
94
+ const now = () => nowValue;
95
+
96
+ await loadRegistry({ source: SOURCE, fetchImpl, ttlMs: 60_000, now });
97
+ nowValue = 1_000_000 + 61_000; // > TTL
98
+ await loadRegistry({ source: SOURCE, fetchImpl, ttlMs: 60_000, now });
99
+
100
+ assert.equal(counter.calls, 2, "expired cache should trigger a refetch");
101
+ });
102
+
103
+ it("throws when the upstream registry returns non-ok", async () => {
104
+ const counter = { calls: 0 };
105
+ const registry = makeRegistry();
106
+ const fetchImpl = makeFakeFetch(registry, counter, {
107
+ ok: false,
108
+ status: 503,
109
+ });
110
+
111
+ await assert.rejects(
112
+ () =>
113
+ loadRegistry({
114
+ source: SOURCE,
115
+ fetchImpl,
116
+ ttlMs: 60_000,
117
+ now: () => 1,
118
+ }),
119
+ /Failed to fetch registry/,
120
+ );
121
+ });
122
+ });
123
+
124
+ describe("filterEntries", () => {
125
+ const registry = makeRegistry();
126
+
127
+ it("filters by exact format match", () => {
128
+ const out = filterEntries(
129
+ registry.entries,
130
+ (e) => e.format === "understand-anything@1",
131
+ );
132
+ assert.equal(out.length, 2);
133
+ assert.deepEqual(
134
+ out.map((e) => e.id),
135
+ ["alice/python-graph", "bob/ts-graph"],
136
+ );
137
+ });
138
+
139
+ it("filters by status", () => {
140
+ const out = filterEntries(registry.entries, (e) => e.status === "ok");
141
+ assert.equal(out.length, 2);
142
+ assert.deepEqual(
143
+ out.map((e) => e.id),
144
+ ["alice/python-graph", "carol/rust-graph"],
145
+ );
146
+ });
147
+
148
+ it("filters by tag membership", () => {
149
+ const out = filterEntries(registry.entries, (e) =>
150
+ (e.tags ?? []).includes("agents"),
151
+ );
152
+ assert.equal(out.length, 2);
153
+ assert.deepEqual(
154
+ out.map((e) => e.id),
155
+ ["alice/python-graph", "carol/rust-graph"],
156
+ );
157
+ });
158
+
159
+ it("filters by combined predicates (status=ok AND tag=agents)", () => {
160
+ const out = filterEntries(
161
+ registry.entries,
162
+ (e) => e.status === "ok" && (e.tags ?? []).includes("agents"),
163
+ );
164
+ assert.equal(out.length, 2);
165
+ });
166
+
167
+ it("returns an empty list when nothing matches", () => {
168
+ const out = filterEntries(registry.entries, (e) => e.format === "nope");
169
+ assert.equal(out.length, 0);
170
+ });
171
+ });