@nonbot/mcp 0.2.0 → 0.2.3

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
@@ -205,6 +205,14 @@ The supervisor surface: what the daemon and the fleet are doing, plus the three
205
205
  | `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
206
  | `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
207
  | `run_stop_daemon` | The panic button — queue a remote shutdown for the operator's daemon (CLI ≥0.9.12). | destructive | `runs:control` |
208
+
209
+ ### radBUGS (1)
210
+
211
+ File a bug straight into the radBUGS inbox, stamped as filed from MCP.
212
+
213
+ | Tool | What it does | Kind | PAT scope |
214
+ |---|---|---|---|
215
+ | `radbug_file` | File a bug report into the non.bot radBUGS inbox, stamped as filed from MCP. | write | `radbugs:write` |
208
216
  <!-- tool-table:end -->
209
217
 
210
218
  **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 +242,7 @@ Safety rails on writes: `portfolio_update` pre-fetches the live version, backs t
234
242
  - `core/http.ts` — token-bound fetch wrapper
235
243
  - `core/validate.ts` — pre-flight graph validation
236
244
  - `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`
245
+ - `core/toolsets/{portfolio,whygraph,run,monitor,ip,radbugs}.ts`, `core/resources.ts`, `core/prompts.ts`
238
246
  - `src/stdio.ts` — the `nonbot-mcp` bin: env/CLI-auth token + filesystem backup hook
239
247
  - `scripts/gen-tool-table.mjs` — prints the tool tables above from the live definitions (`--portfolio-mcp` does the same for the legacy server)
240
248
 
@@ -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.0";
4
+ export declare const SERVER_VERSION = "0.2.3";
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;
@@ -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.0';
12
+ export const SERVER_VERSION = '0.2.3';
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
  }
@@ -25,74 +25,47 @@ export declare const toHtml: (s: string) => string;
25
25
  export declare function newItemId(now?: number): string;
26
26
  declare const itemSpec: z.ZodObject<{
27
27
  key: z.ZodOptional<z.ZodString>;
28
- level: z.ZodEnum<["objective", "key-result", "epic", "feature", "story", "enabler", "milestone"]>;
28
+ level: z.ZodEnum<{
29
+ epic: "epic";
30
+ objective: "objective";
31
+ "key-result": "key-result";
32
+ feature: "feature";
33
+ story: "story";
34
+ enabler: "enabler";
35
+ milestone: "milestone";
36
+ }>;
29
37
  title: z.ZodString;
30
38
  description: z.ZodOptional<z.ZodString>;
31
39
  bluf: z.ZodOptional<z.ZodString>;
32
40
  acceptance_criteria: z.ZodOptional<z.ZodString>;
33
41
  hypothesis: z.ZodOptional<z.ZodString>;
34
- state: z.ZodOptional<z.ZodEnum<["funnel", "reviewing", "analyzing", "ready", "implementing", "done"]>>;
42
+ state: z.ZodOptional<z.ZodEnum<{
43
+ done: "done";
44
+ funnel: "funnel";
45
+ reviewing: "reviewing";
46
+ analyzing: "analyzing";
47
+ ready: "ready";
48
+ implementing: "implementing";
49
+ }>>;
35
50
  parent: z.ZodOptional<z.ZodString>;
36
- okr: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
37
- depends_on: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
51
+ okr: z.ZodOptional<z.ZodArray<z.ZodString>>;
52
+ depends_on: z.ZodOptional<z.ZodArray<z.ZodString>>;
38
53
  start_week: z.ZodOptional<z.ZodNumber>;
39
54
  start_date: z.ZodOptional<z.ZodString>;
40
55
  duration_weeks: z.ZodOptional<z.ZodNumber>;
41
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
56
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
57
  wsjf: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
43
58
  owner: z.ZodOptional<z.ZodString>;
44
59
  kr_target: z.ZodOptional<z.ZodString>;
45
60
  kr_current: z.ZodOptional<z.ZodString>;
46
- okr_status: z.ZodOptional<z.ZodEnum<["on-track", "at-risk", "behind"]>>;
47
- images: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
61
+ okr_status: z.ZodOptional<z.ZodEnum<{
62
+ "on-track": "on-track";
63
+ "at-risk": "at-risk";
64
+ behind: "behind";
65
+ }>>;
66
+ images: z.ZodOptional<z.ZodArray<z.ZodString>>;
48
67
  fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
49
- }, "strip", z.ZodTypeAny, {
50
- title: string;
51
- level: "epic" | "objective" | "key-result" | "feature" | "story" | "enabler" | "milestone";
52
- parent?: string | undefined;
53
- description?: string | undefined;
54
- acceptance_criteria?: string | undefined;
55
- hypothesis?: string | undefined;
56
- owner?: string | undefined;
57
- key?: string | undefined;
58
- bluf?: string | undefined;
59
- state?: "done" | "funnel" | "reviewing" | "analyzing" | "ready" | "implementing" | undefined;
60
- okr?: string[] | undefined;
61
- depends_on?: string[] | undefined;
62
- start_week?: number | undefined;
63
- start_date?: string | undefined;
64
- duration_weeks?: number | undefined;
65
- tags?: string[] | undefined;
66
- wsjf?: [number, number, number, number] | undefined;
67
- kr_target?: string | undefined;
68
- kr_current?: string | undefined;
69
- okr_status?: "on-track" | "at-risk" | "behind" | undefined;
70
- images?: string[] | undefined;
71
- fields?: Record<string, unknown> | undefined;
72
- }, {
73
- title: string;
74
- level: "epic" | "objective" | "key-result" | "feature" | "story" | "enabler" | "milestone";
75
- parent?: string | undefined;
76
- description?: string | undefined;
77
- acceptance_criteria?: string | undefined;
78
- hypothesis?: string | undefined;
79
- owner?: string | undefined;
80
- key?: string | undefined;
81
- bluf?: string | undefined;
82
- state?: "done" | "funnel" | "reviewing" | "analyzing" | "ready" | "implementing" | undefined;
83
- okr?: string[] | undefined;
84
- depends_on?: string[] | undefined;
85
- start_week?: number | undefined;
86
- start_date?: string | undefined;
87
- duration_weeks?: number | undefined;
88
- tags?: string[] | undefined;
89
- wsjf?: [number, number, number, number] | undefined;
90
- kr_target?: string | undefined;
91
- kr_current?: string | undefined;
92
- okr_status?: "on-track" | "at-risk" | "behind" | undefined;
93
- images?: string[] | undefined;
94
- fields?: Record<string, unknown> | undefined;
95
- }>;
68
+ }, z.core.$strip>;
96
69
  type ItemSpec = z.infer<typeof itemSpec>;
