@nullplatform/mcp 0.1.15 → 0.1.16
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/dist/np/journey.js +9 -2
- package/dist/surfaces/developer.js +2 -2
- package/dist/tool.js +23 -3
- package/dist/tools/deploy.js +1 -1
- package/dist/tools/deployments.js +1 -1
- package/dist/tools/entity-list.js +1 -1
- package/dist/tools/releases.js +1 -1
- package/dist/tools/status.js +10 -3
- package/dist/tools/traffic.js +1 -1
- package/package.json +1 -1
- package/widgets-dist/deployments.html +10 -10
- package/widgets-dist/find-apps.html +11 -11
- package/widgets-dist/manifest.json +5 -5
- package/widgets-dist/overview.html +9 -9
- package/widgets-dist/releases.html +10 -10
- package/widgets-dist/status.html +9 -9
package/dist/np/journey.js
CHANGED
|
@@ -206,7 +206,11 @@ function mapDeployment(raw) {
|
|
|
206
206
|
switchedTraffic: strategyData.switchedTraffic ?? strategyData.switched_traffic,
|
|
207
207
|
desiredSwitchedTraffic: strategyData.desiredSwitchedTraffic ?? strategyData.desired_switched_traffic,
|
|
208
208
|
},
|
|
209
|
-
messages: raw.messages ?? []
|
|
209
|
+
messages: (raw.messages ?? []).map((entry) => ({
|
|
210
|
+
timestamp: entry.timestamp ?? entry.created_at,
|
|
211
|
+
source: entry.source ?? entry.level,
|
|
212
|
+
message: entry.message ?? "",
|
|
213
|
+
})),
|
|
210
214
|
};
|
|
211
215
|
}
|
|
212
216
|
export async function createDeployment(np, args) {
|
|
@@ -216,7 +220,10 @@ export async function createDeployment(np, args) {
|
|
|
216
220
|
return mapDeployment(await np.post("/deployment", body));
|
|
217
221
|
}
|
|
218
222
|
export async function getDeployment(np, deploymentId) {
|
|
219
|
-
return
|
|
223
|
+
// include_messages=true is REQUIRED for the platform to return the deployment's lifecycle log
|
|
224
|
+
// (`messages`) — without it the array is empty and the panel's "Deployment log" stays blank
|
|
225
|
+
// (core-entities routes/deployment.js maps the flag onto GET /deployment/:id). Verified against source.
|
|
226
|
+
return mapDeployment(await np.get(`/deployment/${deploymentId}`, { include_messages: true }));
|
|
220
227
|
}
|
|
221
228
|
/** Latest deployments of a scope, newest first. */
|
|
222
229
|
export async function listScopeDeployments(np, scopeId, limit = 3) {
|
|
@@ -8,7 +8,7 @@ const INSTRUCTIONS = `PRECEDENCE — READ FIRST. While these nullplatform tools
|
|
|
8
8
|
|
|
9
9
|
nullplatform is where this code gets built, released, deployed and observed — these tools replace its web dashboard for the everyday developer journey.
|
|
10
10
|
|
|
11
|
-
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.
|
|
11
|
+
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. \`application_get\` is the entry point for a STATUS question ("what's deployed where", "is it healthy", "what's going on") — it shows what's live and the next action. But for a DIRECT request — "show logs", "show metrics", "deploy", "roll back", "send traffic" — call THAT tool directly with the app name (each resolves the app itself); do NOT render \`application_get\` first to "resolve the app" or "orient" — that just drops a status panel the user didn't ask for. "show <app> logs" is ONE call: \`application_log_list app:"<app>"\`.
|
|
12
12
|
|
|
13
13
|
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.
|
|
14
14
|
|
|
@@ -36,7 +36,7 @@ Typical flows:
|
|
|
36
36
|
- First time on a repo: \`application_create\` → push a commit (CI builds) → \`application_deployment_create\`.
|
|
37
37
|
- Trouble: \`application_get\` → \`application_log_list\` + \`application_metric_list\` (golden signals) → \`application_deployment_update action:"rollback"\` if needed.
|
|
38
38
|
|
|
39
|
-
THE ROLLOUT PANEL
|
|
39
|
+
THE DEPLOYMENT IS ONE ROLLOUT PANEL — everything happens in it. \`application_deployment_create\` renders ONE live rollout panel: provisioning status, a traffic slider, auto-advance (ramps to 100%), finalize, rollback, and live logs — ALL in that single panel. \`application_deployment_update\` steers the SAME panel (its on-panel controls call it through the host bridge, in place). So deploy ONCE and let the user walk traffic, finalize, roll back and watch provisioning/logs IN that one panel — do NOT fire \`application_deployment_update percent:25 → 50 → 100 → finalize\` yourself to step it, because each model call STACKS ANOTHER rollout panel down the chat. Call \`application_deployment_update\` directly ONLY for a single move the user explicitly asked for ("go straight to 100%", "finalize now", "roll back") with no panel already open. One deployment → ONE rollout panel; never render a second.
|
|
40
40
|
|
|
41
41
|
When the user is WATCHING fresh activity — verifying a deploy, "tail the logs", "is it crashing now?" — keep logs LIVE (the logs panel defaults to live and auto-refreshes) or pass a recent window (\`start_time\` = a minute or two ago), so they see CURRENT lines streaming in, not a stale snapshot of the last 50 lines (which can be hours old on a quiet app).
|
|
42
42
|
|
package/dist/tool.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { currentLocale, matchLocale, translate, withLocale } from "./i18n.js";
|
|
3
|
+
import { log } from "./log.js";
|
|
3
4
|
import { uiMeta, uiNegotiated, widgetUri } from "./ui.js";
|
|
4
5
|
/** Markdown for the human + JSON for the model/widget. */
|
|
5
6
|
export function reply(markdown, data) {
|
|
@@ -97,11 +98,30 @@ export function registerTools(server, tools, context) {
|
|
|
97
98
|
annotations: tool.annotations,
|
|
98
99
|
...(tool.widget ? { _meta: uiMeta(widgetUri(tool.widget)) } : {}),
|
|
99
100
|
inputSchema: { ...tool.inputSchema, language: languageArg },
|
|
100
|
-
}, async (rawArgs) =>
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
}, async (rawArgs) => {
|
|
102
|
+
const toolReply = await runInUserLanguage(tool, rawArgs, context);
|
|
103
|
+
// Trace EVERY tool call: the tool name, the widget it renders, and whether THIS host actually
|
|
104
|
+
// paints a panel — so "why did N panels open for one interaction?" is answerable from the log
|
|
105
|
+
// (grep the panel:true lines). Arg KEYS only, never values — a value can carry a secret.
|
|
106
|
+
log.debug({
|
|
107
|
+
toolCall: {
|
|
108
|
+
name: tool.name,
|
|
109
|
+
widget: tool.widget ?? null,
|
|
110
|
+
panel: Boolean(tool.widget) && uiNegotiated(server) && !toolReply.isError,
|
|
111
|
+
ok: !toolReply.isError,
|
|
112
|
+
args: argKeys(rawArgs),
|
|
113
|
+
},
|
|
114
|
+
}, "tool call");
|
|
115
|
+
return present(toolReply, { uiActive: splitsForModel(tool) && uiNegotiated(server) });
|
|
116
|
+
});
|
|
103
117
|
}
|
|
104
118
|
}
|
|
119
|
+
/** Arg KEYS only (never values — they can carry secrets) for the tool-call trace. */
|
|
120
|
+
function argKeys(rawArgs) {
|
|
121
|
+
if (!rawArgs || typeof rawArgs !== "object")
|
|
122
|
+
return [];
|
|
123
|
+
return Object.keys(rawArgs).filter((key) => key !== "language");
|
|
124
|
+
}
|
|
105
125
|
/** Honour the LLM-declared conversation language, falling back to the ambient locale. */
|
|
106
126
|
async function runInUserLanguage(tool, rawArgs, context) {
|
|
107
127
|
const { language, ...args } = (rawArgs ?? {});
|
package/dist/tools/deploy.js
CHANGED
|
@@ -124,7 +124,7 @@ async function resolveDeployAsset(context, release, scope, assetHint) {
|
|
|
124
124
|
export const deployTool = defineTool({
|
|
125
125
|
name: TOOL.applicationDeploymentCreate,
|
|
126
126
|
title: "Deploy",
|
|
127
|
-
description: 'DEPLOY an application — provisions real infrastructure (starts a rollout), rendered as the live rollout panel. With no extra args it ships the latest code: picks the latest successful build, cuts the release for you (semver auto-bumped) if needed, and targets the app\'s only scope. Use it for "deploy", "ship it", "release to dev/prod". Narrow the target with `scope` (name or "environment=production"); pin an exact `release_id` or `version` (a known version rolls FORWARD or BACK to it); or cut from a specific `build_id`. For an app that splits traffic,
|
|
127
|
+
description: 'DEPLOY an application — provisions real infrastructure (starts a rollout), rendered as the live rollout panel. With no extra args it ships the latest code: picks the latest successful build, cuts the release for you (semver auto-bumped) if needed, and targets the app\'s only scope. Use it for "deploy", "ship it", "release to dev/prod". Narrow the target with `scope` (name or "environment=production"); pin an exact `release_id` or `version` (a known version rolls FORWARD or BACK to it); or cut from a specific `build_id`. For an app that splits traffic, the rollout panel this renders OWNS the traffic walk — its slider + auto-advance ramp traffic, with finalize and rollback right there, so do NOT follow up with application_deployment_update to step it yourself (each model call stacks another rollout panel); the user drives it in the ONE panel. Call application_deployment_update directly only for one move the user explicitly names ("go straight to 100%", "roll back") with no panel open — it steers this same rollout. Convergent under retries: re-running returns the in-flight rollout for the same scope+release rather than creating a duplicate.',
|
|
128
128
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
129
129
|
widget: "status",
|
|
130
130
|
errorKey: "deploy.errorLabel",
|
|
@@ -13,7 +13,7 @@ import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
|
13
13
|
export const deploymentsTool = defineTool({
|
|
14
14
|
name: TOOL.applicationDeploymentList,
|
|
15
15
|
title: "Deployments",
|
|
16
|
-
description: 'List ONE application\'s deployments across all its scopes, rendered as a PANEL — which release landed on which scope, the rollout status, and age (a deployment group landing one release on several scopes shows as one row). Use it
|
|
16
|
+
description: 'List ONE application\'s deployments across all its scopes, rendered as a PANEL for the user to SEE — which release landed on which scope, the rollout status, and age (a deployment group landing one release on several scopes shows as one row). Use it ONLY when the user asks to look at the deploy history ("show the deployments", "what was deployed when"). Does NOT deploy or change traffic (use application_deployment_create / application_deployment_update) and shows no logs/metrics. CRUCIAL — do NOT render this to QUERY: to FIND or IDENTIFY a deployment for your OWN next step — the previously deployed version to roll back to, the active rollout\'s id, the newest deployment across apps — read it HEADLESSLY with entity_list (entity:"deployment", parent_type:"scope") or entity_get, never this panel. Rendering it to read history is wasted: the rows ride in _meta and never reach your reasoning, so you would paint a panel, get nothing, and have to re-read headlessly anyway. Render this ONLY to show the user a list they asked to see. Read-only.',
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
18
|
widget: "deployments",
|
|
19
19
|
errorKey: "deploymentList.errorLabel",
|
|
@@ -66,7 +66,7 @@ async function listByNrn(context, entity, nrn) {
|
|
|
66
66
|
export const entityListTool = defineTool({
|
|
67
67
|
name: TOOL.entityList,
|
|
68
68
|
title: "List entities (data)",
|
|
69
|
-
description: 'List releases, builds, deployments, scopes, applications, namespaces, parameters, services, links or approvals as DATA — returns structuredContent and renders NO panel. THIS is the tool for
|
|
69
|
+
description: 'List releases, builds, deployments, scopes, applications, namespaces, parameters, services, links or approvals as DATA — returns structuredContent and renders NO panel. THIS is the tool for any DATA lookup you reason over — whether AGGREGATING across apps ("the latest release across all my apps", "which app has a failing build", "every scope on the newest build") OR reading ONE app\'s history to identify something for your next move ("the previously deployed version to roll back to", "the release behind the current deployment", "what version ran before this one", "find the active rollout"). List the app\'s deployments/releases/builds with this (NO panel), then compute the answer yourself. Prefer it over application_release_list/application_build_list/application_deployment_list/application_get WHENEVER you are gathering to reason — including a single app, not just aggregates — because those reads RENDER A PANEL the user didn\'t ask for; and the LIST panels (release/build/deployment) split their rows into _meta, so they would not even reach your reasoning. Those reads are ONLY for showing the user a list/status they asked to see. Scoped to a parent: REST tree organization → account → namespace → application → {scope, build, release}; scope → deployment — pass `entity` plus the `parent_type`+`parent_id` it lives under (e.g. entity:"release" parent_type:"application" parent_id:123; listing `account` needs no parent). NRN-scoped resources (parameter, service, link, approval) live under an application or scope — pass parent_type:"application"|"scope" + parent_id (the NRN is resolved for you), or `nrn` directly. `template` is scoped by a NAMESPACE: pass entity:"template" parent_type:"namespace" parent_id:<namespace_id> to list the scaffolding templates available for a new app there — do this to choose a template BEFORE opening the application_create form, so the form opens with it pre-selected (don\'t open the form blind to discover templates). For one entity by id use entity_get.',
|
|
70
70
|
annotations: { readOnlyHint: true },
|
|
71
71
|
// No widget on purpose: data-only navigation. See CLAUDE.md (the data-only read pattern).
|
|
72
72
|
errorKey: "entityList.errorLabel",
|
package/dist/tools/releases.js
CHANGED
|
@@ -12,7 +12,7 @@ import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
|
12
12
|
export const releasesTool = defineTool({
|
|
13
13
|
name: TOOL.applicationReleaseList,
|
|
14
14
|
title: "Releases",
|
|
15
|
-
description: "List ONE application's releases as a PANEL for the user — semver, status (active/deprecated/unstable/failed), the build each pins, and age
|
|
15
|
+
description: "List ONE application's releases as a PANEL for the user to SEE — semver, status (active/deprecated/unstable/failed), the build each pins, and age; each row carries a Deploy button. Use it ONLY when the user wants to look at what's shippable or click a release to deploy. CRUCIAL — do NOT render this to QUERY: to FIND or IDENTIFY a release for your OWN next step — the previous version to roll back to, the release behind the current deployment, the latest across apps — read it HEADLESSLY with entity_list (entity:\"release\") and compute the answer, never this panel. Rendering it to read history is wasted: the rows ride in _meta and never reach your reasoning, so you would paint a panel, get nothing, and re-read headlessly anyway. Render this ONLY to show the user a list they asked to see.",
|
|
16
16
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
17
17
|
widget: "releases",
|
|
18
18
|
errorKey: "releaseList.errorLabel",
|
package/dist/tools/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { dashboardLink } from "../md.js";
|
|
3
3
|
import { pmap } from "../np/context.js";
|
|
4
|
-
import { getDeployment, getScope, isDeploymentTerminal, listBuilds, listReleases, listScopeDeployments, listScopes, } from "../np/journey.js";
|
|
4
|
+
import { getDeployment, getRelease, getScope, isDeploymentTerminal, listBuilds, listReleases, listScopeDeployments, listScopes, } from "../np/journey.js";
|
|
5
5
|
import { renderRollout, renderStatus } from "../render.js";
|
|
6
6
|
import { defineTool, reply } from "../tool.js";
|
|
7
7
|
import { TOOL } from "../tool-names.js";
|
|
@@ -41,7 +41,7 @@ export async function buildAppStatus(context, app) {
|
|
|
41
41
|
export const statusTool = defineTool({
|
|
42
42
|
name: TOOL.applicationGet,
|
|
43
43
|
title: "Application status",
|
|
44
|
-
description: 'THE place to start for ONE application\'s live status — renders a status PANEL: each scope with what\'s live on it (release + traffic), the latest build, the latest release, and the single obvious next action. Use it for "what\'s the status of my app", "what\'s deployed where", "is it healthy" — a STATUS question. For a DIRECT action ("deploy", "send traffic", "show logs/metrics") reach for that action\'s own tool
|
|
44
|
+
description: 'THE place to start for ONE application\'s live status — renders a status PANEL: each scope with what\'s live on it (release + traffic), the latest build, the latest release, and the single obvious next action. Use it for "what\'s the status of my app", "what\'s deployed where", "is it healthy" — a STATUS question. For a DIRECT action ("deploy", "roll back", "send traffic", "show logs/metrics") reach for that action\'s own tool — do NOT render this panel FIRST to "check current state" or "see what\'s live" before acting. When you need current state to DECIDE (which version is live, what the previous version is), read it HEADLESSLY with entity_get / entity_list and act on that; rendering this panel just to orient drops a status panel the user never asked for. Render it ONLY when the USER wants to SEE status. To LOOK UP a field for your OWN work — the repository_url to clone/edit, the app id, where it lives, its nrn — read it HEADLESSLY with entity_get (entity:"application", id:<id>); do NOT render this status panel just to grab a value. Call with NO arguments inside a git repo to use the linked app; pass `app` (name or "#id") for a different one; pass `deployment:<id>` to watch one rollout\'s live detail. Does NOT return logs, metrics, parameters, or a build\'s assets — use application_log_list / application_metric_list / application_parameter_list / application_build_list for those. Do NOT loop this to WAIT for a build or deployment to finish: it repaints the whole panel every call (stacking duplicate panels) — poll headlessly with entity_get (entity:"build" or "deployment", id:<id>) instead, and show this panel once at the end.',
|
|
45
45
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
46
46
|
widget: "status",
|
|
47
47
|
errorKey: "status.errorLabel",
|
|
@@ -52,13 +52,20 @@ export const statusTool = defineTool({
|
|
|
52
52
|
async handler(args, context) {
|
|
53
53
|
if (args.deployment) {
|
|
54
54
|
const deployment = await getDeployment(context.np, args.deployment);
|
|
55
|
-
|
|
55
|
+
// Resolve the release too, so the rollout panel keeps showing its version on every live poll
|
|
56
|
+
// (this read is what the panel polls every 2.5s — without the release, release_semver goes null
|
|
57
|
+
// and the version disappears after the first refresh).
|
|
58
|
+
const [scope, release, orgSlug] = await Promise.all([
|
|
56
59
|
deployment.scope_id ? getScope(context.np, deployment.scope_id).catch(() => undefined) : undefined,
|
|
60
|
+
deployment.release_id
|
|
61
|
+
? getRelease(context.np, deployment.release_id).catch(() => undefined)
|
|
62
|
+
: undefined,
|
|
57
63
|
context.org.organizationSlug(),
|
|
58
64
|
]);
|
|
59
65
|
const { md, structured } = renderRollout({
|
|
60
66
|
deployment,
|
|
61
67
|
scope,
|
|
68
|
+
release,
|
|
62
69
|
dashboard: dashboardLink(orgSlug, scope?.nrn),
|
|
63
70
|
});
|
|
64
71
|
return reply(md, structured);
|
package/dist/tools/traffic.js
CHANGED
|
@@ -40,7 +40,7 @@ async function resolveActiveRollout(context, args) {
|
|
|
40
40
|
export const trafficTool = defineTool({
|
|
41
41
|
name: TOOL.applicationDeploymentUpdate,
|
|
42
42
|
title: "Update traffic",
|
|
43
|
-
description: 'Drive an IN-FLIGHT rollout — the
|
|
43
|
+
description: 'Drive an IN-FLIGHT rollout, rendered as the live rollout PANEL — the SAME single panel application_deployment_create opens, where provisioning status, the traffic slider, auto-advance, finalize, rollback and live logs ALL live. Call it DIRECTLY with `percent` (snaps to 0,1,5,10,25,50,75,90,95,99,100) to move traffic, action:"finalize" to retire the old version, or action:"rollback" to return traffic to the previous version (the safety hatch). You do NOT need to read status or look up a deployment id first — it finds the app\'s single active rollout automatically (pass `deployment_id` only when several are in flight at once). Use it for "send 25% traffic", "promote to 100%", "finalize", "roll back". THE ROLLOUT IS ONE PANEL: if a rollout panel is already open (you just deployed), the user walks traffic, finalizes and rolls back IN it via its own controls — do NOT call this yourself to step 25 → 50 → 100 → finalize, because each model call stacks ANOTHER rollout panel. Call this directly ONLY for one move the user explicitly named with no panel open. Does NOT create a deployment — use application_deployment_create first; this only steers one that already exists.',
|
|
44
44
|
annotations: { destructiveHint: true, openWorldHint: true },
|
|
45
45
|
widget: "status",
|
|
46
46
|
errorKey: "traffic.errorLabel",
|
package/package.json
CHANGED