@tpsdev-ai/flair-mcp 0.52.0 → 0.54.1
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 +3 -0
- package/dist/adapter-surface.d.ts +66 -0
- package/dist/adapter-surface.js +116 -0
- package/dist/adapter-tools.d.ts +35 -0
- package/dist/adapter-tools.js +381 -0
- package/dist/catchup.d.ts +58 -0
- package/dist/catchup.js +85 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +39 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +20 -374
- package/dist/json-schema-zod.d.ts +10 -0
- package/dist/json-schema-zod.js +43 -0
- package/dist/skills.d.ts +50 -0
- package/dist/skills.js +120 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/LICENSE +19 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/README.md +22 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.d.ts +70 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.js +665 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/package.json +46 -0
- package/package.json +7 -2
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,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter-surface.ts — the stdio adapter's derived tool set (flair#1580).
|
|
3
|
+
*
|
|
4
|
+
* `@tpsdev-ai/flair-mcp` no longer hand-wires per-tool string literals in
|
|
5
|
+
* index.ts. The advertised set is STDIO_TOOL_DESCRIPTORS from the shared
|
|
6
|
+
* `@tpsdev-ai/flair-tool-descriptors` module — the same descriptors the
|
|
7
|
+
* server TOOLS registry binds to Harper impls. Drift is impossible by
|
|
8
|
+
* construction: a new both-surface descriptor appears here once a
|
|
9
|
+
* FlairClient handler is bound.
|
|
10
|
+
*
|
|
11
|
+
* This module remains the reviewed chokepoint for the stdio ↔ TOOLS seam:
|
|
12
|
+
*
|
|
13
|
+
* 1. `ADAPTER_TOOL_NAMES` is DERIVED from STDIO_TOOL_DESCRIPTORS.
|
|
14
|
+
* 2. `parseAdapterToolNames` still scans for leftover string-literal
|
|
15
|
+
* tool names passed to the MCP SDK — hand-wiring is now a CI failure,
|
|
16
|
+
* not the registration path.
|
|
17
|
+
* 3. `STDIO_ADAPTER_EXEMPTIONS` is DERIVED from descriptor surface flags
|
|
18
|
+
* (the #1578 list, now structural rather than hand-synced).
|
|
19
|
+
*/
|
|
20
|
+
/** Tools registered on the stdio adapter — derived from the shared descriptor list. */
|
|
21
|
+
export declare const ADAPTER_TOOL_NAMES: string[];
|
|
22
|
+
export type AdapterToolName = (typeof ADAPTER_TOOL_NAMES)[number];
|
|
23
|
+
/**
|
|
24
|
+
* Reviewed one-sided tools at the stdio-adapter ↔ server TOOLS seam.
|
|
25
|
+
* Derived from descriptor `native` / `stdio` flags (flair#1580) — the same
|
|
26
|
+
* names #1578 listed by hand (attention, archive verbs, relationship_store).
|
|
27
|
+
*/
|
|
28
|
+
export declare const STDIO_ADAPTER_EXEMPTIONS: {
|
|
29
|
+
readonly registryOnly: string[];
|
|
30
|
+
readonly adapterOnly: string[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Collect leftover string-literal tool registrations from adapter source.
|
|
34
|
+
* After #1580 the derived registrar uses `server.tool(d.name, ...)`, so this
|
|
35
|
+
* scan should return empty. A new literal is a CI failure.
|
|
36
|
+
* Does not use `new RegExp` built from runtime input (CodeQL js/regex-injection).
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseAdapterToolNames(source: string): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Handler keys from `STDIO_TOOL_HANDLERS` in adapter-tools.ts source.
|
|
41
|
+
* Root unit tests must not import adapter-tools.ts — that module loads
|
|
42
|
+
* `@tpsdev-ai/flair-client` (built later in the unit lane).
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseStdioHandlerNames(source: string): string[];
|
|
45
|
+
export interface AdapterRegistryParity {
|
|
46
|
+
/** TOOLS names the adapter neither registers nor exempts. */
|
|
47
|
+
missingFromAdapter: string[];
|
|
48
|
+
/** Adapter names that are neither in TOOLS nor adapter-only exempted. */
|
|
49
|
+
extraOnAdapter: string[];
|
|
50
|
+
/** Exemption entries that no longer describe a real one-sided difference. */
|
|
51
|
+
staleExemptions: string[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compare the stdio adapter's tool set to the server TOOLS registry.
|
|
55
|
+
* Equal after applying the reviewed exemption list — otherwise drift.
|
|
56
|
+
* Kept from #1578 as the migration tripwire; #1580 also asserts
|
|
57
|
+
* derived set == descriptor set structurally.
|
|
58
|
+
*/
|
|
59
|
+
export declare function adapterRegistryParity(registryNames: readonly string[], adapterNames: readonly string[]): AdapterRegistryParity;
|
|
60
|
+
/**
|
|
61
|
+
* Structural #1580 assert: the bound handler set equals the stdio descriptor set.
|
|
62
|
+
*/
|
|
63
|
+
export declare function derivedDescriptorParity(handlerNames: readonly string[], descriptorNamesList?: readonly string[]): {
|
|
64
|
+
missingHandlers: string[];
|
|
65
|
+
extraHandlers: string[];
|
|
66
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter-surface.ts — the stdio adapter's derived tool set (flair#1580).
|
|
3
|
+
*
|
|
4
|
+
* `@tpsdev-ai/flair-mcp` no longer hand-wires per-tool string literals in
|
|
5
|
+
* index.ts. The advertised set is STDIO_TOOL_DESCRIPTORS from the shared
|
|
6
|
+
* `@tpsdev-ai/flair-tool-descriptors` module — the same descriptors the
|
|
7
|
+
* server TOOLS registry binds to Harper impls. Drift is impossible by
|
|
8
|
+
* construction: a new both-surface descriptor appears here once a
|
|
9
|
+
* FlairClient handler is bound.
|
|
10
|
+
*
|
|
11
|
+
* This module remains the reviewed chokepoint for the stdio ↔ TOOLS seam:
|
|
12
|
+
*
|
|
13
|
+
* 1. `ADAPTER_TOOL_NAMES` is DERIVED from STDIO_TOOL_DESCRIPTORS.
|
|
14
|
+
* 2. `parseAdapterToolNames` still scans for leftover string-literal
|
|
15
|
+
* tool names passed to the MCP SDK — hand-wiring is now a CI failure,
|
|
16
|
+
* not the registration path.
|
|
17
|
+
* 3. `STDIO_ADAPTER_EXEMPTIONS` is DERIVED from descriptor surface flags
|
|
18
|
+
* (the #1578 list, now structural rather than hand-synced).
|
|
19
|
+
*/
|
|
20
|
+
import { STDIO_TOOL_DESCRIPTORS, SURFACE_EXEMPTIONS, descriptorNames, } from "@tpsdev-ai/flair-tool-descriptors";
|
|
21
|
+
/** Tools registered on the stdio adapter — derived from the shared descriptor list. */
|
|
22
|
+
export const ADAPTER_TOOL_NAMES = descriptorNames(STDIO_TOOL_DESCRIPTORS);
|
|
23
|
+
/**
|
|
24
|
+
* Reviewed one-sided tools at the stdio-adapter ↔ server TOOLS seam.
|
|
25
|
+
* Derived from descriptor `native` / `stdio` flags (flair#1580) — the same
|
|
26
|
+
* names #1578 listed by hand (attention, archive verbs, relationship_store).
|
|
27
|
+
*/
|
|
28
|
+
export const STDIO_ADAPTER_EXEMPTIONS = {
|
|
29
|
+
registryOnly: SURFACE_EXEMPTIONS.registryOnly,
|
|
30
|
+
adapterOnly: SURFACE_EXEMPTIONS.adapterOnly,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Collect leftover string-literal tool registrations from adapter source.
|
|
34
|
+
* After #1580 the derived registrar uses `server.tool(d.name, ...)`, so this
|
|
35
|
+
* scan should return empty. A new literal is a CI failure.
|
|
36
|
+
* Does not use `new RegExp` built from runtime input (CodeQL js/regex-injection).
|
|
37
|
+
*/
|
|
38
|
+
export function parseAdapterToolNames(source) {
|
|
39
|
+
const names = [];
|
|
40
|
+
const re = /server\.tool\(\s*"([a-z][a-z0-9_]*)"/g;
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = re.exec(source)) !== null)
|
|
43
|
+
names.push(match[1]);
|
|
44
|
+
return names;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Handler keys from `STDIO_TOOL_HANDLERS` in adapter-tools.ts source.
|
|
48
|
+
* Root unit tests must not import adapter-tools.ts — that module loads
|
|
49
|
+
* `@tpsdev-ai/flair-client` (built later in the unit lane).
|
|
50
|
+
*/
|
|
51
|
+
export function parseStdioHandlerNames(source) {
|
|
52
|
+
const marker = "export const STDIO_TOOL_HANDLERS";
|
|
53
|
+
const start = source.indexOf(marker);
|
|
54
|
+
if (start < 0)
|
|
55
|
+
return [];
|
|
56
|
+
const open = source.indexOf("{", start);
|
|
57
|
+
if (open < 0)
|
|
58
|
+
return [];
|
|
59
|
+
let depth = 0;
|
|
60
|
+
let close = -1;
|
|
61
|
+
for (let i = open; i < source.length; i++) {
|
|
62
|
+
const c = source[i];
|
|
63
|
+
if (c === "{")
|
|
64
|
+
depth++;
|
|
65
|
+
else if (c === "}") {
|
|
66
|
+
depth--;
|
|
67
|
+
if (depth === 0) {
|
|
68
|
+
close = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (close < 0)
|
|
74
|
+
return [];
|
|
75
|
+
const names = [];
|
|
76
|
+
for (const line of source.slice(open + 1, close).split("\n")) {
|
|
77
|
+
const m = line.match(/^\s*([a-z][a-z0-9_]*)\s*,?\s*$/);
|
|
78
|
+
if (m)
|
|
79
|
+
names.push(m[1]);
|
|
80
|
+
}
|
|
81
|
+
return names.sort();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compare the stdio adapter's tool set to the server TOOLS registry.
|
|
85
|
+
* Equal after applying the reviewed exemption list — otherwise drift.
|
|
86
|
+
* Kept from #1578 as the migration tripwire; #1580 also asserts
|
|
87
|
+
* derived set == descriptor set structurally.
|
|
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
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Structural #1580 assert: the bound handler set equals the stdio descriptor set.
|
|
108
|
+
*/
|
|
109
|
+
export function derivedDescriptorParity(handlerNames, descriptorNamesList = ADAPTER_TOOL_NAMES) {
|
|
110
|
+
const handlers = new Set(handlerNames);
|
|
111
|
+
const descriptors = new Set(descriptorNamesList);
|
|
112
|
+
return {
|
|
113
|
+
missingHandlers: [...descriptors].filter((n) => !handlers.has(n)).sort(),
|
|
114
|
+
extraHandlers: [...handlers].filter((n) => !descriptors.has(n)).sort(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio adapter bindings (flair#1580).
|
|
3
|
+
*
|
|
4
|
+
* The tool SET is derived from STDIO_TOOL_DESCRIPTORS. This module only
|
|
5
|
+
* supplies FlairClient HTTP handlers — one per stdio descriptor. A new
|
|
6
|
+
* descriptor with no handler (or a handler with no descriptor) fails at
|
|
7
|
+
* registration, so the surfaces cannot drift by omission.
|
|
8
|
+
*/
|
|
9
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import type { FlairClient } from "@tpsdev-ai/flair-client";
|
|
11
|
+
import { type PresenceActivity } from "./presence.js";
|
|
12
|
+
export interface AdapterContext {
|
|
13
|
+
flair: FlairClient;
|
|
14
|
+
agentId: string;
|
|
15
|
+
heartbeat: (activity?: PresenceActivity) => void;
|
|
16
|
+
rememberTask: (task: string | undefined) => void;
|
|
17
|
+
}
|
|
18
|
+
type ToolResult = {
|
|
19
|
+
content: Array<{
|
|
20
|
+
type: "text";
|
|
21
|
+
text: string;
|
|
22
|
+
}>;
|
|
23
|
+
isError?: boolean;
|
|
24
|
+
structuredContent?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
type StdioHandler = (args: Record<string, any>, ctx: AdapterContext) => Promise<ToolResult>;
|
|
27
|
+
/** FlairClient bindings keyed by descriptor name — the adapter-side impl map. */
|
|
28
|
+
export declare const STDIO_TOOL_HANDLERS: Record<string, StdioHandler>;
|
|
29
|
+
export declare function stdioHandlerNames(): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Register every stdio descriptor on the MCP server. The advertised set is
|
|
32
|
+
* STDIO_TOOL_DESCRIPTORS — not a hand-written per-tool literal list.
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerStdioTools(server: McpServer, ctx: AdapterContext): string[];
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio adapter bindings (flair#1580).
|
|
3
|
+
*
|
|
4
|
+
* The tool SET is derived from STDIO_TOOL_DESCRIPTORS. This module only
|
|
5
|
+
* supplies FlairClient HTTP handlers — one per stdio descriptor. A new
|
|
6
|
+
* descriptor with no handler (or a handler with no descriptor) fails at
|
|
7
|
+
* registration, so the surfaces cannot drift by omission.
|
|
8
|
+
*/
|
|
9
|
+
import { STDIO_TOOL_DESCRIPTORS, toStdioMcpToolDef, } from "@tpsdev-ai/flair-tool-descriptors";
|
|
10
|
+
import { buildCatchupRequest, summarizeCatchup } from "./catchup.js";
|
|
11
|
+
import { classifyError } from "./errors.js";
|
|
12
|
+
import { jsonSchemaToZodShape } from "./json-schema-zod.js";
|
|
13
|
+
import { deriveActivity } from "./presence.js";
|
|
14
|
+
import { buildRecordUsageBody, citationIds, withCiteNudge } from "./usage.js";
|
|
15
|
+
import { buildSkillSearchBody, buildSkillStoreBody, formatSkillCatalog, isSkillRecord, projectSkillSearchResponse, stripInternalMemoryFields, } from "./skills.js";
|
|
16
|
+
function errorResult(err, flairUrl) {
|
|
17
|
+
return { content: [{ type: "text", text: classifyError(err, flairUrl) }], isError: true };
|
|
18
|
+
}
|
|
19
|
+
const memory_search = async ({ query, limit }, { flair, heartbeat }) => {
|
|
20
|
+
heartbeat();
|
|
21
|
+
try {
|
|
22
|
+
const results = await flair.memory.search(query, { limit: limit ?? 5 });
|
|
23
|
+
if (results.length === 0) {
|
|
24
|
+
return { content: [{ type: "text", text: "No relevant memories found." }] };
|
|
25
|
+
}
|
|
26
|
+
const text = results
|
|
27
|
+
.map((r, i) => {
|
|
28
|
+
const date = r.createdAt ? r.createdAt.slice(0, 10) : "";
|
|
29
|
+
const idStr = r.id ? `id:${r.id}` : "";
|
|
30
|
+
const meta = [date, r.type, idStr].filter(Boolean).join(", ");
|
|
31
|
+
return `${i + 1}. ${r.content}${meta ? ` (${meta})` : ""}`;
|
|
32
|
+
})
|
|
33
|
+
.join("\n");
|
|
34
|
+
return { content: [{ type: "text", text: withCiteNudge(text) }] };
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
return errorResult(err, flair.url);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const memory_store = async ({ content, type, durability, tags, visibility, usedMemoryIds }, { flair, heartbeat }) => {
|
|
41
|
+
heartbeat();
|
|
42
|
+
try {
|
|
43
|
+
const result = await flair.memory.write(content, {
|
|
44
|
+
type: (type ?? "session"),
|
|
45
|
+
durability: (durability ?? "standard"),
|
|
46
|
+
tags,
|
|
47
|
+
visibility: visibility,
|
|
48
|
+
dedup: true,
|
|
49
|
+
dedupThreshold: 0.95,
|
|
50
|
+
usedMemoryIds: citationIds(usedMemoryIds),
|
|
51
|
+
});
|
|
52
|
+
const deduplicated = result.deduplicated === true;
|
|
53
|
+
const matchedId = result.matchedId;
|
|
54
|
+
const effectiveVisibility = result.visibility;
|
|
55
|
+
const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
|
|
56
|
+
const tagStr = tags && tags.length > 0 ? tags.join(", ") : "none";
|
|
57
|
+
const lines = [
|
|
58
|
+
`Memory stored (id: ${result.id})`,
|
|
59
|
+
`Preview: ${preview}`,
|
|
60
|
+
`Size: ${content.length} chars`,
|
|
61
|
+
`Tags: ${tagStr}`,
|
|
62
|
+
`Type: ${type ?? "session"}, Durability: ${durability ?? "standard"}, Visibility: ${effectiveVisibility ?? "(server default)"}`,
|
|
63
|
+
];
|
|
64
|
+
if (deduplicated && matchedId) {
|
|
65
|
+
lines.push("", `Note: similar to existing memory id=${matchedId} — both are kept. ` +
|
|
66
|
+
`If this was meant to UPDATE that memory rather than add a new one, use memory_update instead.`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
70
|
+
structuredContent: { deduplicated, id: result.id, written: true, ...(deduplicated ? { matchedId } : {}) },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return errorResult(err, flair.url);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const memory_update = async ({ id, content, preserveHistory, usedMemoryIds }, { flair, heartbeat }) => {
|
|
78
|
+
heartbeat();
|
|
79
|
+
try {
|
|
80
|
+
const result = await flair.memory.update(id, content, {
|
|
81
|
+
preserveHistory,
|
|
82
|
+
usedMemoryIds: citationIds(usedMemoryIds),
|
|
83
|
+
});
|
|
84
|
+
const text = preserveHistory
|
|
85
|
+
? `Memory updated: new version stored (id: ${result.id}), supersedes ${id}.`
|
|
86
|
+
: `Memory updated (id: ${id}).`;
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text }],
|
|
89
|
+
structuredContent: { id: result.id, supersedes: preserveHistory ? id : undefined, written: true },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return errorResult(err, flair.url);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const memory_get = async ({ id }, { flair, heartbeat }) => {
|
|
97
|
+
heartbeat();
|
|
98
|
+
try {
|
|
99
|
+
const mem = await flair.memory.get(id);
|
|
100
|
+
if (!mem)
|
|
101
|
+
return { content: [{ type: "text", text: `Memory ${id} not found.` }] };
|
|
102
|
+
return { content: [{ type: "text", text: `${mem.content}\n\n(type: ${mem.type}, durability: ${mem.durability}, created: ${mem.createdAt})` }] };
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return errorResult(err, flair.url);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const memory_delete = async ({ id }, { flair, heartbeat }) => {
|
|
109
|
+
heartbeat();
|
|
110
|
+
try {
|
|
111
|
+
await flair.memory.delete(id);
|
|
112
|
+
return { content: [{ type: "text", text: `Memory ${id} deleted.` }] };
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
return errorResult(err, flair.url);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const relationship_store = async ({ subject, predicate, object, confidence, validFrom, validTo, source }, { flair, heartbeat }) => {
|
|
119
|
+
heartbeat();
|
|
120
|
+
try {
|
|
121
|
+
const result = await flair.relationship.write({ subject, predicate, object, confidence, validFrom, validTo, source });
|
|
122
|
+
const confStr = confidence !== undefined ? ` (confidence: ${confidence})` : "";
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: `Relationship recorded: ${subject} → ${predicate} → ${object}${confStr} (id: ${result.id})` }],
|
|
125
|
+
structuredContent: { id: result.id, subject, predicate, object, written: true },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
return errorResult(err, flair.url);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const bootstrap = async ({ maxTokens, currentTask, channel, surface, subjects }, { flair, heartbeat, rememberTask }) => {
|
|
133
|
+
if (currentTask)
|
|
134
|
+
rememberTask(currentTask);
|
|
135
|
+
heartbeat(deriveActivity({ channel, surface }));
|
|
136
|
+
try {
|
|
137
|
+
const result = await flair.bootstrap({ maxTokens, currentTask, channel, surface, subjects });
|
|
138
|
+
if (!result.context) {
|
|
139
|
+
return { content: [{ type: "text", text: "No context available." }] };
|
|
140
|
+
}
|
|
141
|
+
return { content: [{ type: "text", text: withCiteNudge(result.context) }] };
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
return errorResult(err, flair.url);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const soul_set = async ({ key, value }, { flair, heartbeat }) => {
|
|
148
|
+
heartbeat();
|
|
149
|
+
try {
|
|
150
|
+
await flair.soul.set(key, value);
|
|
151
|
+
return { content: [{ type: "text", text: `Soul entry '${key}' set.` }] };
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return errorResult(err, flair.url);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const soul_get = async ({ key }, { flair, heartbeat }) => {
|
|
158
|
+
heartbeat();
|
|
159
|
+
try {
|
|
160
|
+
const entry = await flair.soul.get(key);
|
|
161
|
+
if (!entry)
|
|
162
|
+
return { content: [{ type: "text", text: `No soul entry for '${key}'.` }] };
|
|
163
|
+
return { content: [{ type: "text", text: entry.value }] };
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
return errorResult(err, flair.url);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const flair_workspace_set = async ({ ref, label, provider, task, phase, summary }, { flair, agentId, heartbeat }) => {
|
|
170
|
+
heartbeat();
|
|
171
|
+
try {
|
|
172
|
+
const body = {
|
|
173
|
+
id: `${agentId}:${ref}`,
|
|
174
|
+
ref,
|
|
175
|
+
provider: provider ?? "mcp",
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
};
|
|
178
|
+
if (label)
|
|
179
|
+
body.label = label;
|
|
180
|
+
if (task)
|
|
181
|
+
body.taskId = task;
|
|
182
|
+
if (phase)
|
|
183
|
+
body.phase = phase;
|
|
184
|
+
if (summary)
|
|
185
|
+
body.summary = summary;
|
|
186
|
+
await flair.request("POST", "/WorkspaceState", body);
|
|
187
|
+
return { content: [{ type: "text", text: `Workspace state set: ref=${ref}${phase ? `, phase=${phase}` : ""} (attributed to ${agentId}).` }] };
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
return errorResult(err, flair.url);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const flair_orgevent = async ({ kind, summary, detail, scope, targets }, { flair, agentId, heartbeat }) => {
|
|
194
|
+
heartbeat();
|
|
195
|
+
try {
|
|
196
|
+
const body = { kind, summary };
|
|
197
|
+
if (detail)
|
|
198
|
+
body.detail = detail;
|
|
199
|
+
if (scope)
|
|
200
|
+
body.scope = scope;
|
|
201
|
+
if (targets && targets.length > 0)
|
|
202
|
+
body.targetIds = targets;
|
|
203
|
+
const result = await flair.request("POST", "/OrgEvent", body);
|
|
204
|
+
const targetStr = targets && targets.length > 0 ? ` → ${targets.join(", ")}` : "";
|
|
205
|
+
const idStr = result?.id ? ` (id: ${result.id})` : "";
|
|
206
|
+
return { content: [{ type: "text", text: `OrgEvent published: kind=${kind}${targetStr} (attributed to ${agentId})${idStr}.` }] };
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
return errorResult(err, flair.url);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const flair_catchup = async (args, { flair, agentId, heartbeat }) => {
|
|
213
|
+
heartbeat();
|
|
214
|
+
try {
|
|
215
|
+
// Owner-scope by construction: the request path is built from the CALLER's
|
|
216
|
+
// own agentId (identity), never a tool argument — `args` is consulted only
|
|
217
|
+
// for after/limit/ack. There is deliberately no agentId/participantId
|
|
218
|
+
// parameter, so a caller cannot name another feed (the server also refuses
|
|
219
|
+
// a cross-agent read with 403).
|
|
220
|
+
const request = buildCatchupRequest(agentId, args);
|
|
221
|
+
if (request.ackPosition) {
|
|
222
|
+
// Advance-on-ack (monotonic, re-ack safe) BEFORE the read, so a caller
|
|
223
|
+
// draining page N while acking page N-1 reads page N — and an ack never
|
|
224
|
+
// hides an event that was in the same response.
|
|
225
|
+
await flair.request("POST", request.ackPath, { position: request.ackPosition });
|
|
226
|
+
}
|
|
227
|
+
const page = await flair.request("GET", request.getPath);
|
|
228
|
+
const { text, structuredContent } = summarizeCatchup(page, request.ackPosition);
|
|
229
|
+
return { content: [{ type: "text", text }], structuredContent };
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
return errorResult(err, flair.url);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const record_usage = async ({ memoryId, memoryIds, attribution }, { flair, heartbeat }) => {
|
|
236
|
+
heartbeat();
|
|
237
|
+
try {
|
|
238
|
+
const body = buildRecordUsageBody({ memoryId, memoryIds, attribution });
|
|
239
|
+
if (!body) {
|
|
240
|
+
return {
|
|
241
|
+
content: [{ type: "text", text: "record_usage requires memoryId or memoryIds." }],
|
|
242
|
+
isError: true,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const result = await flair.request("POST", "/RecordUsage", body);
|
|
246
|
+
const text = result?.recorded === true ? "Usage recorded." : "Usage request accepted.";
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text }],
|
|
249
|
+
structuredContent: { recorded: result?.recorded === true },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
return errorResult(err, flair.url);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const skill_store = async ({ content, trigger, name, description, tags }, { flair, heartbeat }) => {
|
|
257
|
+
heartbeat();
|
|
258
|
+
try {
|
|
259
|
+
const { id, body } = buildSkillStoreBody({
|
|
260
|
+
agentId: flair.agentId,
|
|
261
|
+
content,
|
|
262
|
+
trigger,
|
|
263
|
+
name,
|
|
264
|
+
description,
|
|
265
|
+
tags,
|
|
266
|
+
claimedClient: flair.claimedClient,
|
|
267
|
+
});
|
|
268
|
+
const result = await flair.request("PUT", `/Memory/${id}`, body);
|
|
269
|
+
const writtenId = typeof result?.id === "string" && result.id.length > 0 ? result.id : id;
|
|
270
|
+
const preview = content.length > 120 ? content.slice(0, 120) + "..." : content;
|
|
271
|
+
const lines = [
|
|
272
|
+
`Skill stored (id: ${writtenId})`,
|
|
273
|
+
`Preview: ${preview}`,
|
|
274
|
+
name ? `Name: ${name}` : undefined,
|
|
275
|
+
trigger ? `Trigger: ${trigger}` : undefined,
|
|
276
|
+
].filter((line) => line != null);
|
|
277
|
+
return {
|
|
278
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
279
|
+
structuredContent: { id: writtenId, written: true },
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
return errorResult(err, flair.url);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const skill_search = async ({ task, limit }, { flair, heartbeat }) => {
|
|
287
|
+
heartbeat();
|
|
288
|
+
try {
|
|
289
|
+
const raw = await flair.request("POST", "/SemanticSearch", buildSkillSearchBody({ task, limit: limit ?? 5 }));
|
|
290
|
+
const projected = projectSkillSearchResponse(raw);
|
|
291
|
+
if (!projected || typeof projected !== "object" || !Array.isArray(projected.results)) {
|
|
292
|
+
return { content: [{ type: "text", text: "No matching skills found." }] };
|
|
293
|
+
}
|
|
294
|
+
const results = projected.results;
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: "text", text: formatSkillCatalog(results) }],
|
|
297
|
+
structuredContent: { results },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
return errorResult(err, flair.url);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const skill_get = async ({ id }, { flair, heartbeat }) => {
|
|
305
|
+
heartbeat();
|
|
306
|
+
try {
|
|
307
|
+
const mem = await flair.memory.get(id);
|
|
308
|
+
if (!mem || !isSkillRecord(mem)) {
|
|
309
|
+
return { content: [{ type: "text", text: `Skill ${id} not found.` }] };
|
|
310
|
+
}
|
|
311
|
+
const record = stripInternalMemoryFields(mem);
|
|
312
|
+
const trigger = typeof record.trigger === "string" && record.trigger.length > 0 ? record.trigger : "";
|
|
313
|
+
const text = [
|
|
314
|
+
record.content,
|
|
315
|
+
"",
|
|
316
|
+
`(id: ${record.id}${trigger ? `, trigger: ${trigger}` : ""}, tags: ${Array.isArray(record.tags) ? record.tags.join(", ") : "skill"}, created: ${record.createdAt ?? ""})`,
|
|
317
|
+
].join("\n");
|
|
318
|
+
return {
|
|
319
|
+
content: [{ type: "text", text }],
|
|
320
|
+
structuredContent: record,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
return errorResult(err, flair.url);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
/** FlairClient bindings keyed by descriptor name — the adapter-side impl map. */
|
|
328
|
+
export const STDIO_TOOL_HANDLERS = {
|
|
329
|
+
memory_search,
|
|
330
|
+
memory_store,
|
|
331
|
+
memory_update,
|
|
332
|
+
memory_get,
|
|
333
|
+
memory_delete,
|
|
334
|
+
relationship_store,
|
|
335
|
+
bootstrap,
|
|
336
|
+
soul_set,
|
|
337
|
+
soul_get,
|
|
338
|
+
flair_workspace_set,
|
|
339
|
+
flair_orgevent,
|
|
340
|
+
flair_catchup,
|
|
341
|
+
record_usage,
|
|
342
|
+
skill_store,
|
|
343
|
+
skill_search,
|
|
344
|
+
skill_get,
|
|
345
|
+
};
|
|
346
|
+
export function stdioHandlerNames() {
|
|
347
|
+
return Object.keys(STDIO_TOOL_HANDLERS).sort();
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Register every stdio descriptor on the MCP server. The advertised set is
|
|
351
|
+
* STDIO_TOOL_DESCRIPTORS — not a hand-written per-tool literal list.
|
|
352
|
+
*/
|
|
353
|
+
export function registerStdioTools(server, ctx) {
|
|
354
|
+
const registered = [];
|
|
355
|
+
const missing = [];
|
|
356
|
+
for (const d of STDIO_TOOL_DESCRIPTORS) {
|
|
357
|
+
const handler = STDIO_TOOL_HANDLERS[d.name];
|
|
358
|
+
if (!handler) {
|
|
359
|
+
missing.push(d.name);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const def = toStdioMcpToolDef(d);
|
|
363
|
+
const shape = jsonSchemaToZodShape(def.inputSchema);
|
|
364
|
+
const cb = async (args) => handler(args, ctx);
|
|
365
|
+
if (d.annotations) {
|
|
366
|
+
server.tool(d.name, def.description, shape, d.annotations, cb);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
server.tool(d.name, def.description, shape, cb);
|
|
370
|
+
}
|
|
371
|
+
registered.push(d.name);
|
|
372
|
+
}
|
|
373
|
+
if (missing.length > 0) {
|
|
374
|
+
throw new Error(`stdio adapter missing FlairClient bindings for descriptors: ${missing.join(", ")}`);
|
|
375
|
+
}
|
|
376
|
+
const extra = Object.keys(STDIO_TOOL_HANDLERS).filter((n) => !registered.includes(n)).sort();
|
|
377
|
+
if (extra.length > 0) {
|
|
378
|
+
throw new Error(`stdio adapter bindings have no stdio descriptor: ${extra.join(", ")}`);
|
|
379
|
+
}
|
|
380
|
+
return registered;
|
|
381
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catchup.ts — pure helpers for the flair_catchup stdio binding (flair#1583).
|
|
3
|
+
*
|
|
4
|
+
* Owner-scope is enforced BY CONSTRUCTION: the participantId in the request
|
|
5
|
+
* path is always the caller's own agentId (from `FLAIR_AGENT_ID` / the signed
|
|
6
|
+
* identity), never a tool argument. The descriptor advertises no `agentId` /
|
|
7
|
+
* `participantId` property, so there is nothing to name another agent's feed
|
|
8
|
+
* with — and the server independently refuses a cross-agent read (403). This
|
|
9
|
+
* module is HTTP-/Harper-free (plain helpers + types) so the flair-mcp
|
|
10
|
+
* package stays FlairClient-only.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* `GET /OrgEventCatchup/{participantId}` response shape
|
|
14
|
+
* (resources/OrgEventCatchup.ts). `position` is stamped onto every event
|
|
15
|
+
* (see org-event-catchup-lib.ts `withEventPosition`), and is what a caller
|
|
16
|
+
* passes back as `ack` once it has processed the event.
|
|
17
|
+
*/
|
|
18
|
+
export interface CatchupPage {
|
|
19
|
+
events?: Array<Record<string, unknown>> | null;
|
|
20
|
+
/** Resolved exclusive cursor this page was read after. */
|
|
21
|
+
after?: string | null;
|
|
22
|
+
/** Cursor to continue a drain — the last event's position (or `after` when the page is empty). */
|
|
23
|
+
nextAfter?: string | null;
|
|
24
|
+
/** Durable watermark at read time (null when the caller has none yet). */
|
|
25
|
+
watermark?: string | null;
|
|
26
|
+
hasMore?: boolean;
|
|
27
|
+
pageSize?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CatchupArgs {
|
|
30
|
+
after?: unknown;
|
|
31
|
+
limit?: unknown;
|
|
32
|
+
ack?: unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface CatchupRequest {
|
|
35
|
+
/** Owner-scoped base path — the caller's own participantId. */
|
|
36
|
+
path: string;
|
|
37
|
+
/** GET path (base + optional query). */
|
|
38
|
+
getPath: string;
|
|
39
|
+
/** POST path for the ack (same owner-scoped base). */
|
|
40
|
+
ackPath: string;
|
|
41
|
+
/** Non-empty ack position, or null when the caller did not ack. */
|
|
42
|
+
ackPosition: string | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build the owner-scoped catchup request from tool args. `participantId` is
|
|
46
|
+
* ALWAYS `agentId` — the args cannot redirect it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildCatchupRequest(agentId: string, args: CatchupArgs): CatchupRequest;
|
|
49
|
+
export interface CatchupSummary {
|
|
50
|
+
text: string;
|
|
51
|
+
structuredContent: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Project a catchup page into the caller-facing text summary plus the
|
|
55
|
+
* structured echo (the machine-readable payload). Never throws on a
|
|
56
|
+
* malformed/absent page — it degrades to "no new events".
|
|
57
|
+
*/
|
|
58
|
+
export declare function summarizeCatchup(page: CatchupPage | undefined, acked: string | null): CatchupSummary;
|