threadctx-mcp 0.2.1 → 0.3.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/README.md +29 -16
- package/dist/cli.js +10 -55
- package/dist/rules.d.ts +13 -0
- package/dist/rules.js +100 -0
- package/dist/server.js +30 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,11 +21,12 @@ per-client integration work.
|
|
|
21
21
|
## Quick start
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
# Local mode — nothing to configure
|
|
24
|
+
# Local mode — nothing to configure. Also auto-adds the "check team memory"
|
|
25
|
+
# instruction to CLAUDE.md / .cursor/rules/threadctx.mdc on first start.
|
|
25
26
|
npx threadctx-mcp
|
|
26
27
|
|
|
27
|
-
# Optional: write a committable config
|
|
28
|
-
#
|
|
28
|
+
# Optional: write a committable .threadctx.json config, or re-apply the
|
|
29
|
+
# project-rules block explicitly.
|
|
29
30
|
npx threadctx-mcp init
|
|
30
31
|
|
|
31
32
|
# Cloud mode — prints the exact MCP config block to paste
|
|
@@ -42,11 +43,21 @@ The key is read from the `THREADCTX_API_KEY` environment variable at
|
|
|
42
43
|
runtime (set it in your MCP client's `env` block, as shown below), so
|
|
43
44
|
secrets stay out of version control by construction.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
threadctx also adds a small, clearly-marked instruction to your project
|
|
47
|
+
rules — `CLAUDE.md` for Claude Code, `.cursor/rules/threadctx.mdc` for
|
|
48
|
+
Cursor — telling the agent to call `memory_query` before a task and
|
|
49
|
+
`memory_write` after. **This happens automatically the first time the
|
|
50
|
+
server starts in a project — you don't need to run `init` for it.** Running
|
|
51
|
+
`init` just triggers it explicitly and prints the result; either way it's
|
|
52
|
+
idempotent (safe to re-run, never duplicates). Opt out entirely with
|
|
53
|
+
`THREADCTX_NO_AUTO_RULES=1`, or per-`init`-call with `--no-rules`.
|
|
54
|
+
|
|
55
|
+
The same instruction is also sent as part of the MCP `initialize` handshake
|
|
56
|
+
itself (the protocol's `instructions` field), so it reaches the model even
|
|
57
|
+
before any rules file exists, and for clients that don't read project-rules
|
|
58
|
+
files at all. The file-based rules are belt-and-suspenders on top of that,
|
|
59
|
+
since not every MCP client is guaranteed to surface `instructions`
|
|
60
|
+
prominently.
|
|
50
61
|
|
|
51
62
|
## Claude Code setup
|
|
52
63
|
|
|
@@ -100,6 +111,7 @@ MCP is a portable, open protocol.
|
|
|
100
111
|
| `THREADCTX_API_URL` | no | defaults to `https://threadctx.dev/api/v1`; override for self-hosting |
|
|
101
112
|
| `THREADCTX_REPO` | no | overrides repo auto-detection from `git remote` |
|
|
102
113
|
| `THREADCTX_DB_PATH` | no | local-mode store path; defaults to `~/.threadctx/local.json` |
|
|
114
|
+
| `THREADCTX_NO_AUTO_RULES` | no | set to `1` to disable auto-injecting `CLAUDE.md` / `.cursor/rules/threadctx.mdc` on server start |
|
|
103
115
|
|
|
104
116
|
## Local development
|
|
105
117
|
|
|
@@ -120,17 +132,18 @@ npm run build # compiles to dist/ for publishing
|
|
|
120
132
|
hits)`) so the same string is recognizable whether you're reading
|
|
121
133
|
Claude Code's terminal output or Cursor's agent panel.
|
|
122
134
|
|
|
123
|
-
Tool descriptions are
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
Tool descriptions are written to bias the model toward calling
|
|
136
|
+
`memory_query` proactively, and — as of 0.3.0 — the server reinforces this
|
|
137
|
+
two more ways with zero setup required: the MCP `initialize` response
|
|
138
|
+
carries the same instruction to every connecting client, and `CLAUDE.md` /
|
|
139
|
+
`.cursor/rules/threadctx.mdc` get it auto-injected on first start. MCP tools
|
|
140
|
+
are still fundamentally pull-based (no mechanism can force a tool call), but
|
|
141
|
+
these three layers together are the strongest guarantee we can build.
|
|
129
142
|
|
|
130
143
|
## CLI subcommands
|
|
131
144
|
|
|
132
145
|
| Command | What it does |
|
|
133
146
|
|---|---|
|
|
134
|
-
| `npx threadctx-mcp` | Runs the MCP server (this is what Claude Code / Cursor launch). |
|
|
135
|
-
| `npx threadctx-mcp init [--mode=cloud --api-key=…] [--no-rules]` | Writes `.threadctx.json` and the project-rules block. |
|
|
147
|
+
| `npx threadctx-mcp` | Runs the MCP server (this is what Claude Code / Cursor launch). Auto-injects project rules on first start in a project. |
|
|
148
|
+
| `npx threadctx-mcp init [--mode=cloud --api-key=…] [--no-rules]` | Writes `.threadctx.json` and explicitly (re-)applies the project-rules block. |
|
|
136
149
|
| `npx threadctx-mcp list [--all] [--full] [--json]` | Shows what's stored in the local on-disk memory. |
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { loadConfig } from './config.js';
|
|
5
5
|
import { LocalStore } from './local-store.js';
|
|
6
|
+
import { applyRules } from './rules.js';
|
|
6
7
|
import { startServer } from './server.js';
|
|
7
8
|
const [, , command, ...rest] = process.argv;
|
|
8
9
|
function parseFlags(args) {
|
|
@@ -19,53 +20,6 @@ function parseFlags(args) {
|
|
|
19
20
|
}
|
|
20
21
|
return flags;
|
|
21
22
|
}
|
|
22
|
-
// The exact instruction block we drop into an agent's project rules. MCP is
|
|
23
|
-
// pull-based — the model only calls memory_query/memory_write if something tells
|
|
24
|
-
// it to — so this is the single highest-leverage thing for making the memory
|
|
25
|
-
// actually get read. Fenced by markers so re-running `init` updates in place
|
|
26
|
-
// instead of appending duplicates.
|
|
27
|
-
const RULES_START = '<!-- threadctx:start (managed — edit above/below, not between) -->';
|
|
28
|
-
const RULES_END = '<!-- threadctx:end -->';
|
|
29
|
-
const RULES_BODY = [
|
|
30
|
-
'## Team memory (threadctx)',
|
|
31
|
-
'',
|
|
32
|
-
'- **Before** starting any non-trivial task, call `memory_query` with a short',
|
|
33
|
-
' description of what you are about to do. Check for prior decisions, fixes,',
|
|
34
|
-
' and gotchas on this repo before writing code — not after.',
|
|
35
|
-
'- **After** resolving a non-obvious bug, making an architectural decision, or',
|
|
36
|
-
' learning something that would save a teammate time, call `memory_write` to',
|
|
37
|
-
' save it. Write it so a future reader has full context.',
|
|
38
|
-
].join('\n');
|
|
39
|
-
/**
|
|
40
|
-
* Insert (or refresh) the threadctx rules block in a project rules file,
|
|
41
|
-
* idempotently. Creates the file if missing. Returns what happened so the
|
|
42
|
-
* caller can report it honestly.
|
|
43
|
-
*/
|
|
44
|
-
function upsertRulesBlock(filePath) {
|
|
45
|
-
const block = `${RULES_START}\n${RULES_BODY}\n${RULES_END}\n`;
|
|
46
|
-
if (!existsSync(filePath)) {
|
|
47
|
-
writeFileSync(filePath, block);
|
|
48
|
-
return 'created';
|
|
49
|
-
}
|
|
50
|
-
const existing = readFileSync(filePath, 'utf-8');
|
|
51
|
-
const startIdx = existing.indexOf(RULES_START);
|
|
52
|
-
if (startIdx !== -1) {
|
|
53
|
-
const endIdx = existing.indexOf(RULES_END, startIdx);
|
|
54
|
-
if (endIdx !== -1) {
|
|
55
|
-
const before = existing.slice(0, startIdx);
|
|
56
|
-
const after = existing.slice(endIdx + RULES_END.length);
|
|
57
|
-
const next = `${before}${block.trimEnd()}${after}`;
|
|
58
|
-
if (next === existing)
|
|
59
|
-
return 'unchanged';
|
|
60
|
-
writeFileSync(filePath, next);
|
|
61
|
-
return 'updated';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// No managed block yet — append one, keeping the user's existing content.
|
|
65
|
-
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
66
|
-
writeFileSync(filePath, `${existing}${sep}${block}`);
|
|
67
|
-
return 'updated';
|
|
68
|
-
}
|
|
69
23
|
function runInit(args) {
|
|
70
24
|
const flags = parseFlags(args);
|
|
71
25
|
const mode = flags.mode === 'cloud' ? 'cloud' : 'local';
|
|
@@ -87,14 +41,15 @@ function runInit(args) {
|
|
|
87
41
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
88
42
|
console.log(`✅ Wrote ${configPath} (mode: ${mode}) — safe to commit, contains no secret.`);
|
|
89
43
|
// Drop the "always check team memory" instruction into the agent's project
|
|
90
|
-
// rules so the tools actually get used
|
|
44
|
+
// rules so the tools actually get used, even for clients that don't surface
|
|
45
|
+
// MCP's `initialize.instructions` prominently. Opt out with --no-rules. The
|
|
46
|
+
// server also does this automatically on every start (see server.ts) — this
|
|
47
|
+
// just lets you trigger it explicitly and see the result immediately.
|
|
91
48
|
if (flags['no-rules'] !== true) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log(`✅ ${verb} ${rulesFile} — tells your agent to check team memory each task.`);
|
|
97
|
-
}
|
|
49
|
+
const results = applyRules(process.cwd());
|
|
50
|
+
const describe = (r) => (r === 'created' ? 'Created' : r === 'updated' ? 'Updated' : 'Already current in');
|
|
51
|
+
console.log(`✅ ${describe(results.claudeMd)} CLAUDE.md — tells Claude Code to check team memory each task.`);
|
|
52
|
+
console.log(`✅ ${describe(results.cursorRule)} .cursor/rules/threadctx.mdc — tells Cursor to check team memory each task.`);
|
|
98
53
|
}
|
|
99
54
|
console.log('');
|
|
100
55
|
if (mode === 'cloud') {
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const RULES_BODY: string;
|
|
2
|
+
export type RuleResult = 'created' | 'updated' | 'unchanged';
|
|
3
|
+
export interface ApplyRulesResult {
|
|
4
|
+
claudeMd: RuleResult;
|
|
5
|
+
cursorRule: RuleResult;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Idempotently ensure both Claude Code's (CLAUDE.md) and Cursor's
|
|
9
|
+
* (.cursor/rules/threadctx.mdc) project rules contain the "check team
|
|
10
|
+
* memory" instruction. Safe to call on every server start, not just from
|
|
11
|
+
* `threadctx init` — repeat calls are no-ops once both are already current.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyRules(projectRoot: string): ApplyRulesResult;
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
// The exact instruction dropped into an agent's project rules. MCP is
|
|
4
|
+
// pull-based — the model only calls memory_query/memory_write if something
|
|
5
|
+
// tells it to — so this is the highest-leverage thing for making the memory
|
|
6
|
+
// actually get read. Also sent verbatim as the MCP `initialize` response's
|
|
7
|
+
// `instructions` field (see server.ts) so it reaches every client even
|
|
8
|
+
// before any file gets written.
|
|
9
|
+
export const RULES_BODY = [
|
|
10
|
+
'## Team memory (threadctx)',
|
|
11
|
+
'',
|
|
12
|
+
'- **Before** starting any non-trivial task, call `memory_query` with a short',
|
|
13
|
+
' description of what you are about to do. Check for prior decisions, fixes,',
|
|
14
|
+
' and gotchas on this repo before writing code — not after.',
|
|
15
|
+
'- **After** resolving a non-obvious bug, making an architectural decision, or',
|
|
16
|
+
' learning something that would save a teammate time, call `memory_write` to',
|
|
17
|
+
' save it. Write it so a future reader has full context.',
|
|
18
|
+
].join('\n');
|
|
19
|
+
const MARKER_START = '<!-- threadctx:start (managed — edit above/below, not between) -->';
|
|
20
|
+
const MARKER_END = '<!-- threadctx:end -->';
|
|
21
|
+
/**
|
|
22
|
+
* Insert (or refresh) the threadctx rules block in a shared, general-purpose
|
|
23
|
+
* rules file (CLAUDE.md), idempotently. Creates the file if missing, preserves
|
|
24
|
+
* any of the user's own content outside the marker-fenced block.
|
|
25
|
+
*/
|
|
26
|
+
function upsertMarkedBlock(filePath) {
|
|
27
|
+
const block = `${MARKER_START}\n${RULES_BODY}\n${MARKER_END}\n`;
|
|
28
|
+
if (!existsSync(filePath)) {
|
|
29
|
+
writeFileSync(filePath, block);
|
|
30
|
+
return 'created';
|
|
31
|
+
}
|
|
32
|
+
const existing = readFileSync(filePath, 'utf-8');
|
|
33
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
34
|
+
if (startIdx !== -1) {
|
|
35
|
+
const endIdx = existing.indexOf(MARKER_END, startIdx);
|
|
36
|
+
if (endIdx !== -1) {
|
|
37
|
+
const before = existing.slice(0, startIdx);
|
|
38
|
+
const after = existing.slice(endIdx + MARKER_END.length);
|
|
39
|
+
const next = `${before}${block.trimEnd()}${after}`;
|
|
40
|
+
if (next === existing)
|
|
41
|
+
return 'unchanged';
|
|
42
|
+
writeFileSync(filePath, next);
|
|
43
|
+
return 'updated';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// No managed block yet — append one, keeping the user's existing content.
|
|
47
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
48
|
+
writeFileSync(filePath, `${existing}${sep}${block}`);
|
|
49
|
+
return 'updated';
|
|
50
|
+
}
|
|
51
|
+
// Cursor's current, non-deprecated rules format: a dedicated .mdc file per
|
|
52
|
+
// rule under .cursor/rules/, with YAML frontmatter. alwaysApply: true means
|
|
53
|
+
// it's loaded in every chat regardless of which files are open — the right
|
|
54
|
+
// behavior for a project-wide "check team memory" instruction (see
|
|
55
|
+
// https://cursor.com/docs/rules). The legacy single .cursorrules file still
|
|
56
|
+
// works but Cursor's own docs say it will eventually be removed, so new
|
|
57
|
+
// writes target the current format instead.
|
|
58
|
+
function cursorRuleContent() {
|
|
59
|
+
return [
|
|
60
|
+
'---',
|
|
61
|
+
'description: Use threadctx shared team memory',
|
|
62
|
+
'alwaysApply: true',
|
|
63
|
+
'---',
|
|
64
|
+
'',
|
|
65
|
+
MARKER_START,
|
|
66
|
+
RULES_BODY,
|
|
67
|
+
MARKER_END,
|
|
68
|
+
'',
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Insert (or refresh) the threadctx rule at .cursor/rules/threadctx.mdc. This
|
|
73
|
+
* filename is ours alone (not a shared/general file like CLAUDE.md), so on a
|
|
74
|
+
* mismatch we simply rewrite it in full rather than doing marker surgery.
|
|
75
|
+
*/
|
|
76
|
+
function upsertCursorRule(projectRoot) {
|
|
77
|
+
const filePath = join(projectRoot, '.cursor', 'rules', 'threadctx.mdc');
|
|
78
|
+
const content = cursorRuleContent();
|
|
79
|
+
if (!existsSync(filePath)) {
|
|
80
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
81
|
+
writeFileSync(filePath, content);
|
|
82
|
+
return 'created';
|
|
83
|
+
}
|
|
84
|
+
if (readFileSync(filePath, 'utf-8') === content)
|
|
85
|
+
return 'unchanged';
|
|
86
|
+
writeFileSync(filePath, content);
|
|
87
|
+
return 'updated';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Idempotently ensure both Claude Code's (CLAUDE.md) and Cursor's
|
|
91
|
+
* (.cursor/rules/threadctx.mdc) project rules contain the "check team
|
|
92
|
+
* memory" instruction. Safe to call on every server start, not just from
|
|
93
|
+
* `threadctx init` — repeat calls are no-ops once both are already current.
|
|
94
|
+
*/
|
|
95
|
+
export function applyRules(projectRoot) {
|
|
96
|
+
return {
|
|
97
|
+
claudeMd: upsertMarkedBlock(join(projectRoot, 'CLAUDE.md')),
|
|
98
|
+
cursorRule: upsertCursorRule(projectRoot),
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
7
7
|
import { loadConfig } from './config.js';
|
|
8
8
|
import { LocalStore } from './local-store.js';
|
|
9
9
|
import { CloudClient } from './cloud-client.js';
|
|
10
|
+
import { applyRules, RULES_BODY } from './rules.js';
|
|
10
11
|
// Consistent attribution string across every surface (Claude Code terminal
|
|
11
12
|
// output, Cursor's agent panel, future surfaces). See spec section 2.4 —
|
|
12
13
|
// the same short string everywhere is what makes the brand legible.
|
|
@@ -44,7 +45,35 @@ export async function startServer() {
|
|
|
44
45
|
const localStore = useCloud ? null : new LocalStore(config.dbPath);
|
|
45
46
|
const cloudClient = useCloud ? new CloudClient(config.apiUrl, config.apiKey, config.actorId) : null;
|
|
46
47
|
const repo = config.repo;
|
|
47
|
-
|
|
48
|
+
// Reliably getting memory_query/memory_write actually *used* is the whole
|
|
49
|
+
// product, so this doesn't wait for a human to run `threadctx init` — it
|
|
50
|
+
// happens on every server start, idempotently (a no-op once both files are
|
|
51
|
+
// already current). Never let a file-permission hiccup here take down the
|
|
52
|
+
// server; opt out entirely with THREADCTX_NO_AUTO_RULES=1.
|
|
53
|
+
if (!process.env.THREADCTX_NO_AUTO_RULES) {
|
|
54
|
+
try {
|
|
55
|
+
const results = applyRules(process.cwd());
|
|
56
|
+
const touched = Object.entries(results).filter(([, r]) => r !== 'unchanged');
|
|
57
|
+
if (touched.length > 0) {
|
|
58
|
+
console.error('[threadctx] Added team-memory instructions to your project rules ' +
|
|
59
|
+
`(${touched.map(([file]) => (file === 'claudeMd' ? 'CLAUDE.md' : '.cursor/rules/threadctx.mdc')).join(', ')}) ` +
|
|
60
|
+
'— your agent will check shared memory automatically from now on.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error('[threadctx] Could not write project rules (non-fatal):', err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const server = new Server({ name: 'threadctx', version: packageVersion }, {
|
|
68
|
+
capabilities: { tools: {} },
|
|
69
|
+
// Sent to every MCP client on the `initialize` handshake, so the
|
|
70
|
+
// "check memory before/after" guidance reaches the model even for
|
|
71
|
+
// clients that don't read CLAUDE.md/.cursor/rules, and even before any
|
|
72
|
+
// file gets written. Belt-and-suspenders with the file-based rules
|
|
73
|
+
// above — client-side handling of this field isn't guaranteed uniform
|
|
74
|
+
// across every MCP client, so we don't rely on it alone.
|
|
75
|
+
instructions: RULES_BODY,
|
|
76
|
+
});
|
|
48
77
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
49
78
|
tools: [
|
|
50
79
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threadctx-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "io.github.threadctx-dev/threadctx-mcp",
|
|
5
5
|
"description": "Shared memory MCP server for AI coding agents. Local-only by default; point it at threadctx.dev (or your own deployment) to share memory across your team.",
|
|
6
6
|
"type": "module",
|