@nonbot/mcp 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,43 @@
1
+ Copyright (c) 2026 non.bot. All rights reserved.
2
+
3
+ PROPRIETARY SOFTWARE — NOT OPEN SOURCE
4
+
5
+ This software (the "@nonbot/mcp" package and all of its contents, the
6
+ "Software") is the proprietary and confidential property of non.bot. It is
7
+ published to the npm registry for the sole purpose of allowing authorized
8
+ non.bot account holders to install and run it as the Model Context Protocol server that
9
+ connects their AI client to the non.bot service.
10
+
11
+ PERMITTED USE
12
+
13
+ Subject to your compliance with the non.bot Terms of Service, you are granted
14
+ a limited, non-exclusive, non-transferable, revocable license to download,
15
+ install, and execute unmodified copies of the Software on machines you control,
16
+ solely to connect to and operate the non.bot service.
17
+
18
+ PROHIBITED WITHOUT PRIOR WRITTEN PERMISSION FROM non.bot
19
+
20
+ - Copying, redistributing, sublicensing, selling, or publishing the Software
21
+ or any portion of it;
22
+ - Modifying, adapting, translating, or creating derivative works;
23
+ - Reverse-engineering, decompiling, or disassembling the Software except to
24
+ the limited extent such restriction is prohibited by applicable law;
25
+ - Using the Software, or any technique, method, or design embodied in it, to
26
+ build, train, or operate a competing product or service.
27
+
28
+ NO PATENT OR OTHER IP LICENSE
29
+
30
+ No license to any patent, patent application, trademark, trade secret, or other
31
+ intellectual-property right of non.bot is granted, whether expressly, by
32
+ implication, by estoppel, or otherwise, except for the limited execution right
33
+ stated above. All such rights are expressly reserved.
34
+
35
+ NO WARRANTY
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
40
+ non.bot BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT
41
+ OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
42
+
43
+ Contact: legal@non.bot
package/README.md CHANGED
@@ -1,3 +1,243 @@
1
- # Temporary Holding Version
1
+ # @nonbot/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **One MCP server for your non.bot data.** Your terminal agent (Claude Code, Cursor, VS Code, or any MCP client) reads and reasons over your *real* portfolios, decision graph, and Runs, not a copy.
4
+
5
+ ```bash
6
+ npx -y @nonbot/mcp
7
+ ```
8
+
9
+ Built on the official MCP TypeScript SDK and **protocol `2025-11-25`**. Structured tool output, tool annotations, Resources, and Prompts throughout. Needs Node 18 or newer.
10
+
11
+ > This is the unified successor to the separate `portfolio-mcp` and `context-graph` (WhyGraph) servers. The per-run fleet server is shipped inside `@nonbot/cli` as `nonbot run-mcp` (launched by the daemon, never installed by hand).
12
+
13
+ ---
14
+
15
+ ## Install
16
+
17
+ ### 1. Get a token
18
+
19
+ Generate a Personal Access Token at **<https://non.bot/settings/connections>** → *API Tokens* → *Generate New Token*. Copy it (shown once). It looks like `pat_…`.
20
+
21
+ Scoped tokens work too: a token with `portfolio:read` sees only the read tools' data; the table below says what each tool needs. If you have run `nonbot login` (the `@nonbot/cli` daemon) on this machine, you can omit the token entirely: the server falls back to the CLI's auth file.
22
+
23
+ ### 2. Add the server to your client
24
+
25
+ **Claude Code** — one command:
26
+
27
+ ```bash
28
+ claude mcp add nonbot -e NON_BOT_API_TOKEN=pat_<YOUR_TOKEN> -- npx -y @nonbot/mcp
29
+ ```
30
+
31
+ or in `.mcp.json` (project) / `~/.claude.json` (user):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "nonbot": {
37
+ "command": "npx",
38
+ "args": ["-y", "@nonbot/mcp"],
39
+ "env": { "NON_BOT_API_TOKEN": "pat_<YOUR_TOKEN>" }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ **Cursor** — `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project), same shape:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "nonbot": {
51
+ "command": "npx",
52
+ "args": ["-y", "@nonbot/mcp"],
53
+ "env": { "NON_BOT_API_TOKEN": "pat_<YOUR_TOKEN>" }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ **VS Code** — `.vscode/mcp.json` (note the `servers` key; the token is a secret input, so the file is safe to commit):
60
+
61
+ ```json
62
+ {
63
+ "inputs": [
64
+ { "id": "nonbot-pat", "type": "promptString", "description": "non.bot Personal Access Token", "password": true }
65
+ ],
66
+ "servers": {
67
+ "nonbot": {
68
+ "type": "stdio",
69
+ "command": "npx",
70
+ "args": ["-y", "@nonbot/mcp"],
71
+ "env": { "NON_BOT_API_TOKEN": "${input:nonbot-pat}" }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ **Remote (Streamable HTTP)** — the same server runs as a Cloudflare Worker for clients that cannot spawn a local process. Auth is the same `pat_` token, sent as a bearer header:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "nonbot": {
83
+ "url": "https://nonbot-mcp-remote.conradwade.workers.dev/mcp",
84
+ "headers": { "Authorization": "Bearer pat_<YOUR_TOKEN>" }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ Claude Code: `claude mcp add --transport http nonbot https://nonbot-mcp-remote.conradwade.workers.dev/mcp --header "Authorization: Bearer pat_<YOUR_TOKEN>"`. Append `?readonly=1` to the URL for a read-only connection.
91
+
92
+ > **Claude web and mobile:** claude.ai custom connectors authenticate with OAuth and cannot send a custom header, so the remote endpoint is not yet a one-click connector there. OAuth is the next step for the remote server (see `DISTRIBUTION-PLAN.md`); until it ships, use Claude Code, Cursor, or VS Code.
93
+
94
+ ### 3. Use it
95
+
96
+ Ask your agent to "list my non.bot portfolios", "show what rules apply before I change auth", or run the `/wsjf_prioritize` prompt.
97
+
98
+ ---
99
+
100
+ ## Tools
101
+
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 (18)
104
+
105
+ The SAFe portfolio canvas: epics, features, stories, snapshots, personas, and the Run launch surface.
106
+
107
+ | Tool | What it does | Kind | PAT scope |
108
+ |---|---|---|---|
109
+ | `portfolio_list` | List the portfolios the caller can access (owned, team, and shared). | read | `portfolio:read` |
110
+ | `portfolio_get` | Fetch the full node/edge graph (plus viewport and version) for one portfolio. | read | `portfolio:read` |
111
+ | `portfolio_get_activation` | Fetch the launch payload + story-brief snapshot for one activation (Act-3 ▶ Run). | read | `portfolio:read` |
112
+ | `portfolio_subscribe_to_activations` | Poll the caller’s currently-pending activations (the Act-3 ▶ Run fallback). | read | `portfolio:read` |
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
+ | `portfolio_version_get` | Load one snapshot’s full graph (nodes + edges + viewport) — the restore source. | read | `portfolio:read` |
115
+ | `portfolio_characters` | List the Character personas the caller can see (their own + team), optionally scoped to a portfolio. | read | `portfolio:read` |
116
+ | `portfolio_update` | Whole-graph replace: write the complete nodes + edges for a portfolio. | destructive | `portfolio:write` |
117
+ | `portfolio_create_from_plan` | Create a DRAFT portfolio from an LLM-structured plan (epic + optional features/stories). | write | `portfolio:write` |
118
+ | `portfolio_decompose` | Ask the server AI to decompose an item (epic→features or feature→stories) into SUGGESTED children for review. | write | `portfolio:write` |
119
+ | `portfolio_attach_brief` | Attach metadata to one node: a markdown brief, acceptance criteria, description, and/or hypothesis. | write | `portfolio:write` |
120
+ | `portfolio_link_chat_session` | Backlink a portfolio to the chat that birthed it. | write | `portfolio:write` |
121
+ | `portfolio_publish_draft` | Flip a portfolio from "draft" → "active". | write | `portfolio:write` |
122
+ | `portfolio_mark_run_ready` | Tag a story as ready for CLI pickup (or clear it with run_ready=false). | write | `portfolio:write` |
123
+ | `portfolio_repo_path` | Your per-user local repo path for a portfolio — where ▶ Run activations cd before launching. | write | `portfolio:write` |
124
+ | `portfolio_create_activation` | Create an activation (a ▶ Run launch request) for a story. | write | `portfolio:write` + `runs:fire` |
125
+ | `portfolio_update_activation_status` | Transition an activation: pending → running \| failed \| cancelled. | write | `portfolio:write` |
126
+ | `portfolio_snapshot` | Save a labeled version snapshot of a portfolio’s CURRENT graph — a time-travel restore point. | write | `portfolio:write` |
127
+
128
+ ### IP node (10)
129
+
130
+ Patent and trademark disclosures on the canvas: read the bag, edit it as an op list, add server-sealed evidence, export the hash list.
131
+
132
+ | Tool | What it does | Kind | PAT scope |
133
+ |---|---|---|---|
134
+ | `ip_list` | Every IP node (patent or trademark disclosure) in a portfolio: id, title, track, kind, started, live source count, live evidence count, filing date. | read | `portfolio:read` |
135
+ | `ip_get` | One IP node's full disclosure bag (units with status, value, provenance; evidence incl. tombstones; events; filing) plus the graph version to pass as expected_… | read | `portfolio:read` |
136
+ | `ip_evidence_list` | Evidence rows from the saved graph, tombstones included, with a seal verdict per row (true = sealed by this server, false = not, null = no signing key) and a w… | read | `portfolio:read` |
137
+ | `ip_evidence_hashes` | The Export-hash-list file: one `id · sha256 · added(ISO)` line per row (never changed once written) plus a `#` trailer naming removed, missing, unsealed or alt… | read | `portfolio:read` |
138
+ | `ip_packet` | The filing packet of an IP node as the Packet step shows it, rendered by the server from the saved graph: each document's id, name, status (queued / ready / ac… | read | `portfolio:read` |
139
+ | `ip_packet_document` | One packet document's full text (markdown) and its text files (md / csv / txt / svg), rendered by the server from the saved graph. | read | `portfolio:read` |
140
+ | `ip_update` | 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} ·… | write | `portfolio:write` |
141
+ | `ip_evidence_add` | Add one evidence row the SERVER hashes (SHA-256 of the stored copy), timestamps, seals and numbers. | write | `portfolio:write` |
142
+ | `ip_witness_candidates` | Members who may witness evidence on this node (id + name only): editors of the portfolio other than the caller, never someone listed as an inventor in field 5. | write | `portfolio:write` |
143
+ | `ip_build` | Runs `Build draft →` for a PATENT IP node on YOUR OWN model key (the token owner's Anthropic or Gemini key — non.bot never pays for it; no key = HTTP 402 no_ke… | write | `portfolio:write` |
144
+
145
+ ### WhyGraph (7)
146
+
147
+ The PM decision graph. Read and reasoning only over token auth; writes stay cookie-only on the server.
148
+
149
+ | Tool | What it does | Kind | PAT scope |
150
+ |---|---|---|---|
151
+ | `whygraph_search` | Search the PM decision graph by keyword, ranked by weight (how reinforced a decision is) then relevance. | read | `portfolio:read` |
152
+ | `whygraph_context` | Given a task you are about to do, return the decisions that apply: the best keyword matches PLUS every LAW (locked, weight ≥ 5) — because LAWs always apply. | read | `portfolio:read` |
153
+ | `whygraph_neighbors` | Graph reasoning the flat decision list cannot give you: starting from one decision, traverse RELATED_TO relationships breadth-first to surface every connected… | read | `portfolio:read` |
154
+ | `whygraph_stats` | A dashboard view of the decision graph: total decisions, a per-category breakdown, the LAWs (locked, weight ≥ 5), and the most-violated decisions. | read | `portfolio:read` |
155
+ | `whygraph_path` | Find the shortest chain of RELATED_TO links connecting one decision to another — how a decision reaches another through the graph. | read | `portfolio:read` |
156
+ | `whygraph_conflicts` | Surface pairs of decisions that govern the SAME field but are NOT linked to each other — potential unreconciled overlap or contradiction to review. | read | `portfolio:read` |
157
+ | `whygraph_recent_violations` | The most recent times decisions were violated — what broke, when, and which decision. | read | `portfolio:read` |
158
+
159
+ ### Run agent protocol (20)
160
+
161
+ Called by a launched Run agent (telemetry, scope, reflections, close-out), not by a person.
162
+
163
+ | Tool | What it does | Kind | PAT scope |
164
+ |---|---|---|---|
165
+ | `run_get_state` | Ground-truth run state: active_plan, ghost_plan, stickies, pending_violations, pending_reflections, trust_dial, last_user_intervention_at. | read | `portfolio:read` |
166
+ | `run_load_activation` | Load the full context for a run: the story brief, acceptance criteria, applicable WhyGraph rules, and ensemble defaults. | write | `portfolio:write` |
167
+ | `run_register_agent` | Register an ensemble member for this activation (creates the run on the first agent). | write | `portfolio:write` |
168
+ | `run_declare_scope` | Lead declares the file allowlist + intent. | write | `portfolio:write` |
169
+ | `run_request_scope_change` | Request expansion of the file allowlist. | write | `portfolio:write` |
170
+ | `run_emit_diff` | Report a code diff. Triggers a WhyGraph rule-check + scope guard; returns { violations, reinforcements, blocked_paths }. | write | `portfolio:write` |
171
+ | `run_emit_test_run` | Report a test run; updates the dashboard sparkline. | write | `portfolio:write` |
172
+ | `run_emit_lint` | Report lint hits; updates the dashboard. | write | `portfolio:write` |
173
+ | `run_emit_typecheck` | Report type-check errors; updates the dashboard. | write | `portfolio:write` |
174
+ | `run_emit_build` | Report build status; updates the dashboard. | write | `portfolio:write` |
175
+ | `run_emit_file_touch` | Report a file access (read\|write\|create); glows the living codebase map. | write | `portfolio:write` |
176
+ | `run_emit_usage` | Report ONE turn's raw token counts so the command center's Metered Spend tile and per-run cost cap become truthful (instead of showing $0.00). | write | `portfolio:write` |
177
+ | `run_request_reconsider` | Verifier nudges the Lead to reflect; inserts a pending reflection row. | write | `portfolio:write` |
178
+ | `run_post_reflection` | Lead posts a reflection (response to a reconsider, or self-initiated). decision ∈ continue\|revise_plan\|escalate. | write | `portfolio:write` |
179
+ | `run_resolve_violation` | Resolve a rule violation. action ∈ enforce (rollback) \| accept (weight decay) \| override (log). | write | `portfolio:write` |
180
+ | `run_set_trust_dial` | Adjust the trust dial mid-run (0–10). | write | `portfolio:write` |
181
+ | `run_pause_agent` | Pause one ensemble member by role. | write | `portfolio:write` |
182
+ | `run_resume_agent` | Resume one paused ensemble member by role. | write | `portfolio:write` |
183
+ | `run_emit_pr` | Announce that a PR is open; triggers the Act-5 closing sequence. | write | `portfolio:write` |
184
+ | `run_finish` | Final close-out: triggers reinforcement + candidate-rule extraction, sets the story to shipped. | destructive | `portfolio:write` |
185
+
186
+ ### Run monitoring (9)
187
+
188
+ The supervisor surface: what the daemon and the fleet are doing, plus the three actuators.
189
+
190
+ | Tool | What it does | Kind | PAT scope |
191
+ |---|---|---|---|
192
+ | `run_feed` | The unified run feed — daemon heartbeat, fleet (active + last-24h runs with status/health/progress), needs-you items, and reconcile chips. | read | `runs:monitor` or `portfolio:read` |
193
+ | `run_daemon_status` | Is the operator's daemon connected? | read | `runs:monitor` or `portfolio:read` |
194
+ | `run_machines` | The operator's machines with a live daemon (multi-machine runs): machine id, name, terminal kind, last-seen, and (≥0.9.10) per-machine load stats. | read | `runs:monitor` or `portfolio:read` |
195
+ | `run_activations_recent` | The last 10 activations with launch traces (spawn steps, launcher stderr/stdout/exit code), derived status incl. staleness, terminal used, and tmux pane. | read | `runs:monitor` or `portfolio:read` |
196
+ | `run_prompts_list` | Mid-run prompts (a run paused on a menu/question). | read | `runs:monitor` or `portfolio:read` |
197
+ | `run_snapshot_get` | Fetch the result of a run_snapshot_request. status "ready" carries the scrubbed pane tail; "pending" = the daemon hasn't served it yet (re-poll in ~5s, up to ~… | read | `runs:control` |
198
+ | `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` |
199
+ | `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) |
200
+ | `run_stop_daemon` | The panic button — queue a remote shutdown for the operator's daemon (CLI ≥0.9.12). | destructive | `runs:control` |
201
+ <!-- tool-table:end -->
202
+
203
+ **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.
204
+
205
+ **Resources** — `portfolio://<id>` (a portfolio graph) · `whygraph://decisions` (all decisions + edges)
206
+ **Prompts** — `wsjf_prioritize(portfolioId)` · `decision_check(plan)`
207
+
208
+ Safety rails on writes: `portfolio_update` pre-fetches the live version, backs the prior graph up to `~/.non.bot/backups/<id>-<unix-ts>.json`, validates the new graph (orphan parents, duplicate ids, unresolvable links) and writes with `expectedVersion`, so a concurrent edit on the canvas is a `409`, never a clobber.
209
+
210
+ ---
211
+
212
+ ## Environment variables
213
+
214
+ | Var | Default | Purpose |
215
+ |---|---|---|
216
+ | `NON_BOT_API_TOKEN` | — | `pat_` token (https://non.bot/settings/connections). Optional when `nonbot login` has run on this machine: the server falls back to the CLI's auth file (`~/.config/nonbot/auth.json`; `NONBOT_CONFIG_DIR` / `NONBOT_PROFILE` respected), so a committed `.mcp.json` never carries a secret. |
217
+ | `NON_BOT_API_BASE` | `https://non.bot` | Host-allowlisted: the token is only ever sent to non.bot / conrad.pm / deploy previews / localhost. |
218
+ | `NON_BOT_MCP_READONLY` | unset | `1` drops every write and action tool, leaving the read tools. |
219
+
220
+ ---
221
+
222
+ ## Architecture (for contributors)
223
+
224
+ `src/core/` is **Node-free** so the identical tool logic runs in both the local stdio process and the Cloudflare Worker (`../nonbot-mcp-remote`, which imports this package's `dist/`):
225
+
226
+ - `core/config.ts` — `ApiContext` + the SSRF host-allowlist guard
227
+ - `core/http.ts` — token-bound fetch wrapper
228
+ - `core/validate.ts` — pre-flight graph validation
229
+ - `core/server.ts` — `registerAll(server, ctx)` (the transport-agnostic heart) + `createNonbotMcpServer(ctx)`
230
+ - `core/toolsets/{portfolio,whygraph,run,monitor,ip}.ts`, `core/resources.ts`, `core/prompts.ts`
231
+ - `src/stdio.ts` — the `nonbot-mcp` bin: env/CLI-auth token + filesystem backup hook
232
+ - `scripts/gen-tool-table.mjs` — prints the tool tables above from the live definitions (`--portfolio-mcp` does the same for the legacy server)
233
+
234
+ ```bash
235
+ npm install
236
+ npm run typecheck # tsc --noEmit
237
+ npm test # vitest: tool contracts against a mocked non.bot + README/tool parity
238
+ npm run build # tsc → dist/ (comments stripped; this is what ships)
239
+ npm run gen:tools # regenerate the tool tables (paste between the markers in this README)
240
+ npm pack --dry-run # what the tarball would contain: dist/, README.md, LICENSE
241
+ ```
242
+
243
+ Publishing is tag-driven (`bash deploy-npm.sh nonbot-mcp` from the repo root pushes `nonbot-mcp-v<version>`; GitHub Actions publishes via OIDC trusted publishing). `SERVER_VERSION` in `src/core/server.ts` must match `package.json`.
@@ -0,0 +1,8 @@
1
+ export declare function resolveApiBase(raw0?: string): string;
2
+ export interface ApiContext {
3
+ apiBase: string;
4
+ getToken: () => string;
5
+ readonly: boolean;
6
+ backup?: (id: string, json: unknown) => Promise<void>;
7
+ fetchImpl?: typeof fetch;
8
+ }
@@ -0,0 +1,34 @@
1
+ const DEFAULT_API_BASE = 'https://non.bot';
2
+ export function resolveApiBase(raw0) {
3
+ let raw = (raw0 || DEFAULT_API_BASE).replace(/\/$/, '');
4
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(raw))
5
+ raw = `https://${raw}`;
6
+ let url;
7
+ try {
8
+ url = new URL(raw);
9
+ }
10
+ catch {
11
+ throw new Error(`non.bot API base is not a valid URL: ${raw}`);
12
+ }
13
+ const host = url.hostname.toLowerCase();
14
+ const isLocalhost = host === 'localhost' || host === '127.0.0.1' || host === '::1';
15
+ if (url.protocol === 'http:') {
16
+ if (!isLocalhost) {
17
+ throw new Error(`non.bot API base must use https:// — refusing to send the token in cleartext to ${host}. Plain http is only allowed for localhost.`);
18
+ }
19
+ }
20
+ else if (url.protocol !== 'https:') {
21
+ throw new Error(`non.bot API base must use https:// (got "${url.protocol}//${host}").`);
22
+ }
23
+ const allowed = host === 'non.bot' ||
24
+ host.endsWith('.non.bot') ||
25
+ host === 'conrad.pm' ||
26
+ host.endsWith('.conrad.pm') ||
27
+ host === 'conrad-pm.pages.dev' ||
28
+ host.endsWith('.conrad-pm.pages.dev') ||
29
+ isLocalhost;
30
+ if (!allowed) {
31
+ throw new Error(`non.bot API base host "${host}" is not allowlisted — the token may only be sent to non.bot, conrad.pm, conrad-pm.pages.dev (deploy previews), or localhost.`);
32
+ }
33
+ return raw;
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { ApiContext } from './config.js';
2
+ export declare function api(ctx: ApiContext, path: string, init?: RequestInit): Promise<Response>;
3
+ export declare function expectJson<T = unknown>(res: Response, where: string): Promise<T>;
@@ -0,0 +1,20 @@
1
+ export async function api(ctx, path, init = {}) {
2
+ const token = ctx.getToken();
3
+ if (!token) {
4
+ throw new Error('No non.bot API token available. For stdio, set NON_BOT_API_TOKEN — generate one at https://non.bot/settings/connections.');
5
+ }
6
+ const headers = new Headers(init.headers);
7
+ headers.set('Authorization', `Bearer ${token}`);
8
+ if (init.body && !headers.has('Content-Type')) {
9
+ headers.set('Content-Type', 'application/json');
10
+ }
11
+ const doFetch = ctx.fetchImpl ?? fetch;
12
+ return doFetch(`${ctx.apiBase}${path}`, { ...init, headers });
13
+ }
14
+ export async function expectJson(res, where) {
15
+ if (!res.ok) {
16
+ const text = await res.text().catch(() => '');
17
+ throw new Error(`${where}: HTTP ${res.status} — ${text.slice(0, 200)}`);
18
+ }
19
+ return (await res.json());
20
+ }
@@ -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 registerPrompts(server: McpServer, _ctx: ApiContext): void;
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+ export function registerPrompts(server, _ctx) {
3
+ server.registerPrompt('wsjf_prioritize', {
4
+ title: 'WSJF-prioritize a portfolio',
5
+ description: 'Rank a portfolio’s items by Weighted Shortest Job First.',
6
+ argsSchema: { portfolioId: z.string().describe('Portfolio id (from portfolio_list).') },
7
+ }, ({ portfolioId }) => ({
8
+ messages: [
9
+ {
10
+ role: 'user',
11
+ content: {
12
+ type: 'text',
13
+ text: [
14
+ `Load portfolio "${portfolioId}" with the portfolio_get tool.`,
15
+ 'For each epic and feature, estimate the four WSJF inputs on a relative 1–10 scale:',
16
+ 'User/Business Value, Time Criticality, Risk Reduction & Opportunity Enablement, and Job Size.',
17
+ 'Compute WSJF = (Value + Time Criticality + Risk/Opportunity) / Job Size.',
18
+ 'Return a table ranked by WSJF (highest first) showing each input and the score,',
19
+ 'then recommend the top 5 to pull next, one line of justification each.',
20
+ ].join(' '),
21
+ },
22
+ },
23
+ ],
24
+ }));
25
+ server.registerPrompt('decision_check', {
26
+ title: 'Check a plan against WhyGraph rules',
27
+ description: 'Evaluate a plan or decision against the team’s committed decisions and LAWs.',
28
+ argsSchema: { plan: z.string().describe('The plan, change, or decision to check.') },
29
+ }, ({ plan }) => ({
30
+ messages: [
31
+ {
32
+ role: 'user',
33
+ content: {
34
+ type: 'text',
35
+ text: [
36
+ `Call whygraph_context with task="${plan}" to load the LAWs and prior decisions that apply.`,
37
+ 'Evaluate the plan against each. Treat a conflict with a LAW (locked decision) as a blocker;',
38
+ 'treat tension with a lower-weight decision as a caution. Cite the decision id for every point.',
39
+ 'If nothing conflicts, say so explicitly and name the decisions you checked against.',
40
+ ].join(' '),
41
+ },
42
+ },
43
+ ],
44
+ }));
45
+ }
@@ -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 registerResources(server: McpServer, ctx: ApiContext): void;
@@ -0,0 +1,23 @@
1
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { api, expectJson } from './http.js';
3
+ export function registerResources(server, ctx) {
4
+ server.registerResource('portfolio', new ResourceTemplate('portfolio://{id}', { list: undefined }), {
5
+ title: 'Portfolio graph',
6
+ description: 'The full node/edge graph for one portfolio as JSON. Read portfolio://<id> (id from portfolio_list) to load it directly as context — no tool call needed.',
7
+ mimeType: 'application/json',
8
+ }, async (uri, variables) => {
9
+ const id = String(variables.id);
10
+ const res = await api(ctx, `/api/portfolio/graph?id=${encodeURIComponent(id)}`);
11
+ const data = await expectJson(res, 'portfolio resource');
12
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(data, null, 2) }] };
13
+ });
14
+ server.registerResource('whygraph-decisions', 'whygraph://decisions', {
15
+ title: 'WhyGraph decisions',
16
+ description: 'Every PM decision plus the RELATED_TO relationships between them, as JSON.',
17
+ mimeType: 'application/json',
18
+ }, async (uri) => {
19
+ const res = await api(ctx, '/api/whygraph/decisions');
20
+ const data = await expectJson(res, 'whygraph resource');
21
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(data, null, 2) }] };
22
+ });
23
+ }
@@ -0,0 +1,7 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ApiContext } from './config.js';
3
+ export declare const SERVER_NAME = "nonbot-mcp";
4
+ export declare const SERVER_VERSION = "0.1.0";
5
+ export declare const INSTRUCTIONS: string;
6
+ export declare function registerAll(server: McpServer, ctx: ApiContext): void;
7
+ export declare function createNonbotMcpServer(ctx: ApiContext): McpServer;
@@ -0,0 +1,41 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerPortfolioTools } from './toolsets/portfolio.js';
3
+ import { registerIpTools } from './toolsets/ip.js';
4
+ import { registerWhygraphTools } from './toolsets/whygraph.js';
5
+ import { registerRunTools } from './toolsets/run.js';
6
+ import { registerMonitorTools } from './toolsets/monitor.js';
7
+ import { registerResources } from './resources.js';
8
+ import { registerPrompts } from './prompts.js';
9
+ export const SERVER_NAME = 'nonbot-mcp';
10
+ export const SERVER_VERSION = '0.1.0';
11
+ export const INSTRUCTIONS = [
12
+ 'non.bot MCP — work against the caller’s real product-management data, not a copy.',
13
+ 'Two domains: PORTFOLIOS (the SAFe portfolio canvas — epics → features → stories) and',
14
+ 'WHYGRAPH (the PM decision graph — the rules and decisions the team has committed to).',
15
+ 'Read before you write: call the *_list / *_get / *_search tools first.',
16
+ 'Portfolio edits are whole-graph: portfolio_get to load the current graph + version,',
17
+ 'edit the full node/edge set, then portfolio_update (pass expectedVersion for safe',
18
+ 'concurrent editing). There are no per-node mutators by design.',
19
+ 'Before a non-trivial decision, call whygraph_context to surface the LAWs and prior',
20
+ 'decisions that apply. WhyGraph is read/reasoning-only over this transport.',
21
+ 'MONITORING: run_feed / run_daemon_status / run_machines / run_prompts_list answer',
22
+ '"what are my runs and daemon doing?"; run_snapshot_request + run_snapshot_get read a',
23
+ 'live run’s terminal output; run_prompt_answer unblocks a waiting run (supervisor',
24
+ 'tokens with the explicit runs:respond scope only).',
25
+ 'IP NODES: ip_list / ip_get read a patent or trademark disclosure; ip_update edits it (op list + expected_version);',
26
+ 'ip_evidence_add adds evidence the SERVER hashes, timestamps and numbers — never send a hash or a time.',
27
+ ].join(' ');
28
+ export function registerAll(server, ctx) {
29
+ registerPortfolioTools(server, ctx);
30
+ registerIpTools(server, ctx);
31
+ registerWhygraphTools(server, ctx);
32
+ registerRunTools(server, ctx);
33
+ registerMonitorTools(server, ctx);
34
+ registerResources(server, ctx);
35
+ registerPrompts(server, ctx);
36
+ }
37
+ export function createNonbotMcpServer(ctx) {
38
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions: INSTRUCTIONS });
39
+ registerAll(server, ctx);
40
+ return server;
41
+ }
@@ -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 registerIpTools(server: McpServer, ctx: ApiContext): void;
@@ -0,0 +1,130 @@
1
+ import { z } from 'zod';
2
+ import { api, expectJson } from '../http.js';
3
+ const READ = { readOnlyHint: true, openWorldHint: true };
4
+ const WRITE = { readOnlyHint: false, destructiveHint: false, openWorldHint: true };
5
+ const SUPPORTS = ['problem', 'mechanism', 'novelty', 'RTP', 'disclosure'];
6
+ const SECTIONS = ['whatsnew', 'spec', 'numerals', 'search'];
7
+ const KINDS = ['note', 'email', 'commit', 'field', 'doc', 'meeting', 'image'];
8
+ const textResult = (data) => ({ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] });
9
+ const q = (o) => new URLSearchParams(o).toString();
10
+ const ids = { portfolio_id: z.string().min(1).max(128).describe('Portfolio id.'), node_id: z.string().min(1).max(128).describe('The IP node id (radmod-ip-…).') };
11
+ async function getJson(ctx, path) {
12
+ return expectJson(await api(ctx, path), `GET ${path}`);
13
+ }
14
+ async function patchNode(ctx, body) {
15
+ return expectJson(await api(ctx, '/api/ip/node', { method: 'PATCH', body: JSON.stringify(body) }), 'PATCH /api/ip/node');
16
+ }
17
+ export function registerIpTools(server, ctx) {
18
+ server.registerTool('ip_list', {
19
+ title: 'List IP nodes',
20
+ description: 'Every IP node (patent or trademark disclosure) in a portfolio: id, title, track, kind, started, live source count, live evidence count, filing date. Read before ip_get.',
21
+ inputSchema: { portfolio_id: ids.portfolio_id },
22
+ annotations: READ,
23
+ }, async (a) => textResult(await getJson(ctx, `/api/ip/nodes?${q({ portfolioId: a.portfolio_id })}`)));
24
+ server.registerTool('ip_get', {
25
+ title: 'Read one IP node',
26
+ description: 'One IP node\'s full disclosure bag (units with status, value, provenance; evidence incl. tombstones; events; filing) plus the graph version to pass as expected_version, and its live sources.',
27
+ inputSchema: ids,
28
+ annotations: READ,
29
+ }, async (a) => textResult(await getJson(ctx, `/api/ip/nodes?${q({ portfolioId: a.portfolio_id, nodeId: a.node_id })}`)));
30
+ server.registerTool('ip_evidence_list', {
31
+ title: 'List IP evidence with verdicts',
32
+ description: 'Evidence rows from the saved graph, tombstones included, with a seal verdict per row (true = sealed by this server, false = not, null = no signing key) and a witness verdict per row carrying a request.',
33
+ inputSchema: ids,
34
+ annotations: READ,
35
+ }, async (a) => textResult(await getJson(ctx, `/api/ip/evidence?${q({ portfolioId: a.portfolio_id, nodeId: a.node_id })}`)));
36
+ server.registerTool('ip_evidence_hashes', {
37
+ title: 'Export the evidence hash list',
38
+ description: 'The Export-hash-list file: one `id · sha256 · added(ISO)` line per row (never changed once written) plus a `#` trailer naming removed, missing, unsealed or altered rows.',
39
+ inputSchema: ids,
40
+ annotations: READ,
41
+ }, async (a) => {
42
+ const path = `/api/ip/evidence/hashes?${q({ portfolioId: a.portfolio_id, nodeId: a.node_id })}`;
43
+ const res = await api(ctx, path);
44
+ if (!res.ok)
45
+ await expectJson(res, `GET ${path}`);
46
+ return textResult(await res.text());
47
+ });
48
+ server.registerTool('ip_packet', {
49
+ title: 'List the packet documents',
50
+ description: 'The filing packet of an IP node as the Packet step shows it, rendered by the server from the saved graph: each document\'s id, name, status (queued / ready / accepted / needs_you) and formats, plus how many evidence copies the zip would bundle. Read a document with ip_packet_document. The .docx and .zip files are assembled in the browser (Packet step) — not available here.',
51
+ inputSchema: ids,
52
+ annotations: READ,
53
+ }, async (a) => {
54
+ const p = await getJson(ctx, `/api/ip/packet?${q({ portfolioId: a.portfolio_id, nodeId: a.node_id })}`);
55
+ return textResult({
56
+ title: p.title,
57
+ documents: (p.docs ?? []).map(d => ({ id: d.id, name: d.packetName ?? d.name, status: d.status, formats: d.formats })),
58
+ bundle: { stored: p.bundle?.stored?.length ?? 0, text: p.bundle?.text?.length ?? 0, notBundled: p.bundle?.skipped?.length ?? 0 },
59
+ });
60
+ });
61
+ server.registerTool('ip_packet_document', {
62
+ title: 'Read one packet document',
63
+ description: 'One packet document\'s full text (markdown) and its text files (md / csv / txt / svg), rendered by the server from the saved graph. `document` is an id from ip_packet (disclosure, evidence, clocks, risk, inventorship, tradesecret, spec, figures, priorart, cover — or teas, ids, specimens, firstuse, owner, knockout for a trademark).',
64
+ inputSchema: { ...ids, document: z.string().min(1).max(32).regex(/^[a-z]+$/).describe('A document id from ip_packet.') },
65
+ annotations: READ,
66
+ }, async (a) => {
67
+ const p = await getJson(ctx, `/api/ip/packet?${q({ portfolioId: a.portfolio_id, nodeId: a.node_id, doc: a.document })}`);
68
+ const d = p.docs?.[0];
69
+ if (!d)
70
+ return textResult('That document does not exist on this node.');
71
+ return textResult([d.text, ...d.files.filter(f => !f.name.endsWith('.md')).map(f => `\n--- ${f.name} ---\n${f.text}`)].join('\n') + `\n\nfiles: ${d.files.map(f => f.name).join(', ')}`);
72
+ });
73
+ if (ctx.readonly)
74
+ return;
75
+ server.registerTool('ip_update', {
76
+ title: 'Edit an IP node (op list)',
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.') },
79
+ annotations: WRITE,
80
+ }, async (a) => textResult(await patchNode(ctx, { portfolioId: a.portfolio_id, nodeId: a.node_id, expectedVersion: a.expected_version, ops: a.ops })));
81
+ server.registerTool('ip_evidence_add', {
82
+ title: 'Add evidence (server-sealed)',
83
+ description: 'Add one evidence row the SERVER hashes (SHA-256 of the stored copy), timestamps, seals and numbers. `from` is one source and must match `kind`: kind note or email takes {text} (at most 2000 characters), commit takes {commit:{sha?,repo?,pr?}}, field takes {field:{itemId,field}} for a source item\'s field, doc or meeting takes {module:{moduleId}} for an attached Document or radMeeting, image takes {asset} for your own uploaded image key. Returns the new row id in evIds. Two adds with identical bytes (same content, time and author) collapse into ONE row and report the same id, so add each distinct piece once; the server accepts at most 5 evidence.add ops per request (ip_update).',
84
+ inputSchema: {
85
+ ...ids,
86
+ expected_version: z.number().int().describe('The graph version from ip_get.'),
87
+ kind: z.enum(KINDS),
88
+ from: z.record(z.unknown()).describe('Exactly one source (see description).'),
89
+ title: z.string().min(1).max(300),
90
+ describes: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('The date the evidence describes (YYYY-MM-DD).'),
91
+ supports: z.array(z.enum(SUPPORTS)).max(5).optional(),
92
+ visibility: z.enum(['private', 'team', 'nda', 'public']).optional().describe('Who could see it at the time.'),
93
+ shows: z.string().max(600).optional(),
94
+ },
95
+ annotations: WRITE,
96
+ }, async (a) => {
97
+ const op = { op: 'evidence.add', kind: a.kind, from: a.from, title: a.title };
98
+ for (const k of ['describes', 'supports', 'visibility', 'shows'])
99
+ if (a[k] !== undefined)
100
+ op[k] = a[k];
101
+ return textResult(await patchNode(ctx, { portfolioId: a.portfolio_id, nodeId: a.node_id, expectedVersion: a.expected_version, ops: [op] }));
102
+ });
103
+ server.registerTool('ip_witness_candidates', {
104
+ title: 'List possible witnesses',
105
+ description: 'Members who may witness evidence on this node (id + name only): editors of the portfolio other than the caller, never someone listed as an inventor in field 5. Needs a write token (it is a step of asking for a witness), so it is absent in read-only mode. Use an id as `for` in an evidence.witness op.',
106
+ inputSchema: ids,
107
+ annotations: WRITE,
108
+ }, async (a) => textResult(await getJson(ctx, `/api/ip/witness-candidates?${q({ portfolioId: a.portfolio_id, nodeId: a.node_id })}`)));
109
+ server.registerTool('ip_build', {
110
+ title: 'Build the draft (one model pass)',
111
+ description: 'Runs `Build draft →` for a PATENT IP node on YOUR OWN model key (the token owner\'s Anthropic or Gemini key — non.bot never pays for it; no key = HTTP 402 no_key). Each call spends YOUR OWN model usage (draft_mode best = the larger, costlier model): one model pass that writes the What\'s-new elements, the specification paragraphs (each citing the disclosure / code / evidence it rests on, or shown unverified), the reference numerals and prior-art SEARCH SEEDS, then saves them to the node with OCC on expected_version (HTTP 409 version_conflict with currentVersion → re-read with ip_get). Every ★ disclosure field must be confirmed first (HTTP 400 gate). A trademark packet needs no model (HTTP 400 trademark_no_model). `sections` limits the pass (default all four). During a live collaborative session the saved draft can be overwritten by that session — prefer running when no one has the canvas open.',
112
+ inputSchema: {
113
+ ...ids,
114
+ expected_version: z.number().int().describe('The graph version from ip_get.'),
115
+ sections: z.array(z.enum(SECTIONS)).min(1).max(4).optional(),
116
+ steer: z.string().max(300).optional().describe('A short steer, e.g. "shorter background".'),
117
+ draft_mode: z.enum(['fast', 'best']).optional(),
118
+ },
119
+ annotations: WRITE,
120
+ }, async (a) => {
121
+ const body = { portfolioId: a.portfolio_id, nodeId: a.node_id, persist: true, expectedVersion: a.expected_version };
122
+ if (a.sections)
123
+ body.sections = a.sections;
124
+ if (a.steer)
125
+ body.steer = a.steer;
126
+ if (a.draft_mode)
127
+ body.draftMode = a.draft_mode;
128
+ return textResult(await expectJson(await api(ctx, '/api/ip/build', { method: 'POST', body: JSON.stringify(body) }), 'POST /api/ip/build'));
129
+ });
130
+ }
@@ -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 registerMonitorTools(server: McpServer, ctx: ApiContext): void;