@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @bli-cockpit/mcp — MCP server adapter for the BLI Cockpit event stream.
|
|
4
|
+
*
|
|
5
|
+
* Exposes three tools backed by the canonical REST contract at
|
|
6
|
+
* `POST /api/events/emit` (see docs/plans/cockpit-agent-ops-control-plane.md §6).
|
|
7
|
+
*
|
|
8
|
+
* Auth (precedence order, matches the `bli-event` bash CLI):
|
|
9
|
+
* 1. `BLI_OPERATOR_TOKEN` env var — used verbatim (CI / override).
|
|
10
|
+
* 2. Otherwise: shell out to `node scripts/bli-event-session.mjs get-token`
|
|
11
|
+
* which reads `~/.config/bli-event/session.json`, refreshes the
|
|
12
|
+
* access_token if near expiry, and prints a fresh token on stdout.
|
|
13
|
+
* Set `BLI_SESSION_HELPER` to an absolute path if the helper can't be
|
|
14
|
+
* discovered by walking up from this module's location.
|
|
15
|
+
*
|
|
16
|
+
* If neither path is available, the server prints a clear error and exits.
|
|
17
|
+
*/
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { createServer } from "./server.js";
|
|
20
|
+
import { createDefaultTokenResolver } from "./token-resolver.js";
|
|
21
|
+
async function main() {
|
|
22
|
+
const baseUrl = process.env.BLI_API_BASE_URL ?? "http://127.0.0.1:3100";
|
|
23
|
+
let resolver;
|
|
24
|
+
let sourceLabel;
|
|
25
|
+
try {
|
|
26
|
+
const result = createDefaultTokenResolver({ env: process.env });
|
|
27
|
+
resolver = result.resolver;
|
|
28
|
+
sourceLabel =
|
|
29
|
+
result.source === "env"
|
|
30
|
+
? "BLI_OPERATOR_TOKEN env var"
|
|
31
|
+
: `session helper at ${result.helperPath}`;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
process.stderr.write(`[bli-cockpit-mcp] ${err instanceof Error ? err.message : String(err)}\n`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
process.stderr.write(`[bli-cockpit-mcp] auth source: ${sourceLabel}\n`);
|
|
38
|
+
const deps = {
|
|
39
|
+
resolveToken: resolver,
|
|
40
|
+
baseUrl,
|
|
41
|
+
fetchImpl: globalThis.fetch,
|
|
42
|
+
};
|
|
43
|
+
const server = createServer(deps);
|
|
44
|
+
const transport = new StdioServerTransport();
|
|
45
|
+
await server.connect(transport);
|
|
46
|
+
}
|
|
47
|
+
main().catch((err) => {
|
|
48
|
+
process.stderr.write(`[bli-cockpit-mcp] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server factory for @bli-cockpit/mcp.
|
|
3
|
+
*
|
|
4
|
+
* Pure factory — accepts injected fetch + base URL + token so it can be
|
|
5
|
+
* unit-tested without binding to a real network or transport.
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { TokenResolver } from "./token-resolver.js";
|
|
10
|
+
export type FetchImpl = typeof fetch;
|
|
11
|
+
export interface ServerDeps {
|
|
12
|
+
/**
|
|
13
|
+
* Called before every authenticated request. Must return a fresh Supabase
|
|
14
|
+
* access_token. See `token-resolver.ts` for the built-in implementations
|
|
15
|
+
* (`createEnvTokenResolver`, `createSessionHelperTokenResolver`).
|
|
16
|
+
*/
|
|
17
|
+
resolveToken: TokenResolver;
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
fetchImpl: FetchImpl;
|
|
20
|
+
}
|
|
21
|
+
export declare const PACKAGE_NAME = "@bli-cockpit/mcp";
|
|
22
|
+
export declare const PACKAGE_VERSION = "0.1.0";
|
|
23
|
+
export declare const emitEventInput: {
|
|
24
|
+
ticket_id: z.ZodString;
|
|
25
|
+
event_type: z.ZodString;
|
|
26
|
+
event_version: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
kind: z.ZodOptional<z.ZodEnum<{
|
|
28
|
+
agent_asserted: "agent_asserted";
|
|
29
|
+
external_observed: "external_observed";
|
|
30
|
+
human: "human";
|
|
31
|
+
}>>;
|
|
32
|
+
source: z.ZodOptional<z.ZodString>;
|
|
33
|
+
actor_role: z.ZodOptional<z.ZodString>;
|
|
34
|
+
payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
35
|
+
note_md: z.ZodOptional<z.ZodString>;
|
|
36
|
+
correlation_id: z.ZodOptional<z.ZodString>;
|
|
37
|
+
idempotency_key: z.ZodOptional<z.ZodString>;
|
|
38
|
+
occurred_at: z.ZodOptional<z.ZodString>;
|
|
39
|
+
};
|
|
40
|
+
export declare const getTicketTimelineInput: {
|
|
41
|
+
ticket_id: z.ZodString;
|
|
42
|
+
since: z.ZodOptional<z.ZodString>;
|
|
43
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
};
|
|
45
|
+
export declare const getActiveTicketsInput: {
|
|
46
|
+
source: z.ZodOptional<z.ZodString>;
|
|
47
|
+
project: z.ZodOptional<z.ZodString>;
|
|
48
|
+
lane: z.ZodOptional<z.ZodString>;
|
|
49
|
+
assignee: z.ZodOptional<z.ZodString>;
|
|
50
|
+
};
|
|
51
|
+
export interface ApiErrorBody {
|
|
52
|
+
code?: string;
|
|
53
|
+
message?: string;
|
|
54
|
+
field?: string;
|
|
55
|
+
details?: unknown;
|
|
56
|
+
}
|
|
57
|
+
export declare class ApiError extends Error {
|
|
58
|
+
readonly status: number;
|
|
59
|
+
readonly code?: string;
|
|
60
|
+
readonly body: ApiErrorBody | string;
|
|
61
|
+
constructor(status: number, body: ApiErrorBody | string, code?: string);
|
|
62
|
+
}
|
|
63
|
+
export interface EmitEventResult {
|
|
64
|
+
event_id: string;
|
|
65
|
+
occurred_at: string;
|
|
66
|
+
status: "created" | "replay";
|
|
67
|
+
}
|
|
68
|
+
export declare function emitEvent(deps: ServerDeps, args: {
|
|
69
|
+
ticket_id: string;
|
|
70
|
+
event_type: string;
|
|
71
|
+
event_version?: number;
|
|
72
|
+
kind?: "agent_asserted" | "external_observed" | "human";
|
|
73
|
+
source?: string;
|
|
74
|
+
actor_role?: string;
|
|
75
|
+
payload?: Record<string, unknown>;
|
|
76
|
+
note_md?: string;
|
|
77
|
+
correlation_id?: string;
|
|
78
|
+
idempotency_key?: string;
|
|
79
|
+
occurred_at?: string;
|
|
80
|
+
}): Promise<EmitEventResult>;
|
|
81
|
+
export declare function getTicketTimeline(deps: ServerDeps, args: {
|
|
82
|
+
ticket_id: string;
|
|
83
|
+
since?: string;
|
|
84
|
+
limit?: number;
|
|
85
|
+
}): Promise<{
|
|
86
|
+
events: unknown[];
|
|
87
|
+
}>;
|
|
88
|
+
export declare function getActiveTickets(deps: ServerDeps, args: {
|
|
89
|
+
source?: string;
|
|
90
|
+
project?: string;
|
|
91
|
+
lane?: string;
|
|
92
|
+
assignee?: string;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
tickets: unknown[];
|
|
95
|
+
}>;
|
|
96
|
+
export declare function createServer(deps: ServerDeps): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server factory for @bli-cockpit/mcp.
|
|
3
|
+
*
|
|
4
|
+
* Pure factory — accepts injected fetch + base URL + token so it can be
|
|
5
|
+
* unit-tested without binding to a real network or transport.
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { registerDocsMsgTools } from "./docs-msg-tools.js";
|
|
10
|
+
export const PACKAGE_NAME = "@bli-cockpit/mcp";
|
|
11
|
+
export const PACKAGE_VERSION = "0.1.0";
|
|
12
|
+
// ---- input schemas (Zod raw shapes) -----------------------------------------
|
|
13
|
+
export const emitEventInput = {
|
|
14
|
+
ticket_id: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.describe("Linear ticket ID (e.g. BLI-2115, KOLM-87)."),
|
|
18
|
+
event_type: z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.describe("Canonical event type from the event vocabulary (e.g. ticket_claimed, plan_submitted, worker_dispatched, note)."),
|
|
22
|
+
event_version: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.positive()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Event schema version. Defaults to 1 in the API."),
|
|
28
|
+
kind: z
|
|
29
|
+
.enum(["agent_asserted", "external_observed", "human"])
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Event kind. Omit to let the API infer it from event_type."),
|
|
32
|
+
source: z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Advanced producer identity override. Omit during normal use so the API derives source from the authenticated operator."),
|
|
37
|
+
actor_role: z
|
|
38
|
+
.string()
|
|
39
|
+
.min(1)
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Actor role for the event. Defaults to operator."),
|
|
42
|
+
payload: z
|
|
43
|
+
.record(z.string(), z.unknown())
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Event payload object. Defaults to {}."),
|
|
46
|
+
note_md: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Optional markdown note attached to the event."),
|
|
50
|
+
correlation_id: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Correlation ID linking related events (e.g. an agent trace ID)."),
|
|
54
|
+
idempotency_key: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Idempotency key — replays return status 'replay' instead of 'created'."),
|
|
58
|
+
occurred_at: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("ISO-8601 timestamp. Defaults to now in the API."),
|
|
62
|
+
};
|
|
63
|
+
export const getTicketTimelineInput = {
|
|
64
|
+
ticket_id: z.string().min(1).describe("Linear ticket ID."),
|
|
65
|
+
since: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("ISO-8601 timestamp; only return events occurring after this."),
|
|
69
|
+
limit: z
|
|
70
|
+
.number()
|
|
71
|
+
.int()
|
|
72
|
+
.min(1)
|
|
73
|
+
.max(500)
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Max events to return (default 100, max 500)."),
|
|
76
|
+
};
|
|
77
|
+
export const getActiveTicketsInput = {
|
|
78
|
+
source: z
|
|
79
|
+
.string()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Filter by source (e.g. cli:vee, worker, ci)."),
|
|
82
|
+
project: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Filter by project tag (bui, kolm, portpal, bp)."),
|
|
86
|
+
lane: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe("Filter by lane (e.g. Planning, Executing, Awaiting Review)."),
|
|
90
|
+
assignee: z.string().optional().describe("Filter by assignee user ID."),
|
|
91
|
+
};
|
|
92
|
+
export class ApiError extends Error {
|
|
93
|
+
status;
|
|
94
|
+
code;
|
|
95
|
+
body;
|
|
96
|
+
constructor(status, body, code) {
|
|
97
|
+
const summary = typeof body === "string"
|
|
98
|
+
? body
|
|
99
|
+
: (body.message ?? `HTTP ${status}`);
|
|
100
|
+
super(`[${code ?? `HTTP-${status}`}] ${summary}`);
|
|
101
|
+
this.name = "ApiError";
|
|
102
|
+
this.status = status;
|
|
103
|
+
this.body = body;
|
|
104
|
+
this.code = code;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function parseMaybeJson(response) {
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
if (!text)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(text);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// BLI-3238 — deliberately silent. This function's contract is "maybe
|
|
116
|
+
// JSON": a non-JSON body is an expected shape, not a failure, and the
|
|
117
|
+
// text is returned intact rather than discarded — ApiError carries it
|
|
118
|
+
// verbatim to the caller, so nothing is lost for a log to recover.
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function callApi(deps, method, path, body) {
|
|
123
|
+
const url = `${deps.baseUrl.replace(/\/$/, "")}${path}`;
|
|
124
|
+
// Resolve a fresh token per call. Implementations are expected to cache
|
|
125
|
+
// internally (see createSessionHelperTokenResolver) so burst calls don't
|
|
126
|
+
// each spawn a subprocess.
|
|
127
|
+
let token;
|
|
128
|
+
try {
|
|
129
|
+
token = await deps.resolveToken();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// Auth-side failure (no session, revoked refresh token, helper missing).
|
|
133
|
+
// Surface as BLI-E003 per error catalog so it's distinct from network.
|
|
134
|
+
throw new ApiError(401, {
|
|
135
|
+
code: "BLI-E003",
|
|
136
|
+
message: `Token resolution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
137
|
+
}, "BLI-E003");
|
|
138
|
+
}
|
|
139
|
+
const headers = {
|
|
140
|
+
Authorization: `Bearer ${token}`,
|
|
141
|
+
Accept: "application/json",
|
|
142
|
+
};
|
|
143
|
+
if (body !== undefined) {
|
|
144
|
+
headers["Content-Type"] = "application/json";
|
|
145
|
+
}
|
|
146
|
+
let response;
|
|
147
|
+
try {
|
|
148
|
+
response = await deps.fetchImpl(url, {
|
|
149
|
+
method,
|
|
150
|
+
headers,
|
|
151
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
// Network / DNS / TLS failure — surface as BLI-E001 per error catalog.
|
|
156
|
+
throw new ApiError(0, {
|
|
157
|
+
code: "BLI-E001",
|
|
158
|
+
message: `Network error contacting ${url}: ${err instanceof Error ? err.message : String(err)}`,
|
|
159
|
+
}, "BLI-E001");
|
|
160
|
+
}
|
|
161
|
+
const parsed = await parseMaybeJson(response);
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
const errBody = parsed && typeof parsed === "object"
|
|
164
|
+
? parsed
|
|
165
|
+
: parsed ?? response.statusText;
|
|
166
|
+
const code = typeof errBody === "object" && errBody !== null
|
|
167
|
+
? errBody.code
|
|
168
|
+
: undefined;
|
|
169
|
+
throw new ApiError(response.status, errBody, code);
|
|
170
|
+
}
|
|
171
|
+
return parsed;
|
|
172
|
+
}
|
|
173
|
+
export async function emitEvent(deps, args) {
|
|
174
|
+
const body = {
|
|
175
|
+
ticket_id: args.ticket_id,
|
|
176
|
+
event_type: args.event_type,
|
|
177
|
+
actor_role: args.actor_role ?? "operator",
|
|
178
|
+
payload: args.payload ?? {},
|
|
179
|
+
...(args.event_version !== undefined
|
|
180
|
+
? { event_version: args.event_version }
|
|
181
|
+
: {}),
|
|
182
|
+
...(args.kind !== undefined ? { kind: args.kind } : {}),
|
|
183
|
+
...(args.source !== undefined ? { source: args.source } : {}),
|
|
184
|
+
...(args.note_md !== undefined ? { note_md: args.note_md } : {}),
|
|
185
|
+
...(args.correlation_id !== undefined
|
|
186
|
+
? { correlation_id: args.correlation_id }
|
|
187
|
+
: {}),
|
|
188
|
+
...(args.idempotency_key !== undefined
|
|
189
|
+
? { idempotency_key: args.idempotency_key }
|
|
190
|
+
: {}),
|
|
191
|
+
...(args.occurred_at !== undefined ? { occurred_at: args.occurred_at } : {}),
|
|
192
|
+
};
|
|
193
|
+
const response = (await callApi(deps, "POST", "/api/events/emit", body));
|
|
194
|
+
if (!response || typeof response !== "object") {
|
|
195
|
+
throw new ApiError(502, {
|
|
196
|
+
code: "BLI-E001",
|
|
197
|
+
message: "Malformed response from /api/events/emit",
|
|
198
|
+
details: response,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if ("status" in response && response.status === "replay") {
|
|
202
|
+
const eventId = "event_id" in response && typeof response.event_id === "string"
|
|
203
|
+
? response.event_id
|
|
204
|
+
: "existing_event_id" in response &&
|
|
205
|
+
typeof response.existing_event_id === "string"
|
|
206
|
+
? response.existing_event_id
|
|
207
|
+
: "";
|
|
208
|
+
if (!eventId) {
|
|
209
|
+
throw new ApiError(502, {
|
|
210
|
+
code: "BLI-E001",
|
|
211
|
+
message: "Malformed replay response from /api/events/emit",
|
|
212
|
+
details: response,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const occurredAt = "existing_received_at" in response &&
|
|
216
|
+
typeof response.existing_received_at === "string"
|
|
217
|
+
? response.existing_received_at
|
|
218
|
+
: "existing_occurred_at" in response &&
|
|
219
|
+
typeof response.existing_occurred_at === "string"
|
|
220
|
+
? response.existing_occurred_at
|
|
221
|
+
: "";
|
|
222
|
+
if (!occurredAt) {
|
|
223
|
+
throw new ApiError(502, {
|
|
224
|
+
code: "BLI-E001",
|
|
225
|
+
message: "Malformed replay timestamp from /api/events/emit",
|
|
226
|
+
details: response,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
event_id: eventId,
|
|
231
|
+
occurred_at: occurredAt,
|
|
232
|
+
status: "replay",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const event = "event" in response && response.event && typeof response.event === "object"
|
|
236
|
+
? response.event
|
|
237
|
+
: null;
|
|
238
|
+
const eventId = "event_id" in response && typeof response.event_id === "string"
|
|
239
|
+
? response.event_id
|
|
240
|
+
: typeof event?.id === "string"
|
|
241
|
+
? event.id
|
|
242
|
+
: "";
|
|
243
|
+
const occurredAt = "occurred_at" in response && typeof response.occurred_at === "string"
|
|
244
|
+
? response.occurred_at
|
|
245
|
+
: typeof event?.received_at === "string"
|
|
246
|
+
? event.received_at
|
|
247
|
+
: typeof event?.occurred_at === "string"
|
|
248
|
+
? event.occurred_at
|
|
249
|
+
: "";
|
|
250
|
+
if (!eventId || !occurredAt) {
|
|
251
|
+
throw new ApiError(502, {
|
|
252
|
+
code: "BLI-E001",
|
|
253
|
+
message: "Malformed created response from /api/events/emit",
|
|
254
|
+
details: response,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
event_id: eventId,
|
|
259
|
+
occurred_at: occurredAt,
|
|
260
|
+
status: "created",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export async function getTicketTimeline(deps, args) {
|
|
264
|
+
const params = new URLSearchParams({ ticket_id: args.ticket_id });
|
|
265
|
+
if (args.since)
|
|
266
|
+
params.set("since", args.since);
|
|
267
|
+
if (args.limit !== undefined)
|
|
268
|
+
params.set("limit", String(args.limit));
|
|
269
|
+
const response = (await callApi(deps, "GET", `/api/events/timeline?${params.toString()}`));
|
|
270
|
+
return { events: response?.events ?? [] };
|
|
271
|
+
}
|
|
272
|
+
export async function getActiveTickets(deps, args) {
|
|
273
|
+
const params = new URLSearchParams();
|
|
274
|
+
if (args.source)
|
|
275
|
+
params.set("source", args.source);
|
|
276
|
+
if (args.project)
|
|
277
|
+
params.set("project", args.project);
|
|
278
|
+
if (args.lane)
|
|
279
|
+
params.set("lane", args.lane);
|
|
280
|
+
if (args.assignee)
|
|
281
|
+
params.set("assignee", args.assignee);
|
|
282
|
+
const query = params.toString();
|
|
283
|
+
const path = query ? `/api/tickets/active?${query}` : "/api/tickets/active";
|
|
284
|
+
const response = (await callApi(deps, "GET", path));
|
|
285
|
+
return { tickets: response?.tickets ?? [] };
|
|
286
|
+
}
|
|
287
|
+
// ---- result formatting helpers ---------------------------------------------
|
|
288
|
+
function jsonContent(value) {
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: JSON.stringify(value, null, 2),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function errorContent(err) {
|
|
299
|
+
let message;
|
|
300
|
+
let code;
|
|
301
|
+
if (err instanceof ApiError) {
|
|
302
|
+
message = err.message;
|
|
303
|
+
code = err.code;
|
|
304
|
+
}
|
|
305
|
+
else if (err instanceof Error) {
|
|
306
|
+
message = err.message;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
message = String(err);
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
isError: true,
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: code ? `[${code}] ${message}` : message,
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// ---- server factory ---------------------------------------------------------
|
|
322
|
+
export function createServer(deps) {
|
|
323
|
+
const server = new McpServer({
|
|
324
|
+
name: PACKAGE_NAME,
|
|
325
|
+
version: PACKAGE_VERSION,
|
|
326
|
+
});
|
|
327
|
+
server.registerTool("emit_event", {
|
|
328
|
+
title: "Emit a cockpit event",
|
|
329
|
+
description: "Append a canonical event to the BLI Cockpit event stream for a given ticket. Wraps POST /api/events/emit. Supports idempotency_key for at-most-once writes.",
|
|
330
|
+
inputSchema: emitEventInput,
|
|
331
|
+
}, async (args) => {
|
|
332
|
+
try {
|
|
333
|
+
const result = await emitEvent(deps, args);
|
|
334
|
+
return jsonContent(result);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
return errorContent(err);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
server.registerTool("get_ticket_timeline", {
|
|
341
|
+
title: "Get ticket event timeline",
|
|
342
|
+
description: "Return the canonical event timeline for a ticket, optionally filtered by since/limit. Wraps GET /api/events/timeline.",
|
|
343
|
+
inputSchema: getTicketTimelineInput,
|
|
344
|
+
}, async (args) => {
|
|
345
|
+
try {
|
|
346
|
+
const result = await getTicketTimeline(deps, args);
|
|
347
|
+
return jsonContent(result);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
return errorContent(err);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
server.registerTool("get_active_tickets", {
|
|
354
|
+
title: "List active tickets",
|
|
355
|
+
description: "Return task-visible tickets with active orchestration state, optionally filtered by source/project/lane/assignee. Wraps GET /api/tickets/active.",
|
|
356
|
+
inputSchema: getActiveTicketsInput,
|
|
357
|
+
}, async (args) => {
|
|
358
|
+
try {
|
|
359
|
+
const result = await getActiveTickets(deps, args);
|
|
360
|
+
return jsonContent(result);
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return errorContent(err);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
// BLI-3706: docs_*/msg_* over the collector device token, a SEPARATE auth
|
|
367
|
+
// path from the emit_event/get_ticket_timeline/get_active_tickets tools
|
|
368
|
+
// above (which use BLI_OPERATOR_TOKEN / the session helper). Registered
|
|
369
|
+
// unconditionally — the session is loaded fresh per call, so a machine with
|
|
370
|
+
// no `cockpit login` pairing yet still serves the three event tools; only a
|
|
371
|
+
// docs/msg call on that machine fails, by name.
|
|
372
|
+
registerDocsMsgTools(server, { fetchImpl: deps.fetchImpl });
|
|
373
|
+
return server;
|
|
374
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
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 type { ChildProcess, SpawnOptions } from "node:child_process";
|
|
20
|
+
export type TokenResolver = () => Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolver that returns a static token verbatim. Use for the
|
|
23
|
+
* `BLI_OPERATOR_TOKEN` env var path (CI / override).
|
|
24
|
+
*/
|
|
25
|
+
export declare function createEnvTokenResolver(token: string): TokenResolver;
|
|
26
|
+
export type SpawnFn = (command: string, args: readonly string[], options?: SpawnOptions) => ChildProcess;
|
|
27
|
+
export interface SessionHelperOptions {
|
|
28
|
+
/** Absolute path to `bli-event-session.mjs`. */
|
|
29
|
+
helperPath: string;
|
|
30
|
+
/** Node executable used to run the helper. Default: `process.execPath`. */
|
|
31
|
+
nodePath?: string;
|
|
32
|
+
/** Injectable spawn for tests. Default: `child_process.spawn`. */
|
|
33
|
+
spawn?: SpawnFn;
|
|
34
|
+
/**
|
|
35
|
+
* How long (ms) to cache a resolved token in-process before spawning the
|
|
36
|
+
* helper again. The helper itself refreshes the session when the access
|
|
37
|
+
* token is near expiry, so a small cache window is all we need to absorb
|
|
38
|
+
* bursts of tool calls. Default 30_000 (30s). Set to 0 to disable caching.
|
|
39
|
+
*/
|
|
40
|
+
cacheTtlMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Clock source for cache bookkeeping. Default `Date.now`.
|
|
43
|
+
*/
|
|
44
|
+
now?: () => number;
|
|
45
|
+
/** Env vars forwarded to the helper. Default: `process.env`. */
|
|
46
|
+
env?: NodeJS.ProcessEnv;
|
|
47
|
+
}
|
|
48
|
+
/** Thrown when the session helper is missing or fails to resolve a token. */
|
|
49
|
+
export declare class SessionHelperError extends Error {
|
|
50
|
+
readonly exitCode: number | null;
|
|
51
|
+
readonly stderr: string;
|
|
52
|
+
constructor(message: string, exitCode: number | null, stderr: string);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolver that shells out to the Node session helper to get (and refresh)
|
|
56
|
+
* the operator's Supabase access_token.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createSessionHelperTokenResolver(options: SessionHelperOptions): TokenResolver;
|
|
59
|
+
export interface ResolveHelperPathOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Env vars to inspect. Default: `process.env`.
|
|
62
|
+
*/
|
|
63
|
+
env?: NodeJS.ProcessEnv;
|
|
64
|
+
/**
|
|
65
|
+
* The `import.meta.url` of the calling module — used to walk up the tree
|
|
66
|
+
* looking for a sibling `scripts/bli-event-session.mjs`. Default:
|
|
67
|
+
* `import.meta.url` of this module. Tests can inject a synthetic path.
|
|
68
|
+
*/
|
|
69
|
+
fromUrl?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Override for `fs.existsSync` so tests can fake the filesystem.
|
|
72
|
+
*/
|
|
73
|
+
existsSync?: (p: string) => boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Locate `scripts/bli-event-session.mjs` on disk, or return `null`.
|
|
77
|
+
*
|
|
78
|
+
* Order:
|
|
79
|
+
* 1. If `BLI_SESSION_HELPER` env var points at a readable file, use it.
|
|
80
|
+
* 2. Walk up from `import.meta.url` looking for a sibling
|
|
81
|
+
* `scripts/bli-event-session.mjs` (handles dev checkouts + installed
|
|
82
|
+
* npm workspaces where the MCP dist/ lives under `packages/bli-cockpit-mcp/`
|
|
83
|
+
* inside the monorepo root).
|
|
84
|
+
* 3. Return `null` — caller surfaces a clear error.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveSessionHelperPath(options?: ResolveHelperPathOptions): string | null;
|
|
87
|
+
export interface DefaultTokenResolverOptions {
|
|
88
|
+
env?: NodeJS.ProcessEnv;
|
|
89
|
+
/**
|
|
90
|
+
* Injectable dependencies forwarded to `createSessionHelperTokenResolver`
|
|
91
|
+
* when the env override is not set.
|
|
92
|
+
*/
|
|
93
|
+
spawn?: SpawnFn;
|
|
94
|
+
nodePath?: string;
|
|
95
|
+
cacheTtlMs?: number;
|
|
96
|
+
now?: () => number;
|
|
97
|
+
/** Override helper-path discovery (tests). */
|
|
98
|
+
resolveHelperPath?: () => string | null;
|
|
99
|
+
}
|
|
100
|
+
export interface DefaultTokenResolverResult {
|
|
101
|
+
/** The resolver to install on the server. */
|
|
102
|
+
resolver: TokenResolver;
|
|
103
|
+
/** Which source fed the resolver — useful for startup logging. */
|
|
104
|
+
source: "env" | "session-helper";
|
|
105
|
+
/** If source is `session-helper`, the resolved helper path. */
|
|
106
|
+
helperPath?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build a `TokenResolver` using the documented precedence order:
|
|
110
|
+
* 1. `BLI_OPERATOR_TOKEN` env var (verbatim)
|
|
111
|
+
* 2. Session helper (`scripts/bli-event-session.mjs get-token`)
|
|
112
|
+
*
|
|
113
|
+
* Throws with a clear, user-actionable message when neither path is
|
|
114
|
+
* available (no env var, and no session helper on disk).
|
|
115
|
+
*/
|
|
116
|
+
export declare function createDefaultTokenResolver(options?: DefaultTokenResolverOptions): DefaultTokenResolverResult;
|