@nonbot/mcp 0.2.2 → 0.2.4
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
CHANGED
|
@@ -100,7 +100,7 @@ Ask your agent to "list my non.bot portfolios", "show what rules apply before I
|
|
|
100
100
|
## Tools
|
|
101
101
|
|
|
102
102
|
<!-- tool-table:start — generated by `npm run gen:tools`; tests/readme-tool-table.test.ts fails if this drifts from the code -->
|
|
103
|
-
### Portfolio (
|
|
103
|
+
### Portfolio (27)
|
|
104
104
|
|
|
105
105
|
The SAFe portfolio canvas: epics, features, stories, snapshots, personas, and the Run launch surface.
|
|
106
106
|
|
|
@@ -113,6 +113,7 @@ The SAFe portfolio canvas: epics, features, stories, snapshots, personas, and th
|
|
|
113
113
|
| `portfolio_versions` | List the saved version snapshots (time-travel restore points) for a portfolio — id, label, node count, created time. | read | `portfolio:read` |
|
|
114
114
|
| `portfolio_version_get` | Load one snapshot’s full graph (nodes + edges + viewport) — the restore source. | read | `portfolio:read` |
|
|
115
115
|
| `portfolio_characters` | List the Character personas the caller can see (their own + team), optionally scoped to a portfolio. | read | `portfolio:read` |
|
|
116
|
+
| `portfolio_comments_list` | List the threaded comments on one portfolio item (author, content, resolved flag, parent comment). | read | `portfolio:read` |
|
|
116
117
|
| `portfolio_update` | Whole-graph replace: write the complete nodes + edges for a portfolio. | destructive | `portfolio:write` |
|
|
117
118
|
| `portfolio_create_from_plan` | Create a DRAFT portfolio from an LLM-structured plan (epic + optional features/stories). | write | `portfolio:write` |
|
|
118
119
|
| `portfolio_decompose` | Ask the server AI to decompose an item (epic→features or feature→stories) into SUGGESTED children for review. | write | `portfolio:write` |
|
|
@@ -124,6 +125,8 @@ The SAFe portfolio canvas: epics, features, stories, snapshots, personas, and th
|
|
|
124
125
|
| `portfolio_create_activation` | Create an activation (a ▶ Run launch request) for a story. | write | `portfolio:write` + `runs:fire` |
|
|
125
126
|
| `portfolio_update_activation_status` | Transition an activation: pending → running \| failed \| cancelled. | write | `portfolio:write` |
|
|
126
127
|
| `portfolio_snapshot` | Save a labeled version snapshot of a portfolio’s CURRENT graph — a time-travel restore point. | write | `portfolio:write` |
|
|
128
|
+
| `portfolio_move_subtree` | Move one item AND every descendant (by parentItemId) out of a portfolio into another one — an existing portfolio (target_portfolio_id) or a brand-new one (new_… | write | `portfolio:write` |
|
|
129
|
+
| `portfolio_comment_post` | Post a comment on one portfolio item (optionally as a reply via parent_comment_id). | write | `portfolio:write` |
|
|
127
130
|
| `portfolio_create` | Create a new, empty portfolio owned by the caller and return its id + version. | write | `portfolio:write` |
|
|
128
131
|
| `portfolio_add_items` | Add up to 200 items of any level (objective, key-result, epic, feature, story, enabler, milestone) in one versioned write. | write | `portfolio:write` |
|
|
129
132
|
| `portfolio_patch_item` | Change fields of ONE portfolio item without rewriting the graph (PATCH /api/portfolio/graph/node): `set` writes allowlisted PortfolioItemData fields (title, de… | write | `portfolio:write` |
|
|
@@ -205,6 +208,14 @@ The supervisor surface: what the daemon and the fleet are doing, plus the three
|
|
|
205
208
|
| `run_snapshot_request` | Ask the operator's daemon to capture the last lines of a RUNNING run's terminal pane (secrets scrubbed, 16KB tail cap, stored server-side for 5 minutes only). | write | `runs:control` |
|
|
206
209
|
| `run_prompt_answer` | Answer a mid-run prompt on the operator's behalf — the daemon types the answer into the live pane. | destructive | `runs:respond` (explicit; a wildcard token does not satisfy it) |
|
|
207
210
|
| `run_stop_daemon` | The panic button — queue a remote shutdown for the operator's daemon (CLI ≥0.9.12). | destructive | `runs:control` |
|
|
211
|
+
|
|
212
|
+
### radBUGS (1)
|
|
213
|
+
|
|
214
|
+
File a bug straight into the radBUGS inbox, stamped as filed from MCP.
|
|
215
|
+
|
|
216
|
+
| Tool | What it does | Kind | PAT scope |
|
|
217
|
+
|---|---|---|---|
|
|
218
|
+
| `radbug_file` | File a bug report into the non.bot radBUGS inbox, stamped as filed from MCP. | write | `radbugs:write` |
|
|
208
219
|
<!-- tool-table:end -->
|
|
209
220
|
|
|
210
221
|
**Kind** is the tool's MCP annotation: *read* tools never change anything; *destructive* tools are confirm-gated (`portfolio_update` on large removals, `run_prompt_answer`, `run_stop_daemon`). `NON_BOT_MCP_READONLY=1` drops every non-read tool.
|
|
@@ -234,7 +245,7 @@ Safety rails on writes: `portfolio_update` pre-fetches the live version, backs t
|
|
|
234
245
|
- `core/http.ts` — token-bound fetch wrapper
|
|
235
246
|
- `core/validate.ts` — pre-flight graph validation
|
|
236
247
|
- `core/server.ts` — `registerAll(server, ctx)` (the transport-agnostic heart) + `createNonbotMcpServer(ctx)`
|
|
237
|
-
- `core/toolsets/{portfolio,whygraph,run,monitor,ip}.ts`, `core/resources.ts`, `core/prompts.ts`
|
|
248
|
+
- `core/toolsets/{portfolio,whygraph,run,monitor,ip,radbugs}.ts`, `core/resources.ts`, `core/prompts.ts`
|
|
238
249
|
- `src/stdio.ts` — the `nonbot-mcp` bin: env/CLI-auth token + filesystem backup hook
|
|
239
250
|
- `scripts/gen-tool-table.mjs` — prints the tool tables above from the live definitions (`--portfolio-mcp` does the same for the legacy server)
|
|
240
251
|
|
package/dist/core/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { ApiContext } from './config.js';
|
|
3
3
|
export declare const SERVER_NAME = "nonbot-mcp";
|
|
4
|
-
export declare const SERVER_VERSION = "0.2.
|
|
4
|
+
export declare const SERVER_VERSION = "0.2.4";
|
|
5
5
|
export declare const INSTRUCTIONS: string;
|
|
6
6
|
export declare function registerAll(server: McpServer, ctx: ApiContext): void;
|
|
7
7
|
export declare function createNonbotMcpServer(ctx: ApiContext): McpServer;
|
package/dist/core/server.js
CHANGED
|
@@ -5,10 +5,11 @@ import { registerBuildTools } from './toolsets/build.js';
|
|
|
5
5
|
import { registerWhygraphTools } from './toolsets/whygraph.js';
|
|
6
6
|
import { registerRunTools } from './toolsets/run.js';
|
|
7
7
|
import { registerMonitorTools } from './toolsets/monitor.js';
|
|
8
|
+
import { registerRadbugsTools } from './toolsets/radbugs.js';
|
|
8
9
|
import { registerResources } from './resources.js';
|
|
9
10
|
import { registerPrompts } from './prompts.js';
|
|
10
11
|
export const SERVER_NAME = 'nonbot-mcp';
|
|
11
|
-
export const SERVER_VERSION = '0.2.
|
|
12
|
+
export const SERVER_VERSION = '0.2.4';
|
|
12
13
|
export const INSTRUCTIONS = [
|
|
13
14
|
'non.bot MCP — work against the caller’s real product-management data, not a copy.',
|
|
14
15
|
'Two domains: PORTFOLIOS (the SAFe portfolio canvas — epics → features → stories) and',
|
|
@@ -28,6 +29,7 @@ export const INSTRUCTIONS = [
|
|
|
28
29
|
'tokens with the explicit runs:respond scope only).',
|
|
29
30
|
'IP NODES: ip_list / ip_get read a patent or trademark disclosure; ip_update edits it (op list + expected_version);',
|
|
30
31
|
'ip_evidence_add adds evidence the SERVER hashes, timestamps and numbers — never send a hash or a time.',
|
|
32
|
+
'RADBUGS: radbug_file files a bug into the non.bot radBUGS inbox (needs the radbugs:write scope).',
|
|
31
33
|
].join(' ');
|
|
32
34
|
export function registerAll(server, ctx) {
|
|
33
35
|
registerPortfolioTools(server, ctx);
|
|
@@ -36,6 +38,7 @@ export function registerAll(server, ctx) {
|
|
|
36
38
|
registerWhygraphTools(server, ctx);
|
|
37
39
|
registerRunTools(server, ctx);
|
|
38
40
|
registerMonitorTools(server, ctx);
|
|
41
|
+
registerRadbugsTools(server, ctx);
|
|
39
42
|
registerResources(server, ctx);
|
|
40
43
|
registerPrompts(server, ctx);
|
|
41
44
|
}
|
|
@@ -208,6 +208,15 @@ export function registerPortfolioTools(server, ctx) {
|
|
|
208
208
|
const structured = { count: characters.length, characters };
|
|
209
209
|
return { content: [text(structured)], structuredContent: structured };
|
|
210
210
|
});
|
|
211
|
+
server.registerTool('portfolio_comments_list', {
|
|
212
|
+
title: 'List comments on an item',
|
|
213
|
+
description: 'List the threaded comments on one portfolio item (author, content, resolved flag, parent comment).',
|
|
214
|
+
inputSchema: { portfolio_id: z.string(), item_id: z.string() },
|
|
215
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
216
|
+
}, async (a) => {
|
|
217
|
+
const res = await api(ctx, `/api/portfolio/comments?portfolioId=${encodeURIComponent(a.portfolio_id)}&itemId=${encodeURIComponent(a.item_id)}`);
|
|
218
|
+
return { content: [text(await expectJson(res, 'portfolio_comments_list'))] };
|
|
219
|
+
});
|
|
211
220
|
if (ctx.readonly)
|
|
212
221
|
return;
|
|
213
222
|
async function gatedReq(method, path, body, label) {
|
|
@@ -518,4 +527,65 @@ export function registerPortfolioTools(server, ctx) {
|
|
|
518
527
|
const structured = { ok: true, ...(versionId ? { versionId } : {}), ...(a.label ? { label: a.label } : {}) };
|
|
519
528
|
return { content: [text(structured)], structuredContent: structured };
|
|
520
529
|
});
|
|
530
|
+
async function currentVersion(portfolioId, label) {
|
|
531
|
+
const res = await api(ctx, `/api/portfolio/graph?id=${encodeURIComponent(portfolioId)}`);
|
|
532
|
+
const g = await expectJson(res, label);
|
|
533
|
+
const graph = (g.graph ?? g);
|
|
534
|
+
const v = Number(graph.version);
|
|
535
|
+
if (!Number.isFinite(v))
|
|
536
|
+
throw new Error(`${label}: portfolio ${portfolioId} returned no version`);
|
|
537
|
+
return v;
|
|
538
|
+
}
|
|
539
|
+
server.registerTool('portfolio_move_subtree', {
|
|
540
|
+
title: 'Move an item and its descendants to another portfolio',
|
|
541
|
+
description: 'Move one item AND every descendant (by parentItemId) out of a portfolio into another one — an existing portfolio (target_portfolio_id) or a brand-new one (new_portfolio_name). Atomic: both portfolios are written together or not at all. Hierarchy edges that cross the boundary are severed (the moved root becomes a root); dependency edges are kept as cross-portfolio links. Versions are read automatically when omitted.',
|
|
542
|
+
inputSchema: {
|
|
543
|
+
portfolio_id: z.string().describe('Source portfolio.'),
|
|
544
|
+
root_item_id: z.string().describe('The item to move; its whole subtree travels with it.'),
|
|
545
|
+
target_portfolio_id: z.string().optional().describe('Existing destination portfolio.'),
|
|
546
|
+
new_portfolio_name: z.string().optional().describe('Create a new destination portfolio with this name instead.'),
|
|
547
|
+
expected_version: z.number().int().optional().describe('Source version (OCC). Read automatically when omitted.'),
|
|
548
|
+
target_expected_version: z.number().int().optional().describe('Target version (OCC). Read automatically when omitted.'),
|
|
549
|
+
},
|
|
550
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
551
|
+
}, async (a) => {
|
|
552
|
+
if (!!a.target_portfolio_id === !!a.new_portfolio_name) {
|
|
553
|
+
return { content: [{ type: 'text', text: 'Provide exactly one of target_portfolio_id or new_portfolio_name.' }], isError: true };
|
|
554
|
+
}
|
|
555
|
+
const expectedVersion = a.expected_version ?? await currentVersion(a.portfolio_id, 'portfolio_move_subtree (source)');
|
|
556
|
+
const target = a.target_portfolio_id
|
|
557
|
+
? {
|
|
558
|
+
kind: 'existing',
|
|
559
|
+
portfolioId: a.target_portfolio_id,
|
|
560
|
+
expectedVersion: a.target_expected_version ?? await currentVersion(a.target_portfolio_id, 'portfolio_move_subtree (target)'),
|
|
561
|
+
}
|
|
562
|
+
: { kind: 'new', name: a.new_portfolio_name };
|
|
563
|
+
const res = await api(ctx, `/api/portfolio/${encodeURIComponent(a.portfolio_id)}/move-subtree`, {
|
|
564
|
+
method: 'POST',
|
|
565
|
+
body: JSON.stringify({ rootNodeId: a.root_item_id, expectedVersion, target }),
|
|
566
|
+
});
|
|
567
|
+
return { content: [text(await expectJson(res, 'portfolio_move_subtree'))] };
|
|
568
|
+
});
|
|
569
|
+
server.registerTool('portfolio_comment_post', {
|
|
570
|
+
title: 'Comment on an item',
|
|
571
|
+
description: 'Post a comment on one portfolio item (optionally as a reply via parent_comment_id). Max 10 KB; the server allows 20 comments per minute.',
|
|
572
|
+
inputSchema: {
|
|
573
|
+
portfolio_id: z.string(),
|
|
574
|
+
item_id: z.string(),
|
|
575
|
+
content: z.string().min(1).max(10240),
|
|
576
|
+
parent_comment_id: z.string().optional(),
|
|
577
|
+
},
|
|
578
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
579
|
+
}, async (a) => {
|
|
580
|
+
const res = await api(ctx, '/api/portfolio/comments', {
|
|
581
|
+
method: 'POST',
|
|
582
|
+
body: JSON.stringify({
|
|
583
|
+
portfolioId: a.portfolio_id,
|
|
584
|
+
itemId: a.item_id,
|
|
585
|
+
content: a.content,
|
|
586
|
+
...(a.parent_comment_id ? { parentCommentId: a.parent_comment_id } : {}),
|
|
587
|
+
}),
|
|
588
|
+
});
|
|
589
|
+
return { content: [text(await expectJson(res, 'portfolio_comment_post'))] };
|
|
590
|
+
});
|
|
521
591
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { api, expectJson } from '../http.js';
|
|
3
|
+
const SCOPE_MESSAGE = 'This token needs the radbugs:write scope (or the account needs radBUGS access).';
|
|
4
|
+
async function forbiddenText(res) {
|
|
5
|
+
let reason = '';
|
|
6
|
+
try {
|
|
7
|
+
const j = (await res.json());
|
|
8
|
+
const m = j && typeof j === 'object' ? (j.message ?? j.error) : undefined;
|
|
9
|
+
if (typeof m === 'string' && m.trim())
|
|
10
|
+
reason = m.trim().slice(0, 300);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
}
|
|
14
|
+
return reason ? `${SCOPE_MESSAGE} Server said: ${reason}` : SCOPE_MESSAGE;
|
|
15
|
+
}
|
|
16
|
+
function textResult(data) {
|
|
17
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
18
|
+
}
|
|
19
|
+
export function registerRadbugsTools(server, ctx) {
|
|
20
|
+
if (ctx.readonly)
|
|
21
|
+
return;
|
|
22
|
+
server.registerTool('radbug_file', {
|
|
23
|
+
title: 'File a radBUG',
|
|
24
|
+
description: 'File a bug report into the non.bot radBUGS inbox, stamped as filed from MCP. Give a short title; add a description (steps, expected vs actual), the product area, a priority, and the page URL when known. Returns the created inbox item. Requires the radbugs:write scope and an account with radBUGS access.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
title: z.string().min(1).max(500).describe('Short summary of the bug.'),
|
|
27
|
+
description: z.string().max(20000).optional().describe('Steps to reproduce, expected vs actual, context.'),
|
|
28
|
+
area: z.string().max(80).optional().describe('Product area, e.g. "portfolio" or "radmeeting".'),
|
|
29
|
+
priority: z.enum(['low', 'med', 'high']).optional().describe('How urgent it is.'),
|
|
30
|
+
page_url: z.string().max(2048).optional().describe('The page the bug was seen on.'),
|
|
31
|
+
},
|
|
32
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
33
|
+
}, async (a) => {
|
|
34
|
+
const body = { title: a.title };
|
|
35
|
+
if (a.description !== undefined)
|
|
36
|
+
body.description = a.description;
|
|
37
|
+
if (a.area !== undefined)
|
|
38
|
+
body.area = a.area;
|
|
39
|
+
if (a.priority !== undefined)
|
|
40
|
+
body.priority = a.priority;
|
|
41
|
+
if (a.page_url !== undefined)
|
|
42
|
+
body.page_url = a.page_url;
|
|
43
|
+
body.client = 'mcp';
|
|
44
|
+
const res = await api(ctx, '/api/admin/radbugs', { method: 'POST', body: JSON.stringify(body) });
|
|
45
|
+
if (res.status === 403) {
|
|
46
|
+
return { content: [{ type: 'text', text: await forbiddenText(res) }], isError: true };
|
|
47
|
+
}
|
|
48
|
+
return textResult(await expectJson(res, 'POST /api/admin/radbugs'));
|
|
49
|
+
});
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nonbot/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The non.bot MCP server — one server for your portfolios, decision graph, and Runs. Runs over stdio (Claude Code, Cursor, VS Code) with a pat_ token. Built on MCP protocol 2025-11-25.",
|
|
6
6
|
"keywords": [
|