threadctx-mcp 0.2.0 → 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 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 + drop "check team memory" into your
28
- # agent's project rules (CLAUDE.md / .cursorrules). Works in local mode too.
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
- `init` also adds a small, clearly-marked block to your `CLAUDE.md` and
46
- `.cursorrules` instructing the agent to call `memory_query` before a task
47
- and `memory_write` after this is what makes the memory reliably get
48
- *read*, since MCP tools are pull-based (pass `--no-rules` to skip). Re-running
49
- `init` refreshes that block in place instead of duplicating it.
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 deliberately written to bias the model toward
124
- calling `memory_query` proactively — MCP is pull-based, so the agent has
125
- to be prompted by the description to use it; it isn't automatic. For the
126
- most reliable behavior, run `npx threadctx-mcp init`, which adds an explicit
127
- "check team memory before/after each task" instruction to your `CLAUDE.md`
128
- and `.cursorrules`.
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 { existsSync, readFileSync, writeFileSync } from 'node:fs';
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. Opt out with --no-rules.
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
- for (const rulesFile of ['CLAUDE.md', '.cursorrules']) {
93
- const rulesPath = join(process.cwd(), rulesFile);
94
- const result = upsertRulesBlock(rulesPath);
95
- const verb = result === 'created' ? 'Created' : result === 'updated' ? 'Updated' : 'Already current in';
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') {
@@ -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
- const server = new Server({ name: 'threadctx', version: packageVersion }, { capabilities: { tools: {} } });
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,7 @@
1
1
  {
2
2
  "name": "threadctx-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
+ "mcpName": "io.github.threadctx-dev/threadctx-mcp",
4
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.",
5
6
  "type": "module",
6
7
  "bin": {