@tpsdev-ai/flair-mcp 0.49.0 → 0.50.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 CHANGED
@@ -41,12 +41,13 @@ Once configured, Claude Code (or any MCP client) gets these tools:
41
41
  | Tool | Description |
42
42
  |------|-------------|
43
43
  | `memory_search` | Semantic search across memories. Understands "what happened today". |
44
- | `memory_store` | Save a memory with type (lesson/decision/fact) and durability. |
44
+ | `memory_store` | Save a memory with type (lesson/decision/fact) and durability. Optional `usedMemoryIds` cites memories that informed the write. |
45
45
  | `memory_get` | Retrieve a specific memory by ID. |
46
46
  | `memory_delete` | Delete a memory. |
47
47
  | `bootstrap` | Cold-start context — soul + recent memories in one call. |
48
48
  | `soul_set` | Set personality or project context (included in every bootstrap). |
49
49
  | `soul_get` | Get a personality or project context entry. |
50
+ | `record_usage` | Report that recalled memories were actually used (drives `usageCount`). |
50
51
 
51
52
  ## Environment Variables
52
53
 
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  * - soul_get — get a personality/context entry
15
15
  * - flair_workspace_set — write own WorkspaceState (Office Space coordination)
16
16
  * - flair_orgevent — publish an OrgEvent attributed to self (no forging)
