@tpsdev-ai/flair-mcp 0.49.0 → 0.51.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 +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +58 -15
- package/dist/session-start-hook.d.ts +1 -1
- package/dist/session-start-hook.js +1 -1
- package/dist/usage.d.ts +43 -0
- package/dist/usage.js +65 -0
- package/package.json +2 -2
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, RECORD_USAGE_ID_MERGE_CONTRACT } 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
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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,39 @@ 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
|
+
RECORD_USAGE_ID_MERGE_CONTRACT, {
|
|
526
|
+
memoryId: z.string().optional().describe("A single memory id that was used. Merged with memoryIds when both are supplied — not dropped."),
|
|
527
|
+
memoryIds: z.array(z.string()).optional().describe("IDs of the memories that were used (max 20 per call). Merged with memoryId when both are supplied."),
|
|
528
|
+
attribution: z.string().optional().describe("Optional one-line note on how it was used (opaque — stored for audit only)"),
|
|
529
|
+
}, async ({ memoryId, memoryIds, attribution }) => {
|
|
530
|
+
heartbeat(); // auto-presence (flair#598) — fire-and-forget, rate-limited
|
|
531
|
+
try {
|
|
532
|
+
const body = buildRecordUsageBody({ memoryId, memoryIds, attribution });
|
|
533
|
+
if (!body) {
|
|
534
|
+
return {
|
|
535
|
+
content: [{ type: "text", text: "record_usage requires memoryId or memoryIds." }],
|
|
536
|
+
isError: true,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const result = await flair.request("POST", "/RecordUsage", body);
|
|
540
|
+
const text = result?.recorded === true ? "Usage recorded." : "Usage request accepted.";
|
|
541
|
+
return {
|
|
542
|
+
content: [{ type: "text", text }],
|
|
543
|
+
structuredContent: { recorded: result?.recorded === true },
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
return errorResult(err, flair.url);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
507
550
|
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
508
551
|
const transport = new StdioServerTransport();
|
|
509
552
|
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
|
* }
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
/** Stated on the stdio `record_usage` schema so a caller can predict the merge without reading source. */
|
|
17
|
+
export declare const RECORD_USAGE_ID_MERGE_CONTRACT = "When both memoryId and memoryIds are supplied they are merged (union, then deduped) \u2014 a caller who passes both means both.";
|
|
18
|
+
/**
|
|
19
|
+
* Build the POST /RecordUsage body from the MCP tool args.
|
|
20
|
+
* Accepts singular `memoryId` and/or `memoryIds`. Returns null when there is
|
|
21
|
+
* nothing to send (the tool should fail locally rather than POST an empty list).
|
|
22
|
+
* Never includes agentId — the server attributes from the signature.
|
|
23
|
+
*
|
|
24
|
+
* MERGE (flair#1410): this helper unions `memoryId` + `memoryIds`, then
|
|
25
|
+
* dedupes. Native `/mcp` and `POST /RecordUsage` do the same — a caller
|
|
26
|
+
* who passes both means both. Do not "align" this helper to prefer
|
|
27
|
+
* `memoryIds` and drop `memoryId`; that is the delivered-but-uncounted
|
|
28
|
+
* bug #1410 closed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildRecordUsageBody(args: {
|
|
31
|
+
memoryId?: string;
|
|
32
|
+
memoryIds?: string[];
|
|
33
|
+
attribution?: string;
|
|
34
|
+
}): {
|
|
35
|
+
memoryIds: string[];
|
|
36
|
+
attribution?: string;
|
|
37
|
+
} | null;
|
|
38
|
+
/**
|
|
39
|
+
* Citation-on-write passthrough. Only returns a list when the caller actually
|
|
40
|
+
* supplied a non-empty array of non-empty strings — omitted/empty is undefined
|
|
41
|
+
* so the write body stays byte-identical to a pre-#1147 write.
|
|
42
|
+
*/
|
|
43
|
+
export declare function citationIds(usedMemoryIds?: string[]): string[] | undefined;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
/** Stated on the stdio `record_usage` schema so a caller can predict the merge without reading source. */
|
|
21
|
+
export const RECORD_USAGE_ID_MERGE_CONTRACT = "When both memoryId and memoryIds are supplied they are merged (union, then deduped) — a caller who passes both means both.";
|
|
22
|
+
/**
|
|
23
|
+
* Build the POST /RecordUsage body from the MCP tool args.
|
|
24
|
+
* Accepts singular `memoryId` and/or `memoryIds`. Returns null when there is
|
|
25
|
+
* nothing to send (the tool should fail locally rather than POST an empty list).
|
|
26
|
+
* Never includes agentId — the server attributes from the signature.
|
|
27
|
+
*
|
|
28
|
+
* MERGE (flair#1410): this helper unions `memoryId` + `memoryIds`, then
|
|
29
|
+
* dedupes. Native `/mcp` and `POST /RecordUsage` do the same — a caller
|
|
30
|
+
* who passes both means both. Do not "align" this helper to prefer
|
|
31
|
+
* `memoryIds` and drop `memoryId`; that is the delivered-but-uncounted
|
|
32
|
+
* bug #1410 closed.
|
|
33
|
+
*/
|
|
34
|
+
export function buildRecordUsageBody(args) {
|
|
35
|
+
const ids = [];
|
|
36
|
+
if (Array.isArray(args.memoryIds)) {
|
|
37
|
+
for (const id of args.memoryIds) {
|
|
38
|
+
if (typeof id === "string" && id.length > 0)
|
|
39
|
+
ids.push(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (typeof args.memoryId === "string" && args.memoryId.length > 0) {
|
|
43
|
+
ids.push(args.memoryId);
|
|
44
|
+
}
|
|
45
|
+
const memoryIds = [...new Set(ids)];
|
|
46
|
+
if (memoryIds.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
const body = { memoryIds };
|
|
49
|
+
if (typeof args.attribution === "string" && args.attribution.length > 0) {
|
|
50
|
+
body.attribution = args.attribution;
|
|
51
|
+
}
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Citation-on-write passthrough. Only returns a list when the caller actually
|
|
56
|
+
* supplied a non-empty array of non-empty strings — omitted/empty is undefined
|
|
57
|
+
* so the write body stays byte-identical to a pre-#1147 write.
|
|
58
|
+
*/
|
|
59
|
+
export function citationIds(usedMemoryIds) {
|
|
60
|
+
if (!Array.isArray(usedMemoryIds) || usedMemoryIds.length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (!usedMemoryIds.every((id) => typeof id === "string" && id.length > 0))
|
|
63
|
+
return undefined;
|
|
64
|
+
return usedMemoryIds;
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.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.
|
|
31
|
+
"@tpsdev-ai/flair-client": "0.51.0",
|
|
32
32
|
"zod": "4.3.6"
|
|
33
33
|
},
|
|
34
34
|
"license": "Apache-2.0",
|