local-knowledge-graph 1.6.1 → 1.8.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/core.js ADDED
@@ -0,0 +1,258 @@
1
+ 'use strict';
2
+
3
+ // MCP 工具核心层:工具定义 + 处理器 + JSON-RPC 分发
4
+ // 由 stdio(mcp/server.js)与 Streamable HTTP(mcp/http.js)两个传输层共享,保证行为一致。
5
+
6
+ const path = require('path');
7
+
8
+ const ROOT = path.join(__dirname, '..');
9
+ const db = require(path.join(ROOT, 'lib', 'db'));
10
+ const inference = require(path.join(ROOT, 'lib', 'inference'));
11
+ const embeddings = require(path.join(ROOT, 'lib', 'embeddings'));
12
+ const rdf = require(path.join(ROOT, 'lib', 'rdf'));
13
+ const git = require(path.join(ROOT, 'lib', 'git'));
14
+
15
+ const TOOLS = [
16
+ {
17
+ name: 'kg_stats',
18
+ description: '获取知识图谱统计信息:实体数/关系数/日志数/数据库版本/推理关系数',
19
+ inputSchema: { type: 'object', properties: {} },
20
+ },
21
+ {
22
+ name: 'kg_list_entities',
23
+ description: '列出实体(可选按category过滤,limit/offset分页)。返回total便于翻页',
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ category: { type: 'string', description: '按大类过滤:物理实体/抽象实体/数值实体/时间实体' },
28
+ limit: { type: 'number', description: '单页条数,默认100,最大500' },
29
+ offset: { type: 'number', description: '起始偏移,默认0' },
30
+ },
31
+ },
32
+ },
33
+ {
34
+ name: 'kg_get_entity',
35
+ description: '按id或名称获取单个实体详情(含其全部关系)',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: { id: { type: 'number' }, name: { type: 'string' } },
39
+ },
40
+ },
41
+ {
42
+ name: 'kg_get_graph',
43
+ description: '获取图谱(实体+关系)。默认按limit截断防止超大输出;通常优先用kg_ego/kg_search缩小范围',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ entity_limit: { type: 'number', description: '实体上限,默认300,最大2000' },
48
+ relation_limit: { type: 'number', description: '关系上限,默认1000,最大5000' },
49
+ },
50
+ },
51
+ },
52
+ {
53
+ name: 'kg_ego',
54
+ description: '以某实体为中心取N层子图(双向BFS,最短跳数)。depth省略或0表示全部层级',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ center: { type: 'string', description: '中心实体id或名称' },
59
+ depth: { type: 'number', description: '层数,省略或0=全部' },
60
+ },
61
+ required: ['center'],
62
+ },
63
+ },
64
+ {
65
+ name: 'kg_search',
66
+ description: '混合检索实体(语义+关键词RRF融合)。需已配置embedding key,否则退化为关键词检索',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ query: { type: 'string' },
71
+ top_k: { type: 'number', description: '返回条数,默认10,最大50' },
72
+ },
73
+ required: ['query'],
74
+ },
75
+ },
76
+ {
77
+ name: 'kg_cypher',
78
+ description: '迷你Cypher只读关系查询。语法:MATCH (a)-[r:类型]->(b) WHERE a.name contains 值 RETURN ... LIMIT n。WHERE支持 =/contains,类型可按大类或关系名,可用 | 分隔多值',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: { query: { type: 'string' } },
82
+ required: ['query'],
83
+ },
84
+ },
85
+ {
86
+ name: 'kg_inference',
87
+ description: 'OWL推理:按传递/对称/逆规则推导隐性关系(附推导依据)。可传center限定中心实体',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: { center: { type: 'string', description: '可选,实体id或名称' } },
91
+ },
92
+ },
93
+ {
94
+ name: 'kg_path',
95
+ description: '两实体关系路径枚举:返回最多5条按跳数升序的路径(每跳含关系名/类别/置信度),用于验证两个对象如何间接关联',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ from: { type: 'string', description: '起点实体id或名称' },
100
+ to: { type: 'string', description: '终点实体id或名称' },
101
+ max: { type: 'number', description: '可选,最大跳数2-6,默认4' },
102
+ },
103
+ required: ['from', 'to'],
104
+ },
105
+ },
106
+ {
107
+ name: 'kg_digest',
108
+ description: '图谱目录:实体大类、关系大类、高频关系名Top20(含数量)、样例实体、规模。回答关系类问题前先取此目录,可显著提升Cypher查询的准确性',
109
+ inputSchema: { type: 'object', properties: {} },
110
+ },
111
+ {
112
+ name: 'kg_export_rdf',
113
+ description: '导出全部图谱为RDF Turtle文本',
114
+ inputSchema: { type: 'object', properties: {} },
115
+ },
116
+ {
117
+ name: 'kg_apply_ops',
118
+ description: '写入图谱操作(kg-ops协议JSON数组):add_entity/add_relation/update_entity/update_relation/delete_entity/delete_relation。所有写入均记入operation_logs并自动git保存点',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: { ops: { type: 'array', items: { type: 'object' } } },
122
+ required: ['ops'],
123
+ },
124
+ },
125
+ ];
126
+
127
+ function createCore({ readonly = false, source = 'MCP' } = {}) {
128
+ function resolveCenter(key) {
129
+ const ents = db.listEntities();
130
+ if (/^\d+$/.test(key)) {
131
+ const byId = ents.find((e) => e.id === Number(key));
132
+ if (byId) return { id: byId.id };
133
+ throw new Error(`实体id "${key}" 不存在`);
134
+ }
135
+ const hits = ents.filter((e) => e.name === key);
136
+ if (hits.length === 0) throw new Error(`实体 "${key}" 不存在`);
137
+ if (hits.length > 1) {
138
+ const e = new Error(`实体名 "${key}" 存在${hits.length}个候选,请改用id`);
139
+ e.candidates = hits.map((h) => ({ id: h.id, name: h.name, category: h.category }));
140
+ throw e;
141
+ }
142
+ return { id: hits[0].id };
143
+ }
144
+
145
+ const HANDLERS = {
146
+ kg_stats: async () => {
147
+ const c = db.counts();
148
+ const inferred = inference.computeInferred(db.getGraph()).length;
149
+ const st = embeddings.status();
150
+ return { ...c, version: db.getVersion(), inferred_relations: inferred, embeddings_indexed: st.indexed, readonly };
151
+ },
152
+ kg_list_entities: async (args) => {
153
+ let ents = db.listEntities();
154
+ if (args.category) ents = ents.filter((e) => e.category === args.category);
155
+ const total = ents.length;
156
+ const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
157
+ const offset = Math.max(0, Number(args.offset) || 0);
158
+ return { total, count: Math.min(limit, total - offset), offset, entities: ents.slice(offset, offset + limit) };
159
+ },
160
+ kg_get_entity: async (args) => {
161
+ let e = null;
162
+ if (args.id != null) e = db.getEntity(Number(args.id));
163
+ else if (args.name) {
164
+ const hits = db.listEntities().filter((x) => x.name === args.name);
165
+ if (hits.length > 1) throw new Error(`实体名 "${args.name}" 存在${hits.length}个候选: ${hits.map((h) => h.id).join('/')}`);
166
+ e = hits[0] || null;
167
+ }
168
+ if (!e) throw new Error('实体不存在(请提供id或name)');
169
+ const rels = db.listRelations().filter((r) => r.source_id === e.id || r.target_id === e.id);
170
+ return { entity: e, relations: rels };
171
+ },
172
+ kg_get_graph: async (args) => {
173
+ const g = db.getGraph();
174
+ const eLimit = Math.max(1, Math.min(Number(args.entity_limit) || 300, 2000));
175
+ const rLimit = Math.max(1, Math.min(Number(args.relation_limit) || 1000, 5000));
176
+ return {
177
+ total_entities: g.entities.length,
178
+ total_relations: g.relations.length,
179
+ truncated: g.entities.length > eLimit || g.relations.length > rLimit,
180
+ entities: g.entities.slice(0, eLimit),
181
+ relations: g.relations.slice(0, rLimit),
182
+ };
183
+ },
184
+ kg_ego: async (args) => {
185
+ const { id } = resolveCenter(String(args.center));
186
+ let depth = null;
187
+ if (args.depth != null && Number(args.depth) > 0) depth = Number(args.depth);
188
+ return db.egoSubgraph(id, depth);
189
+ },
190
+ kg_search: async (args) => {
191
+ const r = await embeddings.search(String(args.query || ''), args.top_k);
192
+ return { mode: r.mode, results: r.results.map((x) => ({ ...x })) };
193
+ },
194
+ kg_cypher: async (args) => ({ rows: db.miniCypher(String(args.query || '')) }),
195
+ kg_inference: async (args) => {
196
+ const g = db.getGraph();
197
+ let result = inference.computeInferred(g);
198
+ if (args.center) {
199
+ const { id } = resolveCenter(String(args.center));
200
+ result = result.filter((i) => i.source_id === id || i.target_id === id);
201
+ }
202
+ const byId = new Map(g.entities.map((e) => [e.id, e]));
203
+ return { count: result.length, inferred: result.map((i) => ({ ...i, source: byId.get(i.source_id)?.name, target: byId.get(i.target_id)?.name })) };
204
+ },
205
+ kg_digest: async () => {
206
+ const { digest, stats } = require(path.join(ROOT, 'lib', 'ask')).buildDigest();
207
+ return { digest, ...stats };
208
+ },
209
+ kg_path: async (args) => {
210
+ const from = resolveCenter(String(args.from || ''));
211
+ const to = resolveCenter(String(args.to || ''));
212
+ return db.findPaths(from.id, to.id, { maxHops: Number.isInteger(args.max) ? args.max : 4 });
213
+ },
214
+ kg_export_rdf: async () => ({ turtle: rdf.exportTurtle(db.getGraph()) }),
215
+ kg_apply_ops: async (args) => {
216
+ if (readonly) throw new Error('MCP运行于只读模式,写入被拒绝(可在应用设置中关闭只读)');
217
+ if (!Array.isArray(args.ops) || !args.ops.length) throw new Error('ops必须为非空JSON数组');
218
+ const applied = db.applyAgentOps(args.ops);
219
+ try { git.savepoint(`${source}写入: ${applied.length} 项操作`, source); } catch (_) { /* 数据目录无git时忽略 */ }
220
+ return { applied_count: applied.length, applied };
221
+ },
222
+ };
223
+
224
+ return { TOOLS, HANDLERS };
225
+ }
226
+
227
+ // JSON-RPC 分发:返回响应对象;notification 与无需应答的方法返回 null
228
+ function handleRpc(core, req, serverVersion) {
229
+ const { id, method, params } = req || {};
230
+ if (method === 'initialize') {
231
+ return {
232
+ jsonrpc: '2.0', id,
233
+ result: {
234
+ protocolVersion: '2024-11-05',
235
+ capabilities: { tools: {} },
236
+ serverInfo: { name: 'local-knowledge-graph', version: serverVersion },
237
+ },
238
+ };
239
+ }
240
+ if (method === 'notifications/initialized' || method === 'notifications/cancelled') return null;
241
+ if (method === 'ping') return { jsonrpc: '2.0', id, result: {} };
242
+ if (method === 'tools/list') return { jsonrpc: '2.0', id, result: { tools: core.TOOLS } };
243
+ if (method === 'tools/call') {
244
+ const tool = core.TOOLS.find((t) => t.name === params?.name);
245
+ if (!tool) return { jsonrpc: '2.0', id, error: { code: -32602, message: `未知工具: ${params?.name}` } };
246
+ return core.HANDLERS[tool.name](params.arguments || {}).then(
247
+ (result) => ({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 1) }] } }),
248
+ (e) => {
249
+ const extra = e.candidates ? ' 候选: ' + JSON.stringify(e.candidates) : '';
250
+ return { jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text: e.message + extra }] } };
251
+ }
252
+ );
253
+ }
254
+ if (id !== undefined) return { jsonrpc: '2.0', id, error: { code: -32601, message: `未知方法: ${method}` } };
255
+ return null;
256
+ }
257
+
258
+ module.exports = { TOOLS, createCore, handleRpc };
package/mcp/http.js ADDED
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ // MCP Streamable HTTP 传输层(Express 中间件)
4
+ // - 无状态:不签发 Session-Id,每请求独立 JSON 响应
5
+ // - 鉴权:Authorization: Bearer <token> 或 ?token=<token>
6
+ // - CORS:支持浏览器侧客户端(预检放行,无论开关状态)
7
+ // - 支持 JSON-RPC 单条与数组批量;notification 无响应条目
8
+
9
+ const { createCore, handleRpc } = require('./core');
10
+
11
+ const CORS_HEADERS = {
12
+ 'Access-Control-Allow-Origin': '*',
13
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
14
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, X-Requested-With',
15
+ 'Access-Control-Expose-Headers': 'Mcp-Session-Id',
16
+ 'Access-Control-Max-Age': '86400',
17
+ };
18
+
19
+ function tokenOk(provided, expected) {
20
+ if (!expected || !provided) return false;
21
+ const a = Buffer.from(String(provided));
22
+ const b = Buffer.from(String(expected));
23
+ return a.length === b.length && require('crypto').timingSafeEqual(a, b);
24
+ }
25
+
26
+ function extractToken(req) {
27
+ const h = req.headers['authorization'] || '';
28
+ if (/^Bearer\s+/i.test(h)) return h.replace(/^Bearer\s+/i, '').trim();
29
+ if (req.query && req.query.token) return String(req.query.token);
30
+ return '';
31
+ }
32
+
33
+ function createMcpHandler({ isEnabled, getToken, isReadonly, serverVersion }) {
34
+ // 每请求按当前只读状态构建核心(settings 可被随时切换)
35
+ return async function mcpHandler(req, res) {
36
+ const cors = { ...CORS_HEADERS };
37
+ for (const [k, v] of Object.entries(cors)) res.setHeader(k, v);
38
+
39
+ if (req.method === 'OPTIONS') return res.status(204).end();
40
+
41
+ if (!isEnabled()) return res.status(404).json({ jsonrpc: '2.0', error: { code: -32000, message: 'MCP 服务未启用(请在应用设置中开启)' } });
42
+
43
+ if (req.method === 'GET') {
44
+ res.setHeader('Allow', 'POST, OPTIONS');
45
+ return res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: '本端点仅接受 POST(Streamable HTTP, JSON 响应)' } });
46
+ }
47
+ if (req.method !== 'POST') {
48
+ res.setHeader('Allow', 'POST, OPTIONS');
49
+ return res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: '方法不允许' } });
50
+ }
51
+
52
+ if (!tokenOk(extractToken(req), getToken())) {
53
+ return res.status(401).json({ jsonrpc: '2.0', error: { code: -32001, message: '访问令牌缺失或不正确(请在应用 MCP 设置中获取)' } });
54
+ }
55
+
56
+ let body = req.body;
57
+ if (typeof body === 'string') {
58
+ try { body = JSON.parse(body); } catch (_) { return res.status(400).json({ jsonrpc: '2.0', id: null, error: { code: -32700, message: '请求体不是合法JSON' } }); }
59
+ }
60
+ if (!body || (Array.isArray(body) && !body.length)) {
61
+ return res.status(400).json({ jsonrpc: '2.0', id: null, error: { code: -32600, message: '无效请求' } });
62
+ }
63
+
64
+ const core = createCore({ readonly: !!isReadonly(), source: 'MCP' });
65
+ const items = Array.isArray(body) ? body : [body];
66
+ const responses = [];
67
+ for (const item of items) {
68
+ try {
69
+ const r = await Promise.resolve(handleRpc(core, item, serverVersion));
70
+ if (r) responses.push(r);
71
+ } catch (e) {
72
+ if (item && item.id !== undefined) responses.push({ jsonrpc: '2.0', id: item.id, error: { code: -32603, message: e.message } });
73
+ }
74
+ }
75
+
76
+ if (Array.isArray(body)) return res.json(responses);
77
+ if (!responses.length) return res.status(202).end(); // 纯 notification
78
+ return res.json(responses[0]);
79
+ };
80
+ }
81
+
82
+ module.exports = { createMcpHandler, tokenOk, extractToken };
package/mcp/server.js CHANGED
@@ -6,209 +6,23 @@
6
6
  // { "command": "node", "args": ["/absolute/path/to/local-knowledge-graph/mcp/server.js"],
