@nullplatform/mcp 0.1.1 → 0.1.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 +52 -217
- package/dist/i18n.js +2 -0
- package/dist/np/context.js +39 -28
- package/dist/np/journey.js +15 -6
- package/dist/surfaces/developer.js +3 -1
- package/dist/tools/builds.js +11 -4
- package/dist/tools/create-release.js +1 -1
- package/dist/tools/deploy.js +1 -1
- package/dist/tools/find-apps.js +1 -1
- package/dist/tools/logs.js +1 -1
- package/dist/tools/params.js +1 -1
- package/dist/tools/playbook.js +2 -2
- package/dist/tools/status.js +2 -2
- package/dist/tools/traffic.js +2 -2
- package/package.json +1 -1
- package/skills/configuring-safely/SKILL.md +50 -0
- package/skills/deploying-safely/SKILL.md +6 -3
- package/skills/incident-response/SKILL.md +4 -1
- package/skills/managing-environments/SKILL.md +53 -0
- package/skills/platform-conventions/SKILL.md +19 -9
- package/skills/promoting-across-environments/SKILL.md +51 -0
- package/skills/tracing-a-change/SKILL.md +30 -0
- package/skills/understand-a-service/SKILL.md +50 -0
- package/skills/working-from-a-ticket/SKILL.md +47 -0
- package/widgets-dist/create-app.html +2 -2
- package/widgets-dist/find-apps.html +26 -26
- package/widgets-dist/logs.html +2 -2
- package/widgets-dist/manifest.json +6 -6
- package/widgets-dist/metrics.html +2 -2
- package/widgets-dist/np-panel.html +2 -2
- package/widgets-dist/params.html +11 -11
package/README.md
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<h2 align="center">
|
|
2
|
+
<a href="https://nullplatform.com" target="blank_">
|
|
3
|
+
<img height="100" alt="nullplatform" src="https://nullplatform.com/favicon/android-chrome-192x192.png" />
|
|
4
|
+
</a>
|
|
5
|
+
<br>
|
|
6
|
+
<br>
|
|
7
|
+
nullplatform MCP
|
|
8
|
+
<br>
|
|
9
|
+
</h2>
|
|
10
|
+
|
|
11
|
+
Turn your code assistant (Claude Code, Cursor, Claude Desktop, …) into the **frontend for nullplatform**.
|
|
12
|
+
Deploy, move traffic, roll back, read logs and config — from the place you already work, aware of the
|
|
13
|
+
repo you're in. Most actions take **zero arguments** inside a linked repo.
|
|
6
14
|
|
|
7
15
|
```text
|
|
8
|
-
you
|
|
9
|
-
claude ›
|
|
16
|
+
you › deploy this
|
|
17
|
+
claude › Deploying #4312 on dev — waiting for instances
|
|
10
18
|
Created release 1.4.2 from build #991 (main @ab12cd34)
|
|
11
|
-
→ Next: traffic
|
|
19
|
+
→ Next: move traffic to 25%
|
|
12
20
|
```
|
|
13
21
|
|
|
14
|
-
##
|
|
22
|
+
## What it does
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
- **Deploy & release** — ship the latest build, cut the release, walk traffic (canary → 100%), finalize or roll back.
|
|
25
|
+
- **Observe** — recent builds, logs, and golden-signal metrics per scope.
|
|
26
|
+
- **Configure** — environment variables & file parameters (secret values masked).
|
|
27
|
+
- **"Is anything broken?"** — an org-wide digest across all your apps, no app name needed.
|
|
28
|
+
- **Interactive** — on hosts that render MCP Apps (claude.ai, ChatGPT) you get live panels, a traffic slider, and log/metric views; terminals get clean markdown.
|
|
19
29
|
|
|
20
|
-
##
|
|
30
|
+
## Installation
|
|
21
31
|
|
|
22
|
-
You need a nullplatform API key.
|
|
32
|
+
You need a nullplatform API key (create one in the dashboard, or run `np login`). It runs via `npx` — no install, no clone.
|
|
23
33
|
|
|
24
34
|
**Claude Code**
|
|
25
35
|
|
|
@@ -27,7 +37,7 @@ You need a nullplatform API key. The server runs locally via `npx` — no instal
|
|
|
27
37
|
claude mcp add nullplatform -e NP_API_KEY=<your-key> -- npx -y @nullplatform/mcp
|
|
28
38
|
```
|
|
29
39
|
|
|
30
|
-
**Claude Desktop** — Settings → Developer → Edit Config
|
|
40
|
+
**Claude Desktop** — Settings → Developer → Edit Config, then fully restart:
|
|
31
41
|
|
|
32
42
|
```json
|
|
33
43
|
{
|
|
@@ -41,212 +51,37 @@ claude mcp add nullplatform -e NP_API_KEY=<your-key> -- npx -y @nullplatform/mcp
|
|
|
41
51
|
}
|
|
42
52
|
```
|
|
43
53
|
|
|
44
|
-
**Cursor / Windsurf / others** — same shape in their `mcp.json` (`command: "npx"`, `args: ["-y", "@nullplatform/mcp"]`).
|
|
54
|
+
**Cursor / Windsurf / others** — same shape in their `mcp.json` (`command: "npx"`, `args: ["-y", "@nullplatform/mcp"]`, `env.NP_API_KEY`).
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
The server holds **no credential** (it refuses to start if `NP_API_KEY`/`NP_BEARER` are in its
|
|
48
|
-
environment). Every caller authenticates each request with their *own* key, so nullplatform's
|
|
49
|
-
RBAC applies to each user individually — you can only see and do what your platform user can:
|
|
56
|
+
## Usage
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
Open your assistant in a repo linked to a nullplatform application and just ask:
|
|
59
|
+
|
|
60
|
+
- "what's the status?" · "is anything broken?"
|
|
61
|
+
- "deploy this to dev"
|
|
62
|
+
- "move traffic to 50%" · "finalize" · "roll me back"
|
|
63
|
+
- "show logs" · "show metrics"
|
|
64
|
+
- "set `DATABASE_URL` as a secret"
|
|
65
|
+
|
|
66
|
+
Most tools infer the application from your git remote, so you rarely pass arguments. There are also three
|
|
67
|
+
slash-command prompts: **/ship**, **/setup**, **/rollback**.
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
rejected unless allow-listed (DNS-rebinding guard), there's a 1 MiB body cap, and a generous
|
|
60
|
-
per-IP rate limit (default 600 req/min, tunable) so a key-rotating caller can't amplify load
|
|
61
|
-
against the platform's `/token`.
|
|
62
|
-
|
|
63
|
-
**Deploy it behind a TLS-terminating reverse proxy on a trusted network** — every request
|
|
64
|
-
carries a bearer credential, so plain HTTP must never be exposed. The proxy should also enforce
|
|
65
|
-
its own per-IP rate limits and connection caps; the in-process limiter is a backstop, not a
|
|
66
|
-
replacement. `X-Forwarded-For` is only trusted when `NP_TRUST_PROXY` is set.
|
|
67
|
-
|
|
68
|
-
The api_key → access-token **exchange is the trust boundary** and is treated like one. The
|
|
69
|
-
customer's key is **never retained**: it arrives with each request, exists only in that
|
|
70
|
-
request's async scope, and is used at most once per expiry window to (re)exchange — what's
|
|
71
|
-
cached per user (keyed by the key's SHA-256, never the key) is only the platform-issued
|
|
72
|
-
short-lived access token. Every credential is verified with the platform *before* it reaches
|
|
73
|
-
any tool (invalid ones stop at the edge as `401 invalid_token`), verification and exchange are
|
|
74
|
-
single-flighted per credential across concurrent requests, rejected keys are negative-cached
|
|
75
|
-
for a minute (they can't hammer the platform's `/token` or evict verified users), and secrets
|
|
76
|
-
never appear in logs or error bodies.
|
|
77
|
-
|
|
78
|
-
| Env | Meaning |
|
|
79
|
-
| --- | --- |
|
|
80
|
-
| `NP_API_KEY` | **stdio only** — your API key (exchanged for a bearer automatically) |
|
|
81
|
-
| `NP_BEARER` | **stdio only** — a pre-issued token instead (expires; for quick tests) |
|
|
82
|
-
| `NP_API_BASE` | Override the API host (default `https://api.nullplatform.com`) |
|
|
83
|
-
| `NP_BFF_BASE` | Override the dashboard BFF host (metrics) |
|
|
84
|
-
| `NP_ALLOWED_ORIGINS` | `--http` only: comma-separated browser origins allowed to call (server-to-server MCP clients send no Origin and always pass) |
|
|
85
|
-
| `NP_RATE_LIMIT_RPM` | `--http` only: per-IP requests/minute (default `600`; `0` disables — rely on the fronting proxy then) |
|
|
86
|
-
| `NP_TRUST_PROXY` | `--http` only: set to `1`/`true` to key the rate limit off `X-Forwarded-For` (only behind a proxy that sets it) |
|
|
87
|
-
| `NP_LANG` | Fallback answer language (`en`/`es`). The real driver is the **conversation**: every tool takes a `language` argument the assistant sets to the language the user is speaking, which wins over `Accept-Language` (HTTP) and `NP_LANG`/`LANG` (stdio). |
|
|
88
|
-
|
|
89
|
-
## The tools (15)
|
|
90
|
-
|
|
91
|
-
Every answer is scannable markdown with one **→ Next:** hint, plus structured JSON for the model.
|
|
92
|
-
Write tools are **convergent under retries** — an agent that re-issues a call after a timeout
|
|
93
|
-
converges instead of duplicating (see "Built for agents, not browsers" below).
|
|
94
|
-
|
|
95
|
-
| Tool | What it does |
|
|
96
|
-
| --- | --- |
|
|
97
|
-
| `application_get` | **Start here.** Scopes × what's live (release + traffic), latest build/release, next action. Zero-arg in a linked repo; `deployment:<id>` watches one rollout. |
|
|
98
|
-
| `organization_get` | Org-wide digest with no web equivalent: what's mid-rollout and what last failed, across all apps. Answers "is anything broken?" without naming an app. |
|
|
99
|
-
| `application_list` | Org-wide app search (parallel + cached). |
|
|
100
|
-
| `application_build_list` | Recent CI builds — status, branch, commit, age, whether released. Closes the push→CI→deploy gap; `build:<id>` shows its assets. |
|
|
101
|
-
| `application_log_list` | Recent log lines for the app/scope. |
|
|
102
|
-
| `application_parameter_list` | List configuration parameters (secrets masked). |
|
|
103
|
-
| `application_metric_list` | Golden signals per scope — throughput, response time, error rate, CPU, memory — with sparkline trends (1h/3h/24h/7d). |
|
|
104
|
-
| `application_deployment_create` | Ship: picks the latest successful build, cuts the release for you (semver bump), targets the only scope — all overridable. Reuses an in-flight rollout on retry. |
|
|
105
|
-
| `application_deployment_update` | Move traffic (snaps to 1/5/10/25/50/75/90/95/99/100), `finalize`, or `rollback`. Finds the active rollout itself. |
|
|
106
|
-
| `application_release_create` | Cut a release from a build explicitly (reuses an existing one for the same build). |
|
|
107
|
-
| `application_parameter_create` | Create/update env vars & file params (mark `secret`) — upserts, never duplicates. Apply on next deploy. |
|
|
108
|
-
| `application_create` | Link the current repo as a new application (name/URL inferred from the git remote). Returns the existing app if already linked. |
|
|
109
|
-
| `application_scope_create` | Create a deploy target (lists the org's scope types when ambiguous). Returns the existing scope if the name is taken. |
|
|
110
|
-
| `application_approval_list` | List the approvals gating an app (e.g. a deploy stuck on a policy) and `approve`/`cancel` one — using your own permissions, so the platform denies what you can't do. |
|
|
111
|
-
| `application_service_list` | List an app's dependency services (DBs, queues…) and the provisionable catalog; deep-links into the dashboard to create. |
|
|
112
|
-
|
|
113
|
-
Plus three slash-command prompts: **/ship** (deploy + walk traffic to 100% with health checks),
|
|
114
|
-
**/setup** (link this repo), **/rollback** (get me out, now).
|
|
115
|
-
|
|
116
|
-
## Built for agents, not browsers
|
|
117
|
-
|
|
118
|
-
MCP usage differs from web usage, and the tools are shaped for it:
|
|
119
|
-
|
|
120
|
-
- **Convergent writes.** Agents retry on timeout; a web user doesn't double-submit a form. So
|
|
121
|
-
every write reconciles against current state instead of blindly POSTing: `application_parameter_create` reuses
|
|
122
|
-
an existing parameter definition, `application_release_create` reuses a release already cut from the same
|
|
123
|
-
build, `application_create`/`application_scope_create` return the existing entity, and `application_deployment_create` returns the
|
|
124
|
-
in-flight rollout for the same release. Re-running a tool is safe.
|
|
125
|
-
- **Org-wide reads.** `organization_get` answers cross-application questions ("what's broken?") that the
|
|
126
|
-
dashboard only answers one entity-page at a time.
|
|
127
|
-
- **Approvals as a loop, not a notification.** When a deploy blocks on a policy, `application_approval_list`
|
|
128
|
-
lets the agent see and (if permitted) clear the gate rather than dead-ending.
|
|
129
|
-
- **Honest async + permissions.** Long operations say so and hand back an id to re-query;
|
|
130
|
-
`401`/`403` surface in plain language because every call carries the caller's own token.
|
|
131
|
-
|
|
132
|
-
## Interactive UI (MCP Apps)
|
|
133
|
-
|
|
134
|
-
On hosts that render MCP Apps (claude.ai web/desktop, ChatGPT), the journey is interactive —
|
|
135
|
-
text-only hosts (terminals) keep the markdown answers automatically:
|
|
136
|
-
|
|
137
|
-
| Widget | Bound to | What you can do in it |
|
|
138
|
-
| --- | --- | --- |
|
|
139
|
-
| **Application panel** | `application_get`, `application_deployment_create`, `application_deployment_update` | Scope cards with live release/traffic/domain, release chips with one-click **ship**, **Deploy latest**, create-scope when empty — and it morphs into the live rollout: traffic slider (snapped marks), **Finalize**/**Rollback** with confirm, deployment log, self-refreshing. |
|
|
140
|
-
| **Create application** | `application_create` | Name + repo + namespace form (opens automatically when no git remote is inferable), creates and reports provisioning. |
|
|
141
|
-
| **Parameters** | `application_parameter_list` | Editable table — add env vars/files, mark secrets, save via `application_parameter_create`. |
|
|
142
|
-
| **Logs** | `application_log_list` | Terminal pane with filter, refresh and auto-tail. |
|
|
143
|
-
| **Metrics** | `application_metric_list` | Golden-signal cards with live canvas charts, window selector (1h/3h/24h/7d), auto-refresh. |
|
|
144
|
-
| **Applications** | `application_list` | Filterable picker — click an app to open its panel. |
|
|
145
|
-
|
|
146
|
-
Widgets are single self-contained HTML files (~330KB: Preact-compat runtime + the ext-apps
|
|
147
|
-
SDK, whose embedded zod accounts for most of it) built inline with esbuild — the sandbox
|
|
148
|
-
blocks CDN fetches. They speak MCP Apps protocol `2026-01-26`. They **adopt the host's
|
|
149
|
-
design tokens** (`hostContext.styles`: colors, border radii, fonts) so the UI looks native —
|
|
150
|
-
Claude-styled in Claude, ChatGPT-styled in ChatGPT — with dark/light handled live and our own
|
|
151
|
-
palette only as the fallback for hosts that don't send tokens.
|
|
152
|
-
|
|
153
|
-
## Skills ship with the server
|
|
154
|
-
|
|
155
|
-
The connector also ships **operating playbooks** — the methodology travels with the tools:
|
|
156
|
-
|
|
157
|
-
| Playbook | Teaches |
|
|
158
|
-
| --- | --- |
|
|
159
|
-
| `deploying-safely` | Pre-flight checks, canary traffic steps gated on metrics, finalize/rollback criteria |
|
|
160
|
-
| `incident-response` | Mitigate-first triage: rollback, then read logs/metrics/params for the cause |
|
|
161
|
-
| `platform-conventions` | Entity chain semantics, dimensions, versioning, parameters, traffic lifecycle, known gotchas |
|
|
162
|
-
|
|
163
|
-
They're plain markdown under `skills/` — platform teams change agent behavior by editing
|
|
164
|
-
text, no server redeploy. The model reads them through the **`playbook_get` tool**: a tool is
|
|
165
|
-
the one MCP primitive every coding assistant (Claude Code, Cursor, Claude Desktop, …) exposes
|
|
166
|
-
to the model, so this works everywhere and on demand. The server instructions carry the
|
|
167
|
-
catalog (name + when-to-use) so the model knows which to read before which task; they're also
|
|
168
|
-
listed as passive `playbook://nullplatform/<name>` resources. MCP has no "skills" primitive —
|
|
169
|
-
standard Agent Skills load client-side — and the earlier `skill://` scheme made Claude Desktop
|
|
170
|
-
route to its native Agent Skills executor ("Unknown skill") instead, which is why this is a
|
|
171
|
-
tool.
|
|
172
|
-
|
|
173
|
-
## How it knows your app
|
|
174
|
-
|
|
175
|
-
1. Your client's MCP **roots** (workspace folders) → `git remote get-url origin`
|
|
176
|
-
2. Fallback: the server's working directory
|
|
177
|
-
3. The remote URL is matched against applications' `repository_url` (ssh/https equivalent), then by repo name
|
|
178
|
-
4. No match? Tools say exactly that and offer `application_create` / `application_list`
|
|
179
|
-
|
|
180
|
-
## Design principles
|
|
181
|
-
|
|
182
|
-
- **Journey-shaped, not endpoint-shaped** — one tool per developer intent; the API chain
|
|
183
|
-
(build → asset → release → deployment) is the server's problem.
|
|
184
|
-
- **Defaults that match what you meant** — `application_deployment_create` does what the dashboard needs five screens for.
|
|
185
|
-
- **Markdown is the UI** — tables, status glyphs, a traffic bar, one next-step per answer.
|
|
186
|
-
- **Honest writes** — provisioning/asynchronous things say so and tell you how to watch;
|
|
187
|
-
errors carry the platform's message, never a stack trace.
|
|
188
|
-
- **The dashboard stays one click away** — entities link to `https://<org>.app.nullplatform.io/nrn/<nrn>`.
|
|
189
|
-
|
|
190
|
-
## Develop
|
|
69
|
+
## Self-hosting (multi-user)
|
|
70
|
+
|
|
71
|
+
Run it as a shared HTTP server where every caller brings their own key:
|
|
191
72
|
|
|
192
73
|
```bash
|
|
193
|
-
|
|
194
|
-
npm test # unit + full in-memory MCP round-trips over a fake API (builds widgets first)
|
|
195
|
-
npm run lint # Biome (lint + format)
|
|
196
|
-
npm run typecheck # server + widgets, strict (noUncheckedIndexedAccess on)
|
|
197
|
-
npm run dev # stdio server (tsx)
|
|
198
|
-
npm run build # dist/ + minified widgets (NP_WIDGET_DEBUG=1 keeps identifiers)
|
|
74
|
+
npx -y @nullplatform/mcp --http 8080 # → http://host:8080/mcp
|
|
199
75
|
```
|
|
200
76
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
| Black box | `test/stdio.test.ts` | the **raw pattern**: spawns `src/index.ts` as a child process and speaks MCP over stdio like an installed client — entry point, env credential policy, negotiated text-only surface |
|
|
213
|
-
|
|
214
|
-
Everything runs against `test/fake-api.ts`, an in-memory fake of the nullplatform API faithful
|
|
215
|
-
to the live-verified contract (query scoping, snake_case bodies, async statuses, the multi-asset
|
|
216
|
-
deploy trap) — fast, deterministic, and it records every call so action tests assert exactly
|
|
217
|
-
what hit the platform.
|
|
218
|
-
|
|
219
|
-
### Layers
|
|
220
|
-
|
|
221
|
-
| Layer | Where | What it owns |
|
|
222
|
-
| --- | --- | --- |
|
|
223
|
-
| Platform API | `src/np/` | auth + token exchange, HTTP client, org context/caches, the typed journey API (builds → releases → scopes → deployments → metrics/logs). Tools never call the raw client — they go through `journey.ts`. |
|
|
224
|
-
| Tool framework | `src/tool.ts` | `ToolSpec`/`ToolReply`, the presenter (one place replies become wire results), the per-tool error net |
|
|
225
|
-
| Tools | `src/tools/` | one file per tool + `index.ts` registry + `shared.ts` resolution helpers |
|
|
226
|
-
| Prompts | `src/prompts.ts` | declarative slash-command prompts |
|
|
227
|
-
| Presentation (text) | `src/md.ts`, `src/render.ts` | the markdown design language and views |
|
|
228
|
-
| Presentation (ui) | `src/ui.ts`, `src/widgets-react/` | widget registry, MCP Apps glue, the React widgets |
|
|
229
|
-
| i18n | `src/i18n.ts` | `en`/`es` catalogs (compile-checked), locale scoping |
|
|
230
|
-
| Transport | `src/http.ts`, `src/index.ts` | multi-user HTTP boundary (per-request auth, per-user backends, guards) and stdio entry |
|
|
231
|
-
| Assembly | `src/server.ts` | Config → Deps → McpServer, wired from the registries |
|
|
232
|
-
|
|
233
|
-
### Dual-mode replies
|
|
234
|
-
|
|
235
|
-
Every tool returns one `ToolReply { markdown, data }`: the markdown is the complete answer
|
|
236
|
-
for text hosts, the data feeds both the model (structuredContent) and the bound widget on
|
|
237
|
-
ui hosts. In stdio sessions widgets are only registered once the client actually negotiates
|
|
238
|
-
the MCP Apps extension; in stateless HTTP they're always offered and hosts that don't speak
|
|
239
|
-
the extension simply ignore them (that's the spec's graceful degradation).
|
|
240
|
-
|
|
241
|
-
### Extend
|
|
242
|
-
|
|
243
|
-
- **New tool**: create `src/tools/<name>.ts` exporting `defineTool({...})` (schema, handler
|
|
244
|
-
returning a `ToolReply`, optional `widget` binding), add it to `src/tools/index.ts`. Its
|
|
245
|
-
widget auto-registers; the framework supplies error handling and presentation.
|
|
246
|
-
- **New widget**: drop `src/widgets-react/widgets/<name>.tsx`, add it to the `WIDGETS` map in
|
|
247
|
-
`src/ui.ts`, bind it from a tool spec. The build picks it up.
|
|
248
|
-
- **New prompt**: append a spec to `src/prompts.ts`.
|
|
249
|
-
- **New playbook**: drop `skills/<name>/SKILL.md` — picked up automatically by the `playbook_get`
|
|
250
|
-
tool and the instruction catalog.
|
|
251
|
-
- **New language**: add one catalog object in `src/i18n.ts` — completeness is compile-checked.
|
|
252
|
-
- **New platform call**: add a typed function to `src/np/journey.ts`; tools never touch the raw client.
|
|
77
|
+
The server holds **no credentials** — each request authenticates with the caller's own nullplatform key, so
|
|
78
|
+
platform RBAC applies per user. Run it behind a TLS-terminating reverse proxy on a trusted network.
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
Full tool reference, the multi-user security model, design rationale, and the development guide:
|
|
83
|
+
**[docs/ARCHITECTURE.md](https://github.com/nullplatform/ai-mcp/blob/main/docs/ARCHITECTURE.md)**.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
package/dist/i18n.js
CHANGED
|
@@ -160,6 +160,7 @@ const english = {
|
|
|
160
160
|
"builds.commit": "Commit",
|
|
161
161
|
"builds.released": "Released",
|
|
162
162
|
"builds.none": "**{app}** has no builds yet.",
|
|
163
|
+
"builds.noneForCommit": "No build in **{app}** for commit `{commit}` — it may not have built yet, or the commit isn't on a built branch.",
|
|
163
164
|
"builds.noneHint": "push a commit so CI produces one, then `application_deployment_create`.",
|
|
164
165
|
"builds.deployHint": "`application_deployment_create build_id:{build}` ships it (cuts the release for you).",
|
|
165
166
|
"builds.waiting": "build #{build} is still running — `application_build_list` again in a moment to check.",
|
|
@@ -423,6 +424,7 @@ const spanish = {
|
|
|
423
424
|
"builds.commit": "Commit",
|
|
424
425
|
"builds.released": "Released",
|
|
425
426
|
"builds.none": "**{app}** todavía no tiene builds.",
|
|
427
|
+
"builds.noneForCommit": "Ningún build en **{app}** para el commit `{commit}` — puede que todavía no haya buildeado, o que el commit no esté en una branch que buildea.",
|
|
426
428
|
"builds.noneHint": "pusheá un commit para que CI genere uno, después `application_deployment_create`.",
|
|
427
429
|
"builds.deployHint": "`application_deployment_create build_id:{build}` lo publica (crea la release por vos).",
|
|
428
430
|
"builds.waiting": "el build #{build} sigue corriendo — `application_build_list` de nuevo en un momento para chequear.",
|
package/dist/np/context.js
CHANGED
|
@@ -12,10 +12,15 @@ export function baseRepoUrl(provider, prefix) {
|
|
|
12
12
|
: "https://github.com";
|
|
13
13
|
return `${host}/${prefix}`;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
15
|
+
/** Org-wide app discovery must fan out across namespaces: the public `/application` endpoint is
|
|
16
|
+
* namespace-scoped, with no org-wide list and no name search — so the name filter is client-side
|
|
17
|
+
* by necessity. We page each namespace to completion and bound the AGGREGATE at this cap, which
|
|
18
|
+
* is generous and lift-able via NP_MAX_APPS, reporting truncation past it instead of silently
|
|
19
|
+
* hiding a large org (the old code capped the whole org at 200 with a flat `.slice`). */
|
|
20
|
+
const MAX_APPS = Math.max(1, Number(process.env.NP_MAX_APPS) || 1000);
|
|
21
|
+
export const DEFAULT_APP_LIMIT = MAX_APPS;
|
|
22
|
+
/** Safety ceiling when paging the account/namespace skeleton (both rarely exceed one page). */
|
|
23
|
+
const SKELETON_CAP = 2000;
|
|
19
24
|
/** Concurrency-limited parallel map — fast fan-out without hammering the API. */
|
|
20
25
|
export async function pmap(items, mapItem, limit = 12) {
|
|
21
26
|
const results = new Array(items.length);
|
|
@@ -29,6 +34,20 @@ export async function pmap(items, mapItem, limit = 12) {
|
|
|
29
34
|
await Promise.all(Array.from({ length: Math.min(limit, items.length || 1) }, worker));
|
|
30
35
|
return results;
|
|
31
36
|
}
|
|
37
|
+
/** Page an offset-based list endpoint to completion, bounded by `max` items. No list response
|
|
38
|
+
* carries a total, so the boundary is the standard offset heuristic: a page shorter than the
|
|
39
|
+
* requested limit is the last one. */
|
|
40
|
+
async function fetchPaged(fetchPage, max, pageSize = 200) {
|
|
41
|
+
const rows = [];
|
|
42
|
+
for (let offset = 0; offset < max; offset += pageSize) {
|
|
43
|
+
const limit = Math.min(pageSize, max - offset);
|
|
44
|
+
const page = await fetchPage(offset, limit);
|
|
45
|
+
rows.push(...page);
|
|
46
|
+
if (page.length < limit)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
32
51
|
/**
|
|
33
52
|
* The public list endpoints are NRN-authorized and need explicit scoping (account needs
|
|
34
53
|
* organization_id, namespace needs account_id, application needs namespace_id). There is no
|
|
@@ -53,16 +72,17 @@ export class NpContext {
|
|
|
53
72
|
if (!refresh && this.skeleton && Date.now() - this.skeleton.at < NpContext.CACHE_TTL_MS)
|
|
54
73
|
return this.skeleton;
|
|
55
74
|
const orgId = await this.organizationId();
|
|
56
|
-
const
|
|
57
|
-
organization_id: orgId,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const perAccount = await pmap(
|
|
61
|
-
const
|
|
62
|
-
.get("/namespace", { account_id: account.id, limit
|
|
63
|
-
.
|
|
75
|
+
const accounts = await fetchPaged((offset, limit) => this.np
|
|
76
|
+
.get("/account", { organization_id: orgId, offset, limit })
|
|
77
|
+
.then((page) => page.results ?? [])
|
|
78
|
+
.catch(() => []), SKELETON_CAP);
|
|
79
|
+
const perAccount = await pmap(accounts, async (account) => {
|
|
80
|
+
const accountNamespaces = await fetchPaged((offset, limit) => this.np
|
|
81
|
+
.get("/namespace", { account_id: account.id, offset, limit })
|
|
82
|
+
.then((page) => page.results ?? [])
|
|
83
|
+
.catch(() => []), SKELETON_CAP);
|
|
64
84
|
const accountBaseRepoUrl = baseRepoUrl(account.repository_provider, account.repository_prefix);
|
|
65
|
-
return
|
|
85
|
+
return accountNamespaces.map((namespace) => ({
|
|
66
86
|
id: namespace.id,
|
|
67
87
|
name: namespace.name,
|
|
68
88
|
nrn: namespace.nrn,
|
|
@@ -109,21 +129,12 @@ export class NpContext {
|
|
|
109
129
|
? skeleton.namespaces.filter((namespace) => namespace.name.toLowerCase().includes(namespaceFilter))
|
|
110
130
|
: skeleton.namespaces;
|
|
111
131
|
const nameFilter = args.query?.toLowerCase();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (args.query)
|
|
119
|
-
query["name:contains"] = args.query; // server-side when supported
|
|
120
|
-
const page = await this.np.get("/application", query);
|
|
121
|
-
return (page.results ?? []).map((app) => this.mapApp(app, namespace));
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
});
|
|
132
|
+
// `/application` is namespace-scoped with no name filter, so page each namespace to completion
|
|
133
|
+
// and filter by name client-side (below). This lifts the old silent 200-per-org cap.
|
|
134
|
+
const perNamespace = await pmap(namespaces, async (namespace) => fetchPaged((offset, limit) => this.np
|
|
135
|
+
.get("/application", { namespace_id: namespace.id, offset, limit })
|
|
136
|
+
.then((page) => page.results ?? [])
|
|
137
|
+
.catch(() => []), MAX_APPS).then((rows) => rows.map((app) => this.mapApp(app, namespace))));
|
|
127
138
|
const seen = new Set();
|
|
128
139
|
return perNamespace
|
|
129
140
|
.flat()
|
package/dist/np/journey.js
CHANGED
|
@@ -11,13 +11,22 @@ const TERMINAL = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
export const isDeploymentTerminal = (status) => !!status && TERMINAL.has(status);
|
|
13
13
|
export function bumpSemver(semver) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Preserve the optional `v` prefix: an org that enforces a v-prefixed version pattern
|
|
15
|
+
// rejects a bare bump, and dropping it makes the release history inconsistent.
|
|
16
|
+
const parts = /^(v?)(\d+)\.(\d+)\.(\d+)/.exec(semver ?? "");
|
|
17
|
+
return parts ? `${parts[1]}${parts[2]}.${parts[3]}.${Number(parts[4]) + 1}` : "0.0.1";
|
|
16
18
|
}
|
|
17
|
-
export async function listBuilds(np, applicationId,
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
export async function listBuilds(np, applicationId, options = {}) {
|
|
20
|
+
const query = {
|
|
21
|
+
application_id: applicationId,
|
|
22
|
+
sort: "created_at:desc",
|
|
23
|
+
limit: options.limit ?? 10,
|
|
24
|
+
};
|
|
25
|
+
// The public API filters builds by full commit SHA via the `commit.id` query param (in the
|
|
26
|
+
// canonical OpenAPI contract) — this is how a change is traced from a commit to its build.
|
|
27
|
+
if (options.commit)
|
|
28
|
+
query["commit.id"] = options.commit;
|
|
29
|
+
const page = await np.get("/build", query).catch(() => ({ results: [] }));
|
|
21
30
|
return (page.results ?? []).map((build) => ({
|
|
22
31
|
id: build.id,
|
|
23
32
|
status: build.status,
|
|
@@ -6,7 +6,9 @@ import { tools } from "../tools/index.js";
|
|
|
6
6
|
*/
|
|
7
7
|
const INSTRUCTIONS = `nullplatform is where this code gets built, released, deployed and observed — these tools replace its web dashboard for the everyday developer journey.
|
|
8
8
|
|
|
9
|
-
The tools are repo-aware: inside a git repo, omit \`app\` and the linked application is inferred from the git remote. Start with \`application_get\` — it shows what's live where and suggests the next action.
|
|
9
|
+
The tools are repo-aware: inside a git repo, omit \`app\` and the linked application is inferred from the git remote. "This app", or a request that names nothing, means the repo's app — infer it; pass \`app\` only when the user means a different, explicitly named application. Start with \`application_get\` — it shows what's live where and suggests the next action.
|
|
10
|
+
|
|
11
|
+
You run in the developer's own environment, so fuse the local repo with platform state. Read the git remote, branch, HEAD commit, the diff being shipped, and config files (\`.env\`, \`package.json\`, Dockerfile), and correlate them with what the platform reports: does the local HEAD match a built or released commit, does a local config value match what a scope resolves. That correlation is this integration's edge over the web dashboard, which only sees the platform half.
|
|
10
12
|
|
|
11
13
|
Every tool accepts \`language\`: ALWAYS set it to the language the user is conversing in (ISO code, e.g. "es", "en") — answers come back in the user's language.
|
|
12
14
|
|
package/dist/tools/builds.js
CHANGED
|
@@ -12,14 +12,18 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
12
12
|
*/
|
|
13
13
|
export const buildsTool = defineTool({
|
|
14
14
|
name: TOOL.applicationBuildList,
|
|
15
|
-
title: "
|
|
16
|
-
description: "List an application's recent CI builds — status, branch, commit, age, and whether each is already released. Use it to answer 'did my push build yet?' and to pick a build_id to deploy. Pass build:<id> to see that build's assets.",
|
|
15
|
+
title: "Builds",
|
|
16
|
+
description: "List an application's recent CI builds — status, branch, commit, age, and whether each is already released. Use it to answer 'did my push build yet?' and to pick a build_id to deploy. Pass build:<id> to see that build's assets, or commit:<sha> to find the build for a specific commit (tracing a change to its build and release).",
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
18
|
errorKey: "builds.errorLabel",
|
|
19
19
|
inputSchema: {
|
|
20
20
|
app: appArg,
|
|
21
21
|
build: z.number().optional().describe("Build id — show this build's assets instead of the list"),
|
|
22
22
|
limit: z.number().optional().describe("How many recent builds (default 10)"),
|
|
23
|
+
commit: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Full commit SHA — list only the build(s) for that commit (tracing a change to its build/release)"),
|
|
23
27
|
},
|
|
24
28
|
async handler(args, context) {
|
|
25
29
|
const resolved = await requireApp(context, args);
|
|
@@ -43,11 +47,14 @@ export const buildsTool = defineTool({
|
|
|
43
47
|
return reply(markdown, { build_id: args.build, assets });
|
|
44
48
|
}
|
|
45
49
|
const [builds, releases] = await Promise.all([
|
|
46
|
-
listBuilds(context.np, app.id, args.limit ?? 10),
|
|
50
|
+
listBuilds(context.np, app.id, { limit: args.limit ?? 10, commit: args.commit }),
|
|
47
51
|
listReleases(context.np, app.id, { limit: 50 }),
|
|
48
52
|
]);
|
|
49
53
|
if (builds.length === 0) {
|
|
50
|
-
|
|
54
|
+
const empty = args.commit
|
|
55
|
+
? translate("builds.noneForCommit", { app: app.name, commit: shortCommit(args.commit) })
|
|
56
|
+
: translate("builds.none", { app: app.name }) + next(translate("builds.noneHint"));
|
|
57
|
+
return reply(empty, { app: `#${app.id}`, app_name: app.name, builds: [], commit: args.commit ?? null });
|
|
51
58
|
}
|
|
52
59
|
const releasedBuildIds = new Set(releases.map((release) => release.build_id).filter(Boolean));
|
|
53
60
|
const releaseByBuild = new Map(releases.map((release) => [release.build_id, release]));
|
|
@@ -24,7 +24,7 @@ export const createReleaseTool = defineTool({
|
|
|
24
24
|
let buildId = args.build_id;
|
|
25
25
|
let buildNote = "";
|
|
26
26
|
if (!buildId) {
|
|
27
|
-
const builds = await listBuilds(context.np, app.id, 5);
|
|
27
|
+
const builds = await listBuilds(context.np, app.id, { limit: 5 });
|
|
28
28
|
const successful = builds.find((build) => build.status === "successful");
|
|
29
29
|
if (!successful)
|
|
30
30
|
return fail(translate("createRelease.noBuilds", { app: app.name }));
|
package/dist/tools/deploy.js
CHANGED
|
@@ -15,7 +15,7 @@ async function resolveRelease(context, applicationId, args) {
|
|
|
15
15
|
// code under its name — so search deep enough that only pathologically active apps miss.
|
|
16
16
|
const [releases, builds] = await Promise.all([
|
|
17
17
|
listReleases(context.np, applicationId, { status: "active", limit: 200 }),
|
|
18
|
-
listBuilds(context.np, applicationId, 5),
|
|
18
|
+
listBuilds(context.np, applicationId, { limit: 5 }),
|
|
19
19
|
]);
|
|
20
20
|
const successfulBuild = builds.find((build) => build.status === "successful");
|
|
21
21
|
if (args.release_id) {
|
package/dist/tools/find-apps.js
CHANGED
|
@@ -7,7 +7,7 @@ import { defineTool, reply } from "../tool.js";
|
|
|
7
7
|
import { TOOL } from "../tool-names.js";
|
|
8
8
|
export const findAppsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationList,
|
|
10
|
-
title: "
|
|
10
|
+
title: "Applications",
|
|
11
11
|
description: "Search applications across the whole organization by partial name (and optionally namespace). Fast (parallel + cached). Use when the repo isn't linked or the user names an app you don't know.",
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "find-apps",
|
package/dist/tools/logs.js
CHANGED
|
@@ -7,7 +7,7 @@ import { TOOL } from "../tool-names.js";
|
|
|
7
7
|
import { appArg, chooseScope, requireApp } from "./shared.js";
|
|
8
8
|
export const logsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationLogList,
|
|
10
|
-
title: "
|
|
10
|
+
title: "Logs",
|
|
11
11
|
description: "Read recent application logs (optionally for one scope). Returns the latest lines, newest last — good for a quick 'why is it failing?' look.",
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
widget: "logs",
|
package/dist/tools/params.js
CHANGED
|
@@ -6,7 +6,7 @@ import { TOOL } from "../tool-names.js";
|
|
|
6
6
|
import { appArg, requireApp } from "./shared.js";
|
|
7
7
|
export const paramsTool = defineTool({
|
|
8
8
|
name: TOOL.applicationParameterList,
|
|
9
|
-
title: "
|
|
9
|
+
title: "Parameters",
|
|
10
10
|
description: "List an application's configuration parameters (env vars / files; secret values are masked). Use application_parameter_create to add or change them.",
|
|
11
11
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12
12
|
widget: "params",
|
package/dist/tools/playbook.js
CHANGED
|
@@ -13,8 +13,8 @@ import { TOOL } from "../tool-names.js";
|
|
|
13
13
|
const playbookBody = (markdown) => markdown.replace(/^---\n[\s\S]*?\n---\n?/, "").trim();
|
|
14
14
|
export const playbookGetTool = defineTool({
|
|
15
15
|
name: TOOL.playbookGet,
|
|
16
|
-
title: "
|
|
17
|
-
description: "Read a nullplatform operating playbook — the methodology to follow
|
|
16
|
+
title: "Operating playbooks",
|
|
17
|
+
description: "Read a nullplatform operating playbook — the methodology to follow BEFORE the matching non-trivial work (e.g. deploying-safely before a deploy, incident-response when something is broken, configuring-safely before changing parameters or secrets). The server instructions carry the full catalog; call this with no name to list them too.",
|
|
18
18
|
annotations: { readOnlyHint: true },
|
|
19
19
|
errorKey: "playbook.errorLabel",
|
|
20
20
|
inputSchema: {
|
package/dist/tools/status.js
CHANGED
|
@@ -26,7 +26,7 @@ async function scopeViews(context, applicationId) {
|
|
|
26
26
|
}
|
|
27
27
|
export const statusTool = defineTool({
|
|
28
28
|
name: TOOL.applicationGet,
|
|
29
|
-
title: "
|
|
29
|
+
title: "Application status",
|
|
30
30
|
description: "THE place to start. Shows an application's full picture: scopes with what's live on each (release + traffic), latest build, latest release, and the one obvious next action. Call with no arguments inside a repo to use the linked app. Pass deployment:<id> to watch one rollout in detail.",
|
|
31
31
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
32
32
|
widget: "np-panel",
|
|
@@ -54,7 +54,7 @@ export const statusTool = defineTool({
|
|
|
54
54
|
return resolved.out;
|
|
55
55
|
const [views, builds, releases, orgSlug] = await Promise.all([
|
|
56
56
|
scopeViews(context, resolved.app.id),
|
|
57
|
-
listBuilds(context.np, resolved.app.id, 5),
|
|
57
|
+
listBuilds(context.np, resolved.app.id, { limit: 5 }),
|
|
58
58
|
listReleases(context.np, resolved.app.id, { limit: 5 }),
|
|
59
59
|
context.org.organizationSlug(),
|
|
60
60
|
]);
|
package/dist/tools/traffic.js
CHANGED
|
@@ -8,8 +8,8 @@ import { TOOL } from "../tool-names.js";
|
|
|
8
8
|
import { appArg, delays, requireApp, sleep } from "./shared.js";
|
|
9
9
|
export const trafficTool = defineTool({
|
|
10
10
|
name: TOOL.applicationDeploymentUpdate,
|
|
11
|
-
title: "
|
|
12
|
-
description: 'Drive an in-flight rollout: move traffic to the new version (percent snaps to 1,5,10,25,50,75,90,95,99,100), finalize it (action:"finalize" — retire the old version), or roll it back (action:"rollback" — traffic returns to the old version). Finds the app\'s active deployment automatically.',
|
|
11
|
+
title: "Update traffic",
|
|
12
|
+
description: 'Drive an in-flight rollout: move traffic to the new version (percent snaps to 0,1,5,10,25,50,75,90,95,99,100), finalize it (action:"finalize" — retire the old version), or roll it back (action:"rollback" — traffic returns to the old version). Finds the app\'s active deployment automatically.',
|
|
13
13
|
annotations: { destructiveHint: true, openWorldHint: true },
|
|
14
14
|
widget: "np-panel",
|
|
15
15
|
errorKey: "traffic.errorLabel",
|
package/package.json
CHANGED