@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.
- package/README.md +147 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +134 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +53 -0
- package/dist/registry.js +191 -0
- package/dist/registry.js.map +1 -0
- package/dist/tools/find-graph-for-repo.d.ts +53 -0
- package/dist/tools/find-graph-for-repo.js +167 -0
- package/dist/tools/find-graph-for-repo.js.map +1 -0
- package/dist/tools/get-graph.d.ts +17 -0
- package/dist/tools/get-graph.js +28 -0
- package/dist/tools/get-graph.js.map +1 -0
- package/dist/tools/list-repos.d.ts +24 -0
- package/dist/tools/list-repos.js +51 -0
- package/dist/tools/list-repos.js.map +1 -0
- package/dist/tools/search-concepts.d.ts +43 -0
- package/dist/tools/search-concepts.js +171 -0
- package/dist/tools/search-concepts.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/index.ts +168 -0
- package/src/registry.ts +272 -0
- package/src/tools/find-graph-for-repo.ts +221 -0
- package/src/tools/get-graph.ts +36 -0
- package/src/tools/list-repos.ts +62 -0
- package/src/tools/search-concepts.ts +239 -0
- package/src/types.ts +101 -0
- package/tests/registry.test.ts +171 -0
- package/tests/tools.test.ts +351 -0
- package/tests/tsconfig.json +9 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# @looptech-ai/understand-quickly-mcp
|
|
2
|
+
|
|
3
|
+
A thin [Model Context Protocol](https://modelcontextprotocol.io) server that
|
|
4
|
+
exposes the [understand-quickly](https://looptech-ai.github.io/understand-quickly/)
|
|
5
|
+
registry to any MCP client (Claude Desktop, Codex, Cursor, etc.).
|
|
6
|
+
|
|
7
|
+
> Status: stub-quality. It works end-to-end but is intentionally minimal —
|
|
8
|
+
> no streaming, no embeddings, no auth.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
It wraps the public `registry.json` and exposes four tools:
|
|
13
|
+
|
|
14
|
+
| Tool | Params | Returns |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `list_repos` | `{ format?, tag?, status? }` | Array of `{ id, format, description, status, tags, last_synced, graph_url }` |
|
|
17
|
+
| `find_graph_for_repo` | `{ id?, github_url? }` (at least one required) | Single registry entry's graph_url + drift metadata, or `{ found: false, suggestions: [...] }` with up to 5 fuzzy-matched ids |
|
|
18
|
+
| `get_graph` | `{ id }` | Parsed graph JSON for that entry's `graph_url` |
|
|
19
|
+
| `search_concepts` | `{ query, id? }` | Default: aggregated concept matches from the precomputed `stats.json` (single GET, cached 60s). With `id`: substring match across one graph's nodes. Falls back to a capped cross-graph fan-out if `stats.json` is unreachable. |
|
|
20
|
+
|
|
21
|
+
The registry response is cached in-memory for 60 seconds. `stats.json` uses an
|
|
22
|
+
identical 60-second TTL cache.
|
|
23
|
+
|
|
24
|
+
### `find_graph_for_repo`
|
|
25
|
+
|
|
26
|
+
Accepts either an `id` (the registry id, `owner/repo`) or a `github_url`. The
|
|
27
|
+
URL parser tolerates:
|
|
28
|
+
|
|
29
|
+
- `https://github.com/owner/repo`
|
|
30
|
+
- `https://github.com/owner/repo.git`
|
|
31
|
+
- `https://github.com/owner/repo/` (trailing slash)
|
|
32
|
+
- `https://github.com/owner/repo/tree/main/...` (branch / sub-path)
|
|
33
|
+
- `git@github.com:owner/repo.git`
|
|
34
|
+
|
|
35
|
+
When the entry is found, the response includes `last_synced`, `last_sha`,
|
|
36
|
+
`source_sha`, `head_sha`, `commits_behind`, and a pretty `drift_summary`
|
|
37
|
+
(e.g. `"behind by 17 commits"`) when those fields are present in the registry.
|
|
38
|
+
|
|
39
|
+
If the entry is not found, the response is
|
|
40
|
+
`{ found: false, suggestions: [...] }` with up to 5 fuzzy-matched ids
|
|
41
|
+
(Levenshtein distance ≤ 3 against the lowercased id).
|
|
42
|
+
|
|
43
|
+
### `search_concepts`
|
|
44
|
+
|
|
45
|
+
By default — that is, when `id` is not provided — `search_concepts` reads the
|
|
46
|
+
precomputed `stats.json` aggregate (a single, cached GET) and returns matching
|
|
47
|
+
concept terms with their entry counts and up to 3 sample registry ids. This
|
|
48
|
+
replaces the previous behaviour, which fanned out up to 5 graph fetches at
|
|
49
|
+
request time.
|
|
50
|
+
|
|
51
|
+
When `id` is provided, it falls back to the legacy single-graph node search
|
|
52
|
+
(substring match against `id` / `label` / `name`). When `stats.json` is
|
|
53
|
+
unavailable (404 or schema mismatch), it falls back to the capped cross-graph
|
|
54
|
+
fan-out for backward compatibility.
|
|
55
|
+
|
|
56
|
+
The `source` field on the response indicates which mode served the request:
|
|
57
|
+
`"stats"`, `"graph"`, or `"fanout"`.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd mcp
|
|
63
|
+
npm install
|
|
64
|
+
npm run build # compiles TypeScript -> dist/
|
|
65
|
+
npm test # runs node:test across registry/cache and tool logic
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Node 20+ is required (uses the global `fetch`).
|
|
69
|
+
|
|
70
|
+
## Run locally
|
|
71
|
+
|
|
72
|
+
For development:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run dev
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For a built binary:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm start
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The server speaks stdio JSON-RPC. It will not respond to keystrokes — point an
|
|
85
|
+
MCP client at it.
|
|
86
|
+
|
|
87
|
+
## Register with Claude Desktop
|
|
88
|
+
|
|
89
|
+
Add the following to Claude Desktop's `claude_desktop_config.json` (the path is
|
|
90
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"understand-quickly": {
|
|
96
|
+
"command": "npx",
|
|
97
|
+
"args": [
|
|
98
|
+
"tsx",
|
|
99
|
+
"/absolute/path/to/understand-quickly/mcp/src/index.ts"
|
|
100
|
+
],
|
|
101
|
+
"env": {
|
|
102
|
+
"UNDERSTAND_QUICKLY_REGISTRY": "https://looptech-ai.github.io/understand-quickly/registry.json"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Replace `/absolute/path/to/...` with the actual path to your checkout. Restart
|
|
110
|
+
Claude Desktop after saving.
|
|
111
|
+
|
|
112
|
+
If you would rather run the compiled output, swap to:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"mcpServers": {
|
|
117
|
+
"understand-quickly": {
|
|
118
|
+
"command": "node",
|
|
119
|
+
"args": ["/absolute/path/to/understand-quickly/mcp/dist/index.js"]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Environment variables
|
|
126
|
+
|
|
127
|
+
| Variable | Default | Purpose |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| `UNDERSTAND_QUICKLY_REGISTRY` | `https://looptech-ai.github.io/understand-quickly/registry.json` | Override the registry source (e.g. point at a local file or a fork). |
|
|
130
|
+
| `UNDERSTAND_QUICKLY_STATS` | `https://looptech-ai.github.io/understand-quickly/stats.json` | Override the precomputed stats source consumed by `search_concepts`. |
|
|
131
|
+
|
|
132
|
+
## Current limitations
|
|
133
|
+
|
|
134
|
+
- **In-memory cache only.** Every server process refetches once a minute. No
|
|
135
|
+
cross-process or on-disk cache.
|
|
136
|
+
- **Cross-graph fan-out is only a fallback.** When `search_concepts` falls back
|
|
137
|
+
(no stats.json), it scans only the first 5 `status: ok` entries sequentially.
|
|
138
|
+
- **Substring search is dumb.** No fuzzy matching, no ranking, no embeddings.
|
|
139
|
+
- **No streaming or progress reporting.** Tools block until the upstream
|
|
140
|
+
responds.
|
|
141
|
+
- **Best-effort node enumeration.** The single-graph fallback assumes the graph
|
|
142
|
+
has a `nodes` / `entities` / `concepts` / `items` array; otherwise it walks
|
|
143
|
+
top-level array values.
|
|
144
|
+
- **No retries or backoff** on upstream `graph_url` fetch failures — failed
|
|
145
|
+
fetches return an empty result for that entry instead of erroring out.
|
|
146
|
+
|
|
147
|
+
These are all acceptable for an MVP. If you need more, open an issue.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { listRepos } from "./tools/list-repos.js";
|
|
6
|
+
import { getGraph } from "./tools/get-graph.js";
|
|
7
|
+
import { searchConcepts } from "./tools/search-concepts.js";
|
|
8
|
+
import { findGraphForRepo } from "./tools/find-graph-for-repo.js";
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: "understand-quickly-mcp",
|
|
11
|
+
version: "0.1.0",
|
|
12
|
+
}, {
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
function jsonContent(value) {
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: "text",
|
|
22
|
+
text: JSON.stringify(value, null, 2),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function errorContent(err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
return {
|
|
30
|
+
isError: true,
|
|
31
|
+
content: [{ type: "text", text: message }],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
server.registerTool("list_repos", {
|
|
35
|
+
description: "List entries in the understand-quickly registry. Optional filters: format, tag, status.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
format: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Exact match on entry.format (e.g. \"understand-anything@1\")."),
|
|
41
|
+
tag: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Returns entries whose tags array contains this string."),
|
|
45
|
+
status: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Exact match on entry.status (typically \"ok\")."),
|
|
49
|
+
},
|
|
50
|
+
}, async ({ format, tag, status }) => {
|
|
51
|
+
try {
|
|
52
|
+
const repos = await listRepos({ format, tag, status });
|
|
53
|
+
return jsonContent(repos);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return errorContent(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
server.registerTool("get_graph", {
|
|
60
|
+
description: "Fetch and return the parsed knowledge graph JSON for a registry entry by id.",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
id: z
|
|
63
|
+
.string()
|
|
64
|
+
.min(1)
|
|
65
|
+
.describe("Registry entry id, e.g. \"Lum1104/Understand-Anything\"."),
|
|
66
|
+
},
|
|
67
|
+
}, async ({ id }) => {
|
|
68
|
+
try {
|
|
69
|
+
const graph = await getGraph({ id });
|
|
70
|
+
return jsonContent(graph);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return errorContent(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
server.registerTool("search_concepts", {
|
|
77
|
+
description: "Search aggregated concept terms across the registry. By default reads the precomputed stats.json (single GET, cached 60s) and returns matching terms with sample entry ids. If `id` is given, falls back to a single-graph node search. If stats.json is unavailable, falls back to a capped cross-graph node fan-out.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
query: z.string().min(1).describe("Substring to search for (case-insensitive)."),
|
|
80
|
+
id: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Optional entry id. If given, scopes the search to that single graph (legacy node-level mode)."),
|
|
84
|
+
},
|
|
85
|
+
}, async ({ query, id }) => {
|
|
86
|
+
try {
|
|
87
|
+
const result = await searchConcepts({ query, id });
|
|
88
|
+
return jsonContent(result);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return errorContent(err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
server.registerTool("find_graph_for_repo", {
|
|
95
|
+
description: "Look up a registry entry by `id` (\"owner/repo\") or `github_url` (https or ssh form, with optional .git/branch/path). Returns graph_url + drift metadata, or {found:false, suggestions} with up to 5 fuzzy-matched ids.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
id: z
|
|
98
|
+
.string()
|
|
99
|
+
.regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, {
|
|
100
|
+
message: "id must be \"owner/repo\".",
|
|
101
|
+
})
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Registry id, e.g. \"Lum1104/Understand-Anything\"."),
|
|
104
|
+
github_url: z
|
|
105
|
+
.string()
|
|
106
|
+
.min(1)
|
|
107
|
+
.optional()
|
|
108
|
+
.describe("GitHub URL (https or ssh form). Trailing .git, branches, and sub-paths are tolerated."),
|
|
109
|
+
},
|
|
110
|
+
}, async ({ id, github_url }) => {
|
|
111
|
+
try {
|
|
112
|
+
if (!id && !github_url) {
|
|
113
|
+
return errorContent(new Error("Provide at least one of `id` or `github_url`."));
|
|
114
|
+
}
|
|
115
|
+
const result = await findGraphForRepo({ id, github_url });
|
|
116
|
+
return jsonContent(result);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
return errorContent(err);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
async function main() {
|
|
123
|
+
const transport = new StdioServerTransport();
|
|
124
|
+
await server.connect(transport);
|
|
125
|
+
// Log to stderr so we don't pollute the JSON-RPC stream on stdout.
|
|
126
|
+
// eslint-disable-next-line no-console
|
|
127
|
+
console.error("understand-quickly MCP server ready on stdio");
|
|
128
|
+
}
|
|
129
|
+
main().catch((err) => {
|
|
130
|
+
// eslint-disable-next-line no-console
|
|
131
|
+
console.error("Fatal MCP server error:", err);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { FetchImpl, Registry, RegistryEntry, StatsJson } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_REGISTRY_URL = "https://looptech-ai.github.io/understand-quickly/registry.json";
|
|
3
|
+
export declare const DEFAULT_STATS_URL = "https://looptech-ai.github.io/understand-quickly/stats.json";
|
|
4
|
+
export declare const DEFAULT_TTL_MS = 60000;
|
|
5
|
+
export interface LoadRegistryOptions {
|
|
6
|
+
source?: string;
|
|
7
|
+
fetchImpl?: FetchImpl;
|
|
8
|
+
cacheKey?: string;
|
|
9
|
+
ttlMs?: number;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Load `registry.json` with a small in-memory TTL cache.
|
|
14
|
+
*
|
|
15
|
+
* Pure-ish: all I/O and time goes through injected dependencies, so callers in
|
|
16
|
+
* tests can drive the cache with a fake fetch and clock.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadRegistry(options?: LoadRegistryOptions): Promise<Registry>;
|
|
19
|
+
/** Drop the cached registry for a given key (default: the configured source). */
|
|
20
|
+
export declare function clearCache(cacheKey?: string): void;
|
|
21
|
+
export interface LoadStatsOptions {
|
|
22
|
+
source?: string;
|
|
23
|
+
fetchImpl?: FetchImpl;
|
|
24
|
+
cacheKey?: string;
|
|
25
|
+
ttlMs?: number;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Load `stats.json` with the same TTL caching pattern as `loadRegistry`.
|
|
30
|
+
*
|
|
31
|
+
* Validates the minimum shape (schema_version + concepts array). Throws on
|
|
32
|
+
* non-OK HTTP, malformed body, or missing concepts; callers that want a
|
|
33
|
+
* fallback path should catch.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadStats(options?: LoadStatsOptions): Promise<StatsJson>;
|
|
36
|
+
/** Drop the cached stats payload (default: every key). */
|
|
37
|
+
export declare function clearStatsCache(cacheKey?: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Filter entries with a predicate. Trivial wrapper, but exported so the tool
|
|
40
|
+
* layer composes via a single named function rather than ad-hoc `.filter`s.
|
|
41
|
+
*/
|
|
42
|
+
export declare function filterEntries(entries: RegistryEntry[], predicate: (entry: RegistryEntry) => boolean): RegistryEntry[];
|
|
43
|
+
/** Resolve the registry URL from env, falling back to the public default. */
|
|
44
|
+
export declare function resolveRegistrySource(): string;
|
|
45
|
+
/** Resolve the stats URL from env, falling back to the public default. */
|
|
46
|
+
export declare function resolveStatsSource(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Find an entry by id. Exposed for the `get_graph` and `search_concepts` tools.
|
|
49
|
+
*/
|
|
50
|
+
export declare function findEntryById(registry: Registry, id: string): RegistryEntry | undefined;
|
|
51
|
+
export declare function assertSafeFetchUrl(rawUrl: string): URL;
|
|
52
|
+
/** Fetch and parse a single graph URL. Used by `get_graph` and `search_concepts`. */
|
|
53
|
+
export declare function fetchGraph(graphUrl: string, fetchImpl?: FetchImpl): Promise<unknown>;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
export const DEFAULT_REGISTRY_URL = "https://looptech-ai.github.io/understand-quickly/registry.json";
|
|
3
|
+
export const DEFAULT_STATS_URL = "https://looptech-ai.github.io/understand-quickly/stats.json";
|
|
4
|
+
export const DEFAULT_TTL_MS = 60_000;
|
|
5
|
+
// Module-level cache keyed by source URL. Each MCP process gets its own cache;
|
|
6
|
+
// that is fine for a stub server because the registry is small.
|
|
7
|
+
const cache = new Map();
|
|
8
|
+
const statsCache = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Load `registry.json` with a small in-memory TTL cache.
|
|
11
|
+
*
|
|
12
|
+
* Pure-ish: all I/O and time goes through injected dependencies, so callers in
|
|
13
|
+
* tests can drive the cache with a fake fetch and clock.
|
|
14
|
+
*/
|
|
15
|
+
export async function loadRegistry(options = {}) {
|
|
16
|
+
const source = options.source ?? DEFAULT_REGISTRY_URL;
|
|
17
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
18
|
+
const cacheKey = options.cacheKey ?? source;
|
|
19
|
+
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
20
|
+
const now = options.now ?? Date.now;
|
|
21
|
+
if (!fetchImpl) {
|
|
22
|
+
throw new Error("No fetch implementation available. Pass `fetchImpl` or run on Node 20+.");
|
|
23
|
+
}
|
|
24
|
+
const cached = cache.get(cacheKey);
|
|
25
|
+
if (cached && now() - cached.fetchedAt < ttlMs) {
|
|
26
|
+
return cached.registry;
|
|
27
|
+
}
|
|
28
|
+
const response = await fetchImpl(source);
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
// 5xx is transient; if we have a stale cache entry, prefer it over a hard
|
|
31
|
+
// throw so an upstream Pages outage doesn't take down every MCP client.
|
|
32
|
+
if (response.status >= 500 && cached)
|
|
33
|
+
return cached.registry;
|
|
34
|
+
throw new Error(`Failed to fetch registry from ${source}: ${response.status} ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
const body = (await response.json());
|
|
37
|
+
if (!body || !Array.isArray(body.entries)) {
|
|
38
|
+
throw new Error(`Registry at ${source} is malformed: missing \`entries\` array`);
|
|
39
|
+
}
|
|
40
|
+
// Guard against silently consuming a future v2 registry with v1-shaped
|
|
41
|
+
// tools. The registry's meta.schema.json pins schema_version to const 1; an
|
|
42
|
+
// older MCP build hitting a newer registry should fail loudly so users know
|
|
43
|
+
// to upgrade rather than getting half-broken responses.
|
|
44
|
+
const sv = body.schema_version;
|
|
45
|
+
if (sv !== undefined && sv !== 1) {
|
|
46
|
+
throw new Error(`Registry at ${source} reports schema_version=${String(sv)}; this MCP build supports schema_version=1. Upgrade @looptech-ai/understand-quickly-mcp.`);
|
|
47
|
+
}
|
|
48
|
+
cache.set(cacheKey, { fetchedAt: now(), registry: body });
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
/** Drop the cached registry for a given key (default: the configured source). */
|
|
52
|
+
export function clearCache(cacheKey) {
|
|
53
|
+
if (cacheKey === undefined) {
|
|
54
|
+
cache.clear();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
cache.delete(cacheKey);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Load `stats.json` with the same TTL caching pattern as `loadRegistry`.
|
|
61
|
+
*
|
|
62
|
+
* Validates the minimum shape (schema_version + concepts array). Throws on
|
|
63
|
+
* non-OK HTTP, malformed body, or missing concepts; callers that want a
|
|
64
|
+
* fallback path should catch.
|
|
65
|
+
*/
|
|
66
|
+
export async function loadStats(options = {}) {
|
|
67
|
+
const source = options.source ?? DEFAULT_STATS_URL;
|
|
68
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
69
|
+
const cacheKey = options.cacheKey ?? source;
|
|
70
|
+
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
71
|
+
const now = options.now ?? Date.now;
|
|
72
|
+
if (!fetchImpl) {
|
|
73
|
+
throw new Error("No fetch implementation available. Pass `fetchImpl` or run on Node 20+.");
|
|
74
|
+
}
|
|
75
|
+
const cached = statsCache.get(cacheKey);
|
|
76
|
+
if (cached && now() - cached.fetchedAt < ttlMs) {
|
|
77
|
+
return cached.stats;
|
|
78
|
+
}
|
|
79
|
+
const response = await fetchImpl(source);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Failed to fetch stats from ${source}: ${response.status} ${response.statusText}`);
|
|
82
|
+
}
|
|
83
|
+
const body = (await response.json());
|
|
84
|
+
if (!body || !Array.isArray(body.concepts)) {
|
|
85
|
+
throw new Error(`Stats at ${source} is malformed: missing \`concepts\` array`);
|
|
86
|
+
}
|
|
87
|
+
statsCache.set(cacheKey, { fetchedAt: now(), stats: body });
|
|
88
|
+
return body;
|
|
89
|
+
}
|
|
90
|
+
/** Drop the cached stats payload (default: every key). */
|
|
91
|
+
export function clearStatsCache(cacheKey) {
|
|
92
|
+
if (cacheKey === undefined) {
|
|
93
|
+
statsCache.clear();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
statsCache.delete(cacheKey);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Filter entries with a predicate. Trivial wrapper, but exported so the tool
|
|
100
|
+
* layer composes via a single named function rather than ad-hoc `.filter`s.
|
|
101
|
+
*/
|
|
102
|
+
export function filterEntries(entries, predicate) {
|
|
103
|
+
return entries.filter(predicate);
|
|
104
|
+
}
|
|
105
|
+
/** Resolve the registry URL from env, falling back to the public default. */
|
|
106
|
+
export function resolveRegistrySource() {
|
|
107
|
+
return process.env.UNDERSTAND_QUICKLY_REGISTRY ?? DEFAULT_REGISTRY_URL;
|
|
108
|
+
}
|
|
109
|
+
/** Resolve the stats URL from env, falling back to the public default. */
|
|
110
|
+
export function resolveStatsSource() {
|
|
111
|
+
return process.env.UNDERSTAND_QUICKLY_STATS ?? DEFAULT_STATS_URL;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Find an entry by id. Exposed for the `get_graph` and `search_concepts` tools.
|
|
115
|
+
*/
|
|
116
|
+
export function findEntryById(registry, id) {
|
|
117
|
+
return registry.entries.find((entry) => entry.id === id);
|
|
118
|
+
}
|
|
119
|
+
// SSRF guard: reject URLs that resolve to private / link-local / loopback /
|
|
120
|
+
// metadata addresses, or that use a non-https scheme. The registry's own
|
|
121
|
+
// schemas pin graph_url to https; this is defence-in-depth for the MCP path,
|
|
122
|
+
// where a malicious registry mirror or a misconfigured URL could otherwise
|
|
123
|
+
// trick this process into fetching cloud-metadata endpoints.
|
|
124
|
+
//
|
|
125
|
+
// Note: this checks the literal hostname, not a resolved IP. Full DNS-rebind
|
|
126
|
+
// protection requires a custom dispatcher; for v0.1 we accept that gap and
|
|
127
|
+
// rely on the surrounding HTTPS-only invariant (TLS makes rebinding harder
|
|
128
|
+
// since the cert must match the literal hostname).
|
|
129
|
+
export function assertSafeFetchUrl(rawUrl) {
|
|
130
|
+
let u;
|
|
131
|
+
try {
|
|
132
|
+
u = new URL(rawUrl);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new Error(`Invalid URL: ${rawUrl}`);
|
|
136
|
+
}
|
|
137
|
+
if (u.protocol !== "https:") {
|
|
138
|
+
throw new Error(`Refusing non-https URL: ${rawUrl}`);
|
|
139
|
+
}
|
|
140
|
+
const host = u.hostname.toLowerCase();
|
|
141
|
+
// Block obvious local / metadata targets by literal hostname.
|
|
142
|
+
if (host === "localhost" ||
|
|
143
|
+
host === "0.0.0.0" ||
|
|
144
|
+
host === "127.0.0.1" ||
|
|
145
|
+
host === "::1" ||
|
|
146
|
+
host === "metadata.google.internal" ||
|
|
147
|
+
host === "metadata" ||
|
|
148
|
+
host.endsWith(".internal") ||
|
|
149
|
+
host.endsWith(".local")) {
|
|
150
|
+
throw new Error(`Refusing internal host: ${host}`);
|
|
151
|
+
}
|
|
152
|
+
// Block IPv4 literals in private/link-local/loopback ranges.
|
|
153
|
+
const ipKind = isIP(host);
|
|
154
|
+
if (ipKind === 4) {
|
|
155
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
156
|
+
if (ipv4) {
|
|
157
|
+
const [a, b] = ipv4.slice(1).map(Number);
|
|
158
|
+
if (a === 10 ||
|
|
159
|
+
a === 127 ||
|
|
160
|
+
(a === 169 && b === 254) || // link-local incl. AWS/GCP metadata 169.254.169.254
|
|
161
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
162
|
+
(a === 192 && b === 168) ||
|
|
163
|
+
a === 0) {
|
|
164
|
+
throw new Error(`Refusing private IPv4 host: ${host}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (ipKind === 6) {
|
|
169
|
+
// URL.hostname strips the brackets from IPv6 literals (e.g. `fc00::1`,
|
|
170
|
+
// not `[fc00::1]`), so a startsWith("[") check would never fire. Match
|
|
171
|
+
// against the normalized address prefix directly.
|
|
172
|
+
// - fc00::/7 (unique-local) → first hex digit 'f' + second 'c' or 'd'
|
|
173
|
+
// - fe80::/10 (link-local) → first hex digit 'f' + second 'e' + third 8|9|a|b
|
|
174
|
+
// - ::1 loopback (caught earlier by literal hostname check)
|
|
175
|
+
// - ::ffff:0:0/96 IPv4-mapped → defer to Node which will resolve via dual-stack
|
|
176
|
+
if (/^f[cd]/i.test(host) || /^fe[89ab]/i.test(host)) {
|
|
177
|
+
throw new Error(`Refusing private IPv6 host: ${host}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return u;
|
|
181
|
+
}
|
|
182
|
+
/** Fetch and parse a single graph URL. Used by `get_graph` and `search_concepts`. */
|
|
183
|
+
export async function fetchGraph(graphUrl, fetchImpl = globalThis.fetch) {
|
|
184
|
+
assertSafeFetchUrl(graphUrl);
|
|
185
|
+
const response = await fetchImpl(graphUrl);
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new Error(`Failed to fetch graph from ${graphUrl}: ${response.status} ${response.statusText}`);
|
|
188
|
+
}
|
|
189
|
+
return response.json();
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { FetchImpl, FindGraphForRepoParams } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a github URL into a registry id. Accepts:
|
|
4
|
+
* - https://github.com/owner/repo
|
|
5
|
+
* - https://github.com/owner/repo.git
|
|
6
|
+
* - https://github.com/owner/repo/ (trailing slash)
|
|
7
|
+
* - https://github.com/owner/repo/tree/branch/...
|
|
8
|
+
* - git@github.com:owner/repo.git
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseGithubUrl(url: string): string | undefined;
|
|
11
|
+
/** Levenshtein distance, capped early when it exceeds `maxDistance` for speed. */
|
|
12
|
+
export declare function levenshtein(a: string, b: string, maxDistance?: number): number;
|
|
13
|
+
export interface FindGraphForRepoFoundResult {
|
|
14
|
+
found: true;
|
|
15
|
+
id: string;
|
|
16
|
+
format: string;
|
|
17
|
+
graph_url: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
last_synced?: string;
|
|
20
|
+
last_sha?: string;
|
|
21
|
+
source_sha?: string;
|
|
22
|
+
head_sha?: string;
|
|
23
|
+
commits_behind?: number;
|
|
24
|
+
drift_summary?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface FindGraphForRepoNotFoundResult {
|
|
27
|
+
found: false;
|
|
28
|
+
suggestions: string[];
|
|
29
|
+
}
|
|
30
|
+
export type FindGraphForRepoResult = FindGraphForRepoFoundResult | FindGraphForRepoNotFoundResult;
|
|
31
|
+
export interface FindGraphForRepoOptions {
|
|
32
|
+
fetchImpl?: FetchImpl;
|
|
33
|
+
source?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function findGraphForRepo(params: FindGraphForRepoParams, options?: FindGraphForRepoOptions): Promise<FindGraphForRepoResult>;
|
|
36
|
+
export declare const findGraphForRepoToolDefinition: {
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object";
|
|
41
|
+
properties: {
|
|
42
|
+
id: {
|
|
43
|
+
type: string;
|
|
44
|
+
description: string;
|
|
45
|
+
};
|
|
46
|
+
github_url: {
|
|
47
|
+
type: string;
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
additionalProperties: boolean;
|
|
52
|
+
};
|
|
53
|
+
};
|