@tpsdev-ai/flair-mcp 0.4.4 → 0.5.6
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/dist/index.js +129 -51
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -19,8 +19,38 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
-
import { FlairClient } from "@tpsdev-ai/flair-client";
|
|
22
|
+
import { FlairClient, FlairError } from "@tpsdev-ai/flair-client";
|
|
23
23
|
import { z } from "zod";
|
|
24
|
+
// ─── Error helpers ──────────────────────────────────────────────────────────
|
|
25
|
+
function classifyError(err, flairUrl) {
|
|
26
|
+
if (err instanceof FlairError) {
|
|
27
|
+
const { status, body } = err;
|
|
28
|
+
if (status === 400)
|
|
29
|
+
return `validation_error: ${body}`;
|
|
30
|
+
if (status === 401 || status === 403)
|
|
31
|
+
return `auth_error: ${body}`;
|
|
32
|
+
if (status === 413)
|
|
33
|
+
return `payload_too_large: ${body}`;
|
|
34
|
+
if (status === 429)
|
|
35
|
+
return "rate_limited — retry after a moment";
|
|
36
|
+
if (status >= 500)
|
|
37
|
+
return `server_error (retriable): ${body}`;
|
|
38
|
+
return `http_error (${status}): ${body}`;
|
|
39
|
+
}
|
|
40
|
+
if (err instanceof Error) {
|
|
41
|
+
if (err.name.includes("Abort") || err.name.includes("Timeout")) {
|
|
42
|
+
return "timeout — the server took too long. This often happens with large content that requires embedding. Try shorter content or retry.";
|
|
43
|
+
}
|
|
44
|
+
if (err instanceof TypeError && err.message.includes("fetch")) {
|
|
45
|
+
return `connection_error (retriable): could not reach Flair at ${flairUrl}. Is it running?`;
|
|
46
|
+
}
|
|
47
|
+
return `unexpected_error: ${err.message}`;
|
|
48
|
+
}
|
|
49
|
+
return `unexpected_error: ${String(err)}`;
|
|
50
|
+
}
|
|
51
|
+
function errorResult(err, flairUrl) {
|
|
52
|
+
return { content: [{ type: "text", text: classifyError(err, flairUrl) }], isError: true };
|
|
53
|
+
}
|
|
24
54
|
// ─── Client setup ────────────────────────────────────────────────────────────
|
|
25
55
|
const agentId = process.env.FLAIR_AGENT_ID;
|
|
26
56
|
if (!agentId) {
|
|
@@ -42,82 +72,130 @@ server.tool("memory_search", "Search memories by meaning. Understands temporal q
|
|
|
42
72
|
query: z.string().describe("Search query — natural language, semantic matching"),
|
|
43
73
|
limit: z.coerce.number().optional().default(5).describe("Max results (default 5)"),
|
|
44
74
|
}, async ({ query, limit }) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
try {
|
|
76
|
+
const results = await flair.memory.search(query, { limit });
|
|
77
|
+
if (results.length === 0) {
|
|
78
|
+
return { content: [{ type: "text", text: "No relevant memories found." }] };
|
|
79
|
+
}
|
|
80
|
+
const text = results
|
|
81
|
+
.map((r, i) => {
|
|
82
|
+
const date = r.createdAt ? r.createdAt.slice(0, 10) : "";
|
|
83
|
+
const idStr = r.id ? `id:${r.id}` : "";
|
|
84
|
+
const meta = [date, r.type, idStr].filter(Boolean).join(", ");
|
|
85
|
+
return `${i + 1}. ${r.content}${meta ? ` (${meta})` : ""}`;
|
|
86
|
+
})
|
|
87
|
+
.join("\n");
|
|
88
|
+
return { content: [{ type: "text", text }] };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return errorResult(err, flair.url);
|
|
92
|
+
}
|
|
58
93
|
});
|
|
59
94
|
server.tool("memory_store", "Save information to persistent memory. Use for lessons, decisions, preferences, facts.", {
|
|
60
95
|
content: z.string().describe("What to remember"),
|
|
61
96
|
type: z.enum(["session", "lesson", "decision", "preference", "fact", "goal"]).optional().default("session"),
|
|
62
97
|
durability: z.enum(["permanent", "persistent", "standard", "ephemeral"]).optional().default("standard")
|
|
63
|
-
.describe("permanent
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
.describe("permanent — inviolable facts, identity, explicit never-forget (e.g., 'my name is Nathan')\n" +
|
|
99
|
+
"persistent — key decisions and lessons to recall weeks later (e.g., 'PR review process')\n" +
|
|
100
|
+
"standard — default working memory, recent context (e.g., 'discussed auth flow today')\n" +
|
|
101
|
+
"ephemeral — scratch state, auto-expires 72h (e.g., 'currently debugging issue #42')"),
|
|
102
|
+
tags: z.array(z.string()).optional().describe("Array of tag strings"),
|
|
68
103
|
}, async ({ content, type, durability, tags }) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
104
|
+
try {
|
|
105
|
+
const result = await flair.memory.write(content, {
|
|
106
|
+
type: type,
|
|
107
|
+
durability: durability,
|
|
108
|
+
tags,
|
|
109
|
+
dedup: true,
|
|
110
|
+
dedupThreshold: 0.95,
|
|
111
|
+
});
|
|
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)}` }] };
|
|
117
|
+
}
|
|
118
|
+
const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
|
|
119
|
+
const tagStr = tags && tags.length > 0 ? tags.join(", ") : "none";
|
|
120
|
+
const text = [
|
|
121
|
+
`Memory stored (id: ${result.id})`,
|
|
122
|
+
`Preview: ${preview}`,
|
|
123
|
+
`Size: ${content.length} chars`,
|
|
124
|
+
`Tags: ${tagStr}`,
|
|
125
|
+
`Type: ${type}, Durability: ${durability}`,
|
|
126
|
+
].join("\n");
|
|
127
|
+
return { content: [{ type: "text", text }] };
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return errorResult(err, flair.url);
|
|
131
|
+
}
|
|
83
132
|
});
|
|
84
133
|
server.tool("memory_get", "Retrieve a specific memory by ID.", {
|
|
85
134
|
id: z.string().describe("Memory ID"),
|
|
86
135
|
}, async ({ id }) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
136
|
+
try {
|
|
137
|
+
const mem = await flair.memory.get(id);
|
|
138
|
+
if (!mem)
|
|
139
|
+
return { content: [{ type: "text", text: `Memory ${id} not found.` }] };
|
|
140
|
+
return { content: [{ type: "text", text: `${mem.content}\n\n(type: ${mem.type}, durability: ${mem.durability}, created: ${mem.createdAt})` }] };
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
return errorResult(err, flair.url);
|
|
144
|
+
}
|
|
91
145
|
});
|
|
92
146
|
server.tool("memory_delete", "Delete a memory by ID.", {
|
|
93
147
|
id: z.string().describe("Memory ID to delete"),
|
|
94
148
|
}, async ({ id }) => {
|
|
95
|
-
|
|
96
|
-
|
|
149
|
+
try {
|
|
150
|
+
await flair.memory.delete(id);
|
|
151
|
+
return { content: [{ type: "text", text: `Memory ${id} deleted.` }] };
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return errorResult(err, flair.url);
|
|
155
|
+
}
|
|
97
156
|
});
|
|
98
|
-
server.tool("bootstrap", "Get
|
|
157
|
+
server.tool("bootstrap", "Get session context: soul + memories + predicted context. Run at session start. Pass subjects for predictive loading.", {
|
|
99
158
|
maxTokens: z.coerce.number().optional().default(4000).describe("Max tokens in output"),
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
159
|
+
currentTask: z.string().optional().describe("Current task description — enables semantic search for relevant memories"),
|
|
160
|
+
channel: z.string().optional().describe("Channel name (discord, tps-mail, claude-code) — shapes context prediction"),
|
|
161
|
+
surface: z.string().optional().describe("Surface name (tps-build, tps-review, cli-session) — narrows prediction"),
|
|
162
|
+
subjects: z.array(z.string()).optional().describe("Entity names to preload context for (e.g., ['flair', 'auth'])"),
|
|
163
|
+
}, async ({ maxTokens, currentTask, channel, surface, subjects }) => {
|
|
164
|
+
try {
|
|
165
|
+
const result = await flair.bootstrap({ maxTokens, currentTask, channel, surface, subjects });
|
|
166
|
+
if (!result.context) {
|
|
167
|
+
return { content: [{ type: "text", text: "No context available." }] };
|
|
168
|
+
}
|
|
169
|
+
return { content: [{ type: "text", text: result.context }] };
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
return errorResult(err, flair.url);
|
|
104
173
|
}
|
|
105
|
-
return { content: [{ type: "text", text: result.context }] };
|
|
106
174
|
});
|
|
107
175
|
server.tool("soul_set", "Set a personality or project context entry. Included in every bootstrap.", {
|
|
108
176
|
key: z.string().describe("Entry key (e.g., 'role', 'standards', 'project')"),
|
|
109
177
|
value: z.string().describe("Entry value — personality trait, project context, coding standards, etc."),
|
|
110
178
|
}, async ({ key, value }) => {
|
|
111
|
-
|
|
112
|
-
|
|
179
|
+
try {
|
|
180
|
+
await flair.soul.set(key, value);
|
|
181
|
+
return { content: [{ type: "text", text: `Soul entry '${key}' set.` }] };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return errorResult(err, flair.url);
|
|
185
|
+
}
|
|
113
186
|
});
|
|
114
187
|
server.tool("soul_get", "Get a personality or project context entry.", {
|
|
115
188
|
key: z.string().describe("Entry key"),
|
|
116
189
|
}, async ({ key }) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
190
|
+
try {
|
|
191
|
+
const entry = await flair.soul.get(key);
|
|
192
|
+
if (!entry)
|
|
193
|
+
return { content: [{ type: "text", text: `No soul entry for '${key}'.` }] };
|
|
194
|
+
return { content: [{ type: "text", text: entry.value }] };
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
return errorResult(err, flair.url);
|
|
198
|
+
}
|
|
121
199
|
});
|
|
122
200
|
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
123
201
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.6",
|
|
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",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "1.27.1",
|
|
27
|
-
"@tpsdev-ai/flair-client": "0.
|
|
27
|
+
"@tpsdev-ai/flair-client": "0.5.6",
|
|
28
28
|
"zod": "4.3.6"
|
|
29
29
|
},
|
|
30
30
|
"license": "Apache-2.0",
|