17
+ * - record_usage — report that recalled memories were actually used (flair#1147)
17
18
  *
18
19
  * Auto-presence (flair#598): every tool call above triggers a fire-and-forget,
19
20
  * rate-limited `POST /Presence` heartbeat for the calling agent (see
@@ -38,4 +39,5 @@
38
39
  * — the silent `npx -y @tpsdev-ai/flair-mcp` failure. The shim checks the Node
39
40
  * version FIRST, then dynamically imports this module and calls runMcp().
40
41
  */
42
+ export declare function classifyError(err: unknown, flairUrl: string): string;
41
43
  export declare function runMcp(): Promise<void>;
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * - soul_get — get a personality/context entry
15
15
  * - flair_workspace_set — write own WorkspaceState (Office Space coordination)
16
16
  * - flair_orgevent — publish an OrgEvent attributed to self (no forging)
17
+ * - record_usage — report that recalled memories were actually used (flair#1147)
17
18
  *
18
19
  * Auto-presence (flair#598): every tool call above triggers a fire-and-forget,
19
20
  * rate-limited `POST /Presence` heartbeat for the calling agent (see
@@ -40,26 +41,27 @@
40
41
  */
41
42
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
42
43
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
43
- import { FlairClient, FlairError } from "@tpsdev-ai/flair-client";
44
+ import { FlairClient, FlairError, formatKeyLookup, inspectKeyLookup } from "@tpsdev-ai/flair-client";
44
45
  import { z } from "zod";
45
46
  import { deriveActivity, postPresenceSafe, resolveHeartbeatIntervalMs, resolvePresenceTimeoutMs, shouldSendHeartbeat, } from "./presence.js";
46
47
  import { readEnvOrUnset, stripInterpolationLiteralsFromEnv } from "./env-guard.js";
48
+ import { buildRecordUsageBody, citationIds, withCiteNudge } from "./usage.js";
47
49
  import { serverInfo } from "./version.js";
48
50
  // ─── Error helpers ──────────────────────────────────────────────────────────
49
- function classifyError(err, flairUrl) {
51
+ export function classifyError(err, flairUrl) {
50
52
  if (err instanceof FlairError) {
51
53
  const { status, body } = err;
52
54
  if (status === 400)
53
55
  return `validation_error: ${body}`;
54
56
  if (status === 401 || status === 403) {
55
- // Auth failure on a previously-working session usually means the daemon
56
- // restarted (config reload, Harper alter_user, port change). Tell the
57
- // operator how to recover instead of just surfacing the raw 401 body.
58
- return `auth_error: ${body}\n` +
59
- `(Hint: this often follows a Flair daemon restart. Try:\n` +
60
- ` 1. Restart your MCP host (Claude Code, Cursor, etc) to spawn a fresh flair-mcp.\n` +
61
- ` 2. Check daemon: 'flair status' or 'curl ${flairUrl}/Health'.\n` +
62
- ` 3. Verify your agent key still matches the registered Agent record.)`;
57
+ // flair#1271: name the agent, the paths that were looked in, and the
58
+ // remedy. A cached-miss / wrong-HOME 401 is not a daemon-restart hint.
59
+ const lookup = err.keyLookup ?? {
60
+ ...inspectKeyLookup(readEnvOrUnset("FLAIR_AGENT_ID") ?? "", readEnvOrUnset("FLAIR_KEY_PATH")),
61
+ signed: false,
62
+ authMethod: "none",
63
+ };
64
+ return `auth_error: ${body}\n${formatKeyLookup(lookup)}`;
63
65
  }
64
66
  if (status === 413)
65
67
  return `payload_too_large: ${body}`;
@@ -233,7 +235,7 @@ export async function runMcp() {
233
235
  return `${i + 1}. ${r.content}${meta ? ` (${meta})` : ""}`;
234
236
  })
235
237
  .join("\n");
236
- return { content: [{ type: "text", text }] };
238
+ return { content: [{ type: "text", text: withCiteNudge(text) }] };
237
239
  }
238
240
  catch (err) {
239
241
  return errorResult(err, flair.url);
@@ -252,7 +254,9 @@ export async function runMcp() {
252
254
  "permanent/persistent -> shared, standard/ephemeral -> private). " +
253
255
  "private -- never visible to another agent, even one with a memory grant. " +
254
256
  "shared -- visible to the owner and any agent holding a read/search grant."),
255
- }, async ({ content, type, durability, tags, visibility }) => {
257
+ usedMemoryIds: z.array(z.string()).optional().describe("IDs of memories that informed this write (citation-on-write). Credited via the same " +
258
+ "deduped usage ledger as record_usage. Optional."),
259
+ }, async ({ content, type, durability, tags, visibility, usedMemoryIds }) => {
256
260
  heartbeat(); // auto-presence (flair#598) — fire-and-forget, rate-limited
257
261
  try {
258
262
  const result = await flair.memory.write(content, {
@@ -262,6 +266,7 @@ export async function runMcp() {
262
266
  visibility: visibility,
263
267
  dedup: true,
264
268
  dedupThreshold: 0.95,
269
+ usedMemoryIds: citationIds(usedMemoryIds),
265
270
  });
266
271
  // The server's conservative dedup gate NEVER suppresses a write
267
272
  // (memory-integrity fix, flair#526) — `result.deduplicated` is a
@@ -305,10 +310,15 @@ export async function runMcp() {
305
310
  content: z.string().describe("New content"),
306
311
  preserveHistory: z.coerce.boolean().optional().default(false)
307
312
  .describe("Write a new supersedes-linked version instead of overwriting in place (default false)"),
308
- }, async ({ id, content, preserveHistory }) => {
313
+ usedMemoryIds: z.array(z.string()).optional().describe("IDs of memories that informed this update (citation-on-write). Credited via the same " +
314
+ "deduped usage ledger as record_usage. Optional."),
315
+ }, async ({ id, content, preserveHistory, usedMemoryIds }) => {
309
316
  heartbeat(); // auto-presence (flair#598) — fire-and-forget, rate-limited
310
317
  try {
311
- const result = await flair.memory.update(id, content, { preserveHistory });
318
+ const result = await flair.memory.update(id, content, {
319
+ preserveHistory,
320
+ usedMemoryIds: citationIds(usedMemoryIds),
321
+ });
312
322
  const text = preserveHistory
313
323
  ? `Memory updated: new version stored (id: ${result.id}), supersedes ${id}.`
314
324
  : `Memory updated (id: ${id}).`;
@@ -404,7 +414,7 @@ export async function runMcp() {
404
414
  if (!result.context) {
405
415
  return { content: [{ type: "text", text: "No context available." }] };
406
416
  }
407
- return { content: [{ type: "text", text: result.context }] };
417
+ return { content: [{ type: "text", text: withCiteNudge(result.context) }] };
408
418
  }
409
419
  catch (err) {
410
420
  return errorResult(err, flair.url);
@@ -504,6 +514,38 @@ export async function runMcp() {
504
514
  return errorResult(err, flair.url);
505
515
  }
506
516
  });
517
+ // ─── Usage feedback (flair#1147) ─────────────────────────────────────────────
518
+ //
519
+ // POST /RecordUsage already existed; native /mcp already wrapped it. The
520
+ // stdio package did not, so a Claude Code / Cursor client could not close
521
+ // the usageCount loop. Identity is taken from the signed request — the body
522
+ // carries only memory id(s) + optional attribution, never agentId.
523
+ server.tool("record_usage", "Report that one or more memories were actually USED — cited or relied on to ground an answer or decision. " +
524
+ "Distinct from search (surfacing a memory is not usage). Dedup'd (you can only count once per memory) and rate-limited.", {
525
+ memoryId: z.string().optional().describe("A single memory id that was used"),
526
+ memoryIds: z.array(z.string()).optional().describe("IDs of the memories that were used (max 20 per call)"),
527
+ attribution: z.string().optional().describe("Optional one-line note on how it was used (opaque — stored for audit only)"),
528
+ }, async ({ memoryId, memoryIds, attribution }) => {
529
+ heartbeat(); // auto-presence (flair#598) — fire-and-forget, rate-limited
530
+ try {
531
+ const body = buildRecordUsageBody({ memoryId, memoryIds, attribution });
532
+ if (!body) {
533
+ return {
534
+ content: [{ type: "text", text: "record_usage requires memoryId or memoryIds." }],
535
+ isError: true,
536
+ };
537
+ }
538
+ const result = await flair.request("POST", "/RecordUsage", body);
539
+ const text = result?.recorded === true ? "Usage recorded." : "Usage request accepted.";
540
+ return {
541
+ content: [{ type: "text", text }],
542
+ structuredContent: { recorded: result?.recorded === true },
543
+ };
544
+ }
545
+ catch (err) {
546
+ return errorResult(err, flair.url);
547
+ }
548
+ });
507
549
  // ─── Start ───────────────────────────────────────────────────────────────────
508
550
  const transport = new StdioServerTransport();
509
551
  await server.connect(transport);
@@ -68,7 +68,7 @@
68
68
  * "hooks": {
69
69
  * "SessionStart": [
70
70
  * { "hooks": [ { "type": "command",
71
- * "command": "sh -c 'out=$(FLAIR_AGENT_ID=me npx -y @tpsdev-ai/flair-mcp flair-session-start 2>/dev/null) && printf %s \"$out\" || true'" } ] }
71
+ * "command": "sh -c 'out=$(FLAIR_AGENT_ID=me npx -y -p @tpsdev-ai/flair-mcp@<version> flair-session-start 2>/dev/null) && printf %s \"$out\" || true'" } ] }
72
72
  * ]
73
73
  * }
74
74
  * }
@@ -68,7 +68,7 @@
68
68
  * "hooks": {
69
69
  * "SessionStart": [
70
70
  * { "hooks": [ { "type": "command",
71
- * "command": "sh -c 'out=$(FLAIR_AGENT_ID=me npx -y @tpsdev-ai/flair-mcp flair-session-start 2>/dev/null) && printf %s \"$out\" || true'" } ] }
71
+ * "command": "sh -c 'out=$(FLAIR_AGENT_ID=me npx -y -p @tpsdev-ai/flair-mcp@<version> flair-session-start 2>/dev/null) && printf %s \"$out\" || true'" } ] }
72
72
  * ]
73
73
  * }
74
74
  * }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * flair#1147 — the usage-feedback loop on the stdio MCP surface.
3
+ *
4
+ * Native `/mcp` already exposes `record_usage` and `memory_store.usedMemoryIds`.
5
+ * The stdio package (`@tpsdev-ai/flair-mcp`) did not, so a Claude Code / Cursor
6
+ * client had no way to reach POST /RecordUsage. These helpers are the thin
7
+ * client-side half: body construction for that endpoint, citation passthrough
8
+ * on write, and the one-line "cite what you use" nudge on recalled ids.
9
+ *
10
+ * Identity is NEVER in the body — RecordUsage attributes from the signed
11
+ * request, same no-forge contract as flair_workspace_set / flair_orgevent.
12
+ */
13
+ /** One-line instruction on recalled ids (issue ask #3). Search/bootstrap hits are not usage. */
14
+ export declare const CITE_USAGE_NUDGE = "Cite memories you actually use via record_usage (or memory_store.usedMemoryIds). A search or bootstrap hit is not usage.";
15
+ export declare function withCiteNudge(text: string): string;
16
+ /**
17
+ * Build the POST /RecordUsage body from the MCP tool args.
18
+ * Accepts singular `memoryId` and/or `memoryIds`. Returns null when there is
19
+ * nothing to send (the tool should fail locally rather than POST an empty list).
20
+ * Never includes agentId — the server attributes from the signature.
21
+ *
22
+ * MERGE vs PREFER (deliberate, named — Sherlock/Kern #1404):
23
+ * This helper MERGES `memoryId` + `memoryIds`, then dedupes. Native `/mcp`
24
+ * `recordUsage` (resources/mcp-tools.ts) PREFERS `memoryIds` and drops
25
+ * `memoryId` when both are supplied. The HTTP endpoint does the same:
26
+ * `RecordUsage.post()` is `data?.memoryIds ?? [data?.memoryId]` — PREFER,
27
+ * not union. If `memoryIds` is present (even `[]`, which is truthy),
28
+ * `memoryId` is never read.
29
+ *
30
+ * The stdio merge is load-bearing because it flattens first: we send only
31
+ * a single `memoryIds` array, so the server's prefer is never exercised
32
+ * on two fields. A future path that POSTs both fields through to
33
+ * `/RecordUsage` without flattening would silently drop `memoryId`
34
+ * (delivered-but-uncounted; empty `memoryIds: []` alongside a real
35
+ * `memoryId` would 400 rather than fall through).
36
+ *
37
+ * Native prefer is a pre-existing delivered-but-uncounted bug on a
38
+ * different surface, tracked in flair#1410 — not a regression from this
39
+ * PR, and not a blocker for #1147. Do not "align" this helper to prefer.
40
+ */
41
+ export declare function buildRecordUsageBody(args: {
42
+ memoryId?: string;
43
+ memoryIds?: string[];
44
+ attribution?: string;
45
+ }): {
46
+ memoryIds: string[];
47
+ attribution?: string;
48
+ } | null;
49
+ /**
50
+ * Citation-on-write passthrough. Only returns a list when the caller actually
51
+ * supplied a non-empty array of non-empty strings — omitted/empty is undefined
52
+ * so the write body stays byte-identical to a pre-#1147 write.
53
+ */
54
+ export declare function citationIds(usedMemoryIds?: string[]): string[] | undefined;
package/dist/usage.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * flair#1147 — the usage-feedback loop on the stdio MCP surface.
3
+ *
4
+ * Native `/mcp` already exposes `record_usage` and `memory_store.usedMemoryIds`.
5
+ * The stdio package (`@tpsdev-ai/flair-mcp`) did not, so a Claude Code / Cursor
6
+ * client had no way to reach POST /RecordUsage. These helpers are the thin
7
+ * client-side half: body construction for that endpoint, citation passthrough
8
+ * on write, and the one-line "cite what you use" nudge on recalled ids.
9
+ *
10
+ * Identity is NEVER in the body — RecordUsage attributes from the signed
11
+ * request, same no-forge contract as flair_workspace_set / flair_orgevent.
12
+ */
13
+ /** One-line instruction on recalled ids (issue ask #3). Search/bootstrap hits are not usage. */
14
+ export const CITE_USAGE_NUDGE = "Cite memories you actually use via record_usage (or memory_store.usedMemoryIds). A search or bootstrap hit is not usage.";
15
+ export function withCiteNudge(text) {
16
+ if (!text)
17
+ return text;
18
+ return `${text}\n\n${CITE_USAGE_NUDGE}`;
19
+ }
20
+ /**
21
+ * Build the POST /RecordUsage body from the MCP tool args.
22
+ * Accepts singular `memoryId` and/or `memoryIds`. Returns null when there is
23
+ * nothing to send (the tool should fail locally rather than POST an empty list).
24
+ * Never includes agentId — the server attributes from the signature.
25
+ *
26
+ * MERGE vs PREFER (deliberate, named — Sherlock/Kern #1404):
27
+ * This helper MERGES `memoryId` + `memoryIds`, then dedupes. Native `/mcp`
28
+ * `recordUsage` (resources/mcp-tools.ts) PREFERS `memoryIds` and drops
29
+ * `memoryId` when both are supplied. The HTTP endpoint does the same:
30
+ * `RecordUsage.post()` is `data?.memoryIds ?? [data?.memoryId]` — PREFER,
31
+ * not union. If `memoryIds` is present (even `[]`, which is truthy),
32
+ * `memoryId` is never read.
33
+ *
34
+ * The stdio merge is load-bearing because it flattens first: we send only
35
+ * a single `memoryIds` array, so the server's prefer is never exercised
36
+ * on two fields. A future path that POSTs both fields through to
37
+ * `/RecordUsage` without flattening would silently drop `memoryId`
38
+ * (delivered-but-uncounted; empty `memoryIds: []` alongside a real
39
+ * `memoryId` would 400 rather than fall through).
40
+ *
41
+ * Native prefer is a pre-existing delivered-but-uncounted bug on a
42
+ * different surface, tracked in flair#1410 — not a regression from this
43
+ * PR, and not a blocker for #1147. Do not "align" this helper to prefer.
44
+ */
45
+ export function buildRecordUsageBody(args) {
46
+ const ids = [];
47
+ if (Array.isArray(args.memoryIds)) {
48
+ for (const id of args.memoryIds) {
49
+ if (typeof id === "string" && id.length > 0)
50
+ ids.push(id);
51
+ }
52
+ }
53
+ if (typeof args.memoryId === "string" && args.memoryId.length > 0) {
54
+ ids.push(args.memoryId);
55
+ }
56
+ const memoryIds = [...new Set(ids)];
57
+ if (memoryIds.length === 0)
58
+ return null;
59
+ const body = { memoryIds };
60
+ if (typeof args.attribution === "string" && args.attribution.length > 0) {
61
+ body.attribution = args.attribution;
62
+ }
63
+ return body;
64
+ }
65
+ /**
66
+ * Citation-on-write passthrough. Only returns a list when the caller actually
67
+ * supplied a non-empty array of non-empty strings — omitted/empty is undefined
68
+ * so the write body stays byte-identical to a pre-#1147 write.
69
+ */
70
+ export function citationIds(usedMemoryIds) {
71
+ if (!Array.isArray(usedMemoryIds) || usedMemoryIds.length === 0)
72
+ return undefined;
73
+ if (!usedMemoryIds.every((id) => typeof id === "string" && id.length > 0))
74
+ return undefined;
75
+ return usedMemoryIds;
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair-mcp",
3
- "version": "0.49.0",
3
+ "version": "0.50.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",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@modelcontextprotocol/sdk": "1.27.1",
31
- "@tpsdev-ai/flair-client": "0.49.0",
31
+ "@tpsdev-ai/flair-client": "0.50.0",
32
32
  "zod": "4.3.6"
33
33
  },
34
34
  "license": "Apache-2.0",