7
7
  // "env": { "KG_MCP_READONLY": "1" } }
8
8
  // 设置 KG_MCP_READONLY=1 时禁用写入类工具。
9
+ // 工具定义与分发逻辑在 mcp/core.js(与 HTTP 端点共享)。
9
10
 
10
11
  const path = require('path');
11
12
  const readline = require('readline');
12
13
 
13
14
  const ROOT = path.join(__dirname, '..');
14
15
  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'));
16
+ const { createCore, handleRpc } = require(path.join(__dirname, 'core'));
19
17
 
20
18
  const READONLY = process.env.KG_MCP_READONLY === '1';
21
19
  db.open();
20
+ const core = createCore({ readonly: READONLY, source: 'MCP' });
22
21
 
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
- };
22
+ const SERVER_VERSION = (() => {
23
+ try { return require(path.join(ROOT, 'package.json')).version || '1.0.0'; }
24
+ catch (_) { return '1.0.0'; }
25
+ })();
212
26
 
213
27
  function write(msg) {
214
28
  process.stdout.write(JSON.stringify(msg) + '\n');
@@ -219,34 +33,13 @@ function rpcError(id, code, message) {
219
33
  }
220
34
 
221
35
  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
- }
36
+ const { id, method } = req || {};
37
+ try {
38
+ const resp = await Promise.resolve(handleRpc(core, req, SERVER_VERSION));
39
+ if (resp) write(resp);
40
+ } catch (e) {
41
+ if (id !== undefined) rpcError(id, -32603, e.message);
248
42
  }
249
- if (id !== undefined) rpcError(id, -32601, `未知方法: ${method}`);
250
43
  }
251
44
 
252
45
  const rl = readline.createInterface({ input: process.stdin });
@@ -255,8 +48,6 @@ rl.on('line', (line) => {
255
48
  if (!s) return;
256
49
  let req;
257
50
  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
- });
51
+ handle(req);
261
52
  });
262
53
  rl.on('close', () => process.exit(0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-knowledge-graph",
3
- "version": "1.6.1",
3
+ "version": "1.8.0",
4
4
  "description": "本地知识图谱整合器:3D可视化 + AI对话建图 + 智能检索 + Git回溯 + RDF合规,全流程本地运行",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -25,7 +25,8 @@
25
25
  "test": "node --test \"test/*.test.js\""
26
26
  },
27
27
  "dependencies": {
28
- "express": "^4.22.3"
28
+ "express": "^4.22.3",
29
+ "pdf-parse": "^2.4.5"
29
30
  },
30
31
  "keywords": [
31
32
  "knowledge-graph",