@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.
- package/README.md +213 -0
- package/dist/agent-door-session.d.ts +39 -0
- package/dist/agent-door-session.js +80 -0
- package/dist/agent-door.d.ts +15 -0
- package/dist/agent-door.js +55 -0
- package/dist/docs-msg-tools.d.ts +21 -0
- package/dist/docs-msg-tools.js +188 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +50 -0
- package/dist/server.d.ts +96 -0
- package/dist/server.js +374 -0
- package/dist/token-resolver.d.ts +116 -0
- package/dist/token-resolver.js +217 -0
- package/package.json +40 -0
|
@@ -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
|
+
}
|