threadctx-mcp 0.1.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 ADDED
@@ -0,0 +1,110 @@
1
+ # threadctx-mcp
2
+
3
+ Shared memory MCP server for AI coding agents. Works identically with
4
+ **Claude Code** and **Cursor** — same package, same config shape, no
5
+ per-client integration work.
6
+
7
+ ## Modes
8
+
9
+ - **Local (default, free, no signup):** memory stored in SQLite at
10
+ `~/.threadctx/local.json` — **zero native dependencies**, so
11
+ `npx threadctx-mcp` installs instantly on any machine with Node 18+ (no
12
+ compiler, no node-gyp step). No network calls except to whichever LLM
13
+ provider your agent already uses. Matching is keyword-based, scoped to
14
+ the current repo (detected via `git remote`).
15
+ - **Cloud (paid Team tier+):** memory shared across everyone on the repo,
16
+ with real semantic search. Requires an API key from
17
+ [threadctx.dev](https://threadctx.dev) (or your own self-hosted
18
+ deployment — see `../cloud/README.md`).
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ # Local mode — nothing to configure
24
+ npx threadctx-mcp
25
+
26
+ # Cloud mode — prints the exact MCP config block to paste
27
+ npx threadctx-mcp init --mode=cloud --api-key=tctx_xxx
28
+ ```
29
+
30
+ This writes a `.threadctx.json` file (just `{ "mode": "cloud" }`) to the
31
+ current directory. It is safe to commit — it never contains your API key.
32
+ The key is read from the `THREADCTX_API_KEY` environment variable at
33
+ runtime (set it in your MCP client's `env` block, as shown below), so
34
+ secrets stay out of version control by construction.
35
+
36
+ ## Claude Code setup
37
+
38
+ Add to your Claude Code MCP config (`claude mcp add` or edit
39
+ `~/.claude/mcp.json` directly):
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "threadctx": {
45
+ "command": "npx",
46
+ "args": ["-y", "threadctx-mcp"],
47
+ "env": {
48
+ "THREADCTX_MODE": "cloud",
49
+ "THREADCTX_API_KEY": "tctx_xxx"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Cursor setup
57
+
58
+ Add the same block to `.cursor/mcp.json` in your project root (or via
59
+ Cursor Settings → Tools & MCP):
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "threadctx": {
65
+ "command": "npx",
66
+ "args": ["-y", "threadctx-mcp"],
67
+ "env": {
68
+ "THREADCTX_MODE": "cloud",
69
+ "THREADCTX_API_KEY": "tctx_xxx"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ That's it — the same package and config work in both clients because
77
+ MCP is a portable, open protocol.
78
+
79
+ ## Environment variables
80
+
81
+ | Variable | Required | Description |
82
+ |---|---|---|
83
+ | `THREADCTX_MODE` | no | `local` (default) or `cloud` |
84
+ | `THREADCTX_API_KEY` | only in cloud mode | issued via `cloud/scripts/create-tenant.ts` |
85
+ | `THREADCTX_API_URL` | no | defaults to `https://threadctx.dev/api/v1`; override for self-hosting |
86
+ | `THREADCTX_REPO` | no | overrides repo auto-detection from `git remote` |
87
+ | `THREADCTX_DB_PATH` | no | local-mode store path; defaults to `~/.threadctx/local.json` |
88
+
89
+ ## Local development
90
+
91
+ ```bash
92
+ npm install
93
+ npm run dev # runs the server via tsx, watches for changes
94
+ npm run build # compiles to dist/ for publishing
95
+ ```
96
+
97
+ ## How the tools work
98
+
99
+ - `memory_write(content, tags?)` — the agent calls this after resolving a
100
+ non-obvious bug, making an architectural decision, or learning
101
+ something worth remembering.
102
+ - `memory_query(task_description, max_results?)` — the agent calls this
103
+ before starting risky or repeated work. Results are returned with a
104
+ consistent attribution footer (`· via threadctx — shared team memory (N
105
+ hits)`) so the same string is recognizable whether you're reading
106
+ Claude Code's terminal output or Cursor's agent panel.
107
+
108
+ Tool descriptions are deliberately written to bias the model toward
109
+ calling `memory_query` proactively — MCP is pull-based, so the agent has
110
+ to be prompted by the description to use it; it isn't automatic.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { startServer } from './server.js';
5
+ const [, , command, ...rest] = process.argv;
6
+ function parseFlags(args) {
7
+ const flags = {};
8
+ for (const arg of args) {
9
+ const match = arg.match(/^--([^=]+)=(.*)$/);
10
+ if (match)
11
+ flags[match[1]] = match[2];
12
+ }
13
+ return flags;
14
+ }
15
+ function runInit(args) {
16
+ const flags = parseFlags(args);
17
+ const mode = flags.mode === 'cloud' ? 'cloud' : 'local';
18
+ // The config file is meant to be committable, so it deliberately never holds
19
+ // the API key — that is read from the THREADCTX_API_KEY environment variable
20
+ // at runtime (set it in your MCP client's `env` block). This keeps secrets
21
+ // out of version control by construction.
22
+ const config = { mode };
23
+ if (mode === 'cloud' && flags['api-url'])
24
+ config.apiUrl = flags['api-url'];
25
+ const apiKey = flags['api-key'];
26
+ if (mode === 'cloud' && !apiKey) {
27
+ console.error('Cloud mode needs an API key. Pass --api-key=<tctx_...> so this command can');
28
+ console.error('show you the exact config block to paste.');
29
+ console.error('Example: npx threadctx-mcp init --mode=cloud --api-key=tctx_xxx');
30
+ process.exit(1);
31
+ }
32
+ const configPath = join(process.cwd(), '.threadctx.json');
33
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
34
+ console.log(`✅ Wrote ${configPath} (mode: ${mode}) — safe to commit, contains no secret.`);
35
+ console.log('');
36
+ if (mode === 'cloud') {
37
+ console.log('Add threadctx to your MCP client config (~/.claude/mcp.json and/or .cursor/mcp.json).');
38
+ console.log('Same block for Claude Code and Cursor:');
39
+ console.log('');
40
+ console.log(JSON.stringify({
41
+ mcpServers: {
42
+ threadctx: {
43
+ command: 'npx',
44
+ args: ['-y', 'threadctx-mcp'],
45
+ env: { THREADCTX_MODE: 'cloud', THREADCTX_API_KEY: apiKey },
46
+ },
47
+ },
48
+ }, null, 2));
49
+ console.log('');
50
+ console.log('Keep your API key in that env block (or your shell) — not in .threadctx.json.');
51
+ }
52
+ else {
53
+ console.log('Local mode is ready — just add threadctx to your MCP client config. No key needed.');
54
+ console.log('See the README for the exact block (identical for Claude Code and Cursor).');
55
+ }
56
+ }
57
+ async function main() {
58
+ if (command === 'init') {
59
+ runInit(rest);
60
+ return;
61
+ }
62
+ // No subcommand: this is what Claude Code / Cursor actually launch as the
63
+ // MCP server process (they invoke `npx threadctx-mcp` with no arguments).
64
+ await startServer();
65
+ }
66
+ main().catch((err) => {
67
+ console.error('[threadctx] fatal error:', err);
68
+ process.exit(1);
69
+ });
@@ -0,0 +1,25 @@
1
+ export interface CloudWriteResponse {
2
+ status: string;
3
+ memory_id: string;
4
+ }
5
+ export interface CloudQueryResponse {
6
+ context_bundle: string;
7
+ referenced_memory_ids: string[];
8
+ debug?: {
9
+ num_candidates: number;
10
+ };
11
+ }
12
+ /**
13
+ * Thin HTTP client for the threadctx cloud API. Talks to either the
14
+ * hosted threadctx.dev service or a self-hosted deployment (set
15
+ * THREADCTX_API_URL to override).
16
+ */
17
+ export declare class CloudClient {
18
+ private apiUrl;
19
+ private apiKey;
20
+ private actorId?;
21
+ constructor(apiUrl: string, apiKey: string, actorId?: string | undefined);
22
+ private request;
23
+ write(repo: string, content: string, tags: string[]): Promise<CloudWriteResponse>;
24
+ query(repo: string, taskDescription: string, maxResults: number): Promise<CloudQueryResponse>;
25
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Thin HTTP client for the threadctx cloud API. Talks to either the
3
+ * hosted threadctx.dev service or a self-hosted deployment (set
4
+ * THREADCTX_API_URL to override).
5
+ */
6
+ export class CloudClient {
7
+ apiUrl;
8
+ apiKey;
9
+ actorId;
10
+ constructor(apiUrl, apiKey, actorId) {
11
+ this.apiUrl = apiUrl;
12
+ this.apiKey = apiKey;
13
+ this.actorId = actorId;
14
+ }
15
+ async request(path, body) {
16
+ const headers = {
17
+ 'Content-Type': 'application/json',
18
+ Authorization: `Bearer ${this.apiKey}`,
19
+ };
20
+ // Anonymous per-developer id for seat accounting (see config.resolveActorId).
21
+ if (this.actorId)
22
+ headers['X-Threadctx-Actor'] = this.actorId;
23
+ const res = await fetch(`${this.apiUrl}${path}`, {
24
+ method: 'POST',
25
+ headers,
26
+ body: JSON.stringify(body),
27
+ });
28
+ if (!res.ok) {
29
+ const text = await res.text().catch(() => '');
30
+ if (res.status === 402) {
31
+ throw new Error('threadctx free-tier quota reached for this period. Upgrade at https://threadctx.dev/pricing.');
32
+ }
33
+ if (res.status === 401) {
34
+ throw new Error('threadctx API key is invalid or missing. Check THREADCTX_API_KEY.');
35
+ }
36
+ throw new Error(`threadctx cloud request failed (${res.status}): ${text}`);
37
+ }
38
+ return res.json();
39
+ }
40
+ write(repo, content, tags) {
41
+ return this.request('/memory/write', { repo, content, tags });
42
+ }
43
+ query(repo, taskDescription, maxResults) {
44
+ return this.request('/memory/query', {
45
+ repo,
46
+ task_description: taskDescription,
47
+ max_results: maxResults,
48
+ });
49
+ }
50
+ }
@@ -0,0 +1,9 @@
1
+ export interface ThreadctxConfig {
2
+ mode: 'local' | 'cloud';
3
+ apiKey?: string;
4
+ apiUrl: string;
5
+ repo: string;
6
+ dbPath: string;
7
+ actorId: string;
8
+ }
9
+ export declare function loadConfig(): ThreadctxConfig;
package/dist/config.js ADDED
@@ -0,0 +1,82 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { randomUUID } from 'node:crypto';
5
+ import os from 'node:os';
6
+ const DEFAULT_API_URL = 'https://threadctx.dev/api/v1';
7
+ function readFileConfig() {
8
+ const projectConfigPath = join(process.cwd(), '.threadctx.json');
9
+ const homeConfigPath = join(os.homedir(), '.threadctx', 'config.json');
10
+ const path = existsSync(projectConfigPath)
11
+ ? projectConfigPath
12
+ : existsSync(homeConfigPath)
13
+ ? homeConfigPath
14
+ : null;
15
+ if (!path)
16
+ return {};
17
+ try {
18
+ return JSON.parse(readFileSync(path, 'utf-8'));
19
+ }
20
+ catch (err) {
21
+ console.error(`[threadctx] Failed to parse config at ${path}:`, err);
22
+ return {};
23
+ }
24
+ }
25
+ function detectRepoName() {
26
+ try {
27
+ const remote = execSync('git config --get remote.origin.url', {
28
+ cwd: process.cwd(),
29
+ stdio: ['ignore', 'pipe', 'ignore'],
30
+ })
31
+ .toString()
32
+ .trim();
33
+ const match = remote.match(/[:/]([^/]+\/[^/]+?)(\.git)?$/);
34
+ return match ? match[1] : 'unknown-repo';
35
+ }
36
+ catch {
37
+ return 'unknown-repo';
38
+ }
39
+ }
40
+ /**
41
+ * A stable, anonymous per-developer id used only for seat accounting in cloud
42
+ * mode (distinct-actor counting vs. purchased seats). It's a random UUID
43
+ * persisted once at ~/.threadctx/actor — no email, no machine fingerprint, no
44
+ * PII. Can be overridden with THREADCTX_ACTOR_ID (e.g. to pin per-CI identities).
45
+ */
46
+ function resolveActorId(dbPath) {
47
+ if (process.env.THREADCTX_ACTOR_ID)
48
+ return process.env.THREADCTX_ACTOR_ID;
49
+ const actorPath = join(dirname(dbPath), 'actor');
50
+ try {
51
+ if (existsSync(actorPath)) {
52
+ const existing = readFileSync(actorPath, 'utf-8').trim();
53
+ if (existing)
54
+ return existing;
55
+ }
56
+ const id = randomUUID();
57
+ mkdirSync(dirname(actorPath), { recursive: true });
58
+ writeFileSync(actorPath, id, 'utf-8');
59
+ return id;
60
+ }
61
+ catch {
62
+ // If disk isn't writable, fall back to an ephemeral id — seat counts will
63
+ // be slightly inflated for this session, which is acceptable and safe.
64
+ return randomUUID();
65
+ }
66
+ }
67
+ export function loadConfig() {
68
+ const fileConfig = readFileConfig();
69
+ const apiKey = process.env.THREADCTX_API_KEY ?? fileConfig.apiKey;
70
+ const mode = process.env.THREADCTX_MODE ??
71
+ fileConfig.mode ??
72
+ (apiKey ? 'cloud' : 'local');
73
+ const dbPath = process.env.THREADCTX_DB_PATH ?? join(os.homedir(), '.threadctx', 'local.json');
74
+ return {
75
+ mode,
76
+ apiKey,
77
+ apiUrl: process.env.THREADCTX_API_URL ?? fileConfig.apiUrl ?? DEFAULT_API_URL,
78
+ repo: process.env.THREADCTX_REPO ?? fileConfig.repo ?? detectRepoName(),
79
+ dbPath,
80
+ actorId: resolveActorId(dbPath),
81
+ };
82
+ }
@@ -0,0 +1,3 @@
1
+ export { startServer } from './server.js';
2
+ export { loadConfig } from './config.js';
3
+ export type { ThreadctxConfig } from './config.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { startServer } from './server.js';
2
+ export { loadConfig } from './config.js';
@@ -0,0 +1,31 @@
1
+ export interface LocalMemory {
2
+ id: string;
3
+ content: string;
4
+ tags: string[];
5
+ created_at: string;
6
+ score: number;
7
+ }
8
+ /**
9
+ * Local mode storage: a single JSON file on disk, zero network calls,
10
+ * zero accounts, and — deliberately — zero native dependencies.
11
+ *
12
+ * We avoid a native SQLite addon on purpose: the headline install path is
13
+ * `npx threadctx-mcp`, and a node-gyp compile step is the most common way
14
+ * that "30-second install" promise breaks (toolchain missing, Node ABI
15
+ * mismatch, etc.). A flat JSON file is more than enough for one developer's
16
+ * own session history (hundreds–thousands of entries) and works on every
17
+ * Node >= 18 without a compiler.
18
+ *
19
+ * Matching is simple keyword overlap rather than semantic search — that keeps
20
+ * local mode embedding-free (no API key required). Cloud mode (CloudClient)
21
+ * gets real semantic search via Upstash Vector.
22
+ */
23
+ export declare class LocalStore {
24
+ private dbPath;
25
+ private memories;
26
+ constructor(dbPath: string);
27
+ private load;
28
+ private persist;
29
+ write(repo: string, content: string, tags: string[]): string;
30
+ query(repo: string, taskDescription: string, maxResults: number): LocalMemory[];
31
+ }
@@ -0,0 +1,81 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ /**
5
+ * Local mode storage: a single JSON file on disk, zero network calls,
6
+ * zero accounts, and — deliberately — zero native dependencies.
7
+ *
8
+ * We avoid a native SQLite addon on purpose: the headline install path is
9
+ * `npx threadctx-mcp`, and a node-gyp compile step is the most common way
10
+ * that "30-second install" promise breaks (toolchain missing, Node ABI
11
+ * mismatch, etc.). A flat JSON file is more than enough for one developer's
12
+ * own session history (hundreds–thousands of entries) and works on every
13
+ * Node >= 18 without a compiler.
14
+ *
15
+ * Matching is simple keyword overlap rather than semantic search — that keeps
16
+ * local mode embedding-free (no API key required). Cloud mode (CloudClient)
17
+ * gets real semantic search via Upstash Vector.
18
+ */
19
+ export class LocalStore {
20
+ dbPath;
21
+ memories;
22
+ constructor(dbPath) {
23
+ this.dbPath = dbPath;
24
+ mkdirSync(dirname(dbPath), { recursive: true });
25
+ this.memories = this.load();
26
+ }
27
+ load() {
28
+ if (!existsSync(this.dbPath))
29
+ return [];
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(this.dbPath, 'utf-8'));
32
+ return Array.isArray(parsed) ? parsed : [];
33
+ }
34
+ catch {
35
+ // Corrupt or partially-written file: don't crash the agent's session.
36
+ // Start fresh in memory; the next write rewrites a valid file.
37
+ return [];
38
+ }
39
+ }
40
+ persist() {
41
+ // Atomic write: serialize to a temp file then rename, so a crash mid-write
42
+ // can never leave a half-written (and unparseable) store behind.
43
+ const tmp = `${this.dbPath}.${process.pid}.tmp`;
44
+ writeFileSync(tmp, JSON.stringify(this.memories, null, 2), 'utf-8');
45
+ renameSync(tmp, this.dbPath);
46
+ }
47
+ write(repo, content, tags) {
48
+ const id = randomUUID();
49
+ this.memories.push({
50
+ id,
51
+ repo,
52
+ content,
53
+ tags,
54
+ created_at: new Date().toISOString(),
55
+ });
56
+ this.persist();
57
+ return id;
58
+ }
59
+ query(repo, taskDescription, maxResults) {
60
+ const keywords = taskDescription
61
+ .toLowerCase()
62
+ .split(/\W+/)
63
+ .filter((w) => w.length > 3);
64
+ return this.memories
65
+ .filter((m) => m.repo === repo)
66
+ .map((m) => {
67
+ const haystack = `${m.content} ${m.tags.join(' ')}`.toLowerCase();
68
+ const score = keywords.reduce((acc, k) => acc + (haystack.includes(k) ? 1 : 0), 0);
69
+ return {
70
+ id: m.id,
71
+ content: m.content,
72
+ tags: m.tags,
73
+ created_at: m.created_at,
74
+ score,
75
+ };
76
+ })
77
+ .filter((m) => m.score > 0)
78
+ .sort((a, b) => b.score - a.score || (a.created_at < b.created_at ? 1 : -1))
79
+ .slice(0, maxResults);
80
+ }
81
+ }
@@ -0,0 +1 @@
1
+ export declare function startServer(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,110 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { loadConfig } from './config.js';
5
+ import { LocalStore } from './local-store.js';
6
+ import { CloudClient } from './cloud-client.js';
7
+ // Consistent attribution string across every surface (Claude Code terminal
8
+ // output, Cursor's agent panel, future surfaces). See spec section 2.4 —
9
+ // the same short string everywhere is what makes the brand legible.
10
+ const attributionFooter = (n) => `· via threadctx — shared team memory (${n} hit${n === 1 ? '' : 's'})`;
11
+ export async function startServer() {
12
+ const config = loadConfig();
13
+ if (config.mode === 'cloud' && !config.apiKey) {
14
+ console.error('[threadctx] THREADCTX_MODE=cloud but no API key was found. Falling back to local mode. ' +
15
+ 'Set THREADCTX_API_KEY or run `npx threadctx init --mode=cloud --api-key=...`.');
16
+ }
17
+ const useCloud = config.mode === 'cloud' && Boolean(config.apiKey);
18
+ const localStore = useCloud ? null : new LocalStore(config.dbPath);
19
+ const cloudClient = useCloud ? new CloudClient(config.apiUrl, config.apiKey, config.actorId) : null;
20
+ const repo = config.repo;
21
+ const server = new Server({ name: 'threadctx', version: '0.1.0' }, { capabilities: { tools: {} } });
22
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
23
+ tools: [
24
+ {
25
+ name: 'memory_write',
26
+ description: 'Store a learning, decision, fix, or gotcha for this repository so other agents and ' +
27
+ 'teammates can find it later. Call this whenever you resolve a non-obvious bug, make an ' +
28
+ 'architectural decision, or discover something that would save someone time in the future.',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ content: {
33
+ type: 'string',
34
+ description: 'The learning to remember, written so a future reader has full context.',
35
+ },
36
+ tags: {
37
+ type: 'array',
38
+ items: { type: 'string' },
39
+ description: 'Optional short tags, e.g. ["incident", "retry-logic"].',
40
+ },
41
+ },
42
+ required: ['content'],
43
+ },
44
+ },
45
+ {
46
+ name: 'memory_query',
47
+ description: "Retrieve relevant past learnings, fixes, decisions, or gotchas from the team's shared " +
48
+ 'memory before starting risky or repeated work — e.g. touching a service that has caused ' +
49
+ 'incidents before, or implementing something similar to past work. Call this before, not after.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ task_description: { type: 'string', description: 'What you are about to do, in plain language.' },
54
+ max_results: { type: 'number', description: 'Max number of memories to return (default 5).' },
55
+ },
56
+ required: ['task_description'],
57
+ },
58
+ },
59
+ ],
60
+ }));
61
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
62
+ const { name, arguments: args } = request.params;
63
+ try {
64
+ if (name === 'memory_write') {
65
+ const content = String(args?.content ?? '');
66
+ const rawTags = args?.tags;
67
+ const tags = Array.isArray(rawTags) ? rawTags.map(String) : [];
68
+ if (!content.trim())
69
+ throw new Error('content is required');
70
+ const id = localStore
71
+ ? localStore.write(repo, content, tags)
72
+ : (await cloudClient.write(repo, content, tags)).memory_id;
73
+ return { content: [{ type: 'text', text: `Stored memory ${id} for ${repo}.` }] };
74
+ }
75
+ if (name === 'memory_query') {
76
+ const taskDescription = String(args?.task_description ?? '');
77
+ const rawMaxResults = args?.max_results;
78
+ const maxResults = typeof rawMaxResults === 'number' ? rawMaxResults : 5;
79
+ if (!taskDescription.trim())
80
+ throw new Error('task_description is required');
81
+ let bundle;
82
+ let hitCount;
83
+ if (localStore) {
84
+ const hits = localStore.query(repo, taskDescription, maxResults);
85
+ hitCount = hits.length;
86
+ bundle = hits.map((h) => `- ${h.content}`).join('\n');
87
+ }
88
+ else {
89
+ const result = await cloudClient.query(repo, taskDescription, maxResults);
90
+ hitCount = result.referenced_memory_ids?.length ?? 0;
91
+ bundle = result.context_bundle ?? '';
92
+ }
93
+ if (hitCount === 0) {
94
+ return { content: [{ type: 'text', text: 'No relevant team memory found for this task.' }] };
95
+ }
96
+ return {
97
+ content: [{ type: 'text', text: `${bundle}\n\n${attributionFooter(hitCount)}` }],
98
+ };
99
+ }
100
+ throw new Error(`Unknown tool: ${name}`);
101
+ }
102
+ catch (err) {
103
+ const message = err instanceof Error ? err.message : String(err);
104
+ return { content: [{ type: 'text', text: `threadctx error: ${message}` }], isError: true };
105
+ }
106
+ });
107
+ const transport = new StdioServerTransport();
108
+ await server.connect(transport);
109
+ console.error(`[threadctx] MCP server running in ${useCloud ? 'cloud' : 'local'} mode for repo "${repo}".`);
110
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "threadctx-mcp",
3
+ "version": "0.1.0",
4
+ "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
+ "type": "module",
6
+ "bin": {
7
+ "threadctx": "dist/cli.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "dev": "tsx watch src/cli.ts",
14
+ "start": "node dist/cli.js",
15
+ "test": "npm run build && node test/smoke.mjs",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.4"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.14.10",
23
+ "tsx": "^4.16.2",
24
+ "typescript": "^5.5.4"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.18.0"
28
+ },
29
+ "license": "MIT",
30
+ "homepage": "https://threadctx.dev",
31
+ "repository": { "type": "git", "url": "git+https://github.com/threadctx-dev/threadctx.git", "directory": "mcp-server" },
32
+ "bugs": { "url": "https://github.com/threadctx-dev/threadctx/issues" },
33
+ "publishConfig": { "access": "public" },
34
+ "keywords": ["mcp", "model-context-protocol", "claude-code", "cursor", "ai-agent-memory", "shared-memory", "team-memory"]
35
+ }