@bli-cockpit/mcp 0.1.81 → 0.1.83
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 +44 -23
- package/dist/agent-door-session.d.ts +3 -0
- package/dist/agent-door-session.js +20 -0
- package/dist/agent-door.d.ts +1 -1
- package/dist/agent-door.js +3 -1
- package/dist/github-tools.d.ts +18 -0
- package/dist/github-tools.js +116 -0
- package/dist/remote.d.ts +33 -0
- package/dist/remote.js +68 -0
- package/dist/server-icons.d.ts +16 -0
- package/dist/server-icons.js +17 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.js +15 -7
- package/dist/tool-annotations.d.ts +29 -0
- package/dist/tool-annotations.js +51 -0
- package/dist/tool-catalog.d.ts +2 -2
- package/dist/tool-catalog.js +4 -0
- package/dist/tool-registration.d.ts +32 -0
- package/dist/tool-registration.js +48 -0
- package/dist/verb-census.js +5 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @bli-cockpit/mcp — bli-tower
|
|
2
2
|
|
|
3
|
+
**Connecting an agent? Start at [`docs/onboarding/connect-an-agent-to-tower.md`](../../docs/onboarding/connect-an-agent-to-tower.md):** supported clients, setup, verify, troubleshooting. This file is the tool reference.
|
|
4
|
+
|
|
3
5
|
Two, deliberately separate, tool families over one MCP server (bin
|
|
4
6
|
`bli-cockpit-mcp`, server id `bli-tower`):
|
|
5
7
|
|
|
@@ -34,9 +36,13 @@ refusal words, so the choice is about where the agent runs and what it costs.
|
|
|
34
36
|
CLI, `cockpit <noun> <verb>`. Nothing loads into the context window until a
|
|
35
37
|
command runs, the human-readable output is the tersest form of every read,
|
|
36
38
|
and a person can run the exact same line to check it.
|
|
37
|
-
- **The agent has no shell** (
|
|
38
|
-
|
|
39
|
-
have.
|
|
39
|
+
- **The agent has no shell** (Claude Desktop chat, IDE chat panes, any host
|
|
40
|
+
that can start a local stdio server): use this server. It is the only door
|
|
41
|
+
those hosts have. The claude.ai web app, ChatGPT and dot cannot start a local
|
|
42
|
+
program; they use Tower's hosted address, `https://<dashboard>/api/mcp`, with
|
|
43
|
+
"Sign in with Tower" (BLI-12416, pending its first live proof; see the connect
|
|
44
|
+
page's "Remote hosts"). The hosted server is `@bli-cockpit/mcp/remote`, built
|
|
45
|
+
from the same `createServer`, minus the five laptop-only tools.
|
|
40
46
|
|
|
41
47
|
Measured on 2026-09-26 against production, same account, same moment. Bytes
|
|
42
48
|
the model actually reads; tokens are an estimate at about 4 bytes a token.
|
|
@@ -59,7 +65,7 @@ unless a script needs the fields.
|
|
|
59
65
|
|
|
60
66
|
**Which half a host shows the model.** A tool result has a text half
|
|
61
67
|
(`content`) and a structured half (`structuredContent`), and hosts disagree:
|
|
62
|
-
Claude Code
|
|
68
|
+
Claude Code and Claude Desktop give the model the text half; Codex
|
|
63
69
|
gives it the structured half and drops the text. Every read on this server is
|
|
64
70
|
written so EACH half stands alone. That was not always true: until BLI-10432,
|
|
65
71
|
`docs_list`, `msg_channels`, `msg_read`, every `careers_*` tool,
|
|
@@ -103,7 +109,7 @@ about what an agent can reach.
|
|
|
103
109
|
|
|
104
110
|
<!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
|
|
105
111
|
|
|
106
|
-
**
|
|
112
|
+
**123 of 128 Tower verbs have an MCP twin.**
|
|
107
113
|
Each tool goes through the SAME door its CLI verb calls, with the same
|
|
108
114
|
collector device token — never a second route and never a service-role
|
|
109
115
|
reader. `src/verb-census.test.ts` fails when a verb is in none of the
|
|
@@ -150,6 +156,10 @@ three tables below.
|
|
|
150
156
|
| `cockpit docs sync-status` | `docs_sync_status` | `GET /api/docs/notion-sync` |
|
|
151
157
|
| `cockpit docs tree` | `docs_tree` | `GET /api/docs/tree` |
|
|
152
158
|
| `cockpit docs update` | `docs_update` | `PATCH /api/docs/documents/[id]` |
|
|
159
|
+
| `cockpit github merge` | `github_merge` | `POST /api/github/pull-request (merge)` |
|
|
160
|
+
| `cockpit github pr` | `github_pr` | `POST /api/github/pull-request (read)` |
|
|
161
|
+
| `cockpit github rerun` | `github_rerun` | `POST /api/github/pull-request (rerun)` |
|
|
162
|
+
| `cockpit github retitle` | `github_retitle` | `POST /api/github/pull-request (retitle)` |
|
|
153
163
|
| `cockpit issue comment` | `work_comment_issue` | `POST /api/work/issues/[id]/comments` |
|
|
154
164
|
| `cockpit issue create` | `work_create_issue` | `POST /api/work/issues` |
|
|
155
165
|
| `cockpit issue history` | `work_issue_history` | `GET /api/work/issues/[id]/history` |
|
|
@@ -499,16 +509,37 @@ the account.
|
|
|
499
509
|
|
|
500
510
|
## Install
|
|
501
511
|
|
|
512
|
+
**Current path (every tool except the three legacy ones).** Install the CLI and
|
|
513
|
+
let it register the server:
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
npm i -g @bli-cockpit/cli && cockpit doctor
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
`cockpit doctor` signs the machine in (device token in
|
|
520
|
+
`~/.config/bli-cockpit/session.json`) and registers `bli-tower` in
|
|
521
|
+
`~/.claude.json` and `~/.codex/config.toml`. See "Registration on an intern
|
|
522
|
+
machine" above, and the
|
|
523
|
+
[connect page](../../docs/onboarding/connect-an-agent-to-tower.md) for manual
|
|
524
|
+
registration in Claude Desktop or any other stdio host, verification and
|
|
525
|
+
troubleshooting.
|
|
526
|
+
|
|
527
|
+
**Building from this checkout** (development only):
|
|
528
|
+
|
|
502
529
|
```bash
|
|
503
530
|
npm install # from repo root — registers the workspace
|
|
504
531
|
npm run build --workspace=@bli-cockpit/mcp
|
|
505
532
|
```
|
|
506
533
|
|
|
507
|
-
The `bin` entry exposes a `bli-cockpit-mcp`
|
|
534
|
+
The `bin` entry exposes a `bli-cockpit-mcp` program that speaks MCP over stdio. The `./remote` export (`handleRemoteMcpRequest`) is the stateless Streamable HTTP handler the dashboard's `/api/mcp` route calls.
|
|
535
|
+
|
|
536
|
+
### Legacy event-stream install (frozen)
|
|
508
537
|
|
|
509
|
-
|
|
538
|
+
Only needed for `emit_event`, `get_ticket_timeline` and `get_active_tickets`.
|
|
539
|
+
Do not follow this to connect an agent to Tower; the section above is the way.
|
|
510
540
|
|
|
511
541
|
```jsonc
|
|
542
|
+
// ~/.claude/settings.json — LEGACY
|
|
512
543
|
{
|
|
513
544
|
"mcpServers": {
|
|
514
545
|
"bli-cockpit": {
|
|
@@ -524,22 +555,12 @@ The `bin` entry exposes a `bli-cockpit-mcp` CLI that speaks MCP over stdio.
|
|
|
524
555
|
}
|
|
525
556
|
```
|
|
526
557
|
|
|
527
|
-
Run `bli-event login` once on the host; the
|
|
528
|
-
access_token from `~/.config/bli-event/session.json` and
|
|
529
|
-
transparently.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
will be used verbatim without refresh.
|
|
534
|
-
|
|
535
|
-
For deployed cockpits, set `BLI_API_BASE_URL` to the production URL.
|
|
536
|
-
Never commit tokens — keep them in local settings only.
|
|
537
|
-
|
|
538
|
-
### Claude Desktop
|
|
539
|
-
|
|
540
|
-
Same shape, but the file lives at:
|
|
541
|
-
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
542
|
-
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
558
|
+
Run `bli-event login` once on the host; the legacy tools auto-resolve the
|
|
559
|
+
access_token from `~/.config/bli-event/session.json` and refresh it
|
|
560
|
+
transparently. For CI (or any environment without a session file), set
|
|
561
|
+
`BLI_OPERATOR_TOKEN` to a Supabase access_token in the env block; it is used
|
|
562
|
+
verbatim without refresh. For deployed cockpits, set `BLI_API_BASE_URL` to the
|
|
563
|
+
production URL. Never commit tokens.
|
|
543
564
|
|
|
544
565
|
## Example invocations
|
|
545
566
|
|
|
@@ -33,6 +33,9 @@ export type LoadAgentDoorSessionResult = {
|
|
|
33
33
|
message: string;
|
|
34
34
|
};
|
|
35
35
|
export declare function agentDoorSessionFilePath(homeDir?: string): string;
|
|
36
|
+
export declare function runWithAgentDoorSession<T>(session: AgentDoorSession, run: () => T): T;
|
|
37
|
+
/** True inside a hosted request: nothing on this host's disk is the caller's. */
|
|
38
|
+
export declare function inRemoteAgentDoorScope(): boolean;
|
|
36
39
|
export declare function loadAgentDoorSession(options?: {
|
|
37
40
|
env?: NodeJS.ProcessEnv;
|
|
38
41
|
homeDir?: string;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* The token is never logged, printed, or included in an error.
|
|
20
20
|
*/
|
|
21
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
22
|
import fs from "node:fs";
|
|
22
23
|
import os from "node:os";
|
|
23
24
|
import path from "node:path";
|
|
@@ -26,7 +27,26 @@ export const DEFAULT_DASHBOARD_URL = "https://bli-cockpit-dashboard.vercel.app";
|
|
|
26
27
|
export function agentDoorSessionFilePath(homeDir = os.homedir()) {
|
|
27
28
|
return path.join(homeDir, ...SESSION_RELATIVE_PATH);
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* BLI-12416: the hosted server (`remote.ts`) answers many people from one
|
|
32
|
+
* process, so a credential from env or `session.json` would be one person's
|
|
33
|
+
* for everybody. A request-scoped session wins over both: every tool path
|
|
34
|
+
* that calls `loadAgentDoorSession` then works remotely without threading a
|
|
35
|
+
* session through ~100 handlers. Two concurrent scopes never see each other's
|
|
36
|
+
* value (`AsyncLocalStorage`), and nothing here ever logs the token.
|
|
37
|
+
*/
|
|
38
|
+
const requestSession = new AsyncLocalStorage();
|
|
39
|
+
export function runWithAgentDoorSession(session, run) {
|
|
40
|
+
return requestSession.run(session, run);
|
|
41
|
+
}
|
|
42
|
+
/** True inside a hosted request: nothing on this host's disk is the caller's. */
|
|
43
|
+
export function inRemoteAgentDoorScope() {
|
|
44
|
+
return requestSession.getStore() !== undefined;
|
|
45
|
+
}
|
|
29
46
|
export function loadAgentDoorSession(options = {}) {
|
|
47
|
+
const scoped = requestSession.getStore();
|
|
48
|
+
if (scoped)
|
|
49
|
+
return { ok: true, session: scoped };
|
|
30
50
|
const env = options.env ?? process.env;
|
|
31
51
|
const envToken = (env.BLI_COCKPIT_MCP_DEVICE_TOKEN ?? "").trim();
|
|
32
52
|
if (envToken.length > 0) {
|
package/dist/agent-door.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* because both talk to the identical on-disk cache directory on the same
|
|
10
10
|
* machine, and neither depends on the other package to say so.
|
|
11
11
|
*/
|
|
12
|
-
import type
|
|
12
|
+
import { type AgentDoorSession } from "./agent-door-session.js";
|
|
13
13
|
export type FetchImpl = typeof fetch;
|
|
14
14
|
export interface AgentDoorResponse {
|
|
15
15
|
ok: boolean;
|
package/dist/agent-door.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import { getUserLocalCockpitPaths } from "@bli-cockpit/telemetry-core";
|
|
14
14
|
import { invalidateReadCacheSurface, readCacheSurfaceForPath, } from "@bli-cockpit/telemetry-core/node-read-cache";
|
|
15
|
+
import { inRemoteAgentDoorScope } from "./agent-door-session.js";
|
|
15
16
|
/**
|
|
16
17
|
* How long a door gets before this server gives up on it (BLI-10432).
|
|
17
18
|
*
|
|
@@ -76,7 +77,8 @@ export async function callAgentDoor(session, fetchImpl, method, path, body, time
|
|
|
76
77
|
transportError: `Tower answered ${response.status} with a body this server could not read as JSON.`,
|
|
77
78
|
};
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
// Hosted requests have no per-person on-disk read cache to invalidate.
|
|
81
|
+
if (method !== "GET" && response.ok && !inRemoteAgentDoorScope()) {
|
|
80
82
|
const surface = readCacheSurfaceForPath(path);
|
|
81
83
|
if (surface) {
|
|
82
84
|
const stateDir = getUserLocalCockpitPaths(os.homedir()).state_dir;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `github_pr` / `github_merge` / `github_retitle` / `github_rerun` (BLI-12385):
|
|
3
|
+
* the MCP twins of `cockpit github pr|merge|retitle|rerun`, over the same door
|
|
4
|
+
* (`POST /api/github/pull-request`) with the same device token.
|
|
5
|
+
*
|
|
6
|
+
* The door runs JARVIS's own GitHub implementations, so the lock is the same:
|
|
7
|
+
* the caller's OWN GitHub login (on the roster) must hold write, maintain or
|
|
8
|
+
* admin on the repo, and a change is TWO STEPS. The first call (no `confirm`)
|
|
9
|
+
* changes nothing and returns what would happen plus an eight-character code;
|
|
10
|
+
* the second carries `confirm`. The code is signed to the caller, repo, pull
|
|
11
|
+
* request and exact change. An agent must pass a code only after the PERSON has
|
|
12
|
+
* sent it back: whether to merge is theirs, and the descriptions say so.
|
|
13
|
+
* A refusal keeps the door's reason label and sentence verbatim.
|
|
14
|
+
*/
|
|
15
|
+
import { type ToolDeps } from "./tool-result.js";
|
|
16
|
+
export declare function registerGithubTools(server: {
|
|
17
|
+
registerTool: (...args: never[]) => unknown;
|
|
18
|
+
}, deps: ToolDeps): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `github_pr` / `github_merge` / `github_retitle` / `github_rerun` (BLI-12385):
|
|
3
|
+
* the MCP twins of `cockpit github pr|merge|retitle|rerun`, over the same door
|
|
4
|
+
* (`POST /api/github/pull-request`) with the same device token.
|
|
5
|
+
*
|
|
6
|
+
* The door runs JARVIS's own GitHub implementations, so the lock is the same:
|
|
7
|
+
* the caller's OWN GitHub login (on the roster) must hold write, maintain or
|
|
8
|
+
* admin on the repo, and a change is TWO STEPS. The first call (no `confirm`)
|
|
9
|
+
* changes nothing and returns what would happen plus an eight-character code;
|
|
10
|
+
* the second carries `confirm`. The code is signed to the caller, repo, pull
|
|
11
|
+
* request and exact change. An agent must pass a code only after the PERSON has
|
|
12
|
+
* sent it back: whether to merge is theirs, and the descriptions say so.
|
|
13
|
+
* A refusal keeps the door's reason label and sentence verbatim.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
17
|
+
import { dataResult, doorFailureText, errorResult, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
18
|
+
const PATH = "/api/github/pull-request";
|
|
19
|
+
const TWO_STEPS = "TWO STEPS: call it WITHOUT confirm first. That changes nothing and returns what would happen plus an "
|
|
20
|
+
+ "eight-character confirmation code; show the person exactly that and ask them to send the code back. Only then "
|
|
21
|
+
+ "call again with the SAME arguments plus confirm set to the code THE PERSON gave you. Never invent a code, "
|
|
22
|
+
+ "never reuse one the person did not send, and never change the arguments between the two calls (the code is "
|
|
23
|
+
+ "signed to the exact change). It runs only if the person's OWN GitHub login has write, maintain or admin on the "
|
|
24
|
+
+ "repo; otherwise it refuses with github_login_unknown or github_permission_insufficient: relay that as written. "
|
|
25
|
+
+ "Only BLI-INC repositories.";
|
|
26
|
+
const repoField = z.string().min(1).max(100).describe("`owner/repo` or a bare repo name in BLI-INC, e.g. bli-cockpit.");
|
|
27
|
+
const numberField = z.number().int().min(1).describe("The pull request number.");
|
|
28
|
+
const confirmField = z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.max(64)
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("The code the preview returned, which the PERSON sent back. Omit on the first call.");
|
|
34
|
+
export function registerGithubTools(server, deps) {
|
|
35
|
+
const register = registrarFor(server);
|
|
36
|
+
async function press(tool, session, body) {
|
|
37
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", PATH, body);
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
return errorResult(doorFailureText(tool, response));
|
|
40
|
+
const result = (response.body["result"] ?? {});
|
|
41
|
+
if (result["status"] === "needs_confirmation") {
|
|
42
|
+
return textResult(`Nothing has been changed. Would ${String(result["preview"] ?? "make this change")} `
|
|
43
|
+
+ `To go ahead, the person must send back the code ${String(result["confirmationCode"] ?? "")}; `
|
|
44
|
+
+ `then call ${tool} again with the same arguments and confirm set to that code.`, result);
|
|
45
|
+
}
|
|
46
|
+
if (result["status"] === "done")
|
|
47
|
+
return textResult(String(result["sayThis"] ?? "Done."), result);
|
|
48
|
+
return dataResult("GitHub pull request", result);
|
|
49
|
+
}
|
|
50
|
+
register("github_pr", {
|
|
51
|
+
title: "Read one pull request from GitHub",
|
|
52
|
+
description: "GitHub's own live record of ONE pull request in BLI-INC: merged (and by whom), open/closed, draft, mergeable, "
|
|
53
|
+
+ "title and CI. GitHub is the source of truth: when somebody asserts a state, read it here. Pass `claim` "
|
|
54
|
+
+ "(merged, open, closed, ci_green or mergeable) to get GitHub's verdict on that assertion with the correction "
|
|
55
|
+
+ "sentence. `cockpit github pr`, same door.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
repo: repoField,
|
|
58
|
+
pull_number: numberField,
|
|
59
|
+
claim: z.enum(["merged", "open", "closed", "ci_green", "mergeable"]).optional().describe("The state somebody asserted."),
|
|
60
|
+
},
|
|
61
|
+
}, async (args) => withSession(deps, (session) => press("github_pr", session, {
|
|
62
|
+
action: "read",
|
|
63
|
+
repo: String(args.repo),
|
|
64
|
+
pullNumber: args.pull_number,
|
|
65
|
+
...(args.claim ? { claim: args.claim } : {}),
|
|
66
|
+
})));
|
|
67
|
+
register("github_merge", {
|
|
68
|
+
title: "Merge a pull request (two steps, caller's own GitHub access)",
|
|
69
|
+
description: `Merge ONE pull request in BLI-INC, squash by default. \`cockpit github merge\`, same door. ${TWO_STEPS}`,
|
|
70
|
+
inputSchema: {
|
|
71
|
+
repo: repoField,
|
|
72
|
+
pull_number: numberField,
|
|
73
|
+
method: z.enum(["squash", "merge", "rebase"]).optional().describe("Defaults to squash."),
|
|
74
|
+
confirm: confirmField,
|
|
75
|
+
},
|
|
76
|
+
}, async (args) => withSession(deps, (session) => press("github_merge", session, {
|
|
77
|
+
action: "merge",
|
|
78
|
+
repo: String(args.repo),
|
|
79
|
+
pullNumber: args.pull_number,
|
|
80
|
+
...(args.method ? { method: args.method } : {}),
|
|
81
|
+
...(args.confirm ? { confirm: String(args.confirm) } : {}),
|
|
82
|
+
})));
|
|
83
|
+
register("github_retitle", {
|
|
84
|
+
title: "Retitle a pull request (two steps, caller's own GitHub access)",
|
|
85
|
+
description: `Change the title of ONE pull request in BLI-INC. \`cockpit github retitle\`, same door. ${TWO_STEPS}`,
|
|
86
|
+
inputSchema: {
|
|
87
|
+
repo: repoField,
|
|
88
|
+
pull_number: numberField,
|
|
89
|
+
title: z.string().min(1).max(256).describe("The new title."),
|
|
90
|
+
confirm: confirmField,
|
|
91
|
+
},
|
|
92
|
+
}, async (args) => withSession(deps, (session) => press("github_retitle", session, {
|
|
93
|
+
action: "retitle",
|
|
94
|
+
repo: String(args.repo),
|
|
95
|
+
pullNumber: args.pull_number,
|
|
96
|
+
title: String(args.title),
|
|
97
|
+
...(args.confirm ? { confirm: String(args.confirm) } : {}),
|
|
98
|
+
})));
|
|
99
|
+
register("github_rerun", {
|
|
100
|
+
title: "Re-run failed workflow jobs (two steps, caller's own GitHub access)",
|
|
101
|
+
description: "Re-run the FAILED jobs of workflow runs in BLI-INC: pass pull_number for every failed workflow on that pull "
|
|
102
|
+
+ `request's head commit, or run_id for one run (exactly one). \`cockpit github rerun\`, same door. ${TWO_STEPS}`,
|
|
103
|
+
inputSchema: {
|
|
104
|
+
repo: repoField,
|
|
105
|
+
pull_number: numberField.optional(),
|
|
106
|
+
run_id: z.number().int().min(1).optional().describe("One workflow run, instead of pull_number."),
|
|
107
|
+
confirm: confirmField,
|
|
108
|
+
},
|
|
109
|
+
}, async (args) => withSession(deps, (session) => press("github_rerun", session, {
|
|
110
|
+
action: "rerun",
|
|
111
|
+
repo: String(args.repo),
|
|
112
|
+
...(args.pull_number ? { pullNumber: args.pull_number } : {}),
|
|
113
|
+
...(args.run_id ? { runId: args.run_id } : {}),
|
|
114
|
+
...(args.confirm ? { confirm: String(args.confirm) } : {}),
|
|
115
|
+
})));
|
|
116
|
+
}
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower's tools at an HTTPS address (BLI-12416).
|
|
3
|
+
*
|
|
4
|
+
* `handleRemoteMcpRequest` answers ONE Streamable HTTP request: a new server
|
|
5
|
+
* and a stateless web-standard transport per request (no session id, plain
|
|
6
|
+
* JSON replies), built from the SAME `createServer` the stdio bin uses so the
|
|
7
|
+
* tool list cannot fork. The caller's bearer rides an `AsyncLocalStorage`
|
|
8
|
+
* scope, so every tool calls Tower's own `/api/*` doors with THEIR token and
|
|
9
|
+
* the doors enforce who they are. Nothing here widens a read.
|
|
10
|
+
*
|
|
11
|
+
* Left out of the hosted list, with reasons, in `REMOTE_EXCLUDED_TOOLS`.
|
|
12
|
+
* Nothing is logged but tool name, outcome, milliseconds, bytes and a label;
|
|
13
|
+
* never a token, an argument or an answer.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* ChatGPT only guarantees the first 512 characters of server instructions are
|
|
17
|
+
* read, so this is the whole briefing: what Tower is, where to start, how
|
|
18
|
+
* writes behave.
|
|
19
|
+
*/
|
|
20
|
+
export declare const REMOTE_INSTRUCTIONS: string;
|
|
21
|
+
export interface RemoteMcpSession {
|
|
22
|
+
/** The origin the tools call back, e.g. `https://tower.example`. */
|
|
23
|
+
dashboardUrl: string;
|
|
24
|
+
/** The caller's own bearer (device token or OAuth access token). */
|
|
25
|
+
bearer: string;
|
|
26
|
+
/** A short label for the log line, e.g. a token hash prefix. Never the token. */
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
export interface RemoteMcpDeps {
|
|
30
|
+
fetchImpl?: typeof fetch;
|
|
31
|
+
log?: (line: string, fields: Record<string, unknown>) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function handleRemoteMcpRequest(request: Request, session: RemoteMcpSession, deps?: RemoteMcpDeps): Promise<Response>;
|
package/dist/remote.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower's tools at an HTTPS address (BLI-12416).
|
|
3
|
+
*
|
|
4
|
+
* `handleRemoteMcpRequest` answers ONE Streamable HTTP request: a new server
|
|
5
|
+
* and a stateless web-standard transport per request (no session id, plain
|
|
6
|
+
* JSON replies), built from the SAME `createServer` the stdio bin uses so the
|
|
7
|
+
* tool list cannot fork. The caller's bearer rides an `AsyncLocalStorage`
|
|
8
|
+
* scope, so every tool calls Tower's own `/api/*` doors with THEIR token and
|
|
9
|
+
* the doors enforce who they are. Nothing here widens a read.
|
|
10
|
+
*
|
|
11
|
+
* Left out of the hosted list, with reasons, in `REMOTE_EXCLUDED_TOOLS`.
|
|
12
|
+
* Nothing is logged but tool name, outcome, milliseconds, bytes and a label;
|
|
13
|
+
* never a token, an argument or an answer.
|
|
14
|
+
*/
|
|
15
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
16
|
+
import { runWithAgentDoorSession } from "./agent-door-session.js";
|
|
17
|
+
import { createServer } from "./server.js";
|
|
18
|
+
/**
|
|
19
|
+
* ChatGPT only guarantees the first 512 characters of server instructions are
|
|
20
|
+
* read, so this is the whole briefing: what Tower is, where to start, how
|
|
21
|
+
* writes behave.
|
|
22
|
+
*/
|
|
23
|
+
export const REMOTE_INSTRUCTIONS = "Tower is Build Launch Iterate's workspace: documents, messages, issues, mail, calendar, meeting notes and the daily brief. "
|
|
24
|
+
+ "Start with tower_search to find anything, then use the matching docs_*, msg_*, work_*, mail_*, cal_* or notes_* tool. "
|
|
25
|
+
+ "You act as the signed-in person and see only what they can see. Tools that change data are marked as writes, so the app asks the person to confirm first.";
|
|
26
|
+
const TAG = "[mcp-remote]";
|
|
27
|
+
function defaultLog(line, fields) {
|
|
28
|
+
console.info(`${TAG} ${line}`, JSON.stringify(fields));
|
|
29
|
+
}
|
|
30
|
+
export async function handleRemoteMcpRequest(request, session, deps = {}) {
|
|
31
|
+
const log = deps.log ?? defaultLog;
|
|
32
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
const server = createServer({
|
|
35
|
+
// The three legacy event tools are excluded, so this is never called.
|
|
36
|
+
resolveToken: async () => {
|
|
37
|
+
throw new Error("The legacy event stream is not served remotely.");
|
|
38
|
+
},
|
|
39
|
+
baseUrl: session.dashboardUrl,
|
|
40
|
+
fetchImpl,
|
|
41
|
+
}, {
|
|
42
|
+
remote: {
|
|
43
|
+
instructions: REMOTE_INSTRUCTIONS,
|
|
44
|
+
onToolCall: (call) => log("tool call", { ...call, label: session.label }),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
48
|
+
sessionIdGenerator: undefined,
|
|
49
|
+
enableJsonResponse: true,
|
|
50
|
+
});
|
|
51
|
+
return runWithAgentDoorSession({ dashboardUrl: session.dashboardUrl, deviceToken: session.bearer }, async () => {
|
|
52
|
+
try {
|
|
53
|
+
await server.connect(transport);
|
|
54
|
+
const response = await transport.handleRequest(request);
|
|
55
|
+
log("request handled", { method: request.method, status: response.status, ms: Date.now() - started, label: session.label });
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
log("request failed", {
|
|
60
|
+
method: request.method,
|
|
61
|
+
reason: error instanceof Error ? error.name : "unknown",
|
|
62
|
+
ms: Date.now() - started,
|
|
63
|
+
label: session.label,
|
|
64
|
+
});
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower's mark as MCP `serverInfo.icons` (spec 2025-11-25, SEP-973), so a host
|
|
3
|
+
* that renders server icons shows Tower's own logo with nothing to upload
|
|
4
|
+
* (BLI-12426). Data URIs, not URLs: the icon travels inside the `initialize`
|
|
5
|
+
* answer and works on every deployment address and on a laptop's stdio run.
|
|
6
|
+
*
|
|
7
|
+
* Source: `docs/design/brand/tower-icon.svg` (the PNG is its 128 px render).
|
|
8
|
+
* `server-icons.test.ts` fails when the SVG here stops matching that file.
|
|
9
|
+
* Many hosts do not read this field yet; ChatGPT's create dialog also takes an
|
|
10
|
+
* upload (`docs/design/brand/tower-icon-256.png`).
|
|
11
|
+
*/
|
|
12
|
+
export declare const TOWER_SERVER_ICONS: {
|
|
13
|
+
src: string;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
sizes: string[];
|
|
16
|
+
}[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower's mark as MCP `serverInfo.icons` (spec 2025-11-25, SEP-973), so a host
|
|
3
|
+
* that renders server icons shows Tower's own logo with nothing to upload
|
|
4
|
+
* (BLI-12426). Data URIs, not URLs: the icon travels inside the `initialize`
|
|
5
|
+
* answer and works on every deployment address and on a laptop's stdio run.
|
|
6
|
+
*
|
|
7
|
+
* Source: `docs/design/brand/tower-icon.svg` (the PNG is its 128 px render).
|
|
8
|
+
* `server-icons.test.ts` fails when the SVG here stops matching that file.
|
|
9
|
+
* Many hosts do not read this field yet; ChatGPT's create dialog also takes an
|
|
10
|
+
* upload (`docs/design/brand/tower-icon-256.png`).
|
|
11
|
+
*/
|
|
12
|
+
const TOWER_ICON_PNG_128 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAA2FBMVEUAAADy8e3s6+f19PDy7+rx4dXz5tzw1cTqdDTpaSTpZyHtoHXw0r7pZB3oVwjoWQzoVQfslmbdXRjRUg7UVRDmZR+QgXcrHRYvIRktHxhENi7cy8Dz6eCFhYQUFhcYGhsvMDHd3NkwMjLa2tb6+fWHiIb49/O+vbtPUFAjJSV6e3qRkY/m5eEsLi4VFxiUlJIfICFERkbY19RnaGcnKSm0tLGnp6VRU1I8Pj6Oj413d3bKysdbXFzh4N2bm5mAgX/S0s/NzMlqa2qwsK4uLzDX1tOsrKnFxcKJMVDUAAAAAXRSTlMAQObYZgAAA6NJREFUeNrtm2dD2zAQhiMrBdoyunfwyiAmdqBNgEAIo7T9//+osS0neMR3GrY79H4DbN3j92SdLKRWq1hkKYMoVQut8GpK1UYnFM0QBlcdPYEwYAZiVBScIdByBOZUpaIlBDWED2WQ5h6/zISagm8kqO3xmZqOnyWoO3ouC/UDpAkaiB+q6fiyAO0nK20JNbBKguAbsL3zlOnZ87ZQCwYDEKz7u3v7B7FevBQDYBaIDgG7eyz+wb4gAOsFgveqAIgsEB4DVTjwZwCISgkAaR5A+M7GAegaQHQckHRge+fV60hv3gqOhJIA7a137z8s9fHT5y/irYiOw6Fo59AMZdkS0ymZUpwAOK5EIxqgBIBC6vYYgN0HrxUAGHQg9Y6i+ObQAy89HnEDLP0FZTLBV/quCICpTo4G+N8BAksAYOw71omS8MuGTm1uADJy3a/fAgXxJ1PbtQ1+gNCFMwUWBOejshjltcC9UODAjIgDkEv5HPhXEgB0OpTOwJxIAJDr07iZfAUoLgxBvlIsqAwAuYlt9LI1cJ6td55lputibx7d6txKOUAHk7AV666fmQUsupmCPwq9Ss0MvMiSe0MKgIzOo244XvsYD5E5Y6NkPR5x7egNOjkDJqzglGyW7UlIAPo96r8X0HQNBLjyw3Ye7rgdiN7g4BJqH56UzjM5QALY0fsznEKfDCAAXWRygANgGTi9lnfg1knnAOlAPIZ2wOZhAOM+nYMygHXZj9+ByQD8aIMB4pK4zgHKgTgDQCFEArCS+JCMRcUDUT8zEHkBXAixACydq8EXMxR7DqIQYgFYSQwwxSj1E1QI0Q4kJZFbC8S6Aerr+EZsWgIVQjxAXBK5pyJQIcQDsJLIKbAQ4gHILDgs7nRr5S8ACyEHwI9Z7ovfS88Wj3q5K36i1r+QSzS5FY++7aSev9fNXYJbOhNeI3LTAB3RlToNoAE0gAbQABpAA2gADaAB/mIAq2mAxh3QABpAA2gADaAB/h0AsTtpFqBfjwMjl8n+5acAPHv1F769hXx7Sumx7zCl4pvmMPl9yW6NogZ5AeCdLSX7VTY5wNEJMFtr+PbWce5tVw0Q727nsEC5A7y766sB4OiGmD1+Ph4gOWLBYQG8y7Ezhv9bmonPlwRY3BkQP+Qhp8cHjZqInz7sVf9Rs9URm+Y8aPC4YVH8mrNQeOayxvgbTp02+viMoJ40tDar4nPP0eMDh68rdYESKDzrCpUxwNEr7Y5FoX4DijSqxkMGuGsAAAAASUVORK5CYII=";
|
|
13
|
+
const TOWER_ICON_SVG = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTQiIGZpbGw9IiNGMkYxRUQiLz4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0IDQpIHNjYWxlKC44NzUpIj4KICA8cGF0aCBmaWxsPSIjMTgxQTFCIiBkPSJNMzAgOGg0djZoMjB2NEgxMHYtNGgyMHpNMTIgMThoNDBsLTUgMTRIMTd6TTI4IDMyaDh2MjRoNnY0SDIydi00aDZ6Ii8+CiAgPHBhdGggZmlsbD0iI0YyRjFFRCIgZD0iTTE3IDIyaDd2NmgtNXpNMjcgMjJoMTB2NkgyN3pNNDAgMjJoN2wtMiA2aC01eiIvPgogIDxwYXRoIGZpbGw9IiNFODU5MEMiIGQ9Ik0yOSA0aDZ2NmgtNnoiLz4KICA8L2c+Cjwvc3ZnPgo=";
|
|
14
|
+
export const TOWER_SERVER_ICONS = [
|
|
15
|
+
{ src: TOWER_ICON_PNG_128, mimeType: "image/png", sizes: ["128x128"] },
|
|
16
|
+
{ src: TOWER_ICON_SVG, mimeType: "image/svg+xml", sizes: ["any"] },
|
|
17
|
+
];
|
package/dist/server.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
import { type CreateServerOptions } from "./tool-registration.js";
|
|
9
10
|
import type { TokenResolver } from "./token-resolver.js";
|
|
10
11
|
export type FetchImpl = typeof fetch;
|
|
11
12
|
export interface ServerDeps {
|
|
@@ -99,4 +100,5 @@ export declare function getActiveTickets(deps: ServerDeps, args: {
|
|
|
99
100
|
}): Promise<{
|
|
100
101
|
tickets: unknown[];
|
|
101
102
|
}>;
|
|
102
|
-
export
|
|
103
|
+
export type { CreateServerOptions } from "./tool-registration.js";
|
|
104
|
+
export declare function createServer(deps: ServerDeps, options?: CreateServerOptions): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -23,6 +23,7 @@ import { registerMemoryExperienceTools } from "./memory-experience-tools.js";
|
|
|
23
23
|
import { registerMemoryProfileTools } from "./memory-profile-tools.js";
|
|
24
24
|
import { registerMemoryRequeueTools } from "./memory-requeue-tools.js";
|
|
25
25
|
import { registerModelsTools } from "./models-tools.js";
|
|
26
|
+
import { registerGithubTools } from "./github-tools.js";
|
|
26
27
|
import { registerOpsTools } from "./ops-tools.js";
|
|
27
28
|
import { registerPagesTools } from "./pages-tools.js";
|
|
28
29
|
import { registerSearchTool } from "./search-tool.js";
|
|
@@ -34,6 +35,8 @@ import { registerCareersTools } from "./careers-tools.js";
|
|
|
34
35
|
import { registerRenameTools } from "./rename-tools.js";
|
|
35
36
|
import { registerUsageTools } from "./usage-tools.js";
|
|
36
37
|
import { registerSourcesTools } from "./sources-tools.js";
|
|
38
|
+
import { TOWER_SERVER_ICONS } from "./server-icons.js";
|
|
39
|
+
import { installToolRegistration } from "./tool-registration.js";
|
|
37
40
|
export const PACKAGE_NAME = "@bli-cockpit/mcp";
|
|
38
41
|
/**
|
|
39
42
|
* The version this server tells a client in `initialize`, read from the
|
|
@@ -367,12 +370,11 @@ function errorContent(err) {
|
|
|
367
370
|
],
|
|
368
371
|
};
|
|
369
372
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
});
|
|
373
|
+
export function createServer(deps, options = {}) {
|
|
374
|
+
const server = new McpServer({ name: PACKAGE_NAME, version: PACKAGE_VERSION, icons: TOWER_SERVER_ICONS }, options.remote ? { instructions: options.remote.instructions } : undefined);
|
|
375
|
+
const remote = options.remote !== undefined;
|
|
376
|
+
// BLI-12416: annotations from the catalog, hosted exclusions and call logging.
|
|
377
|
+
installToolRegistration(server, options);
|
|
376
378
|
server.registerTool("emit_event", {
|
|
377
379
|
title: "Emit a cockpit event",
|
|
378
380
|
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.",
|
|
@@ -429,6 +431,8 @@ export function createServer(deps) {
|
|
|
429
431
|
// BLI-12378: the twins of `cockpit rename list|resolve`, the human's call on
|
|
430
432
|
// whether a correction becomes a rule.
|
|
431
433
|
registerRenameTools(server, deps);
|
|
434
|
+
// BLI-12385: GitHub PR read, and merge / retitle / re-run behind the two-step code.
|
|
435
|
+
registerGithubTools(server, deps);
|
|
432
436
|
registerUsageTools(server, { fetchImpl: deps.fetchImpl });
|
|
433
437
|
// BLI-3708: the mailboxes a person attached. Reads, a send and a sync over
|
|
434
438
|
// the same /api/mail/** doors `cockpit mail` calls; `add-imap` deliberately
|
|
@@ -444,7 +448,11 @@ export function createServer(deps) {
|
|
|
444
448
|
// BLI-3732: `jarvis_*` — the assistant itself, on the same device-token path
|
|
445
449
|
// as the three families above, over the doors `cockpit jarvis` already
|
|
446
450
|
// calls. It is the last Tower surface that had a CLI door and no MCP one.
|
|
447
|
-
registerJarvisTools(server, {
|
|
451
|
+
registerJarvisTools(server, {
|
|
452
|
+
fetchImpl: deps.fetchImpl,
|
|
453
|
+
// Hosted: "last turn" is never read from the host's own disk.
|
|
454
|
+
...(remote ? { readBookmark: async () => null } : {}),
|
|
455
|
+
});
|
|
448
456
|
// BLI-3912: `models_*` — the model-card shelf, over the same door
|
|
449
457
|
// `cockpit models` calls. Read-only: a card is changed by re-reading the
|
|
450
458
|
// provider, never by an agent editing a number.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool annotations and the hosted server's exclusions (BLI-12416).
|
|
3
|
+
*
|
|
4
|
+
* ChatGPT treats a tool WITHOUT `readOnlyHint: true` as a write and asks the
|
|
5
|
+
* person to confirm every call, so ~109 unannotated tools would nag on every
|
|
6
|
+
* read. `tool-catalog.ts` already sorts every tool into read, dry-run and
|
|
7
|
+
* write; the hints derive from it here and nowhere else, and
|
|
8
|
+
* `tool-annotations.test.ts` fails when a registered tool has none or
|
|
9
|
+
* disagrees with the catalog.
|
|
10
|
+
*/
|
|
11
|
+
export interface TowerToolAnnotations {
|
|
12
|
+
readOnlyHint: boolean;
|
|
13
|
+
destructiveHint?: boolean;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Writes a person cannot take back by writing again: deletes, revokes,
|
|
18
|
+
* detaches and a merge. These also carry `destructiveHint: true`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DESTRUCTIVE_TOOLS: readonly ["docs_delete", "cal_delete", "cal_detach", "mail_detach", "notes_detach", "notes_rmdir", "settings_delete", "team_device_revoke", "github_merge"];
|
|
21
|
+
/**
|
|
22
|
+
* Tools the HOSTED server leaves out, each with the reason. A hosted process
|
|
23
|
+
* is one machine answering many people: a tool that reads that machine's
|
|
24
|
+
* files or spawns a program there would act on the host, not on the caller's
|
|
25
|
+
* computer, so it is absent rather than quietly wrong.
|
|
26
|
+
*/
|
|
27
|
+
export declare const REMOTE_EXCLUDED_TOOLS: Readonly<Record<string, string>>;
|
|
28
|
+
/** `null` when the tool is in no catalog list (the test then fails, loudly). */
|
|
29
|
+
export declare function annotationsFor(name: string, catalog: ReadonlySet<string>): TowerToolAnnotations | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool annotations and the hosted server's exclusions (BLI-12416).
|
|
3
|
+
*
|
|
4
|
+
* ChatGPT treats a tool WITHOUT `readOnlyHint: true` as a write and asks the
|
|
5
|
+
* person to confirm every call, so ~109 unannotated tools would nag on every
|
|
6
|
+
* read. `tool-catalog.ts` already sorts every tool into read, dry-run and
|
|
7
|
+
* write; the hints derive from it here and nowhere else, and
|
|
8
|
+
* `tool-annotations.test.ts` fails when a registered tool has none or
|
|
9
|
+
* disagrees with the catalog.
|
|
10
|
+
*/
|
|
11
|
+
import { READ_TOOLS } from "./tool-catalog.js";
|
|
12
|
+
/**
|
|
13
|
+
* Writes a person cannot take back by writing again: deletes, revokes,
|
|
14
|
+
* detaches and a merge. These also carry `destructiveHint: true`.
|
|
15
|
+
*/
|
|
16
|
+
export const DESTRUCTIVE_TOOLS = [
|
|
17
|
+
"docs_delete",
|
|
18
|
+
"cal_delete",
|
|
19
|
+
"cal_detach",
|
|
20
|
+
"mail_detach",
|
|
21
|
+
"notes_detach",
|
|
22
|
+
"notes_rmdir",
|
|
23
|
+
"settings_delete",
|
|
24
|
+
"team_device_revoke",
|
|
25
|
+
"github_merge",
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Tools the HOSTED server leaves out, each with the reason. A hosted process
|
|
29
|
+
* is one machine answering many people: a tool that reads that machine's
|
|
30
|
+
* files or spawns a program there would act on the host, not on the caller's
|
|
31
|
+
* computer, so it is absent rather than quietly wrong.
|
|
32
|
+
*/
|
|
33
|
+
export const REMOTE_EXCLUDED_TOOLS = {
|
|
34
|
+
emit_event: "legacy event stream; authenticates with an operator token the host does not hold",
|
|
35
|
+
get_ticket_timeline: "legacy event stream; authenticates with an operator token the host does not hold",
|
|
36
|
+
get_active_tickets: "legacy event stream; authenticates with an operator token the host does not hold",
|
|
37
|
+
notes_upload: "reads a file by path from the machine the server runs on; use notes_paste with the text",
|
|
38
|
+
sources_check: "spawns `cockpit sources check` on the machine the server runs on",
|
|
39
|
+
};
|
|
40
|
+
const READS = new Set(READ_TOOLS);
|
|
41
|
+
const DESTRUCTIVE = new Set(DESTRUCTIVE_TOOLS);
|
|
42
|
+
/** `null` when the tool is in no catalog list (the test then fails, loudly). */
|
|
43
|
+
export function annotationsFor(name, catalog) {
|
|
44
|
+
if (!catalog.has(name))
|
|
45
|
+
return null;
|
|
46
|
+
if (READS.has(name))
|
|
47
|
+
return { readOnlyHint: true };
|
|
48
|
+
return DESTRUCTIVE.has(name)
|
|
49
|
+
? { readOnlyHint: false, destructiveHint: true }
|
|
50
|
+
: { readOnlyHint: false, destructiveHint: false };
|
|
51
|
+
}
|
package/dist/tool-catalog.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* from `src/` under Node's type stripping, so it may import nothing.
|
|
11
11
|
*/
|
|
12
12
|
/** Tools that change nothing. The smoke calls every one. */
|
|
13
|
-
export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check"];
|
|
13
|
+
export declare const READ_TOOLS: readonly ["get_ticket_timeline", "get_active_tickets", "docs_list", "docs_tree", "docs_read", "docs_sync_status", "msg_channels", "msg_read", "msg_thread", "work_list_issues", "work_get_issue", "work_issue_history", "work_list_projects", "careers_list", "careers_show", "careers_resume_url", "careers_takehome_show", "careers_settings_show", "rename_pending", "github_pr", "usage_people", "usage_sessions", "mail_accounts", "mail_inbox", "mail_read", "mail_search", "mail_attachment", "mail_sync_status", "cal_today", "cal_week", "cal_next", "cal_find", "cal_calendars", "cal_sync_status", "jarvis_ask", "jarvis_trace", "jarvis_check", "models_list", "models_card", "models_compare", "brief_read", "brief_history", "brief_status", "brief_missed", "notes_folders", "notes_list", "notes_show", "notes_shelf", "notes_shelves", "notes_place", "notes_accounts", "notes_sync_status", "notes_granola_status", "ops_status", "slack_coverage", "slack_read", "memory_profile", "memory_consolidation", "settings_show", "settings_list", "model_show", "team_members", "team_device_list", "scout_board", "workbook_read", "tower_search", "sources_check"];
|
|
14
14
|
/**
|
|
15
15
|
* Tools that write, but answer a dry run that writes nothing. The smoke calls
|
|
16
16
|
* each one ONLY with its dry-run flag set (`scripts/smoke.ts` says which).
|
|
@@ -21,7 +21,7 @@ export declare const DRY_RUN_TOOLS: readonly ["ops_recompile", "memory_requeue"]
|
|
|
21
21
|
* candidates, share calendars. Never called by the smoke; covered by the unit
|
|
22
22
|
* suites (`write-tools.test.ts` and each family's own).
|
|
23
23
|
*/
|
|
24
|
-
export declare const WRITE_TOOLS: readonly ["emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_followup", "careers_interview", "careers_takehome_set", "careers_settings_set", "rename_resolve", "mail_send", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "notes_mkdir", "notes_rmdir", "notes_rename", "notes_upload", "notes_paste", "notes_share", "notes_unshare", "notes_move", "notes_sync", "notes_detach", "memory_experience", "scout_start", "scout_dismiss", "scout_undo", "brief_rewrite", "brief_correct", "brief_done", "brief_undone", "settings_set", "settings_delete", "model_set", "team_invite", "team_role", "team_device_revoke"];
|
|
24
|
+
export declare const WRITE_TOOLS: readonly ["emit_event", "docs_create", "docs_update", "docs_delete", "docs_link", "msg_create_channel", "msg_dm", "msg_send", "work_create_issue", "work_update_issue", "work_move_issue", "work_comment_issue", "careers_rescreen", "careers_send_takehome", "careers_invite", "careers_grade", "careers_decide", "careers_followup", "careers_interview", "careers_takehome_set", "careers_settings_set", "rename_resolve", "github_merge", "github_retitle", "github_rerun", "mail_send", "mail_sync", "mail_detach", "cal_create", "cal_share", "cal_sync", "cal_detach", "cal_delete", "jarvis_dispatch", "notes_mkdir", "notes_rmdir", "notes_rename", "notes_upload", "notes_paste", "notes_share", "notes_unshare", "notes_move", "notes_sync", "notes_detach", "memory_experience", "scout_start", "scout_dismiss", "scout_undo", "brief_rewrite", "brief_correct", "brief_done", "brief_undone", "settings_set", "settings_delete", "model_set", "team_invite", "team_role", "team_device_revoke"];
|
|
25
25
|
/** Reads that spend a model turn. The smoke's `--no-model` leaves these out. */
|
|
26
26
|
export declare const MODEL_SPENDING_TOOLS: readonly ["jarvis_ask", "jarvis_check"];
|
|
27
27
|
/**
|
package/dist/tool-catalog.js
CHANGED
|
@@ -30,6 +30,7 @@ export const READ_TOOLS = [
|
|
|
30
30
|
"careers_takehome_show",
|
|
31
31
|
"careers_settings_show",
|
|
32
32
|
"rename_pending",
|
|
33
|
+
"github_pr",
|
|
33
34
|
"usage_people",
|
|
34
35
|
"usage_sessions",
|
|
35
36
|
"mail_accounts",
|
|
@@ -111,6 +112,9 @@ export const WRITE_TOOLS = [
|
|
|
111
112
|
"careers_takehome_set",
|
|
112
113
|
"careers_settings_set",
|
|
113
114
|
"rename_resolve",
|
|
115
|
+
"github_merge",
|
|
116
|
+
"github_retitle",
|
|
117
|
+
"github_rerun",
|
|
114
118
|
"mail_send",
|
|
115
119
|
"mail_sync",
|
|
116
120
|
"mail_detach",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one hook every tool registers through (BLI-12416).
|
|
3
|
+
*
|
|
4
|
+
* ~30 modules call `server.registerTool`. Rather than edit each, `createServer`
|
|
5
|
+
* installs this wrapper once, before any of them run, and it does three things
|
|
6
|
+
* to every registration:
|
|
7
|
+
*
|
|
8
|
+
* 1. adds `annotations` derived from `tool-catalog.ts` (`tool-annotations.ts`),
|
|
9
|
+
* so ChatGPT runs reads without a prompt and confirms writes;
|
|
10
|
+
* 2. hosted mode only: removes the tools in `REMOTE_EXCLUDED_TOOLS`, which
|
|
11
|
+
* would otherwise touch the host's own disk or spawn a program there;
|
|
12
|
+
* 3. hosted mode only: reports tool, outcome, milliseconds and response bytes
|
|
13
|
+
* per call (never arguments or answers) to `onToolCall`.
|
|
14
|
+
*/
|
|
15
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
export interface ToolCallReport {
|
|
17
|
+
tool: string;
|
|
18
|
+
outcome: "ok" | "tool_error" | "threw";
|
|
19
|
+
ms: number;
|
|
20
|
+
bytes: number;
|
|
21
|
+
}
|
|
22
|
+
export interface CreateServerOptions {
|
|
23
|
+
/**
|
|
24
|
+
* The hosted server: leaves out `REMOTE_EXCLUDED_TOOLS`, never reads this
|
|
25
|
+
* machine's turn bookmark, and carries `instructions`.
|
|
26
|
+
*/
|
|
27
|
+
remote?: {
|
|
28
|
+
instructions: string;
|
|
29
|
+
onToolCall?: (call: ToolCallReport) => void;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare function installToolRegistration(server: McpServer, options: CreateServerOptions): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one hook every tool registers through (BLI-12416).
|
|
3
|
+
*
|
|
4
|
+
* ~30 modules call `server.registerTool`. Rather than edit each, `createServer`
|
|
5
|
+
* installs this wrapper once, before any of them run, and it does three things
|
|
6
|
+
* to every registration:
|
|
7
|
+
*
|
|
8
|
+
* 1. adds `annotations` derived from `tool-catalog.ts` (`tool-annotations.ts`),
|
|
9
|
+
* so ChatGPT runs reads without a prompt and confirms writes;
|
|
10
|
+
* 2. hosted mode only: removes the tools in `REMOTE_EXCLUDED_TOOLS`, which
|
|
11
|
+
* would otherwise touch the host's own disk or spawn a program there;
|
|
12
|
+
* 3. hosted mode only: reports tool, outcome, milliseconds and response bytes
|
|
13
|
+
* per call (never arguments or answers) to `onToolCall`.
|
|
14
|
+
*/
|
|
15
|
+
import { REMOTE_EXCLUDED_TOOLS, annotationsFor } from "./tool-annotations.js";
|
|
16
|
+
import { DRY_RUN_TOOLS, READ_TOOLS, WRITE_TOOLS } from "./tool-catalog.js";
|
|
17
|
+
const CATALOGUED_TOOLS = new Set([...READ_TOOLS, ...DRY_RUN_TOOLS, ...WRITE_TOOLS]);
|
|
18
|
+
export function installToolRegistration(server, options) {
|
|
19
|
+
const remote = options.remote !== undefined;
|
|
20
|
+
const onToolCall = options.remote?.onToolCall;
|
|
21
|
+
const register = server.registerTool.bind(server);
|
|
22
|
+
server.registerTool = (name, config, handler) => {
|
|
23
|
+
const annotations = annotationsFor(name, CATALOGUED_TOOLS);
|
|
24
|
+
const measured = onToolCall
|
|
25
|
+
? async (...args) => {
|
|
26
|
+
const started = Date.now();
|
|
27
|
+
try {
|
|
28
|
+
const result = await handler(...args);
|
|
29
|
+
onToolCall({
|
|
30
|
+
tool: name,
|
|
31
|
+
outcome: result?.isError ? "tool_error" : "ok",
|
|
32
|
+
ms: Date.now() - started,
|
|
33
|
+
bytes: JSON.stringify(result ?? null).length,
|
|
34
|
+
});
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
onToolCall({ tool: name, outcome: "threw", ms: Date.now() - started, bytes: 0 });
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
: handler;
|
|
43
|
+
const handle = register(name, annotations && !config.annotations ? { ...config, annotations } : config, measured);
|
|
44
|
+
if (remote && name in REMOTE_EXCLUDED_TOOLS)
|
|
45
|
+
handle.remove();
|
|
46
|
+
return handle;
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/verb-census.js
CHANGED
|
@@ -234,6 +234,11 @@ export const MCP_TWINS = {
|
|
|
234
234
|
// BLI-12378: the human's call on whether a correction becomes a rule.
|
|
235
235
|
"rename list": { tool: "rename_pending", door: "GET /api/jarvis/rename-scope" },
|
|
236
236
|
"rename resolve": { tool: "rename_resolve", door: "POST /api/jarvis/rename-scope" },
|
|
237
|
+
// BLI-12385: GitHub PR read and the two-step merge / retitle / re-run.
|
|
238
|
+
"github pr": { tool: "github_pr", door: "POST /api/github/pull-request (read)" },
|
|
239
|
+
"github merge": { tool: "github_merge", door: "POST /api/github/pull-request (merge)" },
|
|
240
|
+
"github retitle": { tool: "github_retitle", door: "POST /api/github/pull-request (retitle)" },
|
|
241
|
+
"github rerun": { tool: "github_rerun", door: "POST /api/github/pull-request (rerun)" },
|
|
237
242
|
"usage sessions": { tool: "usage_sessions", door: "GET /api/usage/sessions" },
|
|
238
243
|
"usage people": { tool: "usage_people", door: "GET /api/usage/people" },
|
|
239
244
|
"docs list": { tool: "docs_list", door: "GET /api/docs/documents" },
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.83",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "bli-tower: an MCP server over BLI Cockpit's agent doors: JARVIS (jarvis_*), documents (docs_*), channels (msg_*), issues (work_*), the daily page (brief_*), meeting notes (notes_*), the ops board (ops_status/slack_*), settings/team/model, Scout and the workbook, plus the legacy event-stream tools (emit_event, get_ticket_timeline, get_active_tickets).",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./remote": {
|
|
9
|
+
"types": "./dist/remote.d.ts",
|
|
10
|
+
"default": "./dist/remote.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
7
14
|
"bin": {
|
|
8
15
|
"bli-cockpit-mcp": "./dist/index.js"
|
|
9
16
|
},
|