@tpsdev-ai/flair-mcp 0.4.4 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +129 -51
  2. 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
- const results = await flair.memory.search(query, { limit });
46
- if (results.length === 0) {
47
- return { content: [{ type: "text", text: "No relevant memories found." }] };
48
- }
49
- const text = results
50
- .map((r, i) => {
51
- const date = r.createdAt ? r.createdAt.slice(0, 10) : "";
52
- const idStr = r.id ? `id:${r.id}` : "";
53
- const meta = [date, r.type, idStr].filter(Boolean).join(", ");
54
- return `${i + 1}. ${r.content}${meta ? ` (${meta})` : ""}`;
55
- })
56
- .join("\n");
57
- return { content: [{ type: "text", text }] };
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=inviolable, persistent=key decisions, standard=default, ephemeral=auto-expires 72h"),
64
- tags: z.union([
65
- z.array(z.string()),
66
- z.string().transform(s => s.startsWith("[") ? JSON.parse(s) : s.split(",").map(t => t.trim()).filter(Boolean)),
67
- ]).optional().describe("Optional tags array or comma-separated string"),
98
+ .describe("permanentinviolable 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
- const result = await flair.memory.write(content, {
70
- type: type,
71
- durability: durability,
72
- tags,
73
- dedup: true,
74
- dedupThreshold: 0.95,
75
- });
76
- // Check if dedup returned an existing memory (different ID than what we generated)
77
- const generatedPrefix = `${agentId}-`;
78
- const wasDeduped = result.id && !result.id.startsWith(generatedPrefix);
79
- if (wasDeduped) {
80
- return { content: [{ type: "text", text: `Similar memory already exists (id: ${result.id}): ${result.content?.slice(0, 200)}` }] };
81
- }
82
- return { content: [{ type: "text", text: `Memory stored (id: ${result.id})` }] };
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
- const mem = await flair.memory.get(id);
88
- if (!mem)
89
- return { content: [{ type: "text", text: `Memory ${id} not found.` }] };
90
- return { content: [{ type: "text", text: `${mem.content}\n\n(type: ${mem.type}, durability: ${mem.durability}, created: ${mem.createdAt})` }] };
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
- await flair.memory.delete(id);
96
- return { content: [{ type: "text", text: `Memory ${id} deleted.` }] };
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 cold-start context: soul + recent memories. Run this at the start of every session.", {
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
- }, async ({ maxTokens }) => {
101
- const result = await flair.bootstrap({ maxTokens });
102
- if (!result.context) {
103
- return { content: [{ type: "text", text: "No context available." }] };
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
- await flair.soul.set(key, value);
112
- return { content: [{ type: "text", text: `Soul entry '${key}' set.` }] };
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
- const entry = await flair.soul.get(key);
118
- if (!entry)
119
- return { content: [{ type: "text", text: `No soul entry for '${key}'.` }] };
120
- return { content: [{ type: "text", text: entry.value }] };
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.4.4",
3
+ "version": "0.5.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",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@modelcontextprotocol/sdk": "1.27.1",
27
- "@tpsdev-ai/flair-client": "0.4.3",
27
+ "@tpsdev-ai/flair-client": "0.5.0",
28
28
  "zod": "4.3.6"
29
29
  },
30
30
  "license": "Apache-2.0",