local-knowledge-graph 1.5.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/mcp/server.js ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // 本地知识图谱 MCP Server(stdio, JSON-RPC 2.0, 零外部依赖)
5
+ // 配置示例(Claude Desktop / opencode mcp):
6
+ // { "command": "node", "args": ["/absolute/path/to/local-knowledge-graph/mcp/server.js"],
7
+ // "env": { "KG_MCP_READONLY": "1" } }
8
+ // 设置 KG_MCP_READONLY=1 时禁用写入类工具。
9
+
10
+ const path = require('path');
11
+ const readline = require('readline');
12
+
13
+ const ROOT = path.join(__dirname, '..');
14
+ const db = require(path.join(ROOT, 'lib', 'db'));
15
+ const inference = require(path.join(ROOT, 'lib', 'inference'));
16
+ const embeddings = require(path.join(ROOT, 'lib', 'embeddings'));
17
+ const rdf = require(path.join(ROOT, 'lib', 'rdf'));
18
+ const git = require(path.join(ROOT, 'lib', 'git'));
19
+
20
+ const READONLY = process.env.KG_MCP_READONLY === '1';
21
+ db.open();
22
+
23
+ const TOOLS = [
24
+ {
25
+ name: 'kg_stats',
26
+ description: '获取知识图谱统计信息:实体数/关系数/日志数/数据库版本/推理关系数',
27
+ inputSchema: { type: 'object', properties: {} },
28
+ },
29
+ {
30
+ name: 'kg_list_entities',
31
+ description: '列出实体(可选按category过滤,limit/offset分页)。返回total便于翻页',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ category: { type: 'string', description: '按大类过滤:物理实体/抽象实体/数值实体/时间实体' },
36
+ limit: { type: 'number', description: '单页条数,默认100,最大500' },
37
+ offset: { type: 'number', description: '起始偏移,默认0' },
38
+ },
39
+ },
40
+ },
41
+ {
42
+ name: 'kg_get_entity',
43
+ description: '按id或名称获取单个实体详情(含其全部关系)',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: { id: { type: 'number' }, name: { type: 'string' } },
47
+ },
48
+ },
49
+ {
50
+ name: 'kg_get_graph',
51
+ description: '获取图谱(实体+关系)。默认按limit截断防止超大输出;通常优先用kg_ego/kg_search缩小范围',
52
+ inputSchema: {
53
+ type: 'object',
54
+ properties: {
55
+ entity_limit: { type: 'number', description: '实体上限,默认300,最大2000' },
56
+ relation_limit: { type: 'number', description: '关系上限,默认1000,最大5000' },
57
+ },
58
+ },
59
+ },
60
+ {
61
+ name: 'kg_ego',
62
+ description: '以某实体为中心取N层子图(双向BFS,最短跳数)。depth省略或0表示全部层级',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ center: { type: 'string', description: '中心实体id或名称' },
67
+ depth: { type: 'number', description: '层数,省略或0=全部' },
68
+ },
69
+ required: ['center'],
70
+ },
71
+ },
72
+ {
73
+ name: 'kg_search',
74
+ description: '混合检索实体(语义+关键词RRF融合)。需已配置embedding key,否则退化为关键词检索',
75
+ inputSchema: {
76
+ type: 'object',
77
+ properties: {
78
+ query: { type: 'string' },
79
+ top_k: { type: 'number', description: '返回条数,默认10,最大50' },
80
+ },
81
+ required: ['query'],
82
+ },
83
+ },
84
+ {
85
+ name: 'kg_cypher',
86
+ description: '迷你Cypher只读关系查询。语法:MATCH (a)-[r:类型]->(b) WHERE a.name contains 值 RETURN ... LIMIT n。WHERE支持 =/contains,类型可按大类或关系名,可用 | 分隔多值',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: { query: { type: 'string' } },
90
+ required: ['query'],
91
+ },
92
+ },
93
+ {
94
+ name: 'kg_inference',
95
+ description: 'OWL推理:按传递/对称/逆规则推导隐性关系(附推导依据)。可传center限定中心实体',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: { center: { type: 'string', description: '可选,实体id或名称' } },
99
+ },
100
+ },
101
+ {
102
+ name: 'kg_digest',
103
+ description: '图谱目录:实体大类、关系大类、高频关系名Top20(含数量)、样例实体、规模。回答关系类问题前先取此目录,可显著提升Cypher查询的准确性',
104
+ inputSchema: { type: 'object', properties: {} },
105
+ },
106
+ {
107
+ name: 'kg_export_rdf',
108
+ description: '导出全部图谱为RDF Turtle文本',
109
+ inputSchema: { type: 'object', properties: {} },
110
+ },
111
+ {
112
+ name: 'kg_apply_ops',
113
+ description: '写入图谱操作(kg-ops协议JSON数组):add_entity/add_relation/update_entity/update_relation/delete_entity/delete_relation。所有写入均记入operation_logs并自动git保存点',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: { ops: { type: 'array', items: { type: 'object' } } },
117
+ required: ['ops'],
118
+ },
119
+ },
120
+ ];
121
+
122
+ function resolveCenter(key) {
123
+ const ents = db.listEntities();
124
+ if (/^\d+$/.test(key)) {
125
+ const byId = ents.find((e) => e.id === Number(key));
126
+ if (byId) return { id: byId.id };
127
+ throw new Error(`实体id "${key}" 不存在`);
128
+ }
129
+ const hits = ents.filter((e) => e.name === key);
130
+ if (hits.length === 0) throw new Error(`实体 "${key}" 不存在`);
131
+ if (hits.length > 1) {
132
+ const e = new Error(`实体名 "${key}" 存在${hits.length}个候选,请改用id`);
133
+ e.candidates = hits.map((h) => ({ id: h.id, name: h.name, category: h.category }));
134
+ throw e;
135
+ }
136
+ return { id: hits[0].id };
137
+ }
138
+
139
+ const HANDLERS = {
140
+ kg_stats: async () => {
141
+ const c = db.counts();
142
+ const inferred = inference.computeInferred(db.getGraph()).length;
143
+ const st = embeddings.status();
144
+ return { ...c, version: db.getVersion(), inferred_relations: inferred, embeddings_indexed: st.indexed, readonly: READONLY };
145
+ },
146
+ kg_list_entities: async (args) => {
147
+ let ents = db.listEntities();
148
+ if (args.category) ents = ents.filter((e) => e.category === args.category);
149
+ const total = ents.length;
150
+ const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
151
+ const offset = Math.max(0, Number(args.offset) || 0);
152
+ return { total, count: Math.min(limit, total - offset), offset, entities: ents.slice(offset, offset + limit) };
153
+ },
154
+ kg_get_entity: async (args) => {
155
+ let e = null;
156
+ if (args.id != null) e = db.getEntity(Number(args.id));
157
+ else if (args.name) {
158
+ const hits = db.listEntities().filter((x) => x.name === args.name);
159
+ if (hits.length > 1) throw new Error(`实体名 "${args.name}" 存在${hits.length}个候选: ${hits.map((h) => h.id).join('/')}`);
160
+ e = hits[0] || null;
161
+ }
162
+ if (!e) throw new Error('实体不存在(请提供id或name)');
163
+ const rels = db.listRelations().filter((r) => r.source_id === e.id || r.target_id === e.id);
164
+ return { entity: e, relations: rels };
165
+ },
166
+ kg_get_graph: async (args) => {
167
+ const g = db.getGraph();
168
+ const eLimit = Math.max(1, Math.min(Number(args.entity_limit) || 300, 2000));
169
+ const rLimit = Math.max(1, Math.min(Number(args.relation_limit) || 1000, 5000));
170
+ return {
171
+ total_entities: g.entities.length,
172
+ total_relations: g.relations.length,
173
+ truncated: g.entities.length > eLimit || g.relations.length > rLimit,
174
+ entities: g.entities.slice(0, eLimit),
175
+ relations: g.relations.slice(0, rLimit),
176
+ };
177
+ },
178
+ kg_ego: async (args) => {
179
+ const { id } = resolveCenter(String(args.center));
180
+ let depth = null;
181
+ if (args.depth != null && Number(args.depth) > 0) depth = Number(args.depth);
182
+ return db.egoSubgraph(id, depth);
183
+ },
184
+ kg_search: async (args) => {
185
+ const r = await embeddings.search(String(args.query || ''), args.top_k);
186
+ return { mode: r.mode, results: r.results.map((x) => ({ ...x })) };
187
+ },
188
+ kg_cypher: async (args) => ({ rows: db.miniCypher(String(args.query || '')) }),
189
+ kg_inference: async (args) => {
190
+ const g = db.getGraph();
191
+ let result = inference.computeInferred(g);
192
+ if (args.center) {
193
+ const { id } = resolveCenter(String(args.center));
194
+ result = result.filter((i) => i.source_id === id || i.target_id === id);
195
+ }
196
+ const byId = new Map(g.entities.map((e) => [e.id, e]));
197
+ return { count: result.length, inferred: result.map((i) => ({ ...i, source: byId.get(i.source_id)?.name, target: byId.get(i.target_id)?.name })) };
198
+ },
199
+ kg_digest: async () => {
200
+ const { digest, stats } = require('./lib/ask').buildDigest();
201
+ return { digest, ...stats };
202
+ },
203
+ kg_export_rdf: async () => ({ turtle: rdf.exportTurtle(db.getGraph()) }),
204
+ kg_apply_ops: async (args) => {
205
+ if (READONLY) throw new Error('MCP运行于只读模式(KG_MCP_READONLY=1),写入被拒绝');
206
+ if (!Array.isArray(args.ops) || !args.ops.length) throw new Error('ops必须为非空JSON数组');
207
+ const applied = db.applyAgentOps(args.ops);
208
+ git.savepoint(`MCP写入: ${applied.length} 项操作`, 'MCP');
209
+ return { applied_count: applied.length, applied };
210
+ },
211
+ };
212
+
213
+ function write(msg) {
214
+ process.stdout.write(JSON.stringify(msg) + '\n');
215
+ }
216
+
217
+ function rpcError(id, code, message) {
218
+ write({ jsonrpc: '2.0', id, error: { code, message } });
219
+ }
220
+
221
+ async function handle(req) {
222
+ const { id, method, params } = req;
223
+ if (method === 'initialize') {
224
+ return write({
225
+ jsonrpc: '2.0', id,
226
+ result: {
227
+ protocolVersion: '2024-11-05',
228
+ capabilities: { tools: {} },
229
+ serverInfo: { name: 'local-knowledge-graph', version: '1.3.0' },
230
+ },
231
+ });
232
+ }
233
+ if (method === 'notifications/initialized' || method === 'notifications/cancelled') return;
234
+ if (method === 'ping') return write({ jsonrpc: '2.0', id, result: {} });
235
+ if (method === 'tools/list') {
236
+ return write({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
237
+ }
238
+ if (method === 'tools/call') {
239
+ const tool = TOOLS.find((t) => t.name === params?.name);
240
+ if (!tool) return rpcError(id, -32602, `未知工具: ${params?.name}`);
241
+ try {
242
+ const result = await HANDLERS[tool.name](params.arguments || {});
243
+ return write({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 1) }] } });
244
+ } catch (e) {
245
+ const extra = e.candidates ? ' 候选: ' + JSON.stringify(e.candidates) : '';
246
+ return write({ jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text: e.message + extra }] } });
247
+ }
248
+ }
249
+ if (id !== undefined) rpcError(id, -32601, `未知方法: ${method}`);
250
+ }
251
+
252
+ const rl = readline.createInterface({ input: process.stdin });
253
+ rl.on('line', (line) => {
254
+ const s = line.trim();
255
+ if (!s) return;
256
+ let req;
257
+ try { req = JSON.parse(s); } catch (_) { return; }
258
+ Promise.resolve(handle(req)).catch((e) => {
259
+ if (req && req.id !== undefined) rpcError(req.id, -32603, e.message);
260
+ });
261
+ });
262
+ rl.on('close', () => process.exit(0));
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "local-knowledge-graph",
3
+ "version": "1.5.0",
4
+ "description": "本地知识图谱整合器:3D可视化 + AI对话建图 + 智能检索 + Git回溯 + RDF合规,全流程本地运行",
5
+ "main": "server.js",
6
+ "bin": {
7
+ "kg": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "public/",
13
+ "tools/",
14
+ "mcp/",
15
+ ".opencode/",
16
+ "server.js",
17
+ "HELP.md",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22.5"
22
+ },
23
+ "scripts": {
24
+ "start": "node --no-warnings server.js",
25
+ "test": "node --test \"test/*.test.js\""
26
+ },
27
+ "dependencies": {
28
+ "express": "^4.22.3"
29
+ },
30
+ "keywords": [
31
+ "knowledge-graph",
32
+ "3d-visualization",
33
+ "rdf",
34
+ "local-first",
35
+ "opencode",
36
+ "sqlite"
37
+ ],
38
+ "author": "iamsamyiok",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/iamsamyiok/local-knowledge-graph.git"
43
+ }
44
+ }