@bli-cockpit/mcp 0.1.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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Token resolution for the MCP server.
3
+ *
4
+ * Two auth paths, matching the `bli-event` bash CLI precedence:
5
+ *
6
+ * 1. `BLI_OPERATOR_TOKEN` env var — used verbatim (CI / explicit override)
7
+ * 2. Fallback: spawn `node <bli-event-session.mjs> get-token` — refreshes
8
+ * the cached Supabase session if the access_token is near expiry, and
9
+ * prints a fresh access_token on stdout. Exits non-zero if no session
10
+ * exists (user must run `bli-event login`).
11
+ *
12
+ * The session-helper path spawns a subprocess per resolution. To keep the
13
+ * overhead invisible for bursts of tool calls (e.g. agent emitting a
14
+ * timeline fetch + multiple events in sequence), resolved tokens are cached
15
+ * in-process for a short window. The cache is time-based only — if the
16
+ * underlying session refresh fails, the NEXT call (after TTL expiry) will
17
+ * surface the error.
18
+ */
19
+ import { spawn as defaultSpawn } from "node:child_process";
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ // ---- env resolver -----------------------------------------------------------
24
+ /**
25
+ * Resolver that returns a static token verbatim. Use for the
26
+ * `BLI_OPERATOR_TOKEN` env var path (CI / override).
27
+ */
28
+ export function createEnvTokenResolver(token) {
29
+ if (!token) {
30
+ throw new Error("createEnvTokenResolver: token must be non-empty");
31
+ }
32
+ return async () => token;
33
+ }
34
+ const DEFAULT_CACHE_TTL_MS = 30_000;
35
+ /** Thrown when the session helper is missing or fails to resolve a token. */
36
+ export class SessionHelperError extends Error {
37
+ exitCode;
38
+ stderr;
39
+ constructor(message, exitCode, stderr) {
40
+ super(message);
41
+ this.name = "SessionHelperError";
42
+ this.exitCode = exitCode;
43
+ this.stderr = stderr;
44
+ }
45
+ }
46
+ /**
47
+ * Resolver that shells out to the Node session helper to get (and refresh)
48
+ * the operator's Supabase access_token.
49
+ */
50
+ export function createSessionHelperTokenResolver(options) {
51
+ const spawnImpl = options.spawn ?? defaultSpawn;
52
+ const nodePath = options.nodePath ?? process.execPath;
53
+ const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
54
+ const now = options.now ?? Date.now;
55
+ const env = options.env ?? process.env;
56
+ const helperPath = options.helperPath;
57
+ let cache = null;
58
+ return async () => {
59
+ if (cache && now() < cache.expiresAt) {
60
+ return cache.token;
61
+ }
62
+ const token = await invokeSessionHelper({
63
+ nodePath,
64
+ helperPath,
65
+ spawn: spawnImpl,
66
+ env,
67
+ });
68
+ if (cacheTtlMs > 0) {
69
+ cache = { token, expiresAt: now() + cacheTtlMs };
70
+ }
71
+ else {
72
+ cache = null;
73
+ }
74
+ return token;
75
+ };
76
+ }
77
+ /**
78
+ * Spawn `node <helper> get-token`, capture stdout, and return the token.
79
+ * Rejects with `SessionHelperError` on non-zero exit.
80
+ */
81
+ function invokeSessionHelper(args) {
82
+ return new Promise((resolve, reject) => {
83
+ let child;
84
+ try {
85
+ child = args.spawn(args.nodePath, [args.helperPath, "get-token"], {
86
+ stdio: ["ignore", "pipe", "pipe"],
87
+ env: args.env,
88
+ });
89
+ }
90
+ catch (err) {
91
+ reject(new SessionHelperError(`Failed to spawn session helper: ${err instanceof Error ? err.message : String(err)}`, null, ""));
92
+ return;
93
+ }
94
+ let stdout = "";
95
+ let stderr = "";
96
+ child.stdout?.setEncoding("utf8");
97
+ child.stderr?.setEncoding("utf8");
98
+ child.stdout?.on("data", (chunk) => {
99
+ stdout += chunk;
100
+ });
101
+ child.stderr?.on("data", (chunk) => {
102
+ stderr += chunk;
103
+ });
104
+ child.once("error", (err) => {
105
+ reject(new SessionHelperError(`Session helper process error: ${err.message}`, null, stderr));
106
+ });
107
+ child.once("close", (code) => {
108
+ if (code === 0) {
109
+ const token = stdout.trim();
110
+ if (!token) {
111
+ reject(new SessionHelperError("Session helper exited 0 but produced no token on stdout", code, stderr));
112
+ return;
113
+ }
114
+ resolve(token);
115
+ return;
116
+ }
117
+ // Non-zero exit — helper surfaces errors on stderr (see cmdGetToken).
118
+ // Exit code 2 is the documented "auth/network failure" from the helper
119
+ // (e.g. "Not logged in — run 'bli-event login'.").
120
+ const trimmedStderr = stderr.trim();
121
+ const baseMsg = code === 2
122
+ ? "No session found — run 'bli-event login' first."
123
+ : `Session helper failed (exit code ${code})`;
124
+ const message = trimmedStderr
125
+ ? `${baseMsg} (${trimmedStderr})`
126
+ : baseMsg;
127
+ reject(new SessionHelperError(message, code, trimmedStderr));
128
+ });
129
+ });
130
+ }
131
+ const HELPER_BASENAME = "bli-event-session.mjs";
132
+ /**
133
+ * Locate `scripts/bli-event-session.mjs` on disk, or return `null`.
134
+ *
135
+ * Order:
136
+ * 1. If `BLI_SESSION_HELPER` env var points at a readable file, use it.
137
+ * 2. Walk up from `import.meta.url` looking for a sibling
138
+ * `scripts/bli-event-session.mjs` (handles dev checkouts + installed
139
+ * npm workspaces where the MCP dist/ lives under `packages/bli-cockpit-mcp/`
140
+ * inside the monorepo root).
141
+ * 3. Return `null` — caller surfaces a clear error.
142
+ */
143
+ export function resolveSessionHelperPath(options = {}) {
144
+ const env = options.env ?? process.env;
145
+ const exists = options.existsSync ?? fs.existsSync;
146
+ // 1. Explicit env override.
147
+ const envOverride = env.BLI_SESSION_HELPER;
148
+ if (envOverride && envOverride.length > 0) {
149
+ if (exists(envOverride))
150
+ return path.resolve(envOverride);
151
+ // Env var was set but the file doesn't exist — surface a clear signal by
152
+ // returning null; the caller message references BLI_SESSION_HELPER.
153
+ return null;
154
+ }
155
+ // 2. Walk up from the module URL looking for scripts/bli-event-session.mjs.
156
+ const fromUrl = options.fromUrl ?? (typeof import.meta !== "undefined" ? import.meta.url : undefined);
157
+ if (!fromUrl)
158
+ return null;
159
+ let current;
160
+ try {
161
+ current = path.dirname(fileURLToPath(fromUrl));
162
+ }
163
+ catch (error) {
164
+ // Returning null still makes the caller print its BLI_SESSION_HELPER
165
+ // message, but that message accuses the environment when the real problem
166
+ // is that this module's own URL would not convert to a path — a packaging
167
+ // problem that reads as a user error. stderr, because stdout is the MCP
168
+ // protocol channel.
169
+ process.stderr.write(`[bli-cockpit-mcp] session helper unresolvable: module URL is not a file path (${error instanceof Error ? `${error.name}: ${error.message}`.slice(0, 250) : String(error).slice(0, 250)})\n`);
170
+ return null;
171
+ }
172
+ // Cap the walk at a reasonable depth to avoid pathological cases.
173
+ for (let i = 0; i < 10; i++) {
174
+ const candidate = path.join(current, "scripts", HELPER_BASENAME);
175
+ if (exists(candidate))
176
+ return path.resolve(candidate);
177
+ const parent = path.dirname(current);
178
+ if (parent === current)
179
+ break; // hit filesystem root
180
+ current = parent;
181
+ }
182
+ return null;
183
+ }
184
+ /**
185
+ * Build a `TokenResolver` using the documented precedence order:
186
+ * 1. `BLI_OPERATOR_TOKEN` env var (verbatim)
187
+ * 2. Session helper (`scripts/bli-event-session.mjs get-token`)
188
+ *
189
+ * Throws with a clear, user-actionable message when neither path is
190
+ * available (no env var, and no session helper on disk).
191
+ */
192
+ export function createDefaultTokenResolver(options = {}) {
193
+ const env = options.env ?? process.env;
194
+ const envToken = env.BLI_OPERATOR_TOKEN;
195
+ if (envToken && envToken.length > 0) {
196
+ return {
197
+ resolver: createEnvTokenResolver(envToken),
198
+ source: "env",
199
+ };
200
+ }
201
+ const helperPath = (options.resolveHelperPath ?? (() => resolveSessionHelperPath({ env })))();
202
+ if (!helperPath) {
203
+ const hint = env.BLI_SESSION_HELPER
204
+ ? `BLI_SESSION_HELPER=${env.BLI_SESSION_HELPER} does not point at a readable file`
205
+ : "set BLI_OPERATOR_TOKEN, or set BLI_SESSION_HELPER to the absolute path of bli-event-session.mjs, or run this MCP from inside the bli-cockpit repo where scripts/bli-event-session.mjs is discoverable";
206
+ throw new Error(`No auth available: ${hint}. Run 'bli-event login' once to create a session file.`);
207
+ }
208
+ const resolver = createSessionHelperTokenResolver({
209
+ helperPath,
210
+ spawn: options.spawn,
211
+ nodePath: options.nodePath,
212
+ cacheTtlMs: options.cacheTtlMs,
213
+ now: options.now,
214
+ env,
215
+ });
216
+ return { resolver, source: "session-helper", helperPath };
217
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@bli-cockpit/mcp",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "bli-tower — an MCP server over BLI Cockpit's docs/msg agent doors (docs_*, msg_*), plus the legacy event-stream tools (emit_event, get_ticket_timeline, get_active_tickets).",
6
+ "type": "module",
7
+ "bin": {
8
+ "bli-cockpit-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "README.md"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "prebuild": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/telemetry-core",
22
+ "build": "tsc",
23
+ "prepack": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/telemetry-core && rm -rf dist && tsc",
24
+ "pretypecheck": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/telemetry-core",
25
+ "typecheck": "tsc --noEmit",
26
+ "pretest": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/telemetry-core",
27
+ "test": "vitest run",
28
+ "start": "node dist/index.js"
29
+ },
30
+ "dependencies": {
31
+ "@bli-cockpit/telemetry-core": "0.1.28",
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
33
+ "zod": "^4.3.6"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20",
37
+ "typescript": "^5",
38
+ "vitest": "^3.2.4"
39
+ }
40
+ }