@tpsdev-ai/flair-mcp 0.51.2 → 0.53.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
@@ -44,6 +44,9 @@ Once configured, Claude Code (or any MCP client) gets these tools:
44
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
+ | `skill_store` | Write a skill (trigger + procedure) as a skill-tagged memory. |
48
+ | `skill_search` | Find skills that apply to a task. Returns a catalog, not the procedure. |
49
+ | `skill_get` | Retrieve the full skill by ID (disclosure after `skill_search`). |
47
50
  | `bootstrap` | Cold-start context — soul + recent memories in one call. |
48
51
  | `soul_set` | Set personality or project context (included in every bootstrap). |
49
52
  | `soul_get` | Get a personality or project context entry. |
@@ -0,0 +1,75 @@
1
+ /**
2
+ * adapter-surface.ts — the stdio adapter's declared tool set (flair#1575).
3
+ *
4
+ * `@tpsdev-ai/flair-mcp` hand-wires each tool via `server.tool(...)` in
5
+ * index.ts. The native `/mcp` handler ships a separate `TOOLS` registry in
6
+ * resources/mcp-tools.ts. Those two surfaces drifted: skill_* landed in
7
+ * TOOLS for 0.52.0 and never reached this package — the surface Claude Code
8
+ * and Cursor actually use.
9
+ *
10
+ * This module is the reviewed chokepoint for that seam:
11
+ *
12
+ * 1. `ADAPTER_TOOL_NAMES` is the adapter's declared tool set.
13
+ * 2. `parseAdapterToolNames` reads the names actually passed to
14
+ * `server.tool(...)` in index.ts, so the declaration cannot outrun
15
+ * registration (or vice versa).
16
+ * 3. `STDIO_ADAPTER_EXEMPTIONS` is the explicit, reviewed list of names
17
+ * that exist on one surface but not the other. A TOOLS name that is
18
+ * neither registered here nor exempted is a CI failure — silent drift
19
+ * of the class that hid skill_*.
20
+ *
21
+ * Deriving the adapter's handlers from TOOLS (so a new registry tool appears
22
+ * here for free) is the durable structural fix; it does not fit this chip
23
+ * because TOOLS is Harper-linked server code and this package talks HTTP via
24
+ * FlairClient. Detection + exemption list ships now; derive is a follow-on.
25
+ */
26
+ /** Tools registered on the stdio adapter via `server.tool(...)` in index.ts. */
27
+ export declare const ADAPTER_TOOL_NAMES: readonly ["bootstrap", "flair_orgevent", "flair_workspace_set", "memory_delete", "memory_get", "memory_search", "memory_store", "memory_update", "record_usage", "relationship_store", "skill_get", "skill_search", "skill_store", "soul_get", "soul_set"];
28
+ export type AdapterToolName = (typeof ADAPTER_TOOL_NAMES)[number];
29
+ /**
30
+ * Reviewed exemptions at the stdio-adapter ↔ server TOOLS seam (flair#1575).
31
+ *
32
+ * A name here is a deliberate, reviewed difference — not silent drift.
33
+ * Adding or removing a name is the control: CI fails if an exemption is
34
+ * unused (the tool appeared on both sides, or vanished from the side it
35
+ * was excused on) or if a non-exempt name exists on only one side.
36
+ */
37
+ export declare const STDIO_ADAPTER_EXEMPTIONS: {
38
+ /**
39
+ * Present in resources/mcp-tools.ts `TOOLS`, not wired on the stdio adapter.
40
+ *
41
+ * - attention: native /mcp only (flair#677). mcp-tools.ts's module doc
42
+ * explicitly does not mirror it into this package.
43
+ * - memory_basement / memory_restore: archive verbs (flair#1472) landed
44
+ * on native /mcp; not yet forwarded over FlairClient.
45
+ */
46
+ readonly registryOnly: readonly ["attention", "memory_basement", "memory_restore"];
47
+ /**
48
+ * Wired on the stdio adapter, absent from `TOOLS`.
49
+ *
50
+ * - relationship_store: the adapter predates the record-types mcp
51
+ * declaration. Relationship has no `RECORD_TYPES.mcp` field, so TOOLS
52
+ * ships zero relationship_* names. The adapter still exposes the triple
53
+ * write (FlairClient.relationship.write).
54
+ */
55
+ readonly adapterOnly: readonly ["relationship_store"];
56
+ };
57
+ /**
58
+ * Collect `server.tool("name", ...)` registrations from adapter source.
59
+ * Plain string scan — the first string literal argument is the tool name.
60
+ * Does not use `new RegExp` built from runtime input (CodeQL js/regex-injection).
61
+ */
62
+ export declare function parseAdapterToolNames(source: string): string[];
63
+ export interface AdapterRegistryParity {
64
+ /** TOOLS names the adapter neither registers nor exempts. */
65
+ missingFromAdapter: string[];
66
+ /** Adapter names that are neither in TOOLS nor adapter-only exempted. */
67
+ extraOnAdapter: string[];
68
+ /** Exemption entries that no longer describe a real one-sided difference. */
69
+ staleExemptions: string[];
70
+ }
71
+ /**
72
+ * Compare the stdio adapter's tool set to the server TOOLS registry.
73
+ * Equal after applying the reviewed exemption list — otherwise drift.
74
+ */
75
+ export declare function adapterRegistryParity(registryNames: readonly string[], adapterNames: readonly string[]): AdapterRegistryParity;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * adapter-surface.ts — the stdio adapter's declared tool set (flair#1575).
3
+ *
4
+ * `@tpsdev-ai/flair-mcp` hand-wires each tool via `server.tool(...)` in
5
+ * index.ts. The native `/mcp` handler ships a separate `TOOLS` registry in
6
+ * resources/mcp-tools.ts. Those two surfaces drifted: skill_* landed in
7
+ * TOOLS for 0.52.0 and never reached this package — the surface Claude Code
8
+ * and Cursor actually use.
9
+ *
10
+ * This module is the reviewed chokepoint for that seam:
11
+ *
12
+ * 1. `ADAPTER_TOOL_NAMES` is the adapter's declared tool set.
13
+ * 2. `parseAdapterToolNames` reads the names actually passed to
14
+ * `server.tool(...)` in index.ts, so the declaration cannot outrun
15
+ * registration (or vice versa).
16
+ * 3. `STDIO_ADAPTER_EXEMPTIONS` is the explicit, reviewed list of names
17
+ * that exist on one surface but not the other. A TOOLS name that is
18
+ * neither registered here nor exempted is a CI failure — silent drift
19
+ * of the class that hid skill_*.
20
+ *
21
+ * Deriving the adapter's handlers from TOOLS (so a new registry tool appears
22
+ * here for free) is the durable structural fix; it does not fit this chip
23
+ * because TOOLS is Harper-linked server code and this package talks HTTP via
24
+ * FlairClient. Detection + exemption list ships now; derive is a follow-on.
25
+ */
26
+ /** Tools registered on the stdio adapter via `server.tool(...)` in index.ts. */
27
+ export const ADAPTER_TOOL_NAMES = [
28
+ "bootstrap",
29
+ "flair_orgevent",
30
+ "flair_workspace_set",
31
+ "memory_delete",
32
+ "memory_get",
33
+ "memory_search",
34
+ "memory_store",
35
+ "memory_update",
36
+ "record_usage",
37
+ "relationship_store",
38
+ "skill_get",
39
+ "skill_search",
40
+ "skill_store",
41
+ "soul_get",
42
+ "soul_set",
43
+ ];
44
+ /**
45
+ * Reviewed exemptions at the stdio-adapter ↔ server TOOLS seam (flair#1575).
46
+ *
47
+ * A name here is a deliberate, reviewed difference — not silent drift.
48
+ * Adding or removing a name is the control: CI fails if an exemption is
49
+ * unused (the tool appeared on both sides, or vanished from the side it
50
+ * was excused on) or if a non-exempt name exists on only one side.
51
+ */
52
+ export const STDIO_ADAPTER_EXEMPTIONS = {
53
+ /**
54
+ * Present in resources/mcp-tools.ts `TOOLS`, not wired on the stdio adapter.
55
+ *
56
+ * - attention: native /mcp only (flair#677). mcp-tools.ts's module doc
57
+ * explicitly does not mirror it into this package.
58
+ * - memory_basement / memory_restore: archive verbs (flair#1472) landed
59
+ * on native /mcp; not yet forwarded over FlairClient.
60
+ */
61
+ registryOnly: ["attention", "memory_basement", "memory_restore"],
62
+ /**
63
+ * Wired on the stdio adapter, absent from `TOOLS`.
64
+ *
65
+ * - relationship_store: the adapter predates the record-types mcp
66
+ * declaration. Relationship has no `RECORD_TYPES.mcp` field, so TOOLS
67
+ * ships zero relationship_* names. The adapter still exposes the triple
68
+ * write (FlairClient.relationship.write).
69
+ */
70
+ adapterOnly: ["relationship_store"],
71
+ };
72
+ /**
73
+ * Collect `server.tool("name", ...)` registrations from adapter source.
74
+ * Plain string scan — the first string literal argument is the tool name.
75
+ * Does not use `new RegExp` built from runtime input (CodeQL js/regex-injection).
76
+ */
77
+ export function parseAdapterToolNames(source) {
78
+ const names = [];
79
+ const re = /server\.tool\(\s*"([a-z][a-z0-9_]*)"/g;
80
+ let match;
81
+ while ((match = re.exec(source)) !== null)
82
+ names.push(match[1]);
83
+ return names;
84
+ }
85
+ /**
86
+ * Compare the stdio adapter's tool set to the server TOOLS registry.
87
+ * Equal after applying the reviewed exemption list — otherwise drift.
88
+ */
89
+ export function adapterRegistryParity(registryNames, adapterNames) {
90
+ const registry = new Set(registryNames);
91
+ const adapter = new Set(adapterNames);
92
+ const registryOnly = new Set(STDIO_ADAPTER_EXEMPTIONS.registryOnly);
93
+ const adapterOnly = new Set(STDIO_ADAPTER_EXEMPTIONS.adapterOnly);
94
+ const missingFromAdapter = [...registry]
95
+ .filter((name) => !adapter.has(name) && !registryOnly.has(name))
96
+ .sort();
97
+ const extraOnAdapter = [...adapter]
98
+ .filter((name) => !registry.has(name) && !adapterOnly.has(name))
99
+ .sort();
100
+ const staleExemptions = [
101
+ ...[...registryOnly].filter((name) => !registry.has(name) || adapter.has(name)),
102
+ ...[...adapterOnly].filter((name) => !adapter.has(name) || registry.has(name)),
103
+ ].sort();
104
+ return { missingFromAdapter, extraOnAdapter, staleExemptions };
105
+ }
package/dist/index.d.ts CHANGED
@@ -15,6 +15,9 @@
15
15
  * - flair_workspace_set — write own WorkspaceState (Office Space coordination)
16
16
  * - flair_orgevent — publish an OrgEvent attributed to self (no forging)
17
17
  * - record_usage — report that recalled memories were actually used (flair#1147)
18
+ * - skill_store — write a skill-tagged memory (trigger + procedure)
19
+ * - skill_search — catalog skills that apply to a task (not the procedure)
20
+ * - skill_get — retrieve the full skill by id (disclosure after search)
18
21
  *
19
22
  * Auto-presence (flair#598): every tool call above triggers a fire-and-forget,
20
23
  * rate-limited `POST /Presence` heartbeat for the calling agent (see
package/dist/index.js CHANGED
@@ -15,6 +15,9 @@
15
15
  * - flair_workspace_set — write own WorkspaceState (Office Space coordination)
16
16
  * - flair_orgevent — publish an OrgEvent attributed to self (no forging)
17
17
  * - record_usage — report that recalled memories were actually used (flair#1147)
18
+ * - skill_store — write a skill-tagged memory (trigger + procedure)
19
+ * - skill_search — catalog skills that apply to a task (not the procedure)
20
+ * - skill_get — retrieve the full skill by id (disclosure after search)
18
21
  *
19
22
  * Auto-presence (flair#598): every tool call above triggers a fire-and-forget,
20
23
  * rate-limited `POST /Presence` heartbeat for the calling agent (see
@@ -47,6 +50,7 @@ import { deriveActivity, postPresenceSafe, resolveHeartbeatIntervalMs, resolvePr
47
50
  import { readEnvOrUnset, stripInterpolationLiteralsFromEnv } from "./env-guard.js";
48
51
  import { buildRecordUsageBody, citationIds, withCiteNudge, RECORD_USAGE_ID_MERGE_CONTRACT } from "./usage.js";
49
52
  import { serverInfo } from "./version.js";
53
+ import { buildSkillSearchBody, buildSkillStoreBody, formatSkillCatalog, isSkillRecord, projectSkillSearchResponse, stripInternalMemoryFields, } from "./skills.js";
50
54
  // ─── Error helpers ──────────────────────────────────────────────────────────
51
55
  export function classifyError(err, flairUrl) {
52
56
  if (err instanceof FlairError) {
@@ -547,6 +551,110 @@ export async function runMcp() {
547
551
  return errorResult(err, flair.url);
548
552
  }
549
553
  });
554
+ // ─── Skills as memory (flair#1575 / #1542 / #1546) ──────────────────────────
555
+ //
556
+ // Native `/mcp` already ships skill_store / skill_search / skill_get in the
557
+ // TOOLS registry. This stdio adapter is the surface Claude Code and Cursor
558
+ // actually use, and it never wired them — 0.52.0's headline was unreachable.
559
+ // Same pattern as the other tools: shape the HTTP call via FlairClient,
560
+ // heartbeat, format the result. Write/recall/scope policy stays server-side.
561
+ server.tool("skill_store", "Write a skill (a reusable capability/procedure) as a skill-tagged memory. " +
562
+ "The `trigger` text is what the skill embeds from (the recall signal — 'when to use this'), " +
563
+ "and `content` is the full procedure. Skills are forced durability=persistent and are " +
564
+ "SkillScan-gated before the embed (a dangerous shell/network payload is rejected).", {
565
+ content: z.string().describe("The full procedure (markdown body of the SKILL.md)"),
566
+ trigger: z.string().optional().describe("The 'when to use' text — the recall signal the skill embeds from"),
567
+ name: z.string().optional().describe("Skill name (SKILL.md frontmatter; stored in metadata)"),
568
+ description: z.string().optional().describe("Skill description (SKILL.md frontmatter; stored in metadata)"),
569
+ tags: z.array(z.string()).optional().describe("Additional tags (the 'skill' tag is added automatically)"),
570
+ }, async ({ content, trigger, name, description, tags }) => {
571
+ heartbeat();
572
+ try {
573
+ const { id, body } = buildSkillStoreBody({
574
+ agentId: flair.agentId,
575
+ content,
576
+ trigger,
577
+ name,
578
+ description,
579
+ tags,
580
+ claimedClient: flair.claimedClient,
581
+ });
582
+ const result = await flair.request("PUT", `/Memory/${id}`, body);
583
+ const writtenId = typeof result?.id === "string" && result.id.length > 0 ? result.id : id;
584
+ const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
585
+ const lines = [
586
+ `Skill stored (id: ${writtenId})`,
587
+ `Preview: ${preview}`,
588
+ name ? `Name: ${name}` : undefined,
589
+ trigger ? `Trigger: ${trigger}` : undefined,
590
+ ].filter((line) => line != null);
591
+ return {
592
+ content: [{ type: "text", text: lines.join("\n") }],
593
+ structuredContent: { id: writtenId, written: true },
594
+ };
595
+ }
596
+ catch (err) {
597
+ return errorResult(err, flair.url);
598
+ }
599
+ });
600
+ server.tool("skill_search", "Find skills (reusable capabilities/procedures) that apply to a task. " +
601
+ "Ranks skill-tagged memories by their `trigger` ('when to use') against your task text. " +
602
+ "Returns a lightweight CATALOG — id, name, trigger, description, tags, agentId — NOT the full " +
603
+ "procedure (fetch that with skill_get). Scoped to your own + shared skills; another agent's " +
604
+ "private skill is never returned.", {
605
+ task: z.string().describe("The task/context to match skills against — natural language; ranked against each skill's trigger"),
606
+ limit: z.coerce.number().optional().default(5).describe("Max skills to return (default 5)"),
607
+ }, async ({ task, limit }) => {
608
+ heartbeat();
609
+ try {
610
+ const raw = await flair.request("POST", "/SemanticSearch", buildSkillSearchBody({ task, limit }));
611
+ const projected = projectSkillSearchResponse(raw);
612
+ if (!projected || typeof projected !== "object" || !Array.isArray(projected.results)) {
613
+ return { content: [{ type: "text", text: "No matching skills found." }] };
614
+ }
615
+ const results = projected.results;
616
+ return {
617
+ content: [{ type: "text", text: formatSkillCatalog(results) }],
618
+ structuredContent: { results },
619
+ };
620
+ }
621
+ catch (err) {
622
+ return errorResult(err, flair.url);
623
+ }
624
+ });
625
+ server.tool("skill_get", "Retrieve a full skill by ID — the complete procedure (`content`) plus trigger and metadata. " +
626
+ "The disclosure step after skill_search's catalog. Read-scoped: you can only get your own or a " +
627
+ "shared skill, never another agent's private skill. A non-skill id returns not-found.", {
628
+ id: z.string().describe("Skill (memory) ID"),
629
+ includeEmbedding: z.coerce.boolean().optional().default(false)
630
+ .describe("Include the raw embedding vector (large, rarely useful). Default false."),
631
+ }, async ({ id, includeEmbedding }) => {
632
+ heartbeat();
633
+ try {
634
+ const mem = await flair.memory.get(id);
635
+ if (!mem || !isSkillRecord(mem)) {
636
+ return { content: [{ type: "text", text: `Skill ${id} not found.` }] };
637
+ }
638
+ const record = includeEmbedding
639
+ ? mem
640
+ : stripInternalMemoryFields(mem);
641
+ const trigger = typeof record.trigger === "string" && record.trigger.length > 0
642
+ ? record.trigger
643
+ : "";
644
+ const text = [
645
+ record.content,
646
+ "",
647
+ `(id: ${record.id}${trigger ? `, trigger: ${trigger}` : ""}, tags: ${Array.isArray(record.tags) ? record.tags.join(", ") : "skill"}, created: ${record.createdAt ?? ""})`,
648
+ ].join("\n");
649
+ return {
650
+ content: [{ type: "text", text }],
651
+ structuredContent: record,
652
+ };
653
+ }
654
+ catch (err) {
655
+ return errorResult(err, flair.url);
656
+ }
657
+ });
550
658
  // ─── Start ───────────────────────────────────────────────────────────────────
551
659
  const transport = new StdioServerTransport();
552
660
  await server.connect(transport);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * skills.ts — stdio-adapter shaping for skill_* tools (flair#1575).
3
+ *
4
+ * Native `/mcp` implements skill_store / skill_search / skill_get as thin
5
+ * wrappers over Memory / SemanticSearch (resources/mcp-tools.ts). This
6
+ * package talks HTTP via FlairClient, so the wrappers here only shape
7
+ * the request body and the client-visible result — they re-implement no
8
+ * write, recall, or scoping logic. Server-side SkillScan, forced
9
+ * durability=persistent, and resolveReadScope still run on the daemon.
10
+ *
11
+ * Progressive disclosure matches the native contract: skill_search returns
12
+ * catalog cards (never `content` / embedding); skill_get is the disclosure
13
+ * step for the full procedure.
14
+ */
15
+ /** The tag that marks a Memory as a skill (resources/skill-write.ts). */
16
+ export declare const SKILL_TAG = "skill";
17
+ export declare function isSkillRecord(record: unknown): boolean;
18
+ /**
19
+ * Lightweight skill CATALOG card — id/name/trigger/description/tags/agentId.
20
+ * `name`/`description` live in the opaque metadata JSON blob; a corrupt or
21
+ * absent blob simply yields no name/desc. `content` and the raw embedding
22
+ * are deliberately absent (skill_search progressive-disclosure contract).
23
+ */
24
+ export declare function projectSkillCard(r: unknown): Record<string, unknown>;
25
+ export declare function stripInternalMemoryFields<T extends Record<string, unknown>>(value: T): T;
26
+ /** Body for PUT /Memory/:id — matches native skill_store's Memory.post() shape. */
27
+ export declare function buildSkillStoreBody(opts: {
28
+ agentId: string;
29
+ content: string;
30
+ trigger?: string;
31
+ name?: string;
32
+ description?: string;
33
+ tags?: string[];
34
+ claimedClient?: string;
35
+ }): {
36
+ id: string;
37
+ body: Record<string, unknown>;
38
+ };
39
+ /** SemanticSearch body for skill_search — no agentId (scope is the signed identity). */
40
+ export declare function buildSkillSearchBody(opts: {
41
+ task: string;
42
+ limit?: number;
43
+ }): Record<string, unknown>;
44
+ /**
45
+ * Project a SemanticSearch response onto catalog cards. A guard/error payload
46
+ * (no `results` array) is returned untouched so the caller can surface it.
47
+ */
48
+ export declare function projectSkillSearchResponse(res: unknown): unknown;
49
+ export declare function formatSkillCatalog(results: Array<Record<string, unknown>>): string;
package/dist/skills.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * skills.ts — stdio-adapter shaping for skill_* tools (flair#1575).
3
+ *
4
+ * Native `/mcp` implements skill_store / skill_search / skill_get as thin
5
+ * wrappers over Memory / SemanticSearch (resources/mcp-tools.ts). This
6
+ * package talks HTTP via FlairClient, so the wrappers here only shape
7
+ * the request body and the client-visible result — they re-implement no
8
+ * write, recall, or scoping logic. Server-side SkillScan, forced
9
+ * durability=persistent, and resolveReadScope still run on the daemon.
10
+ *
11
+ * Progressive disclosure matches the native contract: skill_search returns
12
+ * catalog cards (never `content` / embedding); skill_get is the disclosure
13
+ * step for the full procedure.
14
+ */
15
+ /** The tag that marks a Memory as a skill (resources/skill-write.ts). */
16
+ export const SKILL_TAG = "skill";
17
+ const INTERNAL_MEMORY_FIELDS = ["embedding", "embeddingModel"];
18
+ export function isSkillRecord(record) {
19
+ const tags = record?.tags;
20
+ return Array.isArray(tags) && tags.includes(SKILL_TAG);
21
+ }
22
+ /**
23
+ * Lightweight skill CATALOG card — id/name/trigger/description/tags/agentId.
24
+ * `name`/`description` live in the opaque metadata JSON blob; a corrupt or
25
+ * absent blob simply yields no name/desc. `content` and the raw embedding
26
+ * are deliberately absent (skill_search progressive-disclosure contract).
27
+ */
28
+ export function projectSkillCard(r) {
29
+ const row = (r ?? {});
30
+ let name;
31
+ let description;
32
+ if (typeof row.metadata === "string" && row.metadata.length > 0) {
33
+ try {
34
+ const meta = JSON.parse(row.metadata);
35
+ if (meta && typeof meta === "object") {
36
+ if (typeof meta.name === "string")
37
+ name = meta.name;
38
+ if (typeof meta.description === "string")
39
+ description = meta.description;
40
+ }
41
+ }
42
+ catch {
43
+ /* opaque/corrupt metadata → no name/description on the card */
44
+ }
45
+ }
46
+ return {
47
+ id: row.id,
48
+ name,
49
+ trigger: row.trigger,
50
+ description,
51
+ tags: row.tags,
52
+ agentId: row.agentId,
53
+ };
54
+ }
55
+ export function stripInternalMemoryFields(value) {
56
+ const out = { ...value };
57
+ for (const field of INTERNAL_MEMORY_FIELDS)
58
+ delete out[field];
59
+ return out;
60
+ }
61
+ /** Body for PUT /Memory/:id — matches native skill_store's Memory.post() shape. */
62
+ export function buildSkillStoreBody(opts) {
63
+ const id = `${opts.agentId}-${crypto.randomUUID()}`;
64
+ const body = {
65
+ id,
66
+ agentId: opts.agentId,
67
+ content: opts.content,
68
+ tags: [SKILL_TAG, ...(Array.isArray(opts.tags) ? opts.tags : [])],
69
+ };
70
+ // durability is NOT set — the server forces persistent for skill-tagged
71
+ // writes and rejects an explicit ephemeral/session.
72
+ if (typeof opts.trigger === "string" && opts.trigger.length > 0)
73
+ body.trigger = opts.trigger;
74
+ if (opts.claimedClient)
75
+ body.claimedClient = opts.claimedClient;
76
+ const meta = {};
77
+ if (typeof opts.name === "string" && opts.name.length > 0)
78
+ meta.name = opts.name;
79
+ if (typeof opts.description === "string" && opts.description.length > 0)
80
+ meta.description = opts.description;
81
+ if (Object.keys(meta).length > 0)
82
+ body.metadata = JSON.stringify(meta);
83
+ return { id, body };
84
+ }
85
+ /** SemanticSearch body for skill_search — no agentId (scope is the signed identity). */
86
+ export function buildSkillSearchBody(opts) {
87
+ return {
88
+ q: opts.task,
89
+ tag: SKILL_TAG,
90
+ limit: opts.limit ?? 5,
91
+ includeMetadata: true,
92
+ includeTrigger: true,
93
+ };
94
+ }
95
+ /**
96
+ * Project a SemanticSearch response onto catalog cards. A guard/error payload
97
+ * (no `results` array) is returned untouched so the caller can surface it.
98
+ */
99
+ export function projectSkillSearchResponse(res) {
100
+ if (!res || typeof res !== "object" || !Array.isArray(res.results)) {
101
+ return res;
102
+ }
103
+ const payload = res;
104
+ return { ...payload, results: payload.results.map(projectSkillCard) };
105
+ }
106
+ export function formatSkillCatalog(results) {
107
+ if (results.length === 0)
108
+ return "No matching skills found.";
109
+ return results
110
+ .map((card, i) => {
111
+ const title = typeof card.name === "string" && card.name.length > 0 ? card.name : "(unnamed skill)";
112
+ const trigger = typeof card.trigger === "string" && card.trigger.length > 0 ? card.trigger : "";
113
+ const desc = typeof card.description === "string" && card.description.length > 0 ? card.description : "";
114
+ const idStr = card.id ? `id:${card.id}` : "";
115
+ const header = [title, trigger, idStr].filter(Boolean).join(" — ");
116
+ return desc ? `${i + 1}. ${header}\n ${desc}` : `${i + 1}. ${header}`;
117
+ })
118
+ .join("\n");
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair-mcp",
3
- "version": "0.51.2",
3
+ "version": "0.53.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.51.2",
31
+ "@tpsdev-ai/flair-client": "0.53.0",
32
32
  "zod": "4.3.6"
33
33
  },
34
34
  "license": "Apache-2.0",