@kage-core/kage-graph-mcp 1.0.0 → 1.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,373 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PUBLIC_CANDIDATE_BUNDLE_SCHEMA_VERSION = exports.REGISTRY_MANIFEST_SCHEMA_VERSION = void 0;
4
+ exports.canonicalJson = canonicalJson;
5
+ exports.sha256Hex = sha256Hex;
6
+ exports.createSignedManifest = createSignedManifest;
7
+ exports.verifySignedManifest = verifySignedManifest;
8
+ exports.scanPublicCandidateSecrets = scanPublicCandidateSecrets;
9
+ exports.sanitizePublicCandidate = sanitizePublicCandidate;
10
+ exports.validatePublicCandidateBundle = validatePublicCandidateBundle;
11
+ exports.createPublicCandidateBundleManifest = createPublicCandidateBundleManifest;
12
+ exports.generateOrgRegistryManifest = generateOrgRegistryManifest;
13
+ const node_crypto_1 = require("node:crypto");
14
+ exports.REGISTRY_MANIFEST_SCHEMA_VERSION = 1;
15
+ exports.PUBLIC_CANDIDATE_BUNDLE_SCHEMA_VERSION = 1;
16
+ const MAX_TEXT_LENGTH = 20_000;
17
+ const SAFE_ID = /^[a-z0-9][a-z0-9._:-]{1,127}$/;
18
+ const SAFE_TYPE = /^[a-z][a-z0-9_-]{1,63}$/;
19
+ const SAFE_LICENSE = /^[A-Za-z0-9][A-Za-z0-9 .+()-]{0,63}$/;
20
+ const HTTP_URL = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
21
+ const SECRET_PATTERNS = [
22
+ [/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, "email address"],
23
+ [/\b(?:authorization|token|api[_-]?key|secret)\s*[:=]\s*['"]?[A-Za-z0-9._~+/=-]{16,}/i, "credential-like key/value"],
24
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i, "bearer token"],
25
+ [/\b(?:sk|pk)_(?:live|test)_[A-Za-z0-9]{12,}\b/i, "payment provider token"],
26
+ [/\bgh[pousr]_[A-Za-z0-9_]{16,}\b/i, "GitHub token"],
27
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key"],
28
+ ];
29
+ function canonicalJson(value) {
30
+ return JSON.stringify(sortJsonValue(value));
31
+ }
32
+ function sha256Hex(value) {
33
+ return (0, node_crypto_1.createHash)("sha256").update(value).digest("hex");
34
+ }
35
+ function createSignedManifest(input) {
36
+ const generatedAt = input.generatedAt ?? new Date().toISOString();
37
+ const keyId = input.keyId ?? "local-dev";
38
+ const payloadSha256 = sha256Hex(canonicalJson(input.payload));
39
+ return {
40
+ schema_version: exports.REGISTRY_MANIFEST_SCHEMA_VERSION,
41
+ kind: input.kind,
42
+ name: input.name,
43
+ version: input.version,
44
+ generated_at: generatedAt,
45
+ payload: input.payload,
46
+ signature: {
47
+ algorithm: "sha256-canonical-json",
48
+ key_id: keyId,
49
+ payload_sha256: payloadSha256,
50
+ signature: sha256Hex(`${keyId}:${input.kind}:${input.name}:${input.version}:${generatedAt}:${payloadSha256}`),
51
+ },
52
+ };
53
+ }
54
+ function verifySignedManifest(manifest) {
55
+ const errors = [];
56
+ const warnings = [];
57
+ if (manifest.schema_version !== exports.REGISTRY_MANIFEST_SCHEMA_VERSION) {
58
+ errors.push("manifest schema_version must be 1");
59
+ }
60
+ if (!["public_candidate_bundle", "org_registry"].includes(String(manifest.kind))) {
61
+ errors.push("manifest kind is not supported");
62
+ }
63
+ if (!isNonEmptyString(manifest.name)) {
64
+ errors.push("manifest name is required");
65
+ }
66
+ if (!isNonEmptyString(manifest.version)) {
67
+ errors.push("manifest version is required");
68
+ }
69
+ if (!isIsoDate(manifest.generated_at)) {
70
+ errors.push("manifest generated_at must be an ISO date string");
71
+ }
72
+ if (!manifest.signature || manifest.signature.algorithm !== "sha256-canonical-json") {
73
+ errors.push("manifest signature algorithm must be sha256-canonical-json");
74
+ }
75
+ else {
76
+ const payloadSha256 = sha256Hex(canonicalJson(manifest.payload));
77
+ const expectedSignature = sha256Hex(`${manifest.signature.key_id}:${manifest.kind}:${manifest.name}:${manifest.version}:${manifest.generated_at}:${payloadSha256}`);
78
+ if (manifest.signature.payload_sha256 !== payloadSha256) {
79
+ errors.push("manifest payload_sha256 does not match payload");
80
+ }
81
+ if (manifest.signature.signature !== expectedSignature) {
82
+ errors.push("manifest signature does not match signed fields");
83
+ }
84
+ }
85
+ return { ok: errors.length === 0, value: errors.length === 0 ? manifest : undefined, errors, warnings };
86
+ }
87
+ function scanPublicCandidateSecrets(value) {
88
+ const text = collectStrings(value).join("\n");
89
+ const matches = new Set();
90
+ for (const [pattern, label] of SECRET_PATTERNS) {
91
+ if (pattern.test(text)) {
92
+ matches.add(label);
93
+ }
94
+ }
95
+ return [...matches].sort();
96
+ }
97
+ function sanitizePublicCandidate(input) {
98
+ const errors = [];
99
+ const warnings = [];
100
+ const id = stringField(input.id, "id", errors);
101
+ const title = stringField(input.title, "title", errors);
102
+ const summary = stringField(input.summary, "summary", errors);
103
+ const body = stringField(input.body, "body", errors);
104
+ const type = stringField(input.type, "type", errors);
105
+ const license = typeof input.license === "string" && input.license.trim() ? input.license.trim() : "UNLICENSED";
106
+ const tags = stringArrayField(input.tags, "tags", errors);
107
+ const stack = stringArrayField(input.stack, "stack", errors);
108
+ const homepageUrl = optionalUrlField(input.homepage_url, "homepage_url", errors);
109
+ const sourceUrl = optionalUrlField(input.source_url, "source_url", errors);
110
+ const contributorHandle = typeof input.contributor_handle === "string" ? input.contributor_handle.trim().slice(0, 80) : undefined;
111
+ const contributorOrg = typeof input.contributor_org === "string" ? input.contributor_org.trim().slice(0, 80) : undefined;
112
+ const contributionUrl = optionalUrlField(input.contribution_url, "contribution_url", errors);
113
+ const sourceRefs = sanitizeSourceRefs(input.source_refs, warnings);
114
+ const secretMatches = scanPublicCandidateSecrets(input);
115
+ if (id && !SAFE_ID.test(id)) {
116
+ errors.push("id must use lowercase registry-safe characters");
117
+ }
118
+ if (type && !SAFE_TYPE.test(type)) {
119
+ errors.push("type must use lowercase registry-safe characters");
120
+ }
121
+ if (!SAFE_LICENSE.test(license)) {
122
+ errors.push("license contains unsupported characters");
123
+ }
124
+ if (body && body.length > MAX_TEXT_LENGTH) {
125
+ errors.push(`body must be ${MAX_TEXT_LENGTH} characters or fewer`);
126
+ }
127
+ if (input.visibility && input.visibility !== "public") {
128
+ warnings.push("visibility was omitted from sanitized public candidate");
129
+ }
130
+ if (input.sensitivity && input.sensitivity !== "public") {
131
+ errors.push("public candidate sensitivity must be public");
132
+ }
133
+ if (Array.isArray(input.paths) && input.paths.length > 0) {
134
+ warnings.push("paths were omitted from sanitized public candidate");
135
+ }
136
+ if (secretMatches.length > 0) {
137
+ errors.push(`candidate contains sensitive content: ${secretMatches.join(", ")}`);
138
+ }
139
+ if (errors.length > 0 || !id || !title || !summary || !body || !type) {
140
+ return { ok: false, errors, warnings };
141
+ }
142
+ const candidateWithoutDigest = {
143
+ id,
144
+ title,
145
+ summary,
146
+ body,
147
+ type,
148
+ tags,
149
+ stack,
150
+ license,
151
+ ...(homepageUrl ? { homepage_url: homepageUrl } : {}),
152
+ ...(sourceUrl ? { source_url: sourceUrl } : {}),
153
+ trust_level: "community",
154
+ review_status: "needs_public_review",
155
+ reviewer_count: 0,
156
+ uses_30d: 0,
157
+ credit_count: contributorHandle || contributorOrg ? 1 : 0,
158
+ ...(contributorHandle ? { contributor_handle: contributorHandle } : {}),
159
+ ...(contributorOrg ? { contributor_org: contributorOrg } : {}),
160
+ ...(contributionUrl ? { contribution_url: contributionUrl } : {}),
161
+ source_refs: sourceRefs,
162
+ };
163
+ return {
164
+ ok: true,
165
+ value: {
166
+ ...candidateWithoutDigest,
167
+ content_sha256: sha256Hex(canonicalJson(candidateWithoutDigest)),
168
+ },
169
+ errors,
170
+ warnings,
171
+ };
172
+ }
173
+ function validatePublicCandidateBundle(input) {
174
+ const errors = [];
175
+ const warnings = [];
176
+ if (input.schema_version !== undefined && input.schema_version !== exports.PUBLIC_CANDIDATE_BUNDLE_SCHEMA_VERSION) {
177
+ errors.push("bundle schema_version must be 1 when provided");
178
+ }
179
+ if (!Array.isArray(input.candidates)) {
180
+ return { ok: false, errors: [...errors, "bundle candidates must be an array"], warnings };
181
+ }
182
+ const candidates = [];
183
+ const ids = new Set();
184
+ input.candidates.forEach((candidate, index) => {
185
+ if (!isRecord(candidate)) {
186
+ errors.push(`candidates[${index}] must be an object`);
187
+ return;
188
+ }
189
+ const sanitized = sanitizePublicCandidate(candidate);
190
+ errors.push(...sanitized.errors.map((error) => `candidates[${index}]: ${error}`));
191
+ warnings.push(...sanitized.warnings.map((warning) => `candidates[${index}]: ${warning}`));
192
+ if (sanitized.value) {
193
+ if (ids.has(sanitized.value.id)) {
194
+ errors.push(`candidates[${index}]: duplicate id ${sanitized.value.id}`);
195
+ }
196
+ ids.add(sanitized.value.id);
197
+ candidates.push(sanitized.value);
198
+ }
199
+ });
200
+ return {
201
+ ok: errors.length === 0,
202
+ value: errors.length === 0 ? { schema_version: exports.PUBLIC_CANDIDATE_BUNDLE_SCHEMA_VERSION, candidates } : undefined,
203
+ errors,
204
+ warnings,
205
+ };
206
+ }
207
+ function createPublicCandidateBundleManifest(input, manifest) {
208
+ const bundle = validatePublicCandidateBundle(input);
209
+ if (!bundle.value) {
210
+ return { ok: false, errors: bundle.errors, warnings: bundle.warnings };
211
+ }
212
+ return {
213
+ ok: true,
214
+ value: createSignedManifest({
215
+ ...manifest,
216
+ kind: "public_candidate_bundle",
217
+ payload: bundle.value,
218
+ }),
219
+ errors: [],
220
+ warnings: bundle.warnings,
221
+ };
222
+ }
223
+ function generateOrgRegistryManifest(input) {
224
+ const entries = new Map();
225
+ for (const item of input.bundles) {
226
+ if ("payload" in item) {
227
+ const verified = verifySignedManifest(item);
228
+ if (!verified.ok)
229
+ throw new Error(`Invalid signed bundle: ${verified.errors.join(", ")}`);
230
+ }
231
+ const bundle = "payload" in item ? item.payload : item;
232
+ const manifestSha256 = "payload" in item ? sha256Hex(canonicalJson(item)) : undefined;
233
+ for (const candidate of bundle.candidates) {
234
+ const existing = entries.get(candidate.id);
235
+ if (existing && existing.content_sha256 !== candidate.content_sha256) {
236
+ throw new Error(`Conflicting duplicate registry entry: ${candidate.id}`);
237
+ }
238
+ entries.set(candidate.id, {
239
+ id: candidate.id,
240
+ title: candidate.title,
241
+ summary: candidate.summary,
242
+ type: candidate.type,
243
+ tags: [...candidate.tags].sort(),
244
+ stack: [...candidate.stack].sort(),
245
+ license: candidate.license,
246
+ content_sha256: candidate.content_sha256,
247
+ ...(manifestSha256 ? { manifest_sha256: manifestSha256 } : {}),
248
+ ...(candidate.source_url ? { source_url: candidate.source_url } : {}),
249
+ ...(candidate.homepage_url ? { homepage_url: candidate.homepage_url } : {}),
250
+ trust_level: candidate.trust_level,
251
+ review_status: candidate.review_status,
252
+ reviewer_count: candidate.reviewer_count,
253
+ uses_30d: candidate.uses_30d,
254
+ credit_count: candidate.credit_count,
255
+ ...(candidate.contributor_handle ? { contributor_handle: candidate.contributor_handle } : {}),
256
+ ...(candidate.contributor_org ? { contributor_org: candidate.contributor_org } : {}),
257
+ ...(candidate.contribution_url ? { contribution_url: candidate.contribution_url } : {}),
258
+ });
259
+ }
260
+ }
261
+ const sortedEntries = [...entries.values()].sort((a, b) => a.id.localeCompare(b.id));
262
+ const byType = {};
263
+ for (const entry of sortedEntries)
264
+ byType[entry.type] = (byType[entry.type] ?? 0) + 1;
265
+ const payload = {
266
+ org: input.org,
267
+ registry_version: input.version,
268
+ metrics: {
269
+ entry_count: sortedEntries.length,
270
+ bundle_count: input.bundles.length,
271
+ by_type: Object.fromEntries(Object.entries(byType).sort(([a], [b]) => a.localeCompare(b))),
272
+ reviewed_count: sortedEntries.filter((entry) => entry.reviewer_count > 0).length,
273
+ community_count: sortedEntries.filter((entry) => entry.trust_level === "community").length,
274
+ },
275
+ entries: sortedEntries,
276
+ };
277
+ return createSignedManifest({
278
+ kind: "org_registry",
279
+ name: `${input.org} registry`,
280
+ version: input.version,
281
+ payload,
282
+ keyId: input.keyId,
283
+ generatedAt: input.generatedAt,
284
+ });
285
+ }
286
+ function sortJsonValue(value) {
287
+ if (Array.isArray(value)) {
288
+ return value.map(sortJsonValue);
289
+ }
290
+ if (isRecord(value)) {
291
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJsonValue(value[key])]));
292
+ }
293
+ return value;
294
+ }
295
+ function collectStrings(value) {
296
+ if (typeof value === "string") {
297
+ return [value];
298
+ }
299
+ if (Array.isArray(value)) {
300
+ return value.flatMap(collectStrings);
301
+ }
302
+ if (isRecord(value)) {
303
+ return Object.values(value).flatMap(collectStrings);
304
+ }
305
+ return [];
306
+ }
307
+ function stringField(value, field, errors) {
308
+ if (typeof value !== "string" || !value.trim()) {
309
+ errors.push(`${field} is required`);
310
+ return undefined;
311
+ }
312
+ return value.trim();
313
+ }
314
+ function stringArrayField(value, field, errors) {
315
+ if (value === undefined) {
316
+ return [];
317
+ }
318
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
319
+ errors.push(`${field} must be an array of strings`);
320
+ return [];
321
+ }
322
+ return [...new Set(value.map((item) => item.trim()).filter(Boolean))].sort();
323
+ }
324
+ function optionalUrlField(value, field, errors) {
325
+ if (value === undefined || value === null || value === "") {
326
+ return undefined;
327
+ }
328
+ if (typeof value !== "string" || !HTTP_URL.test(value)) {
329
+ errors.push(`${field} must be an http(s) URL`);
330
+ return undefined;
331
+ }
332
+ return value;
333
+ }
334
+ function sanitizeSourceRefs(value, warnings) {
335
+ if (!Array.isArray(value)) {
336
+ return [];
337
+ }
338
+ const refs = [];
339
+ for (const item of value) {
340
+ if (!isRecord(item)) {
341
+ warnings.push("non-object source_ref was omitted");
342
+ continue;
343
+ }
344
+ const sanitized = {};
345
+ for (const [key, raw] of Object.entries(item)) {
346
+ if (typeof raw !== "string") {
347
+ continue;
348
+ }
349
+ if (key === "path" || key === "file" || key === "repo_path") {
350
+ warnings.push(`source_ref ${key} was omitted`);
351
+ continue;
352
+ }
353
+ if (key.endsWith("_url") && !HTTP_URL.test(raw)) {
354
+ warnings.push(`source_ref ${key} was omitted because it is not an http(s) URL`);
355
+ continue;
356
+ }
357
+ sanitized[key] = raw;
358
+ }
359
+ if (Object.keys(sanitized).length > 0) {
360
+ refs.push(sanitized);
361
+ }
362
+ }
363
+ return refs;
364
+ }
365
+ function isRecord(value) {
366
+ return typeof value === "object" && value !== null && !Array.isArray(value);
367
+ }
368
+ function isNonEmptyString(value) {
369
+ return typeof value === "string" && value.trim().length > 0;
370
+ }
371
+ function isIsoDate(value) {
372
+ return typeof value === "string" && !Number.isNaN(Date.parse(value));
373
+ }
package/package.json CHANGED
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.0.0",
4
- "description": "MCP server for the kage community knowledge graph — search gotchas, patterns, and configs across auth, database, payments, and more",
3
+ "version": "1.1.0",
4
+ "description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
5
5
  "main": "dist/index.js",
