@nonbot/mcp 0.0.0-stage → 0.2.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 +43 -0
- package/README.md +249 -2
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.js +34 -0
- package/dist/core/http.d.ts +3 -0
- package/dist/core/http.js +20 -0
- package/dist/core/ipShape.d.ts +7 -0
- package/dist/core/ipShape.js +45 -0
- package/dist/core/layout.d.ts +38 -0
- package/dist/core/layout.js +193 -0
- package/dist/core/prompts.d.ts +3 -0
- package/dist/core/prompts.js +45 -0
- package/dist/core/resources.d.ts +3 -0
- package/dist/core/resources.js +23 -0
- package/dist/core/server.d.ts +7 -0
- package/dist/core/server.js +46 -0
- package/dist/core/toolsets/build.d.ts +105 -0
- package/dist/core/toolsets/build.js +367 -0
- package/dist/core/toolsets/ip.d.ts +3 -0
- package/dist/core/toolsets/ip.js +130 -0
- package/dist/core/toolsets/monitor.d.ts +3 -0
- package/dist/core/toolsets/monitor.js +160 -0
- package/dist/core/toolsets/portfolio.d.ts +3 -0
- package/dist/core/toolsets/portfolio.js +521 -0
- package/dist/core/toolsets/run.d.ts +3 -0
- package/dist/core/toolsets/run.js +202 -0
- package/dist/core/toolsets/whygraph.d.ts +3 -0
- package/dist/core/toolsets/whygraph.js +346 -0
- package/dist/core/validate.d.ts +21 -0
- package/dist/core/validate.js +45 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +72 -0
- package/package.json +65 -4
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,250 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nonbot/mcp
|
|
2
2
|
|
|
3
|
-
|
|
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 (24)
|
|
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
|
+
| `portfolio_create` | Create a new, empty portfolio owned by the caller and return its id + version. | write | `portfolio:write` |
|
|
128
|
+
| `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
|
+
| `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` |
|
|
130
|
+
| `portfolio_layout` | 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… | write | `portfolio:write` |
|
|
131
|
+
| `portfolio_set_meta` | Graph-level settings that travel with the portfolio (the `__graph_meta__` record): `context_text` (the portfolio's context notes — what a newcomer reads first)… | write | `portfolio:write` |
|
|
132
|
+
| `portfolio_asset_upload` | Store one image (PNG / JPEG / WebP / GIF, ≤ 2.2 MB — downscale first; the app keeps images at 1024 px JPEG) in your private asset store and get back its URL, f… | write | `portfolio:write` |
|
|
133
|
+
|
|
134
|
+
### IP node (11)
|
|
135
|
+
|
|
136
|
+
Patent and trademark disclosures on the canvas: read the bag, edit it as an op list, add server-sealed evidence, export the hash list.
|
|
137
|
+
|
|
138
|
+
| Tool | What it does | Kind | PAT scope |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| `ip_create` | Add an IP node — a patent or trademark disclosure — to a portfolio, wired to its source items (the cards the disclosure is prepared FROM: their title, descript… | write | `portfolio:write` |
|
|
141
|
+
| `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` |
|
|
142
|
+
| `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` |
|
|
143
|
+
| `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` |
|
|
144
|
+
| `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` |
|
|
145
|
+
| `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` |
|
|
146
|
+
| `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` |
|
|
147
|
+
| `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` |
|
|
148
|
+
| `ip_evidence_add` | Add one evidence row the SERVER hashes (SHA-256 of the stored copy), timestamps, seals and numbers. | write | `portfolio:write` |
|
|
149
|
+
| `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` |
|
|
150
|
+
| `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` |
|
|
151
|
+
|
|
152
|
+
### WhyGraph (7)
|
|
153
|
+
|
|
154
|
+
The PM decision graph. Read and reasoning only over token auth; writes stay cookie-only on the server.
|
|
155
|
+
|
|
156
|
+
| Tool | What it does | Kind | PAT scope |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `whygraph_search` | Search the PM decision graph by keyword, ranked by weight (how reinforced a decision is) then relevance. | read | `portfolio:read` |
|
|
159
|
+
| `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` |
|
|
160
|
+
| `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` |
|
|
161
|
+
| `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` |
|
|
162
|
+
| `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` |
|
|
163
|
+
| `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` |
|
|
164
|
+
| `whygraph_recent_violations` | The most recent times decisions were violated — what broke, when, and which decision. | read | `portfolio:read` |
|
|
165
|
+
|
|
166
|
+
### Run agent protocol (20)
|
|
167
|
+
|
|
168
|
+
Called by a launched Run agent (telemetry, scope, reflections, close-out), not by a person.
|
|
169
|
+
|
|
170
|
+
| Tool | What it does | Kind | PAT scope |
|
|
171
|
+
|---|---|---|---|
|
|
172
|
+
| `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` |
|
|
173
|
+
| `run_load_activation` | Load the full context for a run: the story brief, acceptance criteria, applicable WhyGraph rules, and ensemble defaults. | write | `portfolio:write` |
|
|
174
|
+
| `run_register_agent` | Register an ensemble member for this activation (creates the run on the first agent). | write | `portfolio:write` |
|
|
175
|
+
| `run_declare_scope` | Lead declares the file allowlist + intent. | write | `portfolio:write` |
|
|
176
|
+
| `run_request_scope_change` | Request expansion of the file allowlist. | write | `portfolio:write` |
|
|
177
|
+
| `run_emit_diff` | Report a code diff. Triggers a WhyGraph rule-check + scope guard; returns { violations, reinforcements, blocked_paths }. | write | `portfolio:write` |
|
|
178
|
+
| `run_emit_test_run` | Report a test run; updates the dashboard sparkline. | write | `portfolio:write` |
|
|
179
|
+
| `run_emit_lint` | Report lint hits; updates the dashboard. | write | `portfolio:write` |
|
|
180
|
+
| `run_emit_typecheck` | Report type-check errors; updates the dashboard. | write | `portfolio:write` |
|
|
181
|
+
| `run_emit_build` | Report build status; updates the dashboard. | write | `portfolio:write` |
|
|
182
|
+
| `run_emit_file_touch` | Report a file access (read\|write\|create); glows the living codebase map. | write | `portfolio:write` |
|
|
183
|
+
| `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` |
|
|
184
|
+
| `run_request_reconsider` | Verifier nudges the Lead to reflect; inserts a pending reflection row. | write | `portfolio:write` |
|
|
185
|
+
| `run_post_reflection` | Lead posts a reflection (response to a reconsider, or self-initiated). decision ∈ continue\|revise_plan\|escalate. | write | `portfolio:write` |
|
|
186
|
+
| `run_resolve_violation` | Resolve a rule violation. action ∈ enforce (rollback) \| accept (weight decay) \| override (log). | write | `portfolio:write` |
|
|
187
|
+
| `run_set_trust_dial` | Adjust the trust dial mid-run (0–10). | write | `portfolio:write` |
|
|
188
|
+
| `run_pause_agent` | Pause one ensemble member by role. | write | `portfolio:write` |
|
|
189
|
+
| `run_resume_agent` | Resume one paused ensemble member by role. | write | `portfolio:write` |
|
|
190
|
+
| `run_emit_pr` | Announce that a PR is open; triggers the Act-5 closing sequence. | write | `portfolio:write` |
|
|
191
|
+
| `run_finish` | Final close-out: triggers reinforcement + candidate-rule extraction, sets the story to shipped. | destructive | `portfolio:write` |
|
|
192
|
+
|
|
193
|
+
### Run monitoring (9)
|
|
194
|
+
|
|
195
|
+
The supervisor surface: what the daemon and the fleet are doing, plus the three actuators.
|
|
196
|
+
|
|
197
|
+
| Tool | What it does | Kind | PAT scope |
|
|
198
|
+
|---|---|---|---|
|
|
199
|
+
| `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` |
|
|
200
|
+
| `run_daemon_status` | Is the operator's daemon connected? | read | `runs:monitor` or `portfolio:read` |
|
|
201
|
+
| `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` |
|
|
202
|
+
| `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` |
|
|
203
|
+
| `run_prompts_list` | Mid-run prompts (a run paused on a menu/question). | read | `runs:monitor` or `portfolio:read` |
|
|
204
|
+
| `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` |
|
|
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
|
+
| `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
|
+
| `run_stop_daemon` | The panic button — queue a remote shutdown for the operator's daemon (CLI ≥0.9.12). | destructive | `runs:control` |
|
|
208
|
+
<!-- tool-table:end -->
|
|
209
|
+
|
|
210
|
+
**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.
|
|
211
|
+
|
|
212
|
+
**Resources** — `portfolio://<id>` (a portfolio graph) · `whygraph://decisions` (all decisions + edges)
|
|
213
|
+
**Prompts** — `wsjf_prioritize(portfolioId)` · `decision_check(plan)`
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Environment variables
|
|
220
|
+
|
|
221
|
+
| Var | Default | Purpose |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `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. |
|
|
224
|
+
| `NON_BOT_API_BASE` | `https://non.bot` | Host-allowlisted: the token is only ever sent to non.bot / conrad.pm / deploy previews / localhost. |
|
|
225
|
+
| `NON_BOT_MCP_READONLY` | unset | `1` drops every write and action tool, leaving the read tools. |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Architecture (for contributors)
|
|
230
|
+
|
|
231
|
+
`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/`):
|
|
232
|
+
|
|
233
|
+
- `core/config.ts` — `ApiContext` + the SSRF host-allowlist guard
|
|
234
|
+
- `core/http.ts` — token-bound fetch wrapper
|
|
235
|
+
- `core/validate.ts` — pre-flight graph validation
|
|
236
|
+
- `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`
|
|
238
|
+
- `src/stdio.ts` — the `nonbot-mcp` bin: env/CLI-auth token + filesystem backup hook
|
|
239
|
+
- `scripts/gen-tool-table.mjs` — prints the tool tables above from the live definitions (`--portfolio-mcp` does the same for the legacy server)
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npm install
|
|
243
|
+
npm run typecheck # tsc --noEmit
|
|
244
|
+
npm test # vitest: tool contracts against a mocked non.bot + README/tool parity
|
|
245
|
+
npm run build # tsc → dist/ (comments stripped; this is what ships)
|
|
246
|
+
npm run gen:tools # regenerate the tool tables (paste between the markers in this README)
|
|
247
|
+
npm pack --dry-run # what the tarball would contain: dist/, README.md, LICENSE
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
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,9 @@
|
|
|
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
|
+
readLocalImage?: (path: string) => Promise<string>;
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
}
|
|
@@ -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,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,7 @@
|
|
|
1
|
+
export type IpTrack = 'patent' | 'trademark';
|
|
2
|
+
export declare const IP_KINDS: Record<IpTrack, string[]>;
|
|
3
|
+
export declare const IP_NODE_SIZE: {
|
|
4
|
+
readonly width: 340;
|
|
5
|
+
readonly height: 184;
|
|
6
|
+
};
|
|
7
|
+
export declare function emptyIp(track: IpTrack, kind?: string): Record<string, unknown>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const UNIT_SHAPES = {
|
|
2
|
+
patent: [
|
|
3
|
+
{ id: 'u1', editor: 'text' }, { id: 'u2', editor: 'textarea' }, { id: 'u3', editor: 'steps' }, { id: 'u4', editor: 'proposals' },
|
|
4
|
+
{ id: 'u5', editor: 'people' }, { id: 'u6', editor: 'where' }, { id: 'u7', editor: 'events' }, { id: 'u8', editor: 'date' },
|
|
5
|
+
{ id: 'u9', editor: 'list' }, { id: 'u10', editor: 'figures' }, { id: 'u11', editor: 'owner' }, { id: 'u12', editor: 'text' },
|
|
6
|
+
],
|
|
7
|
+
trademark: [
|
|
8
|
+
{ id: 't1', editor: 'tm-mark' }, { id: 't2', editor: 'tm-type' }, { id: 't3', editor: 'tm-owner' }, { id: 't4', editor: 'tm-classes' },
|
|
9
|
+
{ id: 't5', editor: 'tm-basis' }, { id: 't6', editor: 'tm-specimen' }, { id: 't7', editor: 'tm-signatory' }, { id: 't8', editor: 'tm-search' },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
export const IP_KINDS = { patent: ['provisional', 'nonprovisional', 'design'], trademark: ['application'] };
|
|
13
|
+
export const IP_NODE_SIZE = { width: 340, height: 184 };
|
|
14
|
+
const TM_EMPTY = {
|
|
15
|
+
'tm-mark': { k: 'tm', mark: '' },
|
|
16
|
+
'tm-type': { k: 'tm', type: 'standard', colour: false },
|
|
17
|
+
'tm-owner': { k: 'tm', rows: [] },
|
|
18
|
+
'tm-classes': { k: 'tm', answers: { downloadable: null, hosted: null, broker: null }, wording: {} },
|
|
19
|
+
'tm-basis': { k: 'tm', perClass: {} },
|
|
20
|
+
'tm-specimen': { k: 'tm', perClass: {} },
|
|
21
|
+
'tm-signatory': { k: 'tm', name: '', email: '' },
|
|
22
|
+
'tm-search': { k: 'tm', rows: [] },
|
|
23
|
+
};
|
|
24
|
+
function emptyValue(editor) {
|
|
25
|
+
switch (editor) {
|
|
26
|
+
case 'text':
|
|
27
|
+
case 'textarea': return { k: 'text', t: '' };
|
|
28
|
+
case 'steps': return { k: 'steps', rows: [] };
|
|
29
|
+
case 'proposals': return { k: 'proposals', cards: [] };
|
|
30
|
+
case 'people': return { k: 'people', rows: [] };
|
|
31
|
+
case 'where': return { k: 'where', side: null, files: [] };
|
|
32
|
+
case 'events': return { k: 'events', none: false };
|
|
33
|
+
case 'date': return { k: 'date', date: null, evIds: [] };
|
|
34
|
+
case 'list': return { k: 'list', rows: [] };
|
|
35
|
+
case 'figures': return { k: 'figures', rows: [] };
|
|
36
|
+
case 'owner': return { k: 'owner', entity: '', signed: {} };
|
|
37
|
+
default: return structuredClone(TM_EMPTY[editor] ?? { k: 'tm' });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function emptyIp(track, kind) {
|
|
41
|
+
const units = {};
|
|
42
|
+
for (const s of UNIT_SHAPES[track])
|
|
43
|
+
units[s.id] = { id: s.id, status: 'empty', rev: 0, value: emptyValue(s.editor), provenance: null, history: [] };
|
|
44
|
+
return { v: 1, track, kind: kind ?? (track === 'patent' ? 'provisional' : 'application'), title: '', blurb: '', started: false, sourceTicks: {}, units, evidence: [], events: [], filing: null, prefill: null };
|
|
45
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const PX_PER_WEEK = 120;
|
|
2
|
+
export declare const LEVELS: readonly ["objective", "key-result", "epic", "feature", "story", "enabler", "milestone"];
|
|
3
|
+
export type Level = (typeof LEVELS)[number];
|
|
4
|
+
export interface LayoutNode {
|
|
5
|
+
id: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
position?: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
};
|
|
11
|
+
data?: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
export interface LayoutOptions {
|
|
14
|
+
now?: Date;
|
|
15
|
+
heights?: Record<string, number>;
|
|
16
|
+
}
|
|
17
|
+
export interface LayoutResult {
|
|
18
|
+
positions: Map<string, {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}>;
|
|
22
|
+
widthOverride: Map<string, number | null>;
|
|
23
|
+
}
|
|
24
|
+
export declare function calendarStart(now: Date): Date;
|
|
25
|
+
export declare const isoDate: (d: Date) => string;
|
|
26
|
+
export declare function columnOf(date: Date, now: Date): number;
|
|
27
|
+
export declare function dateAtColumn(col: number, now: Date): Date;
|
|
28
|
+
export declare function todayColumn(now: Date): number;
|
|
29
|
+
export declare function sprintAtColumn(col: number): {
|
|
30
|
+
pi: string;
|
|
31
|
+
sprint: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function scheduleFields(col: number, weeks: number, now: Date): Record<string, unknown>;
|
|
34
|
+
export declare const levelOf: (n: LayoutNode) => Level;
|
|
35
|
+
export declare const isItem: (n: LayoutNode) => boolean;
|
|
36
|
+
export declare const isIpNode: (n: LayoutNode) => boolean;
|
|
37
|
+
export declare function layoutTimeline(nodes: LayoutNode[], opts?: LayoutOptions): LayoutResult;
|
|
38
|
+
export declare function applyLayout<T extends LayoutNode>(nodes: T[], res: LayoutResult): T[];
|