97
70
  export declare function buildItems(g: Graph, specs: ItemSpec[], now?: Date): {
98
71
  nodes: Node[];
@@ -68,7 +68,7 @@ const itemSpec = z.object({
68
68
  kr_current: z.string().max(200).optional(),
69
69
  okr_status: z.enum(OKR_STATUSES).optional(),
70
70
  images: z.array(z.string().max(2048)).max(8).optional().describe('Image URLs from portfolio_asset_upload — the first is the card hero.'),
71
- fields: z.record(z.unknown()).optional().describe('Any other PortfolioItemData fields, merged last (structural keys are refused).'),
71
+ fields: z.record(z.string(), z.unknown()).optional().describe('Any other PortfolioItemData fields, merged last (structural keys are refused).'),
72
72
  });
73
73
  const DEFAULT_WEEKS = { epic: 8, feature: 4, story: 1, enabler: 2, milestone: 0, objective: 12, 'key-result': 12 };
74
74
  const LEGAL_PARENT = {
@@ -225,7 +225,7 @@ export function registerBuildTools(server, ctx) {
225
225
  inputSchema: {
226
226
  portfolio_id: z.string(),
227
227
  item_id: z.string(),
228
- set: z.record(z.unknown()).optional(),
228
+ set: z.record(z.string(), z.unknown()).optional(),
229
229
  unset: z.array(z.string()).max(40).optional(),
230
230
  expected_version: z.number().int().optional(),
231
231
  },
@@ -251,7 +251,7 @@ export function registerBuildTools(server, ctx) {
251
251
  description: 'Re-position every item so the Timeline reads cleanly: x from each item\'s start date (the canvas reads dates off x), y top-down — objectives, key results, then one block per epic (epic, features, stories + enablers, milestones), each row lane-packed so overlapping items stack, IP nodes directly above their first source. Short items get a minimum painted width so titles fit. Content is untouched. `heights` overrides the estimated card height per id (px) when a card measures taller on screen.',
252
252
  inputSchema: {
253
253
  portfolio_id: z.string(),
254
- heights: z.record(z.number().min(20).max(2000)).optional(),
254
+ heights: z.record(z.string(), z.number().min(20).max(2000)).optional(),
255
255
  expected_version: z.number().int().optional(),
256
256
  },
257
257
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
@@ -270,9 +270,9 @@ export function registerBuildTools(server, ctx) {
270
270
  inputSchema: {
271
271
  portfolio_id: z.string(),
272
272
  context_text: z.string().max(20000).optional(),
273
- palette: z.object({ id: z.string().min(1).max(64), name: z.string().min(1).max(64), swatches: z.array(z.object({ hex: z.string().regex(/^#[0-9a-fA-F]{6}$/), label: z.string().max(32).optional() })).max(10), roles: z.record(z.string()).optional() }).optional(),
274
- canvas_style: z.record(z.unknown()).optional(),
275
- sprint_settings: z.record(z.unknown()).optional(),
273
+ palette: z.object({ id: z.string().min(1).max(64), name: z.string().min(1).max(64), swatches: z.array(z.object({ hex: z.string().regex(/^#[0-9a-fA-F]{6}$/), label: z.string().max(32).optional() })).max(10), roles: z.record(z.string(), z.string()).optional() }).optional(),
274
+ canvas_style: z.record(z.string(), z.unknown()).optional(),
275
+ sprint_settings: z.record(z.string(), z.unknown()).optional(),
276
276
  expected_version: z.number().int().optional(),
277
277
  },
278
278
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
@@ -75,7 +75,7 @@ export function registerIpTools(server, ctx) {
75
75
  server.registerTool('ip_update', {
76
76
  title: 'Edit an IP node (op list)',
77
77
  description: 'Apply an op list to one IP node in ONE versioned write: unit.set {unitId, value, expectedRev} · unit.confirm · unit.unconfirm · evidence.update {evId, patch} · evidence.remove {evId} (a tombstone) · evidence.witness {evId, witness:{for,name,line}|null} · evidence.add (max 5 per request; identical adds collapse into one row) · event.upsert · event.remove · filing.set · meta.set. A stale expected_version is HTTP 409 version_conflict with currentVersion — re-read with ip_get and retry.',
78
- inputSchema: { ...ids, expected_version: z.number().int().describe('The graph version from ip_get.'), ops: z.array(z.record(z.unknown())).min(1).max(50).refine(o => o.filter(x => x.op === 'evidence.add').length <= 5, 'At most 5 evidence.add ops per request.').describe('The op list.') },
78
+ inputSchema: { ...ids, expected_version: z.number().int().describe('The graph version from ip_get.'), ops: z.array(z.record(z.string(), z.unknown())).min(1).max(50).refine(o => o.filter(x => x.op === 'evidence.add').length <= 5, 'At most 5 evidence.add ops per request.').describe('The op list.') },
79
79
  annotations: WRITE,
80
80
  }, async (a) => textResult(await patchNode(ctx, { portfolioId: a.portfolio_id, nodeId: a.node_id, expectedVersion: a.expected_version, ops: a.ops })));
81
81
  server.registerTool('ip_evidence_add', {
@@ -85,7 +85,7 @@ export function registerIpTools(server, ctx) {
85
85
  ...ids,
86
86
  expected_version: z.number().int().describe('The graph version from ip_get.'),
87
87
  kind: z.enum(KINDS),
88
- from: z.record(z.unknown()).describe('Exactly one source (see description).'),
88
+ from: z.record(z.string(), z.unknown()).describe('Exactly one source (see description).'),
89
89
  title: z.string().min(1).max(300),
90
90
  describes: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('The date the evidence describes (YYYY-MM-DD).'),
91
91
  supports: z.array(z.enum(SUPPORTS)).max(5).optional(),
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ApiContext } from '../config.js';
3
+ export declare function registerRadbugsTools(server: McpServer, ctx: ApiContext): void;
@@ -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.0",
3
+ "version": "0.2.3",
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": [
@@ -56,7 +56,7 @@
56
56
  "dependencies": {
57
57
  "@cfworker/json-schema": "^4.1.1",
58
58
  "@modelcontextprotocol/sdk": "^1.29.0",
59
- "zod": "^3.25.0"
59
+ "zod": "^4.6.7"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^22.19.19",