@tpsdev-ai/flair-mcp 0.15.0 → 0.16.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 +5 -6
- package/dist/index.d.ts +13 -3
- package/dist/index.js +307 -267
- package/dist/mcp-shim.cjs +68 -0
- package/dist/mcp-shim.d.cts +31 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ cat > .mcp.json << 'EOF'
|
|
|
13
13
|
"mcpServers": {
|
|
14
14
|
"flair": {
|
|
15
15
|
"command": "npx",
|
|
16
|
-
"args": ["@tpsdev-ai/flair-mcp"],
|
|
16
|
+
"args": ["-y", "@tpsdev-ai/flair-mcp"],
|
|
17
17
|
"env": {
|
|
18
18
|
"FLAIR_AGENT_ID": "my-project"
|
|
19
19
|
}
|
|
@@ -23,16 +23,15 @@ cat > .mcp.json << 'EOF'
|
|
|
23
23
|
EOF
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
`npx -y @tpsdev-ai/flair-mcp` fetches and runs the server on demand — no global install needed. (`flair init` wires this for you automatically; see below.)
|
|
27
27
|
|
|
28
28
|
### Prerequisites
|
|
29
29
|
|
|
30
|
-
You need a running Flair instance:
|
|
30
|
+
You need a running Flair instance. The one-command front door:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
npm install -g @tpsdev-ai/flair
|
|
34
|
-
flair init
|
|
35
|
-
flair agent add my-project
|
|
34
|
+
flair init --agent my-project # installs Harper, creates the agent, wires MCP clients
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
## Tools
|
|
@@ -76,7 +75,7 @@ Point to a remote Flair instance:
|
|
|
76
75
|
"mcpServers": {
|
|
77
76
|
"flair": {
|
|
78
77
|
"command": "npx",
|
|
79
|
-
"args": ["@tpsdev-ai/flair-mcp"],
|
|
78
|
+
"args": ["-y", "@tpsdev-ai/flair-mcp"],
|
|
80
79
|
"env": {
|
|
81
80
|
"FLAIR_AGENT_ID": "my-project",
|
|
82
81
|
"FLAIR_URL": "http://your-server:19926"
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,19 @@
|
|
|
14
14
|
* - flair_orgevent — publish an OrgEvent attributed to self (no forging)
|
|
15
15
|
*
|
|
16
16
|
* Usage:
|
|
17
|
-
* npx @tpsdev-ai/flair-mcp
|
|
17
|
+
* npx -y @tpsdev-ai/flair-mcp
|
|
18
18
|
*
|
|
19
19
|
* Claude Code .mcp.json:
|
|
20
|
-
* { "mcpServers": { "flair": { "command": "npx", "args": ["@tpsdev-ai/flair-mcp"] } } }
|
|
20
|
+
* { "mcpServers": { "flair": { "command": "npx", "args": ["-y", "@tpsdev-ai/flair-mcp"] } } }
|
|
21
|
+
*
|
|
22
|
+
* BIN ENTRY / NODE-VERSION PREFLIGHT
|
|
23
|
+
* ----------------------------------
|
|
24
|
+
* The published `flair-mcp` bin is NOT this file — it is the CommonJS preflight
|
|
25
|
+
* shim (dist/mcp-shim.cjs, compiled from src/mcp-shim.cts). This module is an
|
|
26
|
+
* ES module: its top-level imports are hoisted and the whole graph is linked +
|
|
27
|
+
* evaluated before any in-file guard could run, so on an old Node it crashes
|
|
28
|
+
* during linking (the SDK + flair deps need Node >= 22) before printing anything
|
|
29
|
+
* — the silent `npx -y @tpsdev-ai/flair-mcp` failure. The shim checks the Node
|
|
30
|
+
* version FIRST, then dynamically imports this module and calls runMcp().
|
|
21
31
|
*/
|
|
22
|
-
export
|
|
32
|
+
export declare function runMcp(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -14,10 +14,20 @@
|
|
|
14
14
|
* - flair_orgevent — publish an OrgEvent attributed to self (no forging)
|
|
15
15
|
*
|
|
16
16
|
* Usage:
|
|
17
|
-
* npx @tpsdev-ai/flair-mcp
|
|
17
|
+
* npx -y @tpsdev-ai/flair-mcp
|
|
18
18
|
*
|
|
19
19
|
* Claude Code .mcp.json:
|
|
20
|
-
* { "mcpServers": { "flair": { "command": "npx", "args": ["@tpsdev-ai/flair-mcp"] } } }
|
|
20
|
+
* { "mcpServers": { "flair": { "command": "npx", "args": ["-y", "@tpsdev-ai/flair-mcp"] } } }
|
|
21
|
+
*
|
|
22
|
+
* BIN ENTRY / NODE-VERSION PREFLIGHT
|
|
23
|
+
* ----------------------------------
|
|
24
|
+
* The published `flair-mcp` bin is NOT this file — it is the CommonJS preflight
|
|
25
|
+
* shim (dist/mcp-shim.cjs, compiled from src/mcp-shim.cts). This module is an
|
|
26
|
+
* ES module: its top-level imports are hoisted and the whole graph is linked +
|
|
27
|
+
* evaluated before any in-file guard could run, so on an old Node it crashes
|
|
28
|
+
* during linking (the SDK + flair deps need Node >= 22) before printing anything
|
|
29
|
+
* — the silent `npx -y @tpsdev-ai/flair-mcp` failure. The shim checks the Node
|
|
30
|
+
* version FIRST, then dynamically imports this module and calls runMcp().
|
|
21
31
|
*/
|
|
22
32
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
23
33
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -64,277 +74,307 @@ function classifyError(err, flairUrl) {
|
|
|
64
74
|
function errorResult(err, flairUrl) {
|
|
65
75
|
return { content: [{ type: "text", text: classifyError(err, flairUrl) }], isError: true };
|
|
66
76
|
}
|
|
67
|
-
// ───
|
|
68
|
-
//
|
|
69
|
-
// flair-mcp runs as a child of an MCP host (Claude Code, Cursor, etc) over
|
|
70
|
-
// stdio. When the host exits cleanly it should close stdin/stdout — but in
|
|
71
|
-
// practice we've seen flair-mcp processes orphaned for weeks (PID 1 as
|
|
72
|
-
// parent), holding stale tokens and consuming RAM.
|
|
77
|
+
// ─── Entry point ──────────────────────────────────────────────────────────────
|
|
73
78
|
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
// runMcp() is the real entry point. It is exported so the CommonJS preflight
|
|
80
|
+
// shim (mcp-shim.cts → dist/mcp-shim.cjs, the published bin) can invoke it after
|
|
81
|
+
// its Node-version check passes — the shim imports this module, so a top-level
|
|
82
|
+
// `import.meta.main` guard would be false there. Everything that has runtime
|
|
83
|
+
// side effects (the FLAIR_AGENT_ID check, FlairClient construction, the parent-
|
|
84
|
+
// exit watcher, tool registration, and the stdio connect) lives inside runMcp()
|
|
85
|
+
// so that merely importing this module (e.g. from the shim before the version
|
|
86
|
+
// check, or from a test) does nothing until runMcp() is called.
|
|
87
|
+
export async function runMcp() {
|
|
88
|
+
// ─── Parent-exit watcher ──────────────────────────────────────────────────
|
|
89
|
+
//
|
|
90
|
+
// flair-mcp runs as a child of an MCP host (Claude Code, Cursor, etc) over
|
|
91
|
+
// stdio. When the host exits cleanly it should close stdin/stdout — but in
|
|
92
|
+
// practice we've seen flair-mcp processes orphaned for weeks (PID 1 as
|
|
93
|
+
// parent), holding stale tokens and consuming RAM.
|
|
94
|
+
//
|
|
95
|
+
// Poll process.ppid every 5s. If it drops to 1 (init), the parent died and
|
|
96
|
+
// we got reparented — exit cleanly. Cheap, cross-platform, no native deps.
|
|
97
|
+
// Clamp the poll interval to a safe range. `process.env.FOO ?? 5000` is NOT
|
|
98
|
+
// safe on its own: `??` only falls through on null/undefined, so an empty-string
|
|
99
|
+
// override (`FLAIR_MCP_PARENT_POLL_MS=`) yields `Number("") === 0` and creates
|
|
100
|
+
// a tight CPU-busy loop. Validate explicitly. (Sherlock review on #315.)
|
|
101
|
+
const PARENT_POLL_INTERVAL_MS = (() => {
|
|
102
|
+
const raw = process.env.FLAIR_MCP_PARENT_POLL_MS;
|
|
103
|
+
const parsed = raw != null ? Number(raw) : NaN;
|
|
104
|
+
const FLOOR_MS = 100;
|
|
105
|
+
const CEILING_MS = 30_000;
|
|
106
|
+
return Number.isFinite(parsed) && parsed >= FLOOR_MS && parsed <= CEILING_MS
|
|
107
|
+
? parsed
|
|
108
|
+
: 5000;
|
|
109
|
+
})();
|
|
110
|
+
const initialPpid = process.ppid;
|
|
111
|
+
setInterval(() => {
|
|
112
|
+
// ppid === 1 means init/launchd has adopted us — original parent died.
|
|
113
|
+
if (process.ppid === 1 && initialPpid !== 1) {
|
|
114
|
+
console.error("flair-mcp: parent process died (re-parented to init); exiting cleanly.");
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
}, PARENT_POLL_INTERVAL_MS).unref();
|
|
118
|
+
// Also handle stdin EOF — MCP host closing the pipe means session ended.
|
|
119
|
+
// (StdioServerTransport handles this internally for the MCP protocol, but
|
|
120
|
+
// belt-and-suspenders: if stdin closes we exit, full stop.)
|
|
121
|
+
process.stdin.on("close", () => {
|
|
122
|
+
console.error("flair-mcp: stdin closed; exiting cleanly.");
|
|
94
123
|
process.exit(0);
|
|
124
|
+
});
|
|
125
|
+
process.stdin.on("end", () => {
|
|
126
|
+
console.error("flair-mcp: stdin EOF; exiting cleanly.");
|
|
127
|
+
process.exit(0);
|
|
128
|
+
});
|
|
129
|
+
// ─── Client setup ────────────────────────────────────────────────────────────
|
|
130
|
+
const agentId = process.env.FLAIR_AGENT_ID;
|
|
131
|
+
if (!agentId) {
|
|
132
|
+
console.error("FLAIR_AGENT_ID is required. Set it in your .mcp.json env or shell.");
|
|
133
|
+
process.exit(1);
|
|
95
134
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
server.tool("memory_search", "Search memories by meaning. Understands temporal queries like 'what happened today'.", {
|
|
126
|
-
query: z.string().describe("Search query — natural language, semantic matching"),
|
|
127
|
-
limit: z.coerce.number().optional().default(5).describe("Max results (default 5)"),
|
|
128
|
-
}, async ({ query, limit }) => {
|
|
129
|
-
try {
|
|
130
|
-
const results = await flair.memory.search(query, { limit });
|
|
131
|
-
if (results.length === 0) {
|
|
132
|
-
return { content: [{ type: "text", text: "No relevant memories found." }] };
|
|
135
|
+
const flair = new FlairClient({
|
|
136
|
+
agentId,
|
|
137
|
+
url: process.env.FLAIR_URL,
|
|
138
|
+
keyPath: process.env.FLAIR_KEY_PATH,
|
|
139
|
+
});
|
|
140
|
+
// ─── MCP Server ──────────────────────────────────────────────────────────────
|
|
141
|
+
const server = new McpServer({
|
|
142
|
+
name: "flair",
|
|
143
|
+
version: "0.1.0",
|
|
144
|
+
});
|
|
145
|
+
// ─── Tools ───────────────────────────────────────────────────────────────────
|
|
146
|
+
server.tool("memory_search", "Search memories by meaning. Understands temporal queries like 'what happened today'.", {
|
|
147
|
+
query: z.string().describe("Search query — natural language, semantic matching"),
|
|
148
|
+
limit: z.coerce.number().optional().default(5).describe("Max results (default 5)"),
|
|
149
|
+
}, async ({ query, limit }) => {
|
|
150
|
+
try {
|
|
151
|
+
const results = await flair.memory.search(query, { limit });
|
|
152
|
+
if (results.length === 0) {
|
|
153
|
+
return { content: [{ type: "text", text: "No relevant memories found." }] };
|
|
154
|
+
}
|
|
155
|
+
const text = results
|
|
156
|
+
.map((r, i) => {
|
|
157
|
+
const date = r.createdAt ? r.createdAt.slice(0, 10) : "";
|
|
158
|
+
const idStr = r.id ? `id:${r.id}` : "";
|
|
159
|
+
const meta = [date, r.type, idStr].filter(Boolean).join(", ");
|
|
160
|
+
return `${i + 1}. ${r.content}${meta ? ` (${meta})` : ""}`;
|
|
161
|
+
})
|
|
162
|
+
.join("\n");
|
|
163
|
+
return { content: [{ type: "text", text }] };
|
|
133
164
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
165
|
+
catch (err) {
|
|
166
|
+
return errorResult(err, flair.url);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
server.tool("memory_store", "Save information to persistent memory. Use for lessons, decisions, preferences, facts.", {
|
|
170
|
+
content: z.string().describe("What to remember"),
|
|
171
|
+
type: z.enum(["session", "lesson", "decision", "preference", "fact", "goal"]).optional().default("session"),
|
|
172
|
+
durability: z.enum(["permanent", "persistent", "standard", "ephemeral"]).optional().default("standard")
|
|
173
|
+
.describe("permanent — inviolable facts, identity, explicit never-forget (e.g., 'my name is Nathan')\n" +
|
|
174
|
+
"persistent — key decisions and lessons to recall weeks later (e.g., 'PR review process')\n" +
|
|
175
|
+
"standard — default working memory, recent context (e.g., 'discussed auth flow today')\n" +
|
|
176
|
+
"ephemeral — scratch state, auto-expires 72h (e.g., 'currently debugging issue #42')"),
|
|
177
|
+
tags: z.array(z.string()).optional().describe("Array of tag strings"),
|
|
178
|
+
}, async ({ content, type, durability, tags }) => {
|
|
179
|
+
try {
|
|
180
|
+
const result = await flair.memory.write(content, {
|
|
181
|
+
type: type,
|
|
182
|
+
durability: durability,
|
|
183
|
+
tags,
|
|
184
|
+
dedup: true,
|
|
185
|
+
dedupThreshold: 0.95,
|
|
186
|
+
});
|
|
187
|
+
// Dedup path: existing memory matched, NEW content was NOT written.
|
|
188
|
+
// Emit both prose AND structuredContent so callers can react
|
|
189
|
+
// programmatically even when LLMs compress prose imprecisely
|
|
190
|
+
// (see flair#449 — work agent missed dedup signal and lost data).
|
|
191
|
+
if (result.deduped) {
|
|
192
|
+
const sc = {
|
|
193
|
+
deduplicated: true,
|
|
194
|
+
mergedWith: result.id,
|
|
195
|
+
existingContent: result.content,
|
|
196
|
+
written: false,
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
content: [{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: `⚠️ DEDUPLICATED — new content was NOT written. ` +
|
|
202
|
+
`Matched existing memory id=${result.id}: ${result.content?.slice(0, 200)}\n\n` +
|
|
203
|
+
`If the new content is genuinely distinct, retry with different phrasing ` +
|
|
204
|
+
`or call memory_store with the same id to update.`,
|
|
205
|
+
}],
|
|
206
|
+
structuredContent: sc,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
|
|
210
|
+
const tagStr = tags && tags.length > 0 ? tags.join(", ") : "none";
|
|
211
|
+
const text = [
|
|
212
|
+
`Memory stored (id: ${result.id})`,
|
|
213
|
+
`Preview: ${preview}`,
|
|
214
|
+
`Size: ${content.length} chars`,
|
|
215
|
+
`Tags: ${tagStr}`,
|
|
216
|
+
`Type: ${type}, Durability: ${durability}`,
|
|
217
|
+
].join("\n");
|
|
177
218
|
return {
|
|
178
|
-
content: [{
|
|
179
|
-
|
|
180
|
-
text: `⚠️ DEDUPLICATED — new content was NOT written. ` +
|
|
181
|
-
`Matched existing memory id=${result.id}: ${result.content?.slice(0, 200)}\n\n` +
|
|
182
|
-
`If the new content is genuinely distinct, retry with different phrasing ` +
|
|
183
|
-
`or call memory_store with the same id to update.`,
|
|
184
|
-
}],
|
|
185
|
-
structuredContent: sc,
|
|
219
|
+
content: [{ type: "text", text }],
|
|
220
|
+
structuredContent: { deduplicated: false, id: result.id, written: true },
|
|
186
221
|
};
|
|
187
222
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const text = [
|
|
191
|
-
`Memory stored (id: ${result.id})`,
|
|
192
|
-
`Preview: ${preview}`,
|
|
193
|
-
`Size: ${content.length} chars`,
|
|
194
|
-
`Tags: ${tagStr}`,
|
|
195
|
-
`Type: ${type}, Durability: ${durability}`,
|
|
196
|
-
].join("\n");
|
|
197
|
-
return {
|
|
198
|
-
content: [{ type: "text", text }],
|
|
199
|
-
structuredContent: { deduplicated: false, id: result.id, written: true },
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
catch (err) {
|
|
203
|
-
return errorResult(err, flair.url);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
server.tool("memory_get", "Retrieve a specific memory by ID.", {
|
|
207
|
-
id: z.string().describe("Memory ID"),
|
|
208
|
-
}, async ({ id }) => {
|
|
209
|
-
try {
|
|
210
|
-
const mem = await flair.memory.get(id);
|
|
211
|
-
if (!mem)
|
|
212
|
-
return { content: [{ type: "text", text: `Memory ${id} not found.` }] };
|
|
213
|
-
return { content: [{ type: "text", text: `${mem.content}\n\n(type: ${mem.type}, durability: ${mem.durability}, created: ${mem.createdAt})` }] };
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
return errorResult(err, flair.url);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
server.tool("memory_delete", "Delete a memory by ID.", {
|
|
220
|
-
id: z.string().describe("Memory ID to delete"),
|
|
221
|
-
}, async ({ id }) => {
|
|
222
|
-
try {
|
|
223
|
-
await flair.memory.delete(id);
|
|
224
|
-
return { content: [{ type: "text", text: `Memory ${id} deleted.` }] };
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
return errorResult(err, flair.url);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
server.tool("bootstrap", "Get session context: soul + memories + predicted context. Run at session start. Pass subjects for predictive loading.", {
|
|
231
|
-
maxTokens: z.coerce.number().optional().default(4000).describe("Max tokens in output"),
|
|
232
|
-
currentTask: z.string().optional().describe("Current task description — enables semantic search for relevant memories"),
|
|
233
|
-
channel: z.string().optional().describe("Channel name (discord, tps-mail, claude-code) — shapes context prediction"),
|
|
234
|
-
surface: z.string().optional().describe("Surface name (tps-build, tps-review, cli-session) — narrows prediction"),
|
|
235
|
-
subjects: z.array(z.string()).optional().describe("Entity names to preload context for (e.g., ['flair', 'auth'])"),
|
|
236
|
-
}, async ({ maxTokens, currentTask, channel, surface, subjects }) => {
|
|
237
|
-
try {
|
|
238
|
-
const result = await flair.bootstrap({ maxTokens, currentTask, channel, surface, subjects });
|
|
239
|
-
if (!result.context) {
|
|
240
|
-
return { content: [{ type: "text", text: "No context available." }] };
|
|
223
|
+
catch (err) {
|
|
224
|
+
return errorResult(err, flair.url);
|
|
241
225
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
226
|
+
});
|
|
227
|
+
server.tool("memory_get", "Retrieve a specific memory by ID.", {
|
|
228
|
+
id: z.string().describe("Memory ID"),
|
|
229
|
+
}, async ({ id }) => {
|
|
230
|
+
try {
|
|
231
|
+
const mem = await flair.memory.get(id);
|
|
232
|
+
if (!mem)
|
|
233
|
+
return { content: [{ type: "text", text: `Memory ${id} not found.` }] };
|
|
234
|
+
return { content: [{ type: "text", text: `${mem.content}\n\n(type: ${mem.type}, durability: ${mem.durability}, created: ${mem.createdAt})` }] };
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
return errorResult(err, flair.url);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
server.tool("memory_delete", "Delete a memory by ID.", {
|
|
241
|
+
id: z.string().describe("Memory ID to delete"),
|
|
242
|
+
}, async ({ id }) => {
|
|
243
|
+
try {
|
|
244
|
+
await flair.memory.delete(id);
|
|
245
|
+
return { content: [{ type: "text", text: `Memory ${id} deleted.` }] };
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
return errorResult(err, flair.url);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
server.tool("bootstrap", "Get session context: soul + memories + predicted context. Run at session start. Pass subjects for predictive loading.", {
|
|
252
|
+
maxTokens: z.coerce.number().optional().default(4000).describe("Max tokens in output"),
|
|
253
|
+
currentTask: z.string().optional().describe("Current task description — enables semantic search for relevant memories"),
|
|
254
|
+
channel: z.string().optional().describe("Channel name (discord, tps-mail, claude-code) — shapes context prediction"),
|
|
255
|
+
surface: z.string().optional().describe("Surface name (tps-build, tps-review, cli-session) — narrows prediction"),
|
|
256
|
+
subjects: z.array(z.string()).optional().describe("Entity names to preload context for (e.g., ['flair', 'auth'])"),
|
|
257
|
+
}, async ({ maxTokens, currentTask, channel, surface, subjects }) => {
|
|
258
|
+
try {
|
|
259
|
+
const result = await flair.bootstrap({ maxTokens, currentTask, channel, surface, subjects });
|
|
260
|
+
if (!result.context) {
|
|
261
|
+
return { content: [{ type: "text", text: "No context available." }] };
|
|
262
|
+
}
|
|
263
|
+
return { content: [{ type: "text", text: result.context }] };
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
return errorResult(err, flair.url);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
server.tool("soul_set", "Set a personality or project context entry. Included in every bootstrap.", {
|
|
270
|
+
key: z.string().describe("Entry key (e.g., 'role', 'standards', 'project')"),
|
|
271
|
+
value: z.string().describe("Entry value — personality trait, project context, coding standards, etc."),
|
|
272
|
+
}, async ({ key, value }) => {
|
|
273
|
+
try {
|
|
274
|
+
await flair.soul.set(key, value);
|
|
275
|
+
return { content: [{ type: "text", text: `Soul entry '${key}' set.` }] };
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
return errorResult(err, flair.url);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
server.tool("soul_get", "Get a personality or project context entry.", {
|
|
282
|
+
key: z.string().describe("Entry key"),
|
|
283
|
+
}, async ({ key }) => {
|
|
284
|
+
try {
|
|
285
|
+
const entry = await flair.soul.get(key);
|
|
286
|
+
if (!entry)
|
|
287
|
+
return { content: [{ type: "text", text: `No soul entry for '${key}'.` }] };
|
|
288
|
+
return { content: [{ type: "text", text: entry.value }] };
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
return errorResult(err, flair.url);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
// ─── Coordination write surface ──────────────────────────────────────────────
|
|
295
|
+
//
|
|
296
|
+
// flair_workspace_set + flair_orgevent let an agent write the Office Space
|
|
297
|
+
// coordination layer without hand-rolling signed HTTP. Both go through
|
|
298
|
+
// flair.request(), which signs with the agent's Ed25519 key — so identity
|
|
299
|
+
// (WorkspaceState.agentId / OrgEvent.authorId) is taken from the SIGNATURE on
|
|
300
|
+
// the server side, NEVER the body. We deliberately do NOT send agentId/authorId
|
|
301
|
+
// in the body; the handlers attribute the write to the authenticated agent, so
|
|
302
|
+
// an agent can only write AS itself (no forging).
|
|
303
|
+
server.tool("flair_workspace_set", "Set your agent's current workspace state in the Office Space coordination layer (ref/branch, phase, task). Attributed to you from your signed identity — you can only write your own state.", {
|
|
304
|
+
ref: z.string().describe("Workspace ref — branch, worktree, or task ref"),
|
|
305
|
+
label: z.string().optional().describe("Human-readable label for this workspace"),
|
|
306
|
+
provider: z.string().optional().default("mcp").describe("Provider/runtime (e.g. claude-code, openclaw)"),
|
|
307
|
+
task: z.string().optional().describe("Task/issue id this workspace is attached to"),
|
|
308
|
+
phase: z.string().optional().describe("Current phase (e.g. design, implement, review)"),
|
|
309
|
+
summary: z.string().optional().describe("Short summary of current workspace state"),
|
|
310
|
+
}, async ({ ref, label, provider, task, phase, summary }) => {
|
|
311
|
+
try {
|
|
312
|
+
// No agentId in body — the server attributes from the signed identity.
|
|
313
|
+
const body = {
|
|
314
|
+
id: `${agentId}:${ref}`,
|
|
315
|
+
ref,
|
|
316
|
+
provider: provider ?? "mcp",
|
|
317
|
+
timestamp: new Date().toISOString(),
|
|
318
|
+
};
|
|
319
|
+
if (label)
|
|
320
|
+
body.label = label;
|
|
321
|
+
if (task)
|
|
322
|
+
body.taskId = task;
|
|
323
|
+
if (phase)
|
|
324
|
+
body.phase = phase;
|
|
325
|
+
if (summary)
|
|
326
|
+
body.summary = summary;
|
|
327
|
+
await flair.request("POST", "/WorkspaceState", body);
|
|
328
|
+
return { content: [{ type: "text", text: `Workspace state set: ref=${ref}${phase ? `, phase=${phase}` : ""} (attributed to ${agentId}).` }] };
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
return errorResult(err, flair.url);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
server.tool("flair_orgevent", "Publish an org-wide coordination event (claim/release/status) to the Office Space. Attributed to you from your signed identity — you cannot publish as another agent.", {
|
|
335
|
+
kind: z.string().describe("Event kind (e.g. coord.claim, coord.release, status)"),
|
|
336
|
+
summary: z.string().describe("Short summary of the event"),
|
|
337
|
+
detail: z.string().optional().describe("Longer detail payload"),
|
|
338
|
+
scope: z.string().optional().describe("Scope of the event (e.g. an agent id, repo, or 'org')"),
|
|
339
|
+
targets: z.array(z.string()).optional().describe("Recipient agent ids"),
|
|
340
|
+
}, async ({ kind, summary, detail, scope, targets }) => {
|
|
341
|
+
try {
|
|
342
|
+
// No authorId in body — the server attributes from the signed identity.
|
|
343
|
+
const body = { kind, summary };
|
|
344
|
+
if (detail)
|
|
345
|
+
body.detail = detail;
|
|
346
|
+
if (scope)
|
|
347
|
+
body.scope = scope;
|
|
348
|
+
if (targets && targets.length > 0)
|
|
349
|
+
body.targetIds = targets;
|
|
350
|
+
const result = await flair.request("POST", "/OrgEvent", body);
|
|
351
|
+
const targetStr = targets && targets.length > 0 ? ` → ${targets.join(", ")}` : "";
|
|
352
|
+
const idStr = result?.id ? ` (id: ${result.id})` : "";
|
|
353
|
+
return { content: [{ type: "text", text: `OrgEvent published: kind=${kind}${targetStr} (attributed to ${agentId})${idStr}.` }] };
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
return errorResult(err, flair.url);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
360
|
+
const transport = new StdioServerTransport();
|
|
361
|
+
await server.connect(transport);
|
|
362
|
+
}
|
|
363
|
+
// ─── Entry point dispatch ──────────────────────────────────────────────────────
|
|
274
364
|
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// No agentId in body — the server attributes from the signed identity.
|
|
292
|
-
const body = {
|
|
293
|
-
id: `${agentId}:${ref}`,
|
|
294
|
-
ref,
|
|
295
|
-
provider: provider ?? "mcp",
|
|
296
|
-
timestamp: new Date().toISOString(),
|
|
297
|
-
};
|
|
298
|
-
if (label)
|
|
299
|
-
body.label = label;
|
|
300
|
-
if (task)
|
|
301
|
-
body.taskId = task;
|
|
302
|
-
if (phase)
|
|
303
|
-
body.phase = phase;
|
|
304
|
-
if (summary)
|
|
305
|
-
body.summary = summary;
|
|
306
|
-
await flair.request("POST", "/WorkspaceState", body);
|
|
307
|
-
return { content: [{ type: "text", text: `Workspace state set: ref=${ref}${phase ? `, phase=${phase}` : ""} (attributed to ${agentId}).` }] };
|
|
308
|
-
}
|
|
309
|
-
catch (err) {
|
|
310
|
-
return errorResult(err, flair.url);
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
server.tool("flair_orgevent", "Publish an org-wide coordination event (claim/release/status) to the Office Space. Attributed to you from your signed identity — you cannot publish as another agent.", {
|
|
314
|
-
kind: z.string().describe("Event kind (e.g. coord.claim, coord.release, status)"),
|
|
315
|
-
summary: z.string().describe("Short summary of the event"),
|
|
316
|
-
detail: z.string().optional().describe("Longer detail payload"),
|
|
317
|
-
scope: z.string().optional().describe("Scope of the event (e.g. an agent id, repo, or 'org')"),
|
|
318
|
-
targets: z.array(z.string()).optional().describe("Recipient agent ids"),
|
|
319
|
-
}, async ({ kind, summary, detail, scope, targets }) => {
|
|
320
|
-
try {
|
|
321
|
-
// No authorId in body — the server attributes from the signed identity.
|
|
322
|
-
const body = { kind, summary };
|
|
323
|
-
if (detail)
|
|
324
|
-
body.detail = detail;
|
|
325
|
-
if (scope)
|
|
326
|
-
body.scope = scope;
|
|
327
|
-
if (targets && targets.length > 0)
|
|
328
|
-
body.targetIds = targets;
|
|
329
|
-
const result = await flair.request("POST", "/OrgEvent", body);
|
|
330
|
-
const targetStr = targets && targets.length > 0 ? ` → ${targets.join(", ")}` : "";
|
|
331
|
-
const idStr = result?.id ? ` (id: ${result.id})` : "";
|
|
332
|
-
return { content: [{ type: "text", text: `OrgEvent published: kind=${kind}${targetStr} (attributed to ${agentId})${idStr}.` }] };
|
|
333
|
-
}
|
|
334
|
-
catch (err) {
|
|
335
|
-
return errorResult(err, flair.url);
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
339
|
-
const transport = new StdioServerTransport();
|
|
340
|
-
await server.connect(transport);
|
|
365
|
+
// Run directly when this module is the entry point — covers `bun src/index.ts`
|
|
366
|
+
// and `node dist/index.js`. The packaged bin goes through mcp-shim.cjs → runMcp()
|
|
367
|
+
// after its Node-version check, so import.meta.main is false there; without this
|
|
368
|
+
// the server would never start when invoked through the shim. (Matches the
|
|
369
|
+
// session-start-hook + CLI shim entry-point pattern.)
|
|
370
|
+
const importMeta = import.meta;
|
|
371
|
+
const isMain = importMeta.main === true ||
|
|
372
|
+
(typeof process !== "undefined" &&
|
|
373
|
+
process.argv[1] != null &&
|
|
374
|
+
import.meta.url === `file://${process.argv[1]}`);
|
|
375
|
+
if (isMain) {
|
|
376
|
+
void runMcp().catch((err) => {
|
|
377
|
+
console.error(err && err.stack ? err.stack : err);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* mcp-shim.cts — Node-version PREFLIGHT for the Flair MCP server.
|
|
5
|
+
*
|
|
6
|
+
* THIS IS THE BIN ENTRY (`package.json` "bin": { "flair-mcp": "dist/mcp-shim.cjs" }).
|
|
7
|
+
*
|
|
8
|
+
* Why a separate CommonJS file instead of a guard inside index.ts:
|
|
9
|
+
* The real MCP server (dist/index.js) is an ES module. In ESM, every top-level
|
|
10
|
+
* `import` is HOISTED and the whole module graph is LINKED + EVALUATED before
|
|
11
|
+
* the first statement in the file body runs. flair-mcp's deps (@modelcontext-
|
|
12
|
+
* protocol/sdk, @tpsdev-ai/flair-client and its transitive deps) require a
|
|
13
|
+
* modern engine, so on an older Node the import graph fails to load — and a
|
|
14
|
+
* Node-version check placed even at the very top of index.ts never executes:
|
|
15
|
+
* the import graph crashes first. That is exactly the silent-failure bug — a
|
|
16
|
+
* user wiring `npx -y @tpsdev-ai/flair-mcp` on an old Node gets zero output and
|
|
17
|
+
* a dead MCP server, with no actionable signal (the same exposure flair's CLI
|
|
18
|
+
* had before #524).
|
|
19
|
+
*
|
|
20
|
+
* CommonJS evaluates top-to-bottom and `require()`/`import()` are evaluated
|
|
21
|
+
* lazily — so the version check below runs and prints BEFORE anything tries to
|
|
22
|
+
* load the ESM server or any modern dependency. Because every Node since v0.x
|
|
23
|
+
* parses and runs CommonJS, this shim is guaranteed to run and print on the
|
|
24
|
+
* oldest Node a user could plausibly have.
|
|
25
|
+
*
|
|
26
|
+
* The check itself deliberately uses ONLY ancient-safe syntax — `var`, plain
|
|
27
|
+
* functions, string `.split`/`parseInt`, `console.error`, `process.exit`. No
|
|
28
|
+
* top-level await, no optional chaining, no template literals reaching
|
|
29
|
+
* modern-only APIs — so the guard can never become the thing that fails to
|
|
30
|
+
* parse.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
// ── Node-version preflight (must stay parse-safe + runtime-safe on old Node) ──
|
|
34
|
+
var MIN_NODE_MAJOR = 22;
|
|
35
|
+
function flairMcpCurrentNodeMajor() {
|
|
36
|
+
// process.versions.node is e.g. "18.20.4"; take the leading integer.
|
|
37
|
+
var raw = (process && process.versions && process.versions.node) || "0";
|
|
38
|
+
return parseInt(String(raw).split(".")[0], 10) || 0;
|
|
39
|
+
}
|
|
40
|
+
var flairMcpNodeMajor = flairMcpCurrentNodeMajor();
|
|
41
|
+
if (flairMcpNodeMajor < MIN_NODE_MAJOR) {
|
|
42
|
+
// Plain string concatenation — no template literals, no chalk, nothing that
|
|
43
|
+
// could itself trip on an old engine.
|
|
44
|
+
console.error("");
|
|
45
|
+
console.error(" flair-mcp requires Node.js >= " + MIN_NODE_MAJOR + ".");
|
|
46
|
+
console.error(" You are running Node.js " + (process.versions && process.versions.node ? process.versions.node : "(unknown)") + ".");
|
|
47
|
+
console.error("");
|
|
48
|
+
console.error(" Please upgrade Node and try again:");
|
|
49
|
+
console.error(" https://nodejs.org/ (or use nvm / fnm / volta)");
|
|
50
|
+
console.error("");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
// Node is new enough — hand off to the real ESM MCP server. Dynamic import()
|
|
54
|
+
// from CommonJS is the supported ESM-from-CJS bridge. We only reach this line on
|
|
55
|
+
// a supported Node, so the import() expression is never evaluated on an engine
|
|
56
|
+
// that can't load the ESM graph.
|
|
57
|
+
import("./index.js")
|
|
58
|
+
.then(function (mod) {
|
|
59
|
+
if (mod && typeof mod.runMcp === "function") {
|
|
60
|
+
return mod.runMcp();
|
|
61
|
+
}
|
|
62
|
+
// Older builds auto-ran on import via import.meta.main; nothing to call.
|
|
63
|
+
return undefined;
|
|
64
|
+
})
|
|
65
|
+
.catch(function (err) {
|
|
66
|
+
console.error(err && err.stack ? err.stack : err);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-shim.cts — Node-version PREFLIGHT for the Flair MCP server.
|
|
4
|
+
*
|
|
5
|
+
* THIS IS THE BIN ENTRY (`package.json` "bin": { "flair-mcp": "dist/mcp-shim.cjs" }).
|
|
6
|
+
*
|
|
7
|
+
* Why a separate CommonJS file instead of a guard inside index.ts:
|
|
8
|
+
* The real MCP server (dist/index.js) is an ES module. In ESM, every top-level
|
|
9
|
+
* `import` is HOISTED and the whole module graph is LINKED + EVALUATED before
|
|
10
|
+
* the first statement in the file body runs. flair-mcp's deps (@modelcontext-
|
|
11
|
+
* protocol/sdk, @tpsdev-ai/flair-client and its transitive deps) require a
|
|
12
|
+
* modern engine, so on an older Node the import graph fails to load — and a
|
|
13
|
+
* Node-version check placed even at the very top of index.ts never executes:
|
|
14
|
+
* the import graph crashes first. That is exactly the silent-failure bug — a
|
|
15
|
+
* user wiring `npx -y @tpsdev-ai/flair-mcp` on an old Node gets zero output and
|
|
16
|
+
* a dead MCP server, with no actionable signal (the same exposure flair's CLI
|
|
17
|
+
* had before #524).
|
|
18
|
+
*
|
|
19
|
+
* CommonJS evaluates top-to-bottom and `require()`/`import()` are evaluated
|
|
20
|
+
* lazily — so the version check below runs and prints BEFORE anything tries to
|
|
21
|
+
* load the ESM server or any modern dependency. Because every Node since v0.x
|
|
22
|
+
* parses and runs CommonJS, this shim is guaranteed to run and print on the
|
|
23
|
+
* oldest Node a user could plausibly have.
|
|
24
|
+
*
|
|
25
|
+
* The check itself deliberately uses ONLY ancient-safe syntax — `var`, plain
|
|
26
|
+
* functions, string `.split`/`parseInt`, `console.error`, `process.exit`. No
|
|
27
|
+
* top-level await, no optional chaining, no template literals reaching
|
|
28
|
+
* modern-only APIs — so the guard can never become the thing that fails to
|
|
29
|
+
* parse.
|
|
30
|
+
*/
|
|
31
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"flair-mcp": "dist/
|
|
8
|
+
"flair-mcp": "dist/mcp-shim.cjs",
|
|
9
9
|
"flair-session-start": "dist/session-start-hook.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
"build": "tsc --noCheck",
|
|
18
18
|
"test": "bun test",
|
|
19
19
|
"prepublishOnly": "npm run build",
|
|
20
|
-
"postinstall": "node -e \"try{const{chmodSync,statSync}=require('fs');for(const p of ['dist/index.js','dist/session-start-hook.js']){try{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)}}}catch(e){console.error('postinstall warn:',e.message)}\""
|
|
20
|
+
"postinstall": "node -e \"try{const{chmodSync,statSync}=require('fs');for(const p of ['dist/mcp-shim.cjs','dist/index.js','dist/session-start-hook.js']){try{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)}}}catch(e){console.error('postinstall warn:',e.message)}\""
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": ">=
|
|
26
|
+
"node": ">=22"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@modelcontextprotocol/sdk": "1.27.1",
|
|
30
|
-
"@tpsdev-ai/flair-client": "0.
|
|
30
|
+
"@tpsdev-ai/flair-client": "0.16.0",
|
|
31
31
|
"zod": "4.3.6"
|
|
32
32
|
},
|
|
33
33
|
"license": "Apache-2.0",
|