@kage-core/kage-graph-mcp 1.0.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/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const BASE_URL = "https://raw.githubusercontent.com/kage-core/kage-graph/master";
8
+ async function fetchText(url) {
9
+ const res = await fetch(url);
10
+ if (!res.ok)
11
+ throw new Error(`HTTP ${res.status}: ${url}`);
12
+ return res.text();
13
+ }
14
+ async function fetchJSON(url) {
15
+ const text = await fetchText(url);
16
+ return JSON.parse(text);
17
+ }
18
+ function scoreMatch(query, node) {
19
+ const terms = query.toLowerCase().split(/\s+/);
20
+ let score = 0;
21
+ const title = node.title.toLowerCase();
22
+ const summary = (node.summary || "").toLowerCase();
23
+ const tags = node.tags.map((t) => t.toLowerCase());
24
+ for (const term of terms) {
25
+ if (title.includes(term))
26
+ score += 3;
27
+ if (tags.some((t) => t.includes(term)))
28
+ score += 2;
29
+ if (summary.includes(term))
30
+ score += 1;
31
+ }
32
+ return score;
33
+ }
34
+ function scoreDomainMatch(query, domain) {
35
+ const terms = query.toLowerCase().split(/\s+/);
36
+ return terms.reduce((sum, term) => {
37
+ return sum + domain.top_tags.filter((t) => t.includes(term)).length;
38
+ }, 0);
39
+ }
40
+ const server = new index_js_1.Server({ name: "kage-graph", version: "1.0.0" }, { capabilities: { tools: {} } });
41
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
42
+ tools: [
43
+ {
44
+ name: "kage_search",
45
+ description: "Search the kage community knowledge graph for gotchas, patterns, configs, and architectural decisions across auth, database, payments, deployment, frontend, testing, and more. Returns node summaries ranked by relevance.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ query: {
50
+ type: "string",
51
+ description: "What you are looking for, e.g. 'prisma serverless connection', 'stripe webhook signature', 'JWT refresh token'",
52
+ },
53
+ domain: {
54
+ type: "string",
55
+ description: "Optional: restrict search to a specific domain. One of: auth, database, payments, deployment, frontend, testing, api-design, email, storage, ai-agents, security, performance, observability, infrastructure, mobile, tooling, data",
56
+ },
57
+ },
58
+ required: ["query"],
59
+ },
60
+ },
61
+ {
62
+ name: "kage_fetch",
63
+ description: "Fetch the full content of a specific node from the kage knowledge graph. Use after kage_search to get the complete fix, pattern, or decision.",
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: {
67
+ domain: {
68
+ type: "string",
69
+ description: "The domain the node belongs to, e.g. 'database', 'auth', 'payments'",
70
+ },
71
+ node_id: {
72
+ type: "string",
73
+ description: "The node slug, e.g. 'prisma-serverless-connection-exhaustion'",
74
+ },
75
+ },
76
+ required: ["domain", "node_id"],
77
+ },
78
+ },
79
+ {
80
+ name: "kage_list_domains",
81
+ description: "List all domains in the kage knowledge graph with their node counts and top tags. Use to orient before searching.",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {},
85
+ },
86
+ },
87
+ ],
88
+ }));
89
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
90
+ const { name, arguments: args } = request.params;
91
+ if (name === "kage_list_domains") {
92
+ const catalog = await fetchJSON(`${BASE_URL}/catalog.json`);
93
+ const lines = Object.entries(catalog.domains)
94
+ .filter(([, d]) => d.node_count > 0)
95
+ .sort(([, a], [, b]) => b.node_count - a.node_count)
96
+ .map(([domain, d]) => `**${domain}** — ${d.node_count} nodes | tags: ${d.top_tags.slice(0, 5).join(", ")}`);
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: `# kage-graph Domains\n\n${lines.join("\n")}`,
102
+ },
103
+ ],
104
+ };
105
+ }
106
+ if (name === "kage_search") {
107
+ const query = String(args?.query ?? "");
108
+ const domainFilter = args?.domain ? String(args.domain) : null;
109
+ const catalog = await fetchJSON(`${BASE_URL}/catalog.json`);
110
+ // Pick domains to search
111
+ let domainsToSearch;
112
+ if (domainFilter) {
113
+ domainsToSearch = [domainFilter];
114
+ }
115
+ else {
116
+ domainsToSearch = Object.entries(catalog.domains)
117
+ .filter(([, d]) => d.node_count > 0)
118
+ .map(([name, d]) => ({ name, score: scoreDomainMatch(query, d) }))
119
+ .sort((a, b) => b.score - a.score)
120
+ .slice(0, 3)
121
+ .filter((d) => d.score > 0)
122
+ .map((d) => d.name);
123
+ // Fall back to all non-empty domains if no tag match
124
+ if (domainsToSearch.length === 0) {
125
+ domainsToSearch = Object.entries(catalog.domains)
126
+ .filter(([, d]) => d.node_count > 0)
127
+ .map(([name]) => name);
128
+ }
129
+ }
130
+ // Fetch indexes in parallel
131
+ const indexResults = await Promise.allSettled(domainsToSearch.map(async (domain) => {
132
+ const index = await fetchJSON(`${BASE_URL}/domains/${domain}/index.json`);
133
+ return { domain, nodes: index.nodes };
134
+ }));
135
+ // Score and rank all nodes
136
+ const scored = [];
137
+ for (const result of indexResults) {
138
+ if (result.status === "fulfilled") {
139
+ const { domain, nodes } = result.value;
140
+ for (const node of nodes) {
141
+ const s = scoreMatch(query, node);
142
+ if (s > 0)
143
+ scored.push({ domain, node, score: s });
144
+ }
145
+ }
146
+ }
147
+ scored.sort((a, b) => b.score - a.score || b.node.score - a.node.score);
148
+ const top = scored.slice(0, 5);
149
+ if (top.length === 0) {
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: `No nodes found matching "${query}". Try kage_list_domains to see what's available.`,
155
+ },
156
+ ],
157
+ };
158
+ }
159
+ const lines = top.map((r, i) => {
160
+ const n = r.node;
161
+ return [
162
+ `### [${i + 1}] ${n.title}`,
163
+ `**Domain:** ${r.domain} | **Type:** ${n.type} | **Score:** ${n.score} | **Updated:** ${n.updated}`,
164
+ `**Tags:** ${n.tags.join(", ")}`,
165
+ n.summary ? `**Summary:** ${n.summary}` : "",
166
+ `**Fetch:** domain="${r.domain}" node_id="${n.id}"`,
167
+ ]
168
+ .filter(Boolean)
169
+ .join("\n");
170
+ });
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: `# kage-graph results for "${query}"\n\n${lines.join("\n\n---\n\n")}`,
176
+ },
177
+ ],
178
+ };
179
+ }
180
+ if (name === "kage_fetch") {
181
+ const domain = String(args?.domain ?? "");
182
+ const nodeId = String(args?.node_id ?? "");
183
+ const content = await fetchText(`${BASE_URL}/domains/${domain}/nodes/${nodeId}.md`);
184
+ return {
185
+ content: [{ type: "text", text: content }],
186
+ };
187
+ }
188
+ throw new Error(`Unknown tool: ${name}`);
189
+ });
190
+ async function main() {
191
+ const transport = new stdio_js_1.StdioServerTransport();
192
+ await server.connect(transport);
193
+ }
194
+ main().catch(console.error);
package/index.ts ADDED
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+
9
+ const BASE_URL = "https://raw.githubusercontent.com/kage-core/kage-graph/master";
10
+
11
+ async function fetchText(url: string): Promise<string> {
12
+ const res = await fetch(url);
13
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
14
+ return res.text();
15
+ }
16
+
17
+ async function fetchJSON<T>(url: string): Promise<T> {
18
+ const text = await fetchText(url);
19
+ return JSON.parse(text) as T;
20
+ }
21
+
22
+ interface CatalogDomain {
23
+ node_count: number;
24
+ top_tags: string[];
25
+ hot_nodes: string[];
26
+ }
27
+
28
+ interface Catalog {
29
+ domains: Record<string, CatalogDomain>;
30
+ }
31
+
32
+ interface IndexNode {
33
+ id: string;
34
+ title: string;
35
+ type: string;
36
+ tags: string[];
37
+ summary: string;
38
+ score: number;
39
+ updated: string;
40
+ }
41
+
42
+ interface DomainIndex {
43
+ nodes: IndexNode[];
44
+ }
45
+
46
+ function scoreMatch(query: string, node: IndexNode): number {
47
+ const terms = query.toLowerCase().split(/\s+/);
48
+ let score = 0;
49
+ const title = node.title.toLowerCase();
50
+ const summary = (node.summary || "").toLowerCase();
51
+ const tags = node.tags.map((t) => t.toLowerCase());
52
+
53
+ for (const term of terms) {
54
+ if (title.includes(term)) score += 3;
55
+ if (tags.some((t) => t.includes(term))) score += 2;
56
+ if (summary.includes(term)) score += 1;
57
+ }
58
+ return score;
59
+ }
60
+
61
+ function scoreDomainMatch(query: string, domain: CatalogDomain): number {
62
+ const terms = query.toLowerCase().split(/\s+/);
63
+ return terms.reduce((sum, term) => {
64
+ return sum + domain.top_tags.filter((t) => t.includes(term)).length;
65
+ }, 0);
66
+ }
67
+
68
+ const server = new Server(
69
+ { name: "kage-graph", version: "1.0.0" },
70
+ { capabilities: { tools: {} } }
71
+ );
72
+
73
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
74
+ tools: [
75
+ {
76
+ name: "kage_search",
77
+ description:
78
+ "Search the kage community knowledge graph for gotchas, patterns, configs, and architectural decisions across auth, database, payments, deployment, frontend, testing, and more. Returns node summaries ranked by relevance.",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ query: {
83
+ type: "string",
84
+ description: "What you are looking for, e.g. 'prisma serverless connection', 'stripe webhook signature', 'JWT refresh token'",
85
+ },
86
+ domain: {
87
+ type: "string",
88
+ description:
89
+ "Optional: restrict search to a specific domain. One of: auth, database, payments, deployment, frontend, testing, api-design, email, storage, ai-agents, security, performance, observability, infrastructure, mobile, tooling, data",
90
+ },
91
+ },
92
+ required: ["query"],
93
+ },
94
+ },
95
+ {
96
+ name: "kage_fetch",
97
+ description:
98
+ "Fetch the full content of a specific node from the kage knowledge graph. Use after kage_search to get the complete fix, pattern, or decision.",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: {
102
+ domain: {
103
+ type: "string",
104
+ description: "The domain the node belongs to, e.g. 'database', 'auth', 'payments'",
105
+ },
106
+ node_id: {
107
+ type: "string",
108
+ description: "The node slug, e.g. 'prisma-serverless-connection-exhaustion'",
109
+ },
110
+ },
111
+ required: ["domain", "node_id"],
112
+ },
113
+ },
114
+ {
115
+ name: "kage_list_domains",
116
+ description:
117
+ "List all domains in the kage knowledge graph with their node counts and top tags. Use to orient before searching.",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {},
121
+ },
122
+ },
123
+ ],
124
+ }));
125
+
126
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
127
+ const { name, arguments: args } = request.params;
128
+
129
+ if (name === "kage_list_domains") {
130
+ const catalog = await fetchJSON<Catalog>(`${BASE_URL}/catalog.json`);
131
+ const lines = Object.entries(catalog.domains)
132
+ .filter(([, d]) => d.node_count > 0)
133
+ .sort(([, a], [, b]) => b.node_count - a.node_count)
134
+ .map(
135
+ ([domain, d]) =>
136
+ `**${domain}** — ${d.node_count} nodes | tags: ${d.top_tags.slice(0, 5).join(", ")}`
137
+ );
138
+
139
+ return {
140
+ content: [
141
+ {
142
+ type: "text",
143
+ text: `# kage-graph Domains\n\n${lines.join("\n")}`,
144
+ },
145
+ ],
146
+ };
147
+ }
148
+
149
+ if (name === "kage_search") {
150
+ const query = String(args?.query ?? "");
151
+ const domainFilter = args?.domain ? String(args.domain) : null;
152
+
153
+ const catalog = await fetchJSON<Catalog>(`${BASE_URL}/catalog.json`);
154
+
155
+ // Pick domains to search
156
+ let domainsToSearch: string[];
157
+ if (domainFilter) {
158
+ domainsToSearch = [domainFilter];
159
+ } else {
160
+ domainsToSearch = Object.entries(catalog.domains)
161
+ .filter(([, d]) => d.node_count > 0)
162
+ .map(([name, d]) => ({ name, score: scoreDomainMatch(query, d) }))
163
+ .sort((a, b) => b.score - a.score)
164
+ .slice(0, 3)
165
+ .filter((d) => d.score > 0)
166
+ .map((d) => d.name);
167
+
168
+ // Fall back to all non-empty domains if no tag match
169
+ if (domainsToSearch.length === 0) {
170
+ domainsToSearch = Object.entries(catalog.domains)
171
+ .filter(([, d]) => d.node_count > 0)
172
+ .map(([name]) => name);
173
+ }
174
+ }
175
+
176
+ // Fetch indexes in parallel
177
+ const indexResults = await Promise.allSettled(
178
+ domainsToSearch.map(async (domain) => {
179
+ const index = await fetchJSON<DomainIndex>(
180
+ `${BASE_URL}/domains/${domain}/index.json`
181
+ );
182
+ return { domain, nodes: index.nodes };
183
+ })
184
+ );
185
+
186
+ // Score and rank all nodes
187
+ const scored: Array<{ domain: string; node: IndexNode; score: number }> = [];
188
+ for (const result of indexResults) {
189
+ if (result.status === "fulfilled") {
190
+ const { domain, nodes } = result.value;
191
+ for (const node of nodes) {
192
+ const s = scoreMatch(query, node);
193
+ if (s > 0) scored.push({ domain, node, score: s });
194
+ }
195
+ }
196
+ }
197
+
198
+ scored.sort((a, b) => b.score - a.score || b.node.score - a.node.score);
199
+ const top = scored.slice(0, 5);
200
+
201
+ if (top.length === 0) {
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: `No nodes found matching "${query}". Try kage_list_domains to see what's available.`,
207
+ },
208
+ ],
209
+ };
210
+ }
211
+
212
+ const lines = top.map((r, i) => {
213
+ const n = r.node;
214
+ return [
215
+ `### [${i + 1}] ${n.title}`,
216
+ `**Domain:** ${r.domain} | **Type:** ${n.type} | **Score:** ${n.score} | **Updated:** ${n.updated}`,
217
+ `**Tags:** ${n.tags.join(", ")}`,
218
+ n.summary ? `**Summary:** ${n.summary}` : "",
219
+ `**Fetch:** domain="${r.domain}" node_id="${n.id}"`,
220
+ ]
221
+ .filter(Boolean)
222
+ .join("\n");
223
+ });
224
+
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: `# kage-graph results for "${query}"\n\n${lines.join("\n\n---\n\n")}`,
230
+ },
231
+ ],
232
+ };
233
+ }
234
+
235
+ if (name === "kage_fetch") {
236
+ const domain = String(args?.domain ?? "");
237
+ const nodeId = String(args?.node_id ?? "");
238
+ const content = await fetchText(
239
+ `${BASE_URL}/domains/${domain}/nodes/${nodeId}.md`
240
+ );
241
+ return {
242
+ content: [{ type: "text", text: content }],
243
+ };
244
+ }
245
+
246
+ throw new Error(`Unknown tool: ${name}`);
247
+ });
248
+
249
+ async function main() {
250
+ const transport = new StdioServerTransport();
251
+ await server.connect(transport);
252
+ }
253
+
254
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
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",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "kage-graph-mcp": "dist/index.js"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/index.js",
15
+ "dev": "ts-node index.ts"
16
+ },
17
+ "keywords": ["mcp", "kage", "knowledge-graph", "claude", "ai-agents"],
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.10.2"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.0.0",
24
+ "typescript": "^5.0.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ }
29
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["*.ts"],
13
+ "exclude": ["node_modules", "dist"]
14
+ }