6
+ "files": [
7
+ "dist/**/*.js",
8
+ "!dist/**/*.test.js",
9
+ "viewer/**/*",
10
+ "!viewer/graphs/*.json",
11
+ "README.md"
12
+ ],
6
13
  "bin": {
7
- "kage-graph-mcp": "dist/index.js"
14
+ "kage-graph-mcp": "dist/index.js",
15
+ "kage": "dist/cli.js"
8
16
  },
9
17
  "publishConfig": {
10
18
  "access": "public"
@@ -12,16 +20,17 @@
12
20
  "scripts": {
13
21
  "build": "tsc",
14
22
  "start": "node dist/index.js",
15
- "dev": "ts-node index.ts"
23
+ "dev": "ts-node index.ts",
24
+ "test": "npm run build && node --test dist/**/*.test.js"
16
25
  },
17
- "keywords": ["mcp", "kage", "knowledge-graph", "claude", "ai-agents"],
26
+ "keywords": ["mcp", "kage", "memory", "knowledge-graph", "codex", "claude", "ai-agents", "code-graph"],
18
27
  "license": "MIT",
19
28
  "dependencies": {
20
- "@modelcontextprotocol/sdk": "^1.10.2"
29
+ "@modelcontextprotocol/sdk": "^1.10.2",
30
+ "typescript": "^5.0.0"
21
31
  },
22
32
  "devDependencies": {
23
- "@types/node": "^22.0.0",
24
- "typescript": "^5.0.0"
33
+ "@types/node": "^22.0.0"
25
34
  },
26
35
  "engines": {
27
36
  "node": ">=18"