r2mcp 0.2.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/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/dist/breadcrumbs.d.ts +123 -0
- package/dist/breadcrumbs.js +135 -0
- package/dist/cli/classify-edges.d.ts +2 -0
- package/dist/cli/classify-edges.js +130 -0
- package/dist/cli/compile-wiki.d.ts +2 -0
- package/dist/cli/compile-wiki.js +173 -0
- package/dist/cli/dump-edges-json.d.ts +2 -0
- package/dist/cli/dump-edges-json.js +21 -0
- package/dist/cli/extract-entities.d.ts +17 -0
- package/dist/cli/extract-entities.js +166 -0
- package/dist/cli/lint-memory.d.ts +16 -0
- package/dist/cli/lint-memory.js +94 -0
- package/dist/cli/migrate.d.ts +17 -0
- package/dist/cli/migrate.js +146 -0
- package/dist/cli/setup-helpers.d.ts +7 -0
- package/dist/cli/setup-helpers.js +72 -0
- package/dist/cli/setup.d.ts +15 -0
- package/dist/cli/setup.js +95 -0
- package/dist/compiler/clustering.d.ts +29 -0
- package/dist/compiler/clustering.js +66 -0
- package/dist/compiler/frontmatter.d.ts +35 -0
- package/dist/compiler/frontmatter.js +168 -0
- package/dist/compiler/manifest.d.ts +32 -0
- package/dist/compiler/manifest.js +82 -0
- package/dist/compiler/prompts.d.ts +17 -0
- package/dist/compiler/prompts.js +82 -0
- package/dist/compiler/run.d.ts +52 -0
- package/dist/compiler/run.js +186 -0
- package/dist/compiler/tier.d.ts +10 -0
- package/dist/compiler/tier.js +85 -0
- package/dist/compiler/topic.d.ts +16 -0
- package/dist/compiler/topic.js +105 -0
- package/dist/compiler/types.d.ts +101 -0
- package/dist/compiler/types.js +4 -0
- package/dist/db.d.ts +10 -0
- package/dist/db.js +46 -0
- package/dist/edges/candidate-pairs.d.ts +24 -0
- package/dist/edges/candidate-pairs.js +35 -0
- package/dist/edges/classifier.d.ts +45 -0
- package/dist/edges/classifier.js +172 -0
- package/dist/edges/signals.d.ts +13 -0
- package/dist/edges/signals.js +45 -0
- package/dist/edges/stage1-haiku.d.ts +21 -0
- package/dist/edges/stage1-haiku.js +33 -0
- package/dist/edges/stage2-opus.d.ts +41 -0
- package/dist/edges/stage2-opus.js +101 -0
- package/dist/edges/state.d.ts +44 -0
- package/dist/edges/state.js +79 -0
- package/dist/edges/types.d.ts +20 -0
- package/dist/edges/types.js +1 -0
- package/dist/embeddings.d.ts +13 -0
- package/dist/embeddings.js +54 -0
- package/dist/entities/db.d.ts +49 -0
- package/dist/entities/db.js +109 -0
- package/dist/entities/extractor.d.ts +14 -0
- package/dist/entities/extractor.js +154 -0
- package/dist/entities/normalize.d.ts +5 -0
- package/dist/entities/normalize.js +7 -0
- package/dist/entities/prompt.d.ts +19 -0
- package/dist/entities/prompt.js +100 -0
- package/dist/entities/state.d.ts +44 -0
- package/dist/entities/state.js +99 -0
- package/dist/entities/types.d.ts +62 -0
- package/dist/entities/types.js +6 -0
- package/dist/env.d.ts +13 -0
- package/dist/env.js +32 -0
- package/dist/fingerprint.d.ts +2 -0
- package/dist/fingerprint.js +12 -0
- package/dist/graph-rebuild.d.ts +6 -0
- package/dist/graph-rebuild.js +20 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +403 -0
- package/dist/instrumentation.d.ts +10 -0
- package/dist/instrumentation.js +37 -0
- package/dist/lint/checks/contradictions.d.ts +30 -0
- package/dist/lint/checks/contradictions.js +52 -0
- package/dist/lint/checks/drift.d.ts +5 -0
- package/dist/lint/checks/drift.js +34 -0
- package/dist/lint/checks/orphans.d.ts +5 -0
- package/dist/lint/checks/orphans.js +25 -0
- package/dist/lint/checks/stale.d.ts +6 -0
- package/dist/lint/checks/stale.js +29 -0
- package/dist/lint/checks/superseded-unflagged.d.ts +5 -0
- package/dist/lint/checks/superseded-unflagged.js +47 -0
- package/dist/lint/run.d.ts +11 -0
- package/dist/lint/run.js +95 -0
- package/dist/lint/types.d.ts +60 -0
- package/dist/lint/types.js +13 -0
- package/dist/mcp-response.d.ts +7 -0
- package/dist/mcp-response.js +13 -0
- package/dist/providers/anthropic.d.ts +13 -0
- package/dist/providers/anthropic.js +56 -0
- package/dist/providers/claude-code.d.ts +35 -0
- package/dist/providers/claude-code.js +175 -0
- package/dist/providers/errors.d.ts +12 -0
- package/dist/providers/errors.js +19 -0
- package/dist/providers/index.d.ts +30 -0
- package/dist/providers/index.js +71 -0
- package/dist/providers/openrouter.d.ts +19 -0
- package/dist/providers/openrouter.js +76 -0
- package/dist/providers/semaphore.d.ts +19 -0
- package/dist/providers/semaphore.js +51 -0
- package/dist/providers/types.d.ts +27 -0
- package/dist/providers/types.js +7 -0
- package/dist/schema.sql +116 -0
- package/dist/server-instructions.d.ts +9 -0
- package/dist/server-instructions.js +20 -0
- package/dist/telemetry.d.ts +39 -0
- package/dist/telemetry.js +130 -0
- package/dist/tools/classify.d.ts +44 -0
- package/dist/tools/classify.js +121 -0
- package/dist/tools/compile.d.ts +31 -0
- package/dist/tools/compile.js +132 -0
- package/dist/tools/dump-edges-sidecar.d.ts +37 -0
- package/dist/tools/dump-edges-sidecar.js +80 -0
- package/dist/tools/extract-entities.d.ts +53 -0
- package/dist/tools/extract-entities.js +169 -0
- package/dist/tools/lint.d.ts +10 -0
- package/dist/tools/lint.js +13 -0
- package/dist/tools/meditate.d.ts +25 -0
- package/dist/tools/meditate.js +128 -0
- package/dist/tools/recall.d.ts +66 -0
- package/dist/tools/recall.js +409 -0
- package/dist/tools/reject.d.ts +10 -0
- package/dist/tools/reject.js +24 -0
- package/dist/tools/remember.d.ts +26 -0
- package/dist/tools/remember.js +140 -0
- package/dist/tools/search.d.ts +30 -0
- package/dist/tools/search.js +69 -0
- package/dist/tools/spawn-cli.d.ts +14 -0
- package/dist/tools/spawn-cli.js +41 -0
- package/dist/tools/stats.d.ts +31 -0
- package/dist/tools/stats.js +88 -0
- package/package.json +86 -0
- package/skills/remember/SKILL.md +357 -0
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared .env loader (claw-8cjf.1).
|
|
3
|
+
*
|
|
4
|
+
* MCP servers and launchd-spawned subprocesses don't inherit shell env, so
|
|
5
|
+
* the server entrypoint and every CLI driver load .env themselves. This is
|
|
6
|
+
* the single implementation — the previous five hand-rolled copies all used
|
|
7
|
+
* /^([A-Z_]+)=/, which cannot match any R2MCP_* key (digit '2').
|
|
8
|
+
*
|
|
9
|
+
* Semantics: never clobbers vars already set in process.env, trims values,
|
|
10
|
+
* strips one pair of matching surrounding quotes, ignores comments / blank
|
|
11
|
+
* lines / empty values, and no-ops when the file is absent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadEnvFile(envPath: string): void;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Shared .env loader (claw-8cjf.1).
|
|
4
|
+
*
|
|
5
|
+
* MCP servers and launchd-spawned subprocesses don't inherit shell env, so
|
|
6
|
+
* the server entrypoint and every CLI driver load .env themselves. This is
|
|
7
|
+
* the single implementation — the previous five hand-rolled copies all used
|
|
8
|
+
* /^([A-Z_]+)=/, which cannot match any R2MCP_* key (digit '2').
|
|
9
|
+
*
|
|
10
|
+
* Semantics: never clobbers vars already set in process.env, trims values,
|
|
11
|
+
* strips one pair of matching surrounding quotes, ignores comments / blank
|
|
12
|
+
* lines / empty values, and no-ops when the file is absent.
|
|
13
|
+
*/
|
|
14
|
+
export function loadEnvFile(envPath) {
|
|
15
|
+
if (!existsSync(envPath))
|
|
16
|
+
return;
|
|
17
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
18
|
+
for (const line of content.split('\n')) {
|
|
19
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.+)$/);
|
|
20
|
+
if (!match || process.env[match[1]] !== undefined)
|
|
21
|
+
continue;
|
|
22
|
+
let value = match[2].trim();
|
|
23
|
+
if (value.length >= 2 &&
|
|
24
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
25
|
+
(value.startsWith("'") && value.endsWith("'")))) {
|
|
26
|
+
value = value.slice(1, -1);
|
|
27
|
+
}
|
|
28
|
+
if (value === '')
|
|
29
|
+
continue;
|
|
30
|
+
process.env[match[1]] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function normalizeContent(content) {
|
|
3
|
+
return content
|
|
4
|
+
.replace(/<!--.*?-->/gs, '')
|
|
5
|
+
.replace(/\[see also:[^\]]*\]/g, '')
|
|
6
|
+
.replace(/\s+/g, ' ')
|
|
7
|
+
.trim();
|
|
8
|
+
}
|
|
9
|
+
export function fingerprint(content) {
|
|
10
|
+
const normalized = normalizeContent(content);
|
|
11
|
+
return createHash('sha256').update(normalized).digest('hex');
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optionally trigger a graph rebuild script if it exists in the project root.
|
|
3
|
+
* This is a no-op for most r2mcp installations — the script is a ClaudeClaw
|
|
4
|
+
* extension. r2mcp bundles the Memory Explorer separately.
|
|
5
|
+
*/
|
|
6
|
+
export declare function triggerGraphRebuild(projectRoot: string): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
const GRAPH_SCRIPT = 'scripts/build-memory-graph.js';
|
|
5
|
+
/**
|
|
6
|
+
* Optionally trigger a graph rebuild script if it exists in the project root.
|
|
7
|
+
* This is a no-op for most r2mcp installations — the script is a ClaudeClaw
|
|
8
|
+
* extension. r2mcp bundles the Memory Explorer separately.
|
|
9
|
+
*/
|
|
10
|
+
export function triggerGraphRebuild(projectRoot) {
|
|
11
|
+
const scriptPath = resolve(projectRoot, GRAPH_SCRIPT);
|
|
12
|
+
if (!existsSync(scriptPath)) {
|
|
13
|
+
return; // No graph script — skip silently
|
|
14
|
+
}
|
|
15
|
+
execFile('node', [scriptPath], { cwd: projectRoot, timeout: 30000 }, (err) => {
|
|
16
|
+
if (err) {
|
|
17
|
+
console.error(`Graph rebuild failed (non-blocking): ${err.message}`);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// OTel instrumentation MUST be imported first — before any other module
|
|
3
|
+
import './instrumentation.js';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { initDb } from './db.js';
|
|
9
|
+
import { remember } from './tools/remember.js';
|
|
10
|
+
import { recall } from './tools/recall.js';
|
|
11
|
+
import { search } from './tools/search.js';
|
|
12
|
+
import { stats } from './tools/stats.js';
|
|
13
|
+
import { reject } from './tools/reject.js';
|
|
14
|
+
import { meditate } from './tools/meditate.js';
|
|
15
|
+
import { compile } from './tools/compile.js';
|
|
16
|
+
import { classify } from './tools/classify.js';
|
|
17
|
+
import { extractEntitiesTool } from './tools/extract-entities.js';
|
|
18
|
+
import { dumpEdgesSidecarTool } from './tools/dump-edges-sidecar.js';
|
|
19
|
+
import { lint } from './tools/lint.js';
|
|
20
|
+
import { loadEnvFile } from './env.js';
|
|
21
|
+
import { SERVER_INSTRUCTIONS } from './server-instructions.js';
|
|
22
|
+
import { EMBEDDINGS_DISABLED_WARNING } from './embeddings.js';
|
|
23
|
+
import { withToolSpan } from './telemetry.js';
|
|
24
|
+
import { asMcpResponse } from './mcp-response.js';
|
|
25
|
+
// Re-export for backwards compatibility with anything that imported asMcpResponse
|
|
26
|
+
// from src/index.js before SPEC-047's mcp-response.ts split. New callers should
|
|
27
|
+
// import directly from './mcp-response.js'.
|
|
28
|
+
export { asMcpResponse };
|
|
29
|
+
// Load .env from project root — MCP servers don't inherit parent env vars
|
|
30
|
+
const PROJECT_ROOT = process.env.PROJECT_ROOT || process.cwd();
|
|
31
|
+
loadEnvFile(resolve(PROJECT_ROOT, '.env'));
|
|
32
|
+
const server = new McpServer({
|
|
33
|
+
name: 'r2mcp',
|
|
34
|
+
version: '0.2.0',
|
|
35
|
+
},
|
|
36
|
+
// Sent in the initialize response; Claude Code loads this into the agent's
|
|
37
|
+
// context at session start (claw-8cjf.8 — first-session guidance).
|
|
38
|
+
{ instructions: SERVER_INSTRUCTIONS });
|
|
39
|
+
server.tool('remember', 'Store, update, or archive a memory in the long-term memory system. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
40
|
+
operation: z.enum(['ADD', 'UPDATE', 'ARCHIVE', 'REJECTION', 'NOOP']),
|
|
41
|
+
tier: z.enum(['preferences', 'project-context', 'conversations']),
|
|
42
|
+
content: z.string(),
|
|
43
|
+
metadata: z.object({
|
|
44
|
+
type: z.enum([
|
|
45
|
+
'preference',
|
|
46
|
+
'decision',
|
|
47
|
+
'context',
|
|
48
|
+
'relationship',
|
|
49
|
+
'observation',
|
|
50
|
+
'rejection',
|
|
51
|
+
]),
|
|
52
|
+
topics: z.array(z.string()).optional(),
|
|
53
|
+
people: z.array(z.string()).optional(),
|
|
54
|
+
section: z.string().optional(),
|
|
55
|
+
date: z.string().optional(),
|
|
56
|
+
}),
|
|
57
|
+
target_id: z.string().optional(),
|
|
58
|
+
}, async (args) => {
|
|
59
|
+
const result = await withToolSpan('remember', {
|
|
60
|
+
operation: args.operation,
|
|
61
|
+
tier: args.tier,
|
|
62
|
+
}, async (span) => {
|
|
63
|
+
const r = await remember({
|
|
64
|
+
operation: args.operation,
|
|
65
|
+
tier: args.tier,
|
|
66
|
+
content: args.content,
|
|
67
|
+
metadata: args.metadata,
|
|
68
|
+
target_id: args.target_id,
|
|
69
|
+
}, PROJECT_ROOT);
|
|
70
|
+
span.setAttribute('dedup_triggered', r.dedup ?? false);
|
|
71
|
+
return r;
|
|
72
|
+
});
|
|
73
|
+
return asMcpResponse('remember', result, args);
|
|
74
|
+
});
|
|
75
|
+
server.tool('recall', 'Search memory using semantic similarity and full-text search. Supports progressive tier search, MMR diversity, relevance floor, and token-budget-based retrieval. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
76
|
+
query: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.default('')
|
|
80
|
+
.describe('Free-text query. Optional when `entity` is provided — SPEC-046 entity-only recall short-circuits without a query.'),
|
|
81
|
+
top_k: z.number().optional().default(10),
|
|
82
|
+
tier: z.enum(['preferences', 'project-context', 'conversations']).optional(),
|
|
83
|
+
max_tokens: z
|
|
84
|
+
.number()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('Token budget — return results until budget is exhausted'),
|
|
87
|
+
min_score: z
|
|
88
|
+
.number()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Minimum relevance score threshold (default: 0.0). Suggested: 0.3 for semantic, 0.1 for fulltext.'),
|
|
91
|
+
diversity: z
|
|
92
|
+
.number()
|
|
93
|
+
.min(0)
|
|
94
|
+
.max(1)
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('MMR lambda: 1.0 = pure relevance, 0.0 = pure diversity (default: 0.7)'),
|
|
97
|
+
progressive: z
|
|
98
|
+
.boolean()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('Search tiers top-down, stopping early when high-confidence results found (default: true)'),
|
|
101
|
+
confidence_threshold: z
|
|
102
|
+
.number()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe('Raw score threshold for progressive early-stop (default: 0.82)'),
|
|
105
|
+
entity: z
|
|
106
|
+
.string()
|
|
107
|
+
.optional()
|
|
108
|
+
.describe('SPEC-046: filter results to memories linked to this entity (canonical_name or alias; case-insensitive). When set, response adds entity_resolved/entity_id and per-result entity_links.'),
|
|
109
|
+
}, async (args) => {
|
|
110
|
+
const queryStr = args.query ?? '';
|
|
111
|
+
const result = await withToolSpan('recall', {
|
|
112
|
+
query_length: queryStr.length,
|
|
113
|
+
tier: args.tier || 'all',
|
|
114
|
+
top_k: args.top_k,
|
|
115
|
+
progressive: args.progressive ?? true,
|
|
116
|
+
entity: args.entity ?? '',
|
|
117
|
+
}, async (span) => {
|
|
118
|
+
const r = await recall({
|
|
119
|
+
query: queryStr,
|
|
120
|
+
top_k: args.top_k,
|
|
121
|
+
tier: args.tier,
|
|
122
|
+
max_tokens: args.max_tokens,
|
|
123
|
+
min_score: args.min_score,
|
|
124
|
+
diversity: args.diversity,
|
|
125
|
+
progressive: args.progressive,
|
|
126
|
+
confidence_threshold: args.confidence_threshold,
|
|
127
|
+
entity: args.entity,
|
|
128
|
+
});
|
|
129
|
+
span.setAttribute('result_count', r.total_results ?? 0);
|
|
130
|
+
span.setAttribute('search_mode', r.search_mode ?? 'unknown');
|
|
131
|
+
span.setAttribute('early_stopped', r.early_stopped ?? false);
|
|
132
|
+
span.setAttribute('tiers_searched', (r.tiers_searched ?? []).join(','));
|
|
133
|
+
span.setAttribute('tokens_used', r.tokens_used ?? 0);
|
|
134
|
+
return r;
|
|
135
|
+
});
|
|
136
|
+
return asMcpResponse('recall', result, args);
|
|
137
|
+
});
|
|
138
|
+
server.tool('search', 'Search memory using structured metadata filters (type, tier, topics, persons, date range) with optional full-text query. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
139
|
+
filter: z
|
|
140
|
+
.object({
|
|
141
|
+
type: z.string().optional(),
|
|
142
|
+
tier: z.string().optional(),
|
|
143
|
+
topics: z.array(z.string()).optional(),
|
|
144
|
+
persons: z.array(z.string()).optional(),
|
|
145
|
+
created_after: z.string().optional(),
|
|
146
|
+
created_before: z.string().optional(),
|
|
147
|
+
})
|
|
148
|
+
.optional(),
|
|
149
|
+
query: z.string().optional(),
|
|
150
|
+
limit: z.number().optional(),
|
|
151
|
+
}, async (args) => {
|
|
152
|
+
const result = await withToolSpan('search', {
|
|
153
|
+
has_query: !!args.query,
|
|
154
|
+
tier_filter: args.filter?.tier || 'all',
|
|
155
|
+
}, async (span) => {
|
|
156
|
+
const r = await search({
|
|
157
|
+
filter: args.filter,
|
|
158
|
+
query: args.query,
|
|
159
|
+
limit: args.limit,
|
|
160
|
+
});
|
|
161
|
+
span.setAttribute('result_count', r.count ?? 0);
|
|
162
|
+
return r;
|
|
163
|
+
});
|
|
164
|
+
return asMcpResponse('search', result, args);
|
|
165
|
+
});
|
|
166
|
+
server.tool('stats', 'Get system health statistics for r2mcp memory — counts by tier/type, staleness, top topics, embedding index status. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {}, async () => {
|
|
167
|
+
const result = await withToolSpan('stats', {}, async (span) => {
|
|
168
|
+
const r = await stats();
|
|
169
|
+
span.setAttribute('total_entries', r.total ?? 0);
|
|
170
|
+
return r;
|
|
171
|
+
});
|
|
172
|
+
return asMcpResponse('stats', result, {});
|
|
173
|
+
});
|
|
174
|
+
server.tool('reject', 'Mark an existing memory as rejected and store the rejection reason. Rejected memories are excluded from recall and search results. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
175
|
+
id: z.string(),
|
|
176
|
+
reason: z.string(),
|
|
177
|
+
}, async (args) => {
|
|
178
|
+
const result = await withToolSpan('reject', { target_id: args.id }, async () => {
|
|
179
|
+
return reject({ id: args.id, reason: args.reason });
|
|
180
|
+
});
|
|
181
|
+
return asMcpResponse('reject', result, args);
|
|
182
|
+
});
|
|
183
|
+
server.tool('meditate', 'Run memory consolidation — archives stale entries, checks for duplicates, finds cross-references, clusters by theme, and surfaces gaps. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
184
|
+
mode: z.enum(['full']).default('full'),
|
|
185
|
+
dry_run: z.boolean().optional().default(false),
|
|
186
|
+
include_lint: z.boolean().optional().default(false),
|
|
187
|
+
}, async (args) => {
|
|
188
|
+
const result = await withToolSpan('meditate', {
|
|
189
|
+
mode: args.mode,
|
|
190
|
+
dry_run: args.dry_run,
|
|
191
|
+
include_lint: args.include_lint,
|
|
192
|
+
}, async (span) => {
|
|
193
|
+
const r = await meditate({ mode: args.mode, dry_run: args.dry_run, include_lint: args.include_lint }, PROJECT_ROOT);
|
|
194
|
+
span.setAttribute('entries_affected', r.total_changes ?? 0);
|
|
195
|
+
return r;
|
|
196
|
+
});
|
|
197
|
+
return asMcpResponse('meditate', result, args);
|
|
198
|
+
});
|
|
199
|
+
server.tool('compile', 'Regenerate the wiki view of memory — synthesize tier or topic markdown from pgvector. Output to memory/compiled/. Modes: tier (single tier), all (three tiers), topic (per-topic page), dry_run (preview to stdout). Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
200
|
+
tier: z.enum(['preferences', 'project-context', 'conversations']).optional(),
|
|
201
|
+
all: z.boolean().optional(),
|
|
202
|
+
topic: z.string().optional(),
|
|
203
|
+
dry_run: z.boolean().optional(),
|
|
204
|
+
max_cost_usd: z.number().optional(),
|
|
205
|
+
provider: z.enum(['claude-code', 'anthropic', 'openrouter']).optional(),
|
|
206
|
+
}, async (args) => {
|
|
207
|
+
const result = await withToolSpan('compile', {
|
|
208
|
+
scope: args.tier ?? args.topic ?? (args.all ? 'all' : 'unknown'),
|
|
209
|
+
dry_run: args.dry_run ?? false,
|
|
210
|
+
}, async (span) => {
|
|
211
|
+
const r = await compile({
|
|
212
|
+
tier: args.tier,
|
|
213
|
+
all: args.all,
|
|
214
|
+
topic: args.topic,
|
|
215
|
+
dry_run: args.dry_run,
|
|
216
|
+
max_cost_usd: args.max_cost_usd,
|
|
217
|
+
provider: args.provider,
|
|
218
|
+
}, { cwd: PROJECT_ROOT });
|
|
219
|
+
span.setAttribute('files_written', r.files_written.length);
|
|
220
|
+
span.setAttribute('hit_cost_cap', r.hit_cost_cap);
|
|
221
|
+
span.setAttribute('provider', r.provider);
|
|
222
|
+
return r;
|
|
223
|
+
});
|
|
224
|
+
return asMcpResponse('compile', result, args);
|
|
225
|
+
});
|
|
226
|
+
server.tool('classify', 'Classify candidate memory pairs into typed edges (supports, contradicts, supersedes, evolved_into, depends_on, related_to). Wraps the SPEC-043 edge classifier with cost cap and provider auto-fallback. Subprocess-spawned per the MCP-server-makes-no-LLM-calls invariant. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
227
|
+
since_days: z
|
|
228
|
+
.number()
|
|
229
|
+
.optional()
|
|
230
|
+
.describe('Filter candidate pairs to memories updated in the last N days'),
|
|
231
|
+
max_cost_usd: z
|
|
232
|
+
.number()
|
|
233
|
+
.optional()
|
|
234
|
+
.describe('Per-run cost cap in USD; default $1.00 from R2MCP_EDGE_MAX_USD'),
|
|
235
|
+
dry_run: z.boolean().optional().describe('Estimate-only — no edges written'),
|
|
236
|
+
resume_run_id: z
|
|
237
|
+
.string()
|
|
238
|
+
.optional()
|
|
239
|
+
.describe('Resume a prior run_id; terminal pairs not re-classified'),
|
|
240
|
+
provider: z
|
|
241
|
+
.enum(['claude-code', 'anthropic', 'openrouter'])
|
|
242
|
+
.optional()
|
|
243
|
+
.describe('Force a specific provider for this run'),
|
|
244
|
+
}, async (args) => {
|
|
245
|
+
const result = await withToolSpan('classify', {
|
|
246
|
+
since_days: args.since_days ?? 0,
|
|
247
|
+
provider: args.provider ?? 'auto',
|
|
248
|
+
}, async (span) => {
|
|
249
|
+
const r = await classify({
|
|
250
|
+
since_days: args.since_days,
|
|
251
|
+
max_cost_usd: args.max_cost_usd,
|
|
252
|
+
dry_run: args.dry_run,
|
|
253
|
+
resume_run_id: args.resume_run_id,
|
|
254
|
+
provider: args.provider,
|
|
255
|
+
}, { cwd: PROJECT_ROOT });
|
|
256
|
+
span.setAttribute('edges_written', r.edges_written);
|
|
257
|
+
span.setAttribute('total_cost_usd', r.total_cost_usd);
|
|
258
|
+
span.setAttribute('hit_cost_cap', r.hit_cost_cap);
|
|
259
|
+
return r;
|
|
260
|
+
});
|
|
261
|
+
return asMcpResponse('classify', result, args);
|
|
262
|
+
});
|
|
263
|
+
server.tool('extract_entities', 'Extract structured entities (project / person / tool / decision) from memories. Spawns a subprocess driver that uses LLMProvider (Haiku-class). Inherits cost cap, resumable runs, and candidate pre-filtering from SPEC-043 patterns. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
264
|
+
since_days: z
|
|
265
|
+
.number()
|
|
266
|
+
.int()
|
|
267
|
+
.min(0)
|
|
268
|
+
.optional()
|
|
269
|
+
.describe('Filter candidate memories to those updated in the last N days'),
|
|
270
|
+
max_cost_usd: z
|
|
271
|
+
.number()
|
|
272
|
+
.nonnegative()
|
|
273
|
+
.optional()
|
|
274
|
+
.describe('Per-run cost cap in USD; default $1.00 from R2MCP_ENTITY_MAX_USD'),
|
|
275
|
+
provider: z
|
|
276
|
+
.enum(['claude-code', 'anthropic', 'openrouter'])
|
|
277
|
+
.optional()
|
|
278
|
+
.describe('Force a specific provider for this run'),
|
|
279
|
+
resume: z
|
|
280
|
+
.string()
|
|
281
|
+
.uuid()
|
|
282
|
+
.optional()
|
|
283
|
+
.describe('Resume a prior run_id; memories already terminal in that run are skipped'),
|
|
284
|
+
full: z
|
|
285
|
+
.boolean()
|
|
286
|
+
.optional()
|
|
287
|
+
.describe('Backfill mode — process all memories regardless of recency. Mutually exclusive with since_days.'),
|
|
288
|
+
context_top_n: z
|
|
289
|
+
.number()
|
|
290
|
+
.int()
|
|
291
|
+
.positive()
|
|
292
|
+
.optional()
|
|
293
|
+
.describe('Top-N existing entities to include in extraction context'),
|
|
294
|
+
}, async (args) => {
|
|
295
|
+
const result = await withToolSpan('extract_entities', {
|
|
296
|
+
since_days: args.since_days ?? 0,
|
|
297
|
+
provider: args.provider ?? 'auto',
|
|
298
|
+
full: args.full ?? false,
|
|
299
|
+
}, async (span) => {
|
|
300
|
+
const r = await extractEntitiesTool({
|
|
301
|
+
since_days: args.since_days,
|
|
302
|
+
max_cost_usd: args.max_cost_usd,
|
|
303
|
+
provider: args.provider,
|
|
304
|
+
resume: args.resume,
|
|
305
|
+
full: args.full,
|
|
306
|
+
context_top_n: args.context_top_n,
|
|
307
|
+
}, { cwd: PROJECT_ROOT });
|
|
308
|
+
span.setAttribute('memories_seen', r.memories_seen);
|
|
309
|
+
span.setAttribute('memories_extracted', r.memories_extracted);
|
|
310
|
+
span.setAttribute('entities_created', r.entities_created);
|
|
311
|
+
span.setAttribute('entities_updated', r.entities_updated);
|
|
312
|
+
span.setAttribute('links_created', r.links_created);
|
|
313
|
+
span.setAttribute('total_cost_usd', r.total_cost_usd);
|
|
314
|
+
span.setAttribute('hit_cost_cap', r.hit_cost_cap);
|
|
315
|
+
return r;
|
|
316
|
+
});
|
|
317
|
+
return asMcpResponse('extract_entities', result, args);
|
|
318
|
+
});
|
|
319
|
+
server.tool('dump_edges_sidecar', 'Write memory_edges and memories as JSON sidecar files for downstream consumers (Memory Explorer, /memory-doctor, etc.). In-process pgvector dump — no subprocess, no LLM calls. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
320
|
+
out_dir: z
|
|
321
|
+
.string()
|
|
322
|
+
.describe('Absolute path to the directory where edges.json + memories.json land. Required.'),
|
|
323
|
+
}, async (args) => {
|
|
324
|
+
const result = await withToolSpan('dump_edges_sidecar', {
|
|
325
|
+
out_dir: args.out_dir,
|
|
326
|
+
}, async (span) => {
|
|
327
|
+
const r = await dumpEdgesSidecarTool({ out_dir: args.out_dir });
|
|
328
|
+
span.setAttribute('memories_count', r.memories_count);
|
|
329
|
+
span.setAttribute('edges_count', r.edges_count);
|
|
330
|
+
return r;
|
|
331
|
+
});
|
|
332
|
+
return asMcpResponse('dump_edges_sidecar', result, args);
|
|
333
|
+
});
|
|
334
|
+
server.tool('lint', 'Surface structural feedback on the memory store: contradictions, stale, orphans, drift, superseded_unflagged. SQL-only (no LLM calls). Pass `fix: true` to apply auto-fixes for findings with confidence >= 0.9. Response includes a next_tools[] array of {name, usage, why} suggested follow-ups (may be empty).', {
|
|
335
|
+
check: z
|
|
336
|
+
.enum(['contradictions', 'stale', 'orphans', 'drift', 'superseded_unflagged'])
|
|
337
|
+
.optional(),
|
|
338
|
+
since_days: z.number().optional(),
|
|
339
|
+
limit: z.number().optional(),
|
|
340
|
+
fix: z.boolean().optional(),
|
|
341
|
+
memory_id: z
|
|
342
|
+
.string()
|
|
343
|
+
.uuid()
|
|
344
|
+
.optional()
|
|
345
|
+
.describe('SPEC-047: scope `contradictions` to edges where either endpoint matches this memory id. Ignored by other checks.'),
|
|
346
|
+
}, async (args) => {
|
|
347
|
+
const result = await withToolSpan('lint', {
|
|
348
|
+
check: args.check ?? 'all',
|
|
349
|
+
fix: args.fix ?? false,
|
|
350
|
+
}, async (span) => {
|
|
351
|
+
const r = await lint({
|
|
352
|
+
check: args.check,
|
|
353
|
+
since_days: args.since_days,
|
|
354
|
+
limit: args.limit,
|
|
355
|
+
fix: args.fix,
|
|
356
|
+
memory_id: args.memory_id,
|
|
357
|
+
});
|
|
358
|
+
span.setAttribute('total_findings', r.summary.total_findings);
|
|
359
|
+
span.setAttribute('fixes_applied', r.fixes_applied?.length ?? 0);
|
|
360
|
+
return r;
|
|
361
|
+
});
|
|
362
|
+
return asMcpResponse('lint', result, args);
|
|
363
|
+
});
|
|
364
|
+
/**
|
|
365
|
+
* Exit when the parent client disconnects.
|
|
366
|
+
*
|
|
367
|
+
* MCP transport is stdio — the parent (Claude Code, Slack bot, etc.) owns
|
|
368
|
+
* this subprocess via a private pipe. When the parent exits cleanly it
|
|
369
|
+
* sends SIGTERM and we never reach this path. When the parent crashes
|
|
370
|
+
* (force-quit, SSH disconnect, kernel OOM), the pipe closes silently and
|
|
371
|
+
* we'd otherwise sit forever waiting for input that never comes —
|
|
372
|
+
* holding a Postgres connection slot for nothing.
|
|
373
|
+
*
|
|
374
|
+
* Watching `stdin` for `end` (EOF) catches both clean and crashed parents.
|
|
375
|
+
* Watching `stdout` for EPIPE covers the rarer "we tried to write back
|
|
376
|
+
* after the parent disappeared" case.
|
|
377
|
+
*/
|
|
378
|
+
function wireParentDisconnectHandlers() {
|
|
379
|
+
const exit = (reason) => {
|
|
380
|
+
console.error(`r2mcp exiting: ${reason}`);
|
|
381
|
+
process.exit(0);
|
|
382
|
+
};
|
|
383
|
+
process.stdin.on('end', () => exit('parent disconnected (stdin EOF)'));
|
|
384
|
+
process.stdin.on('close', () => exit('parent disconnected (stdin closed)'));
|
|
385
|
+
process.stdout.on('error', (err) => {
|
|
386
|
+
if (err.code === 'EPIPE')
|
|
387
|
+
exit('parent disconnected (stdout EPIPE)');
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
async function main() {
|
|
391
|
+
await initDb();
|
|
392
|
+
if (!process.env.R2MCP_OPENROUTER_API_KEY) {
|
|
393
|
+
console.error(`[r2mcp] ${EMBEDDINGS_DISABLED_WARNING}`);
|
|
394
|
+
}
|
|
395
|
+
const transport = new StdioServerTransport();
|
|
396
|
+
await server.connect(transport);
|
|
397
|
+
wireParentDisconnectHandlers();
|
|
398
|
+
console.error('r2mcp running on stdio');
|
|
399
|
+
}
|
|
400
|
+
main().catch((err) => {
|
|
401
|
+
console.error('Fatal error:', err);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry instrumentation for r2mcp.
|
|
3
|
+
* MUST be imported before any other module to enable auto-instrumentation.
|
|
4
|
+
*
|
|
5
|
+
* Enable with: OTEL_ENABLED=true in .env (plus OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
6
|
+
* Disabled by default — no OTel deps needed for basic usage.
|
|
7
|
+
*/
|
|
8
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
9
|
+
declare let sdk: NodeSDK | null;
|
|
10
|
+
export { sdk };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry instrumentation for r2mcp.
|
|
3
|
+
* MUST be imported before any other module to enable auto-instrumentation.
|
|
4
|
+
*
|
|
5
|
+
* Enable with: OTEL_ENABLED=true in .env (plus OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
6
|
+
* Disabled by default — no OTel deps needed for basic usage.
|
|
7
|
+
*/
|
|
8
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
9
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
|
|
10
|
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
|
|
11
|
+
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
12
|
+
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
|
|
13
|
+
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
14
|
+
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
15
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
16
|
+
const otelEnabled = process.env.OTEL_ENABLED === 'true';
|
|
17
|
+
let sdk = null;
|
|
18
|
+
if (otelEnabled) {
|
|
19
|
+
sdk = new NodeSDK({
|
|
20
|
+
resource: resourceFromAttributes({
|
|
21
|
+
[ATTR_SERVICE_NAME]: 'r2mcp',
|
|
22
|
+
[ATTR_SERVICE_VERSION]: '0.1.0',
|
|
23
|
+
'deployment.environment': process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
|
24
|
+
}),
|
|
25
|
+
traceExporter: new OTLPTraceExporter(),
|
|
26
|
+
metricReader: new PeriodicExportingMetricReader({
|
|
27
|
+
exporter: new OTLPMetricExporter(),
|
|
28
|
+
exportIntervalMillis: 60_000,
|
|
29
|
+
}),
|
|
30
|
+
instrumentations: [new PgInstrumentation(), new HttpInstrumentation()],
|
|
31
|
+
});
|
|
32
|
+
sdk.start();
|
|
33
|
+
console.error('OTel initialized — r2mcp telemetry active');
|
|
34
|
+
process.on('SIGTERM', () => sdk?.shutdown());
|
|
35
|
+
process.on('SIGINT', () => sdk?.shutdown());
|
|
36
|
+
}
|
|
37
|
+
export { sdk };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { LintFinding } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Contradictions: rows where `relation='contradicts'`, both endpoints are
|
|
4
|
+
* unarchived, and the edge is currently valid (`valid_until IS NULL`).
|
|
5
|
+
*
|
|
6
|
+
* The edge already carries `confidence` and `rationale` from the classifier;
|
|
7
|
+
* we surface those directly.
|
|
8
|
+
*
|
|
9
|
+
* `suggested_action` routes the finding:
|
|
10
|
+
* - if the from-side is strictly newer than the to-side → `add_supersedes_edge`
|
|
11
|
+
* (the contradicts edge probably should have been supersedes)
|
|
12
|
+
* - if confidence >= 0.85 and the contents look factual → `archive_one`
|
|
13
|
+
* - otherwise → `human_review`
|
|
14
|
+
*
|
|
15
|
+
* SPEC-047 additions:
|
|
16
|
+
* - Surface the from-side's first topic as `topic` so the lint→compile
|
|
17
|
+
* breadcrumb (`compile --topic=<t>`) can emit without a second query.
|
|
18
|
+
* - Accept an optional `memoryId` filter so the recall→lint breadcrumb
|
|
19
|
+
* (`lint --check=contradictions --memory-id=<id>`) can scope to a single
|
|
20
|
+
* memory (matches either endpoint of the edge).
|
|
21
|
+
*/
|
|
22
|
+
export interface PoolLike {
|
|
23
|
+
query<T = unknown>(sql: string, params?: unknown[]): Promise<{
|
|
24
|
+
rows: T[];
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export declare function findContradictions(pool: PoolLike, opts: {
|
|
28
|
+
limit: number;
|
|
29
|
+
memoryId?: string;
|
|
30
|
+
}): Promise<LintFinding[]>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const BASE_SQL = `
|
|
2
|
+
SELECT
|
|
3
|
+
e.from_memory_id AS from_id,
|
|
4
|
+
e.to_memory_id AS to_id,
|
|
5
|
+
e.confidence::float AS confidence,
|
|
6
|
+
e.rationale AS rationale,
|
|
7
|
+
m1.created_at::text AS from_created_at,
|
|
8
|
+
m2.created_at::text AS to_created_at,
|
|
9
|
+
m1.topics AS from_topics
|
|
10
|
+
FROM memory_edges e
|
|
11
|
+
JOIN memories m1 ON m1.id = e.from_memory_id
|
|
12
|
+
JOIN memories m2 ON m2.id = e.to_memory_id
|
|
13
|
+
WHERE e.relation = 'contradicts'
|
|
14
|
+
AND e.valid_until IS NULL
|
|
15
|
+
AND m1.type != 'archived'
|
|
16
|
+
AND m2.type != 'archived'`;
|
|
17
|
+
export async function findContradictions(pool, opts) {
|
|
18
|
+
const params = [];
|
|
19
|
+
let scope = '';
|
|
20
|
+
if (opts.memoryId) {
|
|
21
|
+
params.push(opts.memoryId);
|
|
22
|
+
scope = ` AND (e.from_memory_id = $${params.length} OR e.to_memory_id = $${params.length})`;
|
|
23
|
+
}
|
|
24
|
+
params.push(opts.limit);
|
|
25
|
+
const query = `${BASE_SQL}${scope}\n ORDER BY e.confidence DESC\n LIMIT $${params.length}`;
|
|
26
|
+
const rows = await pool.query(query, params);
|
|
27
|
+
return rows.rows.map((r) => {
|
|
28
|
+
const fromIsNewer = r.from_created_at > r.to_created_at;
|
|
29
|
+
let action;
|
|
30
|
+
if (fromIsNewer && r.confidence >= 0.7) {
|
|
31
|
+
action = 'add_supersedes_edge';
|
|
32
|
+
}
|
|
33
|
+
else if (r.confidence >= 0.85) {
|
|
34
|
+
action = 'archive_one';
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
action = 'human_review';
|
|
38
|
+
}
|
|
39
|
+
const topic = Array.isArray(r.from_topics) && r.from_topics.length > 0
|
|
40
|
+
? r.from_topics[0]
|
|
41
|
+
: undefined;
|
|
42
|
+
return {
|
|
43
|
+
check: 'contradictions',
|
|
44
|
+
memory_id: r.from_id,
|
|
45
|
+
related_memory_id: r.to_id,
|
|
46
|
+
topic,
|
|
47
|
+
rationale: r.rationale,
|
|
48
|
+
suggested_action: action,
|
|
49
|
+
confidence: r.confidence,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|