@tpsdev-ai/flair-mcp 0.7.0 → 0.8.1

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.
Files changed (2) hide show
  1. package/dist/index.js +58 -8
  2. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -27,8 +27,16 @@ function classifyError(err, flairUrl) {
27
27
  const { status, body } = err;
28
28
  if (status === 400)
29
29
  return `validation_error: ${body}`;
30
- if (status === 401 || status === 403)
31
- return `auth_error: ${body}`;
30
+ if (status === 401 || status === 403) {
31
+ // Auth failure on a previously-working session usually means the daemon
32
+ // restarted (config reload, Harper alter_user, port change). Tell the
33
+ // operator how to recover instead of just surfacing the raw 401 body.
34
+ return `auth_error: ${body}\n` +
35
+ `(Hint: this often follows a Flair daemon restart. Try:\n` +
36
+ ` 1. Restart your MCP host (Claude Code, Cursor, etc) to spawn a fresh flair-mcp.\n` +
37
+ ` 2. Check daemon: 'flair status' or 'curl ${flairUrl}/Health'.\n` +
38
+ ` 3. Verify your agent key still matches the registered Agent record.)`;
39
+ }
32
40
  if (status === 413)
33
41
  return `payload_too_large: ${body}`;
34
42
  if (status === 429)
@@ -42,7 +50,10 @@ function classifyError(err, flairUrl) {
42
50
  return "timeout — the server took too long. This often happens with large content that requires embedding. Try shorter content or retry.";
43
51
  }
44
52
  if (err instanceof TypeError && err.message.includes("fetch")) {
45
- return `connection_error (retriable): could not reach Flair at ${flairUrl}. Is it running?`;
53
+ return `connection_error (retriable): could not reach Flair at ${flairUrl}. Is it running?\n` +
54
+ `(Diagnostics:\n` +
55
+ ` - 'curl ${flairUrl}/Health' — if this responds 200 or 401, daemon is up + this is an auth issue not a connection one.\n` +
56
+ ` - 'launchctl list | grep flair' (macOS) or 'systemctl status flair' (Linux).)`;
46
57
  }
47
58
  return `unexpected_error: ${err.message}`;
48
59
  }
@@ -51,6 +62,47 @@ function classifyError(err, flairUrl) {
51
62
  function errorResult(err, flairUrl) {
52
63
  return { content: [{ type: "text", text: classifyError(err, flairUrl) }], isError: true };
53
64
  }
65
+ // ─── Parent-exit watcher ────────────────────────────────────────────────────
66
+ //
67
+ // flair-mcp runs as a child of an MCP host (Claude Code, Cursor, etc) over
68
+ // stdio. When the host exits cleanly it should close stdin/stdout — but in
69
+ // practice we've seen flair-mcp processes orphaned for weeks (PID 1 as
70
+ // parent), holding stale tokens and consuming RAM.
71
+ //
72
+ // Poll process.ppid every 5s. If it drops to 1 (init), the parent died and
73
+ // we got reparented — exit cleanly. Cheap, cross-platform, no native deps.
74
+ // Clamp the poll interval to a safe range. `process.env.FOO ?? 5000` is NOT
75
+ // safe on its own: `??` only falls through on null/undefined, so an empty-string
76
+ // override (`FLAIR_MCP_PARENT_POLL_MS=`) yields `Number("") === 0` and creates
77
+ // a tight CPU-busy loop. Validate explicitly. (Sherlock review on #315.)
78
+ const PARENT_POLL_INTERVAL_MS = (() => {
79
+ const raw = process.env.FLAIR_MCP_PARENT_POLL_MS;
80
+ const parsed = raw != null ? Number(raw) : NaN;
81
+ const FLOOR_MS = 100;
82
+ const CEILING_MS = 30_000;
83
+ return Number.isFinite(parsed) && parsed >= FLOOR_MS && parsed <= CEILING_MS
84
+ ? parsed
85
+ : 5000;
86
+ })();
87
+ const initialPpid = process.ppid;
88
+ setInterval(() => {
89
+ // ppid === 1 means init/launchd has adopted us — original parent died.
90
+ if (process.ppid === 1 && initialPpid !== 1) {
91
+ console.error("flair-mcp: parent process died (re-parented to init); exiting cleanly.");
92
+ process.exit(0);
93
+ }
94
+ }, PARENT_POLL_INTERVAL_MS).unref();
95
+ // Also handle stdin EOF — MCP host closing the pipe means session ended.
96
+ // (StdioServerTransport handles this internally for the MCP protocol, but
97
+ // belt-and-suspenders: if stdin closes we exit, full stop.)
98
+ process.stdin.on("close", () => {
99
+ console.error("flair-mcp: stdin closed; exiting cleanly.");
100
+ process.exit(0);
101
+ });
102
+ process.stdin.on("end", () => {
103
+ console.error("flair-mcp: stdin EOF; exiting cleanly.");
104
+ process.exit(0);
105
+ });
54
106
  // ─── Client setup ────────────────────────────────────────────────────────────
55
107
  const agentId = process.env.FLAIR_AGENT_ID;
56
108
  if (!agentId) {
@@ -109,11 +161,9 @@ server.tool("memory_store", "Save information to persistent memory. Use for less
109
161
  dedup: true,
110
162
  dedupThreshold: 0.95,
111
163
  });
112
- // Check if dedup returned an existing memory (different ID than what we generated)
113
- const generatedPrefix = `${agentId}-`;
114
- const wasDeduped = result.id && !result.id.startsWith(generatedPrefix);
115
- if (wasDeduped) {
116
- return { content: [{ type: "text", text: `Similar memory already exists (id: ${result.id}): ${result.content?.slice(0, 200)}` }] };
164
+ // Signal when dedup returned an existing memory instead of writing.
165
+ if (result.deduped) {
166
+ return { content: [{ type: "text", text: `Similar memory already exists (id: ${result.id}): ${result.content?.slice(0, 200)}\n(no new entry written)` }] };
117
167
  }
118
168
  const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
119
169
  const tagStr = tags && tags.length > 0 ? tags.join(", ") : "none";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server for Flair — persistent memory for Claude Code, Cursor, and any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "tsc --noCheck",
17
- "prepublishOnly": "npm run build"
17
+ "prepublishOnly": "npm run build",
18
+ "postinstall": "node -e \"try{const{chmodSync,statSync}=require('fs');const p='dist/index.js';if(statSync(p).isFile()){chmodSync(p,0o755);console.error('@tpsdev-ai/flair-mcp: chmod +x ' + p + ' OK')}}catch(e){if(e.code!=='ENOENT')console.error('postinstall warn:',e.message)}\""
18
19
  },
19
20
  "publishConfig": {
20
21
  "access": "public"
@@ -24,13 +25,13 @@
24
25
  },
25
26
  "dependencies": {
26
27
  "@modelcontextprotocol/sdk": "1.27.1",
27
- "@tpsdev-ai/flair-client": "0.7.0",
28
+ "@tpsdev-ai/flair-client": "0.8.1",
28
29
  "zod": "4.3.6"
29
30
  },
30
31
  "license": "Apache-2.0",
31
32
  "repository": {
32
33
  "type": "git",
33
- "url": "https://github.com/tpsdev-ai/flair.git",
34
+ "url": "git+https://github.com/tpsdev-ai/flair.git",
34
35
  "directory": "packages/flair-mcp"
35
36
  },
36
37
  "homepage": "https://tps.dev/#flair",