@tpsdev-ai/flair-mcp 0.44.12 → 0.45.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/dist/env-guard.d.ts +56 -0
- package/dist/env-guard.js +70 -0
- package/dist/index.js +12 -5
- package/dist/session-start-hook.js +10 -3
- package/package.json +2 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interpolation-literal env guard (flair#1250).
|
|
3
|
+
*
|
|
4
|
+
* An MCP host config template often forwards env like
|
|
5
|
+
* "env": { "FLAIR_URL": "${FLAIR_URL}" }
|
|
6
|
+
* expecting the host to substitute `${FLAIR_URL}` with the real value. When the
|
|
7
|
+
* variable is unset and the host does NOT substitute (Cursor's plugin `mcp.json`
|
|
8
|
+
* is exactly this shape — see packages/cursor-flair/mcp.json), flair-mcp is
|
|
9
|
+
* handed the LITERAL string `"${FLAIR_URL}"`. That literal is truthy, so it wins
|
|
10
|
+
* every `value ?? default` / `value || default` chain and the sensible default
|
|
11
|
+
* (flair-client's `http://localhost:19926`) never applies — the connection then
|
|
12
|
+
* fails in a way that points nowhere near the real cause.
|
|
13
|
+
*
|
|
14
|
+
* This module treats such a literal as "unset" so the default applies instead.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* True iff `value` is an unsubstituted interpolation literal like `${FLAIR_URL}`
|
|
18
|
+
* — i.e. the whole (trimmed) value is a single `${...}` placeholder the host
|
|
19
|
+
* never expanded. A value that merely CONTAINS a `${...}` (e.g. a real URL with
|
|
20
|
+
* a fragment) is left alone: only a wholesale placeholder is treated as unset.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isUnsubstitutedInterpolation(value: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Read an env var, treating an unsubstituted `${...}` interpolation literal as
|
|
25
|
+
* unset: returns `undefined` for such a literal (and for a genuinely absent
|
|
26
|
+
* var) so a downstream default applies instead of the literal being used
|
|
27
|
+
* verbatim. A real value is returned unchanged (NOT trimmed — only the
|
|
28
|
+
* placeholder check trims).
|
|
29
|
+
*/
|
|
30
|
+
export declare function readEnvOrUnset(name: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Connection env vars that flair-client's OWN constructor reads straight from
|
|
33
|
+
* `process.env` as a fallback (see flair-client/src/client.ts:
|
|
34
|
+
* `this.url = config.url ?? readEnvOrUnset("FLAIR_URL") ?? DEFAULT_URL`). For
|
|
35
|
+
* these, skipping the literal only at flair-mcp's call site was NOT enough:
|
|
36
|
+
* flair-client re-read `process.env` and resurrected the `${...}` literal,
|
|
37
|
+
* defeating its own default. As of flair#1254 flair-client's env fallbacks
|
|
38
|
+
* apply this same literal-as-unset guard themselves, so the strip below is
|
|
39
|
+
* defense-in-depth rather than the only line — kept deliberately (an older
|
|
40
|
+
* flair-client on a consumer's disk does not have #1254).
|
|
41
|
+
*
|
|
42
|
+
* Only FLAIR_URL qualifies today: it is re-read by flair-client AND flair-mcp
|
|
43
|
+
* still constructs a client when it is a literal. FLAIR_AGENT_ID is also re-read
|
|
44
|
+
* by flair-client, but flair-mcp's `if (!agentId)` guard (using readEnvOrUnset)
|
|
45
|
+
* short-circuits before any client is built, so no strip is needed. FLAIR_KEY_PATH
|
|
46
|
+
* flair-client never reads from env, so guarding it at the call site suffices.
|
|
47
|
+
*/
|
|
48
|
+
export declare const ENV_RESURRECTED_BY_FLAIR_CLIENT: readonly ["FLAIR_URL"];
|
|
49
|
+
/**
|
|
50
|
+
* Delete any of `names` whose value is an unsubstituted `${...}` interpolation
|
|
51
|
+
* literal from `env`, so downstream readers — flair-mcp's own reads AND
|
|
52
|
+
* flair-client's internal `process.env` fallback — see the var as unset and
|
|
53
|
+
* apply their default. Idempotent; mutates the live process env by default and
|
|
54
|
+
* accepts an injectable env for tests. Real (substituted) values are untouched.
|
|
55
|
+
*/
|
|
56
|
+
export declare function stripInterpolationLiteralsFromEnv(env?: NodeJS.ProcessEnv, names?: readonly string[]): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interpolation-literal env guard (flair#1250).
|
|
3
|
+
*
|
|
4
|
+
* An MCP host config template often forwards env like
|
|
5
|
+
* "env": { "FLAIR_URL": "${FLAIR_URL}" }
|
|
6
|
+
* expecting the host to substitute `${FLAIR_URL}` with the real value. When the
|
|
7
|
+
* variable is unset and the host does NOT substitute (Cursor's plugin `mcp.json`
|
|
8
|
+
* is exactly this shape — see packages/cursor-flair/mcp.json), flair-mcp is
|
|
9
|
+
* handed the LITERAL string `"${FLAIR_URL}"`. That literal is truthy, so it wins
|
|
10
|
+
* every `value ?? default` / `value || default` chain and the sensible default
|
|
11
|
+
* (flair-client's `http://localhost:19926`) never applies — the connection then
|
|
12
|
+
* fails in a way that points nowhere near the real cause.
|
|
13
|
+
*
|
|
14
|
+
* This module treats such a literal as "unset" so the default applies instead.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* True iff `value` is an unsubstituted interpolation literal like `${FLAIR_URL}`
|
|
18
|
+
* — i.e. the whole (trimmed) value is a single `${...}` placeholder the host
|
|
19
|
+
* never expanded. A value that merely CONTAINS a `${...}` (e.g. a real URL with
|
|
20
|
+
* a fragment) is left alone: only a wholesale placeholder is treated as unset.
|
|
21
|
+
*/
|
|
22
|
+
export function isUnsubstitutedInterpolation(value) {
|
|
23
|
+
return /^\$\{.*\}$/.test(value.trim());
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read an env var, treating an unsubstituted `${...}` interpolation literal as
|
|
27
|
+
* unset: returns `undefined` for such a literal (and for a genuinely absent
|
|
28
|
+
* var) so a downstream default applies instead of the literal being used
|
|
29
|
+
* verbatim. A real value is returned unchanged (NOT trimmed — only the
|
|
30
|
+
* placeholder check trims).
|
|
31
|
+
*/
|
|
32
|
+
export function readEnvOrUnset(name, env = process.env) {
|
|
33
|
+
const v = env[name];
|
|
34
|
+
if (v === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
return isUnsubstitutedInterpolation(v) ? undefined : v;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Connection env vars that flair-client's OWN constructor reads straight from
|
|
40
|
+
* `process.env` as a fallback (see flair-client/src/client.ts:
|
|
41
|
+
* `this.url = config.url ?? readEnvOrUnset("FLAIR_URL") ?? DEFAULT_URL`). For
|
|
42
|
+
* these, skipping the literal only at flair-mcp's call site was NOT enough:
|
|
43
|
+
* flair-client re-read `process.env` and resurrected the `${...}` literal,
|
|
44
|
+
* defeating its own default. As of flair#1254 flair-client's env fallbacks
|
|
45
|
+
* apply this same literal-as-unset guard themselves, so the strip below is
|
|
46
|
+
* defense-in-depth rather than the only line — kept deliberately (an older
|
|
47
|
+
* flair-client on a consumer's disk does not have #1254).
|
|
48
|
+
*
|
|
49
|
+
* Only FLAIR_URL qualifies today: it is re-read by flair-client AND flair-mcp
|
|
50
|
+
* still constructs a client when it is a literal. FLAIR_AGENT_ID is also re-read
|
|
51
|
+
* by flair-client, but flair-mcp's `if (!agentId)` guard (using readEnvOrUnset)
|
|
52
|
+
* short-circuits before any client is built, so no strip is needed. FLAIR_KEY_PATH
|
|
53
|
+
* flair-client never reads from env, so guarding it at the call site suffices.
|
|
54
|
+
*/
|
|
55
|
+
export const ENV_RESURRECTED_BY_FLAIR_CLIENT = ["FLAIR_URL"];
|
|
56
|
+
/**
|
|
57
|
+
* Delete any of `names` whose value is an unsubstituted `${...}` interpolation
|
|
58
|
+
* literal from `env`, so downstream readers — flair-mcp's own reads AND
|
|
59
|
+
* flair-client's internal `process.env` fallback — see the var as unset and
|
|
60
|
+
* apply their default. Idempotent; mutates the live process env by default and
|
|
61
|
+
* accepts an injectable env for tests. Real (substituted) values are untouched.
|
|
62
|
+
*/
|
|
63
|
+
export function stripInterpolationLiteralsFromEnv(env = process.env, names = ENV_RESURRECTED_BY_FLAIR_CLIENT) {
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
const v = env[name];
|
|
66
|
+
if (typeof v === "string" && isUnsubstitutedInterpolation(v)) {
|
|
67
|
+
delete env[name];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
43
43
|
import { FlairClient, FlairError } from "@tpsdev-ai/flair-client";
|
|
44
44
|
import { z } from "zod";
|
|
45
45
|
import { deriveActivity, postPresenceSafe, resolveHeartbeatIntervalMs, resolvePresenceTimeoutMs, shouldSendHeartbeat, } from "./presence.js";
|
|
46
|
+
import { readEnvOrUnset, stripInterpolationLiteralsFromEnv } from "./env-guard.js";
|
|
46
47
|
// ─── Error helpers ──────────────────────────────────────────────────────────
|
|
47
48
|
function classifyError(err, flairUrl) {
|
|
48
49
|
if (err instanceof FlairError) {
|
|
@@ -137,15 +138,21 @@ export async function runMcp() {
|
|
|
137
138
|
process.exit(0);
|
|
138
139
|
});
|
|
139
140
|
// ─── Client setup ────────────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
+
// flair#1250: an MCP host that forwards `"FLAIR_URL": "${FLAIR_URL}"` without
|
|
142
|
+
// substituting hands us the literal `${FLAIR_URL}`. Remove such literals from
|
|
143
|
+
// the process env first, because flair-client's constructor ALSO reads
|
|
144
|
+
// process.env.FLAIR_URL as a fallback — guarding only our own reads below
|
|
145
|
+
// would be silently defeated when flair-client re-reads the raw env.
|
|
146
|
+
stripInterpolationLiteralsFromEnv();
|
|
147
|
+
const agentId = readEnvOrUnset("FLAIR_AGENT_ID");
|
|
141
148
|
if (!agentId) {
|
|
142
149
|
console.error("FLAIR_AGENT_ID is required. Set it in your .mcp.json env or shell.");
|
|
143
150
|
process.exit(1);
|
|
144
151
|
}
|
|
145
152
|
const flair = new FlairClient({
|
|
146
153
|
agentId,
|
|
147
|
-
url:
|
|
148
|
-
keyPath:
|
|
154
|
+
url: readEnvOrUnset("FLAIR_URL"),
|
|
155
|
+
keyPath: readEnvOrUnset("FLAIR_KEY_PATH"),
|
|
149
156
|
// flair#718 authorship-provenance: forward this stdio proxy's own
|
|
150
157
|
// FLAIR_CLIENT env (set by `flair init`'s per-client wiring, e.g.
|
|
151
158
|
// "claude-code"/"codex"/"gemini"/"cursor") into the client it constructs
|
|
@@ -172,8 +179,8 @@ export async function runMcp() {
|
|
|
172
179
|
// whatever timeout the main client is configured with.
|
|
173
180
|
const presenceFlair = new FlairClient({
|
|
174
181
|
agentId,
|
|
175
|
-
url:
|
|
176
|
-
keyPath:
|
|
182
|
+
url: readEnvOrUnset("FLAIR_URL"),
|
|
183
|
+
keyPath: readEnvOrUnset("FLAIR_KEY_PATH"),
|
|
177
184
|
timeoutMs: resolvePresenceTimeoutMs(),
|
|
178
185
|
});
|
|
179
186
|
// Rate-limit clock + last-known task, in-process only (no persistence —
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
import { FlairClient } from "@tpsdev-ai/flair-client";
|
|
77
77
|
import { basename } from "node:path";
|
|
78
78
|
import { deriveActivity, postPresenceSafe, resolvePresenceTimeoutMs } from "./presence.js";
|
|
79
|
+
import { readEnvOrUnset, stripInterpolationLiteralsFromEnv } from "./env-guard.js";
|
|
79
80
|
/** Claude Code SessionStart additionalContext hard limit (chars). */
|
|
80
81
|
const MAX_CHARS = 10_000;
|
|
81
82
|
/** Token budget for the bootstrap call — matches the proven prototype. */
|
|
@@ -148,7 +149,13 @@ function hookOutput(context) {
|
|
|
148
149
|
* @param makeClient factory for the bootstrap client (defaults to FlairClient)
|
|
149
150
|
*/
|
|
150
151
|
export async function runHook(rawInput, makeClient = defaultClientFactory) {
|
|
151
|
-
|
|
152
|
+
// flair#1250: drop any unsubstituted `${...}` interpolation literal from the
|
|
153
|
+
// env before the client is built, so flair-client's own process.env fallback
|
|
154
|
+
// (e.g. FLAIR_URL) can't resurrect the literal and defeat its default. See
|
|
155
|
+
// ./env-guard.ts. Runs here (not just at the call site) because the default
|
|
156
|
+
// client factory below reads process.env directly.
|
|
157
|
+
stripInterpolationLiteralsFromEnv();
|
|
158
|
+
const agentId = readEnvOrUnset("FLAIR_AGENT_ID");
|
|
152
159
|
if (!agentId)
|
|
153
160
|
return NOOP_OUTPUT; // no identity → no-op, never break the session
|
|
154
161
|
let input = {};
|
|
@@ -200,8 +207,8 @@ export async function runHook(rawInput, makeClient = defaultClientFactory) {
|
|
|
200
207
|
function defaultClientFactory(agentId) {
|
|
201
208
|
return new FlairClient({
|
|
202
209
|
agentId,
|
|
203
|
-
url:
|
|
204
|
-
keyPath:
|
|
210
|
+
url: readEnvOrUnset("FLAIR_URL"),
|
|
211
|
+
keyPath: readEnvOrUnset("FLAIR_KEY_PATH"),
|
|
205
212
|
});
|
|
206
213
|
}
|
|
207
214
|
/** Entry point. Reads stdin, runs the hook, prints the result, exits 0.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@modelcontextprotocol/sdk": "1.27.1",
|
|
30
|
-
"@tpsdev-ai/flair-client": "0.
|
|
30
|
+
"@tpsdev-ai/flair-client": "0.45.0",
|
|
31
31
|
"zod": "4.3.6"
|
|
32
32
|
},
|
|
33
33
|
"license": "Apache-2.0",
|