@morningljn/mnemo 0.1.2
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/LICENSE +21 -0
- package/README.md +156 -0
- package/README_zh.md +156 -0
- package/banner.png +0 -0
- package/dist/init.d.ts +10 -0
- package/dist/init.js +138 -0
- package/dist/init.js.map +1 -0
- package/dist/retriever.d.ts +70 -0
- package/dist/retriever.js +689 -0
- package/dist/retriever.js.map +1 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +62 -0
- package/dist/schema.js.map +1 -0
- package/dist/security.d.ts +15 -0
- package/dist/security.js +116 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +150 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +122 -0
- package/dist/store.js +696 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
- package/src/init.ts +157 -0
- package/src/retriever.ts +806 -0
- package/src/schema.ts +61 -0
- package/src/security.ts +132 -0
- package/src/server.ts +172 -0
- package/src/store.ts +805 -0
- package/src/types.ts +81 -0
- package/tests/retriever.test.ts +55 -0
- package/tests/security.test.ts +30 -0
- package/tests/store.test.ts +104 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +10 -0
package/src/schema.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const SCHEMA = `
|
|
2
|
+
-- 事实表
|
|
3
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
4
|
+
fact_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5
|
+
content TEXT NOT NULL UNIQUE,
|
|
6
|
+
category TEXT DEFAULT 'general',
|
|
7
|
+
tags TEXT DEFAULT '',
|
|
8
|
+
keywords TEXT DEFAULT '[]',
|
|
9
|
+
trust_score REAL DEFAULT 0.5,
|
|
10
|
+
retrieval_count INTEGER DEFAULT 0,
|
|
11
|
+
helpful_count INTEGER DEFAULT 0,
|
|
12
|
+
created_at TEXT DEFAULT (datetime('now', 'localtime')),
|
|
13
|
+
updated_at TEXT DEFAULT (datetime('now', 'localtime'))
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- 实体表
|
|
17
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
18
|
+
entity_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
19
|
+
name TEXT NOT NULL,
|
|
20
|
+
entity_type TEXT DEFAULT 'unknown',
|
|
21
|
+
aliases TEXT DEFAULT '',
|
|
22
|
+
created_at TEXT DEFAULT (datetime('now', 'localtime'))
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- 事实-实体关联表
|
|
26
|
+
CREATE TABLE IF NOT EXISTS fact_entities (
|
|
27
|
+
fact_id INTEGER NOT NULL REFERENCES facts(fact_id) ON DELETE CASCADE,
|
|
28
|
+
entity_id INTEGER NOT NULL REFERENCES entities(entity_id) ON DELETE CASCADE,
|
|
29
|
+
PRIMARY KEY (fact_id, entity_id)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- 索引
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_facts_trust ON facts(trust_score DESC);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_facts_category ON facts(category);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_fact_entities_entity ON fact_entities(entity_id);
|
|
37
|
+
|
|
38
|
+
-- FTS5 全文索引
|
|
39
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts
|
|
40
|
+
USING fts5(content, tags, content=facts, content_rowid=fact_id);
|
|
41
|
+
|
|
42
|
+
-- FTS5 同步触发器:插入
|
|
43
|
+
CREATE TRIGGER IF NOT EXISTS facts_ai AFTER INSERT ON facts BEGIN
|
|
44
|
+
INSERT INTO facts_fts(rowid, content, tags)
|
|
45
|
+
VALUES (new.fact_id, new.content, new.tags);
|
|
46
|
+
END;
|
|
47
|
+
|
|
48
|
+
-- FTS5 同步触发器:删除
|
|
49
|
+
CREATE TRIGGER IF NOT EXISTS facts_ad AFTER DELETE ON facts BEGIN
|
|
50
|
+
INSERT INTO facts_fts(facts_fts, rowid, content, tags)
|
|
51
|
+
VALUES ('delete', old.fact_id, old.content, old.tags);
|
|
52
|
+
END;
|
|
53
|
+
|
|
54
|
+
-- FTS5 同步触发器:更新
|
|
55
|
+
CREATE TRIGGER IF NOT EXISTS facts_au AFTER UPDATE ON facts BEGIN
|
|
56
|
+
INSERT INTO facts_fts(facts_fts, rowid, content, tags)
|
|
57
|
+
VALUES ('delete', old.fact_id, old.content, old.tags);
|
|
58
|
+
INSERT INTO facts_fts(rowid, content, tags)
|
|
59
|
+
VALUES (new.fact_id, new.content, new.tags);
|
|
60
|
+
END;
|
|
61
|
+
`
|
package/src/security.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆安全扫描器。
|
|
3
|
+
* 移植自 Hermes memory_tool.py 的 _scan_memory_content 并扩展。
|
|
4
|
+
*
|
|
5
|
+
* 三重扫描:提示注入检测、PII 检测、不可见 Unicode 检测。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SecurityScanResult } from './types.js'
|
|
9
|
+
|
|
10
|
+
// -- 提示注入检测 ---
|
|
11
|
+
|
|
12
|
+
/** 检测伪造围栏标签 */
|
|
13
|
+
const MEMORY_CONTEXT_INJECTION_RE = /<\s*\/?\s*memory-context\s*>/i
|
|
14
|
+
/** 检测 system-reminder 伪装 */
|
|
15
|
+
const SYSTEM_REMINDER_INJECTION_RE = /<\s*system-reminder\s*>/i
|
|
16
|
+
|
|
17
|
+
/** 提示注入模式 */
|
|
18
|
+
const PROMPT_INJECTION_PATTERNS = [
|
|
19
|
+
/ignore\s+(?:all\s+)?(?:previous|above|prior)\s+(?:instructions|context)/i,
|
|
20
|
+
/forget\s+(?:all\s+)?(?:previous|above|prior)\s+(?:instructions|context)/i,
|
|
21
|
+
/you\s+are\s+now\s+(?:a|an|the)\s+/i,
|
|
22
|
+
/do\s+not\s+tell\s+the\s+user/i,
|
|
23
|
+
/(?:system|admin|root)\s+(?:prompt|instruction|message)/i,
|
|
24
|
+
/disregard\s+(?:your|all|any)\s+(?:instructions|rules|guidelines)/i,
|
|
25
|
+
/act\s+as\s+(?:if|though)\s+you\s+(?:have\s+no|don'?t\s+have)\s+(?:restrictions|limits|rules)/i,
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
// -- 数据外泄检测 ---
|
|
29
|
+
|
|
30
|
+
const EXFIL_PATTERNS = [
|
|
31
|
+
/curl\s+[^\n]*\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i,
|
|
32
|
+
/wget\s+[^\n]*\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i,
|
|
33
|
+
/(?:cat|type|read)\s+[^\n]*(?:\.env|credentials|\.netrc|\.pgpass|\.npmrc)/i,
|
|
34
|
+
/authorized_keys/i,
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
// -- PII 检测 ---
|
|
38
|
+
|
|
39
|
+
const EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g
|
|
40
|
+
const API_KEY_PATTERNS = [
|
|
41
|
+
/(?:sk|pk|api[_-]?key|token|secret)[_-][a-zA-Z0-9]{20,}/gi,
|
|
42
|
+
/ghp_[a-zA-Z0-9]{36}/g,
|
|
43
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
44
|
+
/AIza[0-9A-Za-z_-]{35}/g,
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
// -- 不可见 Unicode ---
|
|
48
|
+
|
|
49
|
+
const INVISIBLE_UNICODE_RANGES: Array<[number, number, string]> = [
|
|
50
|
+
[0x200B, 0x200F, '零宽字符'],
|
|
51
|
+
[0x2028, 0x202E, '控制字符'],
|
|
52
|
+
[0x2060, 0x206F, '不可见格式字符'],
|
|
53
|
+
[0xFEFF, 0xFEFF, 'BOM'],
|
|
54
|
+
[0xFFF9, 0xFFFB, '注解字符'],
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
// -- 导出函数 ---
|
|
58
|
+
|
|
59
|
+
/** 扫描注入尝试(硬拦截级别) */
|
|
60
|
+
export function scanForInjection(text: string): SecurityScanResult {
|
|
61
|
+
const injectionAttempts: string[] = []
|
|
62
|
+
|
|
63
|
+
if (MEMORY_CONTEXT_INJECTION_RE.test(text)) {
|
|
64
|
+
injectionAttempts.push('检测到 memory-context 标签注入')
|
|
65
|
+
}
|
|
66
|
+
if (SYSTEM_REMINDER_INJECTION_RE.test(text)) {
|
|
67
|
+
injectionAttempts.push('检测到 system-reminder 标签注入')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const pattern of PROMPT_INJECTION_PATTERNS) {
|
|
71
|
+
if (pattern.test(text)) {
|
|
72
|
+
injectionAttempts.push(`匹配提示注入模式: ${pattern.source}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const pattern of EXFIL_PATTERNS) {
|
|
77
|
+
if (pattern.test(text)) {
|
|
78
|
+
injectionAttempts.push(`匹配数据外泄模式`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
safe: injectionAttempts.length === 0,
|
|
84
|
+
warnings: [],
|
|
85
|
+
hasPii: false,
|
|
86
|
+
injectionAttempts,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 扫描 PII(警告级别,不阻止存储) */
|
|
91
|
+
export function scanForPii(text: string): SecurityScanResult {
|
|
92
|
+
const warnings: string[] = []
|
|
93
|
+
let hasPii = false
|
|
94
|
+
|
|
95
|
+
// 重置 lastIndex(因为使用了 g flag)
|
|
96
|
+
EMAIL_RE.lastIndex = 0
|
|
97
|
+
if (EMAIL_RE.test(text)) { warnings.push('包含邮箱地址'); hasPii = true }
|
|
98
|
+
|
|
99
|
+
for (const p of API_KEY_PATTERNS) {
|
|
100
|
+
p.lastIndex = 0
|
|
101
|
+
if (p.test(text)) { warnings.push('包含 API 密钥模式'); hasPii = true; break }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { safe: true, warnings, hasPii, injectionAttempts: [] }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 扫描不可见 Unicode(拦截级别) */
|
|
108
|
+
export function scanForInvisibleUnicode(text: string): SecurityScanResult {
|
|
109
|
+
const warnings: string[] = []
|
|
110
|
+
for (const [lo, hi, name] of INVISIBLE_UNICODE_RANGES) {
|
|
111
|
+
for (let cp = lo; cp <= hi; cp++) {
|
|
112
|
+
if (text.includes(String.fromCodePoint(cp))) {
|
|
113
|
+
warnings.push(`包含不可见 Unicode: ${name} (U+${cp.toString(16).toUpperCase()})`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { safe: warnings.length === 0, warnings, hasPii: false, injectionAttempts: [] }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 综合安全扫描 */
|
|
121
|
+
export function fullSecurityScan(text: string): SecurityScanResult {
|
|
122
|
+
const injection = scanForInjection(text)
|
|
123
|
+
const pii = scanForPii(text)
|
|
124
|
+
const unicode = scanForInvisibleUnicode(text)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
safe: injection.safe && unicode.safe,
|
|
128
|
+
warnings: [...injection.warnings, ...pii.warnings, ...unicode.warnings],
|
|
129
|
+
hasPii: pii.hasPii,
|
|
130
|
+
injectionAttempts: injection.injectionAttempts,
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { z } from 'zod/v4'
|
|
8
|
+
import { MemoryStore } from './store.js'
|
|
9
|
+
import { FactRetriever } from './retriever.js'
|
|
10
|
+
import { fullSecurityScan } from './security.js'
|
|
11
|
+
import type { FactStoreArgs, FactFeedbackArgs, FactCategory } from './types.js'
|
|
12
|
+
|
|
13
|
+
const FACT_STORE_DESCRIPTION = `结构化事实记忆系统(SQLite+FTS5 索引)。支持读写。
|
|
14
|
+
|
|
15
|
+
操作:
|
|
16
|
+
- search — 关键词查找
|
|
17
|
+
- probe — 实体探测:关于某人/某事的所有事实
|
|
18
|
+
- related — 实体关联
|
|
19
|
+
- reason — 组合推理:同时关联多个实体的事实
|
|
20
|
+
- contradict — 矛盾检测
|
|
21
|
+
- list — 浏览事实
|
|
22
|
+
- add — 添加新事实(自动去重,相似则更新)
|
|
23
|
+
- update — 更新已有事实
|
|
24
|
+
- remove — 删除事实
|
|
25
|
+
|
|
26
|
+
写入时先 search 检查是否已存在相似事实。identity/coding_style/tool_pref/workflow/general → 全局库,project → 项目库。`
|
|
27
|
+
|
|
28
|
+
const factStoreSchema = {
|
|
29
|
+
action: z.enum(['add', 'search', 'probe', 'related', 'reason', 'contradict', 'update', 'remove', 'list']),
|
|
30
|
+
content: z.string().optional().describe("事实内容('add' 必需)"),
|
|
31
|
+
query: z.string().optional().describe("搜索查询('search' 必需)"),
|
|
32
|
+
entity: z.string().optional().describe("实体名('probe'/'related' 使用)"),
|
|
33
|
+
entities: z.array(z.string()).optional().describe("实体列表('reason' 使用)"),
|
|
34
|
+
fact_id: z.number().optional().describe("事实 ID('update'/'remove' 使用)"),
|
|
35
|
+
category: z.enum(['identity', 'coding_style', 'tool_pref', 'workflow', 'general']).optional(),
|
|
36
|
+
tags: z.string().optional().describe('逗号分隔标签'),
|
|
37
|
+
trust_delta: z.number().optional().describe("'update' 的信任调整值"),
|
|
38
|
+
min_trust: z.number().optional().describe('最低信任过滤(默认 0.3)'),
|
|
39
|
+
limit: z.number().optional().describe('最大结果数(默认 10)'),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const factFeedbackSchema = {
|
|
43
|
+
action: z.enum(['helpful', 'unhelpful']),
|
|
44
|
+
fact_id: z.number().describe('要评分的事实 ID'),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveCategory(category?: string): FactCategory {
|
|
48
|
+
if (!category) return 'general'
|
|
49
|
+
const valid: FactCategory[] = ['identity', 'coding_style', 'tool_pref', 'workflow', 'general']
|
|
50
|
+
return valid.includes(category as FactCategory) ? (category as FactCategory) : 'general'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const minTrust = 0.3
|
|
54
|
+
|
|
55
|
+
// -- Initialize store + retriever --
|
|
56
|
+
const dbPath = join(homedir(), '.mnemo', 'facts.db')
|
|
57
|
+
const store = new MemoryStore(dbPath)
|
|
58
|
+
const retriever = new FactRetriever(store, { temporalDecayHalfLife: 30 })
|
|
59
|
+
|
|
60
|
+
// Startup maintenance
|
|
61
|
+
store.decayTrustScores()
|
|
62
|
+
store.auditContradictions()
|
|
63
|
+
|
|
64
|
+
// -- MCP Server --
|
|
65
|
+
const server = new McpServer({ name: 'mnemo-mcp', version: '0.1.0' })
|
|
66
|
+
|
|
67
|
+
server.tool(
|
|
68
|
+
'fact_store',
|
|
69
|
+
FACT_STORE_DESCRIPTION,
|
|
70
|
+
factStoreSchema,
|
|
71
|
+
async (args) => {
|
|
72
|
+
try {
|
|
73
|
+
const a = args as unknown as FactStoreArgs
|
|
74
|
+
const category = resolveCategory(a.category)
|
|
75
|
+
|
|
76
|
+
switch (a.action) {
|
|
77
|
+
case 'add': {
|
|
78
|
+
if (!a.content) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: content' }) }] }
|
|
79
|
+
const similar = store.findSimilarFact(a.content, category) ?? store.findSimilarFact(a.content)
|
|
80
|
+
let warnings: string[] | undefined
|
|
81
|
+
const scan = fullSecurityScan(a.content)
|
|
82
|
+
if (scan.warnings.length > 0 || scan.hasPii) warnings = [...scan.warnings]
|
|
83
|
+
|
|
84
|
+
if (similar) {
|
|
85
|
+
store.updateFact(similar.factId, { content: a.content, tags: a.tags, trustDelta: 0.05 })
|
|
86
|
+
const demoted = store.demoteContradictingFacts(similar.factId, a.content, category)
|
|
87
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ fact_id: similar.factId, status: 'updated', reason: 'similar_fact_merged', ...(demoted > 0 ? { contradicted_demoted: demoted } : {}), ...(warnings ? { warnings } : {}) }) }] }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const factId = store.addFact(a.content, category, a.tags ?? '')
|
|
91
|
+
const demoted = store.demoteContradictingFacts(factId, a.content, category)
|
|
92
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ fact_id: factId, status: 'added', category, ...(demoted > 0 ? { contradicted_demoted: demoted } : {}), ...(warnings ? { warnings } : {}) }) }] }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'search': {
|
|
96
|
+
if (!a.query) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: query' }) }] }
|
|
97
|
+
const results = retriever.search(a.query, { category: a.category ? category : undefined, minTrust: a.min_trust ?? minTrust, limit: a.limit ?? 10 })
|
|
98
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results, count: results.length }) }] }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'probe': {
|
|
102
|
+
if (!a.entity) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: entity' }) }] }
|
|
103
|
+
const results = retriever.probe(a.entity, { minTrust: a.min_trust ?? minTrust, limit: a.limit ?? 10 })
|
|
104
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results, count: results.length }) }] }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'related': {
|
|
108
|
+
if (!a.entity) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: entity' }) }] }
|
|
109
|
+
const results = retriever.related(a.entity, { minTrust: a.min_trust ?? minTrust, limit: a.limit ?? 10 })
|
|
110
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results, count: results.length }) }] }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'reason': {
|
|
114
|
+
const entities = a.entities ?? []
|
|
115
|
+
if (entities.length === 0) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: "reason requires 'entities' list" }) }] }
|
|
116
|
+
const results = retriever.reason(entities, { minTrust: a.min_trust ?? minTrust, limit: a.limit ?? 10 })
|
|
117
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results, count: results.length }) }] }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'contradict': {
|
|
121
|
+
const results = retriever.contradict({ threshold: 0.3, limit: a.limit ?? 10 })
|
|
122
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ results, count: results.length }) }] }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case 'update': {
|
|
126
|
+
if (!a.fact_id) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: fact_id' }) }] }
|
|
127
|
+
const updated = store.updateFact(a.fact_id, { content: a.content, tags: a.tags, category, trustDelta: a.trust_delta })
|
|
128
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ updated }) }] }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'remove': {
|
|
132
|
+
if (!a.fact_id) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: fact_id' }) }] }
|
|
133
|
+
const removed = store.removeFact(a.fact_id)
|
|
134
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ removed }) }] }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'list': {
|
|
138
|
+
const facts = store.listFacts(category, a.min_trust ?? 0.0, a.limit ?? 10)
|
|
139
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ facts, count: facts.length }) }] }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Unknown action: ${(a as { action: string }).action}` }) }] }
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(err) }) }] }
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
server.tool(
|
|
152
|
+
'fact_feedback',
|
|
153
|
+
'使用事实后评分。标记 helpful 如果准确,unhelpful 如果过时。训练记忆系统 — 好事实上升,坏事实下降。',
|
|
154
|
+
factFeedbackSchema,
|
|
155
|
+
async (args) => {
|
|
156
|
+
try {
|
|
157
|
+
const a = args as unknown as FactFeedbackArgs
|
|
158
|
+
if (!a.fact_id) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: fact_id' }) }] }
|
|
159
|
+
const result = store.recordFeedback(a.fact_id, a.action === 'helpful')
|
|
160
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: String(err) }) }] }
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
// -- Start --
|
|
168
|
+
const transport = new StdioServerTransport()
|
|
169
|
+
server.connect(transport).catch(err => {
|
|
170
|
+
console.error('mnemo-mcp failed to start:', err)
|
|
171
|
+
process.exit(1)
|
|
172
|
+
})
|