@nullplatform/mcp 0.1.13 → 0.1.15
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 +10 -0
- package/dist/http.js +16 -12
- package/dist/i18n.js +6 -0
- package/dist/log.js +53 -0
- package/dist/np/client.js +10 -1
- package/dist/np/context.js +30 -21
- package/dist/np/journey.js +14 -4
- package/dist/render.js +12 -13
- package/dist/surfaces/developer.js +21 -4
- package/dist/tool.js +64 -6
- package/dist/tools/approvals.js +1 -1
- package/dist/tools/builds.js +1 -1
- package/dist/tools/create-app.js +158 -132
- package/dist/tools/create-link.js +82 -55
- package/dist/tools/create-release.js +1 -1
- package/dist/tools/create-scope.js +35 -23
- package/dist/tools/create-service.js +39 -25
- package/dist/tools/delete-link.js +1 -1
- package/dist/tools/delete-param.js +1 -1
- package/dist/tools/delete-service.js +1 -1
- package/dist/tools/deploy.js +52 -30
- package/dist/tools/deployments.js +1 -1
- package/dist/tools/entity-get.js +1 -1
- package/dist/tools/entity-list.js +14 -6
- package/dist/tools/find-apps.js +5 -2
- package/dist/tools/logs.js +114 -17
- package/dist/tools/metrics.js +1 -1
- package/dist/tools/overview.js +1 -1
- package/dist/tools/params.js +8 -2
- package/dist/tools/playbook.js +1 -1
- package/dist/tools/releases.js +1 -1
- package/dist/tools/services.js +1 -1
- package/dist/tools/set-params.js +18 -19
- package/dist/tools/shared.js +52 -0
- package/dist/tools/status.js +4 -4
- package/dist/tools/traffic.js +38 -26
- package/dist/tools/update-link.js +1 -1
- package/dist/tools/update-service.js +1 -1
- package/dist/ui.js +1 -1
- package/package.json +5 -1
- package/widgets-dist/approvals.html +120 -10
- package/widgets-dist/builds.html +127 -17
- package/widgets-dist/create-app.html +129 -19
- package/widgets-dist/deployments.html +132 -22
- package/widgets-dist/find-apps.html +131 -21
- package/widgets-dist/logs.html +133 -23
- package/widgets-dist/manifest.json +16 -16
- package/widgets-dist/metrics.html +120 -10
- package/widgets-dist/overview.html +136 -26
- package/widgets-dist/params.html +121 -11
- package/widgets-dist/releases.html +136 -26
- package/widgets-dist/service-action.html +124 -14
- package/widgets-dist/service-create.html +123 -13
- package/widgets-dist/service-delete.html +120 -10
- package/widgets-dist/service-link.html +123 -13
- package/widgets-dist/services.html +120 -10
- package/widgets-dist/{np-panel.html → status.html} +135 -25
package/dist/tools/overview.js
CHANGED
|
@@ -14,7 +14,7 @@ const MAX_APPS = 30;
|
|
|
14
14
|
export const overviewTool = defineTool({
|
|
15
15
|
name: TOOL.organizationGet,
|
|
16
16
|
title: "Organization overview",
|
|
17
|
-
description:
|
|
17
|
+
description: 'A cross-application health digest for the WHOLE org, rendered as a panel: what\'s mid-rollout right now, and which scopes\' last deployment failed or rolled back. Use it ONLY for org-wide HEALTH questions that name no app — "is anything broken?", "what\'s deploying?", "any failed rollouts?". Scans up to the first ~30 applications (truncation is reported, never silent). This is HEALTH only — do NOT call it to understand org STRUCTURE or to gather context for CREATING an app (where apps live, which accounts/namespaces exist, where to place a new one): navigate that headlessly with entity_list (account → namespace → application), which renders no panel. Also does NOT show one app\'s detail (use application_get) or logs/metrics, and for the latest release/build ACROSS apps use entity_list and compute it. Read-only.',
|
|
18
18
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
19
19
|
widget: "overview",
|
|
20
20
|
errorKey: "overview.errorLabel",
|
package/dist/tools/params.js
CHANGED
|
@@ -24,7 +24,7 @@ function valueSummary(parameter, scopeNameById) {
|
|
|
24
24
|
export const paramsTool = defineTool({
|
|
25
25
|
name: TOOL.applicationParameterList,
|
|
26
26
|
title: "Parameters",
|
|
27
|
-
description:
|
|
27
|
+
description: 'List ONE application\'s configuration parameters and EVERY value each holds across scopes and dimensions (env vars / files; secret values are masked, never shown in plaintext), rendered as a PANEL. Use it for "what env vars does this app have", "show the config", "what\'s DATABASE_URL set to". Pass `scope` to have the PLATFORM resolve the single effective value per parameter for that scope (it collapses scope value › most-specific dimension match › app default — never reimplement that precedence yourself). Does NOT change anything — use application_parameter_create to add/update, application_parameter_delete to remove. Read-only.',
|
|
28
28
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
29
29
|
widget: "params",
|
|
30
30
|
errorKey: "params.errorLabel",
|
|
@@ -61,7 +61,13 @@ export const paramsTool = defineTool({
|
|
|
61
61
|
resolvedScope = { id: picked.scope.id, name: picked.scope.name };
|
|
62
62
|
readNrn = picked.scope.nrn;
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
// A scope read interpolates so the platform resolves the EFFECTIVE value per parameter (scope
|
|
65
|
+
// value › most-specific dimension match › app default) — app-level/dimension values inherit
|
|
66
|
+
// down. Without it a scope NRN returns only values pinned at that exact scope (usually none).
|
|
67
|
+
const parameters = await listParameters(context.np, readNrn, {
|
|
68
|
+
offset: args.offset,
|
|
69
|
+
interpolate: Boolean(resolvedScope),
|
|
70
|
+
});
|
|
65
71
|
if (parameters === undefined) {
|
|
66
72
|
return reply(translate("params.unavailable") + linkLine(translate("params.viewInDashboard"), dashboard), { available: false, ...appRef });
|
|
67
73
|
}
|
package/dist/tools/playbook.js
CHANGED
|
@@ -14,7 +14,7 @@ const playbookBody = (markdown) => markdown.replace(/^---\n[\s\S]*?\n---\n?/, ""
|
|
|
14
14
|
export const playbookGetTool = defineTool({
|
|
15
15
|
name: TOOL.playbookGet,
|
|
16
16
|
title: "Operating playbooks",
|
|
17
|
-
description:
|
|
17
|
+
description: 'Read a nullplatform operating PLAYBOOK — model-facing methodology for RISKY or MULTI-STEP work: deploying-safely (canary steps + metric gates for a production rollout), incident-response (triage when something is broken), configuring-safely (changing parameters or secrets carefully). Renders NO panel — prose for YOU to consult, NOT a step you must run before every action. Reach for it only when you genuinely want the grounded procedure for a non-trivial operation; for a direct, simple command ("deploy", "set X", "roll back") just use that action\'s tool — do not detour here first. Call with no `name` to list the catalog (also in the server instructions).',
|
|
18
18
|
annotations: { readOnlyHint: true },
|
|
19
19
|
// No widget: a playbook is model-facing methodology prose, not an interactive entity — it
|
|
20
20
|
// renders as text (the markdown below) for the model to read and act on. See CLAUDE.md.
|
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
|
|
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. Use to pick a release to deploy or promote, or to see what's shippable. Do NOT call this once per app to compare across apps (it renders a panel each time): for an AGGREGATE like 'the latest release across all my apps' use entity_list (entity:\"release\"), gather headlessly, and compute the answer.",
|
|
16
16
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
17
17
|
widget: "releases",
|
|
18
18
|
errorKey: "releaseList.errorLabel",
|
package/dist/tools/services.js
CHANGED
|
@@ -13,7 +13,7 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
13
13
|
export const servicesTool = defineTool({
|
|
14
14
|
name: TOOL.applicationServiceList,
|
|
15
15
|
title: "Services & dependencies",
|
|
16
|
-
description:
|
|
16
|
+
description: 'List the dependency services (databases, queues, caches…) attached to ONE application, plus the catalog of dependency types available to provision, rendered as a PANEL. Use it for "what databases/queues does this app use", "show its dependencies", "what can I provision". To provision a new dependency use application_service_create; to connect one to a scope use application_link_create. Read-only — it does not provision or link anything itself.',
|
|
17
17
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
18
|
widget: "services",
|
|
19
19
|
errorKey: "services.errorLabel",
|
package/dist/tools/set-params.js
CHANGED
|
@@ -5,10 +5,26 @@ import { listParameters, listScopes, setParameters } from "../np/journey.js";
|
|
|
5
5
|
import { defineTool, errorMessage, fail, reply } from "../tool.js";
|
|
6
6
|
import { TOOL } from "../tool-names.js";
|
|
7
7
|
import { appArg, pickScope, requireApp } from "./shared.js";
|
|
8
|
+
/** Honest upsert summary: "created N" / "updated N" / a mixed count — so a retry reads as "0 new". */
|
|
9
|
+
function paramSummary(result, app, names) {
|
|
10
|
+
if (result.updated === 0) {
|
|
11
|
+
return plural(result.created, "setParams.created.one", "setParams.created.many", { app, names });
|
|
12
|
+
}
|
|
13
|
+
if (result.created === 0) {
|
|
14
|
+
return plural(result.updated, "setParams.updated.one", "setParams.updated.many", { app, names });
|
|
15
|
+
}
|
|
16
|
+
return translate("setParams.mixed", {
|
|
17
|
+
created: result.created,
|
|
18
|
+
updated: result.updated,
|
|
19
|
+
total: result.created + result.updated,
|
|
20
|
+
app,
|
|
21
|
+
names,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
8
24
|
export const setParamsTool = defineTool({
|
|
9
25
|
name: TOOL.applicationParameterCreate,
|
|
10
26
|
title: "Set parameters",
|
|
11
|
-
description:
|
|
27
|
+
description: 'Create or UPDATE application configuration parameters — environment variables or files, secrets supported (stored masked, updatable; NOT write-once). A parameter is a value-SET: one definition holds many values keyed by target. Target a value at the APPLICATION (default — every scope), BY DIMENSION (e.g. environment=production — applies to any matching scope), or pinned to ONE scope (dimensions and a scope are mutually exclusive). Use it for "set DATABASE_URL", "add an env var", "change a secret", "set a prod-only value". Values apply on the NEXT deploy. Renders the parameters panel after applying. Convergent: reuses the existing definition and updates in place rather than duplicating. View current values with application_parameter_list; remove with application_parameter_delete.',
|
|
12
28
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
13
29
|
widget: "params",
|
|
14
30
|
errorKey: "setParams.errorLabel",
|
|
@@ -72,24 +88,7 @@ export const setParamsTool = defineTool({
|
|
|
72
88
|
const result = await setParameters(context.np, app.nrn, inputs);
|
|
73
89
|
const names = args.params.map((param) => `\`${param.name}\``).join(", ");
|
|
74
90
|
const total = result.created + result.updated;
|
|
75
|
-
|
|
76
|
-
const summary = result.updated === 0
|
|
77
|
-
? plural(result.created, "setParams.created.one", "setParams.created.many", {
|
|
78
|
-
app: app.name,
|
|
79
|
-
names,
|
|
80
|
-
})
|
|
81
|
-
: result.created === 0
|
|
82
|
-
? plural(result.updated, "setParams.updated.one", "setParams.updated.many", {
|
|
83
|
-
app: app.name,
|
|
84
|
-
names,
|
|
85
|
-
})
|
|
86
|
-
: translate("setParams.mixed", {
|
|
87
|
-
created: result.created,
|
|
88
|
-
updated: result.updated,
|
|
89
|
-
total,
|
|
90
|
-
app: app.name,
|
|
91
|
-
names,
|
|
92
|
-
});
|
|
91
|
+
const summary = paramSummary(result, app.name, names);
|
|
93
92
|
// Mirror the params widget's shape so the SAME panel renders the resulting set (with its
|
|
94
93
|
// scope picker + add affordance) after the write; the markdown stays the plain confirmation.
|
|
95
94
|
const parameters = await listParameters(context.np, app.nrn).catch(() => undefined);
|
package/dist/tools/shared.js
CHANGED
|
@@ -75,12 +75,64 @@ export async function requireApp(context, args) {
|
|
|
75
75
|
}
|
|
76
76
|
return { out: fail(notFoundMessage(resolution), { not_found: true }) };
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* The ONE pre-selection rule for every create/fill form selector (namespace, template, service spec,
|
|
80
|
+
* scope type…). It turns the model's soft inference — an explicit id, or the free-text stack/type it
|
|
81
|
+
* reasoned out of the conversation — into a concrete option, or `undefined` when it's a genuine
|
|
82
|
+
* unhinted choice. It NEVER returns an arbitrary first entry: a wrong pre-selection is worse than an
|
|
83
|
+
* honest blank. Division of labour (see CLAUDE.md): the MODEL infers (it holds the context) and passes
|
|
84
|
+
* the hint as a tool arg; the TOOL resolves it here; the WIDGET only mirrors the result.
|
|
85
|
+
*
|
|
86
|
+
* Order, most to least certain: explicit id → exact name → every query token present in the option's
|
|
87
|
+
* haystack (fuzzy "sql database") → an option name contained in the hint ("Golang Hexagonal - API" ⊃
|
|
88
|
+
* the "golang-hexagonal" template) → with `pinOnly`, the sole candidate. `haystack` widens the
|
|
89
|
+
* searchable text per field (name + tags, name + category…); it defaults to the option's name.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveChoice(options, hint, settings = {}) {
|
|
92
|
+
if (hint.id != null) {
|
|
93
|
+
const byId = options.find((option) => option.id === hint.id);
|
|
94
|
+
if (byId)
|
|
95
|
+
return byId;
|
|
96
|
+
}
|
|
97
|
+
const query = hint.text?.trim().toLowerCase();
|
|
98
|
+
if (query) {
|
|
99
|
+
const haystackOf = settings.haystack ?? ((option) => option.name);
|
|
100
|
+
const exact = options.filter((option) => option.name.trim().toLowerCase() === query);
|
|
101
|
+
if (exact.length === 1)
|
|
102
|
+
return exact[0];
|
|
103
|
+
const tokens = query.split(/\s+/).filter(Boolean);
|
|
104
|
+
const tokenMatch = options.filter((option) => {
|
|
105
|
+
const haystack = haystackOf(option).toLowerCase();
|
|
106
|
+
return tokens.every((token) => haystack.includes(token));
|
|
107
|
+
});
|
|
108
|
+
if (tokenMatch.length === 1)
|
|
109
|
+
return tokenMatch[0];
|
|
110
|
+
const contained = options.filter((option) => {
|
|
111
|
+
const name = option.name.trim().toLowerCase();
|
|
112
|
+
return name.length >= 2 && query.includes(name);
|
|
113
|
+
});
|
|
114
|
+
if (contained.length === 1)
|
|
115
|
+
return contained[0];
|
|
116
|
+
}
|
|
117
|
+
if (settings.pinOnly && options.length === 1)
|
|
118
|
+
return options[0];
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
78
121
|
/**
|
|
79
122
|
* Match scopes by name OR by dimensions: "dev" (name substring), "production"
|
|
80
123
|
* (dimension value substring), "environment=production" (exact key=value).
|
|
81
124
|
*/
|
|
82
125
|
export function matchScopes(scopes, wanted) {
|
|
83
126
|
const query = wanted.trim().toLowerCase();
|
|
127
|
+
// "#<id>" — an exact, unambiguous scope id. This is the form widgets send from their scope picker
|
|
128
|
+
// (the option value is the numeric id) and a documented `scope` arg form. Matched FIRST so a numeric
|
|
129
|
+
// id never falls through to name/substring matching — which can't see `id` and would return nothing,
|
|
130
|
+
// failing the call before it ever reads parameters (the widget scope-resolve bug).
|
|
131
|
+
const byId = /^#(\d+)$/.exec(query);
|
|
132
|
+
if (byId) {
|
|
133
|
+
const id = Number(byId[1]);
|
|
134
|
+
return scopes.filter((scope) => scope.id === id);
|
|
135
|
+
}
|
|
84
136
|
const keyValue = /^([a-z0-9_-]+)\s*=\s*(.+)$/.exec(query);
|
|
85
137
|
if (keyValue?.[1] && keyValue[2] !== undefined) {
|
|
86
138
|
const [, dimensionKey, dimensionValue] = keyValue;
|
package/dist/tools/status.js
CHANGED
|
@@ -25,9 +25,9 @@ async function scopeViews(context, applicationId) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Build an application's full status view (the
|
|
28
|
+
* Build an application's full status view (the app overview payload). Exported so write
|
|
29
29
|
* tools that should hand off into the same live panel — e.g. create-scope, after provisioning a
|
|
30
|
-
* new deploy target — can return this exact shape and let
|
|
30
|
+
* new deploy target — can return this exact shape and let the status panel render the app + Deploy.
|
|
31
31
|
*/
|
|
32
32
|
export async function buildAppStatus(context, app) {
|
|
33
33
|
const [views, builds, releases, orgSlug] = await Promise.all([
|
|
@@ -41,9 +41,9 @@ export async function buildAppStatus(context, app) {
|
|
|
41
41
|
export const statusTool = defineTool({
|
|
42
42
|
name: TOOL.applicationGet,
|
|
43
43
|
title: "Application status",
|
|
44
|
-
description:
|
|
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 instead of starting here; only read status first when you genuinely need to see state before acting. 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
|
-
widget: "
|
|
46
|
+
widget: "status",
|
|
47
47
|
errorKey: "status.errorLabel",
|
|
48
48
|
inputSchema: {
|
|
49
49
|
app: appArg,
|
package/dist/tools/traffic.js
CHANGED
|
@@ -6,12 +6,43 @@ import { renderRollout } from "../render.js";
|
|
|
6
6
|
import { defineTool, fail, reply } from "../tool.js";
|
|
7
7
|
import { TOOL } from "../tool-names.js";
|
|
8
8
|
import { appArg, delays, requireApp, sleep } from "./shared.js";
|
|
9
|
+
/**
|
|
10
|
+
* Find which deployment to steer. An explicit `deployment_id` wins; otherwise resolve the app and
|
|
11
|
+
* fan out across its scopes for the single non-terminal rollout. Mirrors requireApp's shape:
|
|
12
|
+
* returns { deploymentId, scope } or an `out` reply — a `fail` when nothing is in flight, or the
|
|
13
|
+
* disambiguation table when several rollouts are active at once (pass deployment_id to pick one).
|
|
14
|
+
*/
|
|
15
|
+
async function resolveActiveRollout(context, args) {
|
|
16
|
+
if (args.deployment_id)
|
|
17
|
+
return { deploymentId: args.deployment_id };
|
|
18
|
+
const resolved = await requireApp(context, args);
|
|
19
|
+
if ("out" in resolved)
|
|
20
|
+
return { out: resolved.out };
|
|
21
|
+
const scopes = await listScopes(context.np, resolved.app.id);
|
|
22
|
+
const activeRollouts = [];
|
|
23
|
+
for (const candidate of scopes) {
|
|
24
|
+
const deployments = await listScopeDeployments(context.np, candidate.id, 3);
|
|
25
|
+
const active = deployments.find((deployment) => !isDeploymentTerminal(deployment.status));
|
|
26
|
+
if (active)
|
|
27
|
+
activeRollouts.push({ deploymentId: active.id, scope: candidate, status: active.status });
|
|
28
|
+
}
|
|
29
|
+
if (activeRollouts.length === 0) {
|
|
30
|
+
return { out: fail(translate("traffic.noActive", { app: resolved.app.name })) };
|
|
31
|
+
}
|
|
32
|
+
if (activeRollouts.length > 1) {
|
|
33
|
+
return {
|
|
34
|
+
out: reply(`${translate("traffic.several")}\n\n${table([translate("header.deployment"), translate("header.scope"), translate("header.status")], activeRollouts.map((rollout) => [`#${rollout.deploymentId}`, rollout.scope.name, rollout.status]))}`),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const only = activeRollouts[0]; // length === 1 checked above
|
|
38
|
+
return { deploymentId: only.deploymentId, scope: only.scope };
|
|
39
|
+
}
|
|
9
40
|
export const trafficTool = defineTool({
|
|
10
41
|
name: TOOL.applicationDeploymentUpdate,
|
|
11
42
|
title: "Update traffic",
|
|
12
|
-
description: 'Drive an
|
|
43
|
+
description: 'Drive an IN-FLIGHT rollout — the step AFTER application_deployment_create. 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". Renders the live rollout panel. Call it ONLY for a single move the user explicitly named — in a ui client the rollout panel application_deployment_create renders already has a traffic slider, auto-advance (ramps to 100%) and finalize/rollback that drive this for the user; do NOT call this yourself to step 25 → 50 → 100 → finalize (each call stacks another rollout panel). Does NOT create a deployment — use application_deployment_create first; this only steers one that already exists.',
|
|
13
44
|
annotations: { destructiveHint: true, openWorldHint: true },
|
|
14
|
-
widget: "
|
|
45
|
+
widget: "status",
|
|
15
46
|
errorKey: "traffic.errorLabel",
|
|
16
47
|
inputSchema: {
|
|
17
48
|
app: appArg,
|
|
@@ -23,30 +54,11 @@ export const trafficTool = defineTool({
|
|
|
23
54
|
if (args.percent === undefined && !args.action) {
|
|
24
55
|
return fail(translate("traffic.sayWhat"));
|
|
25
56
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return resolved.out;
|
|
32
|
-
const scopes = await listScopes(context.np, resolved.app.id);
|
|
33
|
-
const activeRollouts = [];
|
|
34
|
-
for (const candidate of scopes) {
|
|
35
|
-
const deployments = await listScopeDeployments(context.np, candidate.id, 3);
|
|
36
|
-
const active = deployments.find((deployment) => !isDeploymentTerminal(deployment.status));
|
|
37
|
-
if (active)
|
|
38
|
-
activeRollouts.push({ deploymentId: active.id, scope: candidate, status: active.status });
|
|
39
|
-
}
|
|
40
|
-
if (activeRollouts.length === 0) {
|
|
41
|
-
return fail(translate("traffic.noActive", { app: resolved.app.name }));
|
|
42
|
-
}
|
|
43
|
-
if (activeRollouts.length > 1) {
|
|
44
|
-
return reply(`${translate("traffic.several")}\n\n${table([translate("header.deployment"), translate("header.scope"), translate("header.status")], activeRollouts.map((rollout) => [`#${rollout.deploymentId}`, rollout.scope.name, rollout.status]))}`);
|
|
45
|
-
}
|
|
46
|
-
const only = activeRollouts[0]; // length === 1 checked above
|
|
47
|
-
deploymentId = only.deploymentId;
|
|
48
|
-
scope = only.scope;
|
|
49
|
-
}
|
|
57
|
+
const rollout = await resolveActiveRollout(context, args);
|
|
58
|
+
if ("out" in rollout)
|
|
59
|
+
return rollout.out;
|
|
60
|
+
const deploymentId = rollout.deploymentId;
|
|
61
|
+
let scope = rollout.scope;
|
|
50
62
|
if (args.action === "finalize")
|
|
51
63
|
await deploymentAction(context.np, deploymentId, "finalize");
|
|
52
64
|
else if (args.action === "rollback")
|
|
@@ -15,7 +15,7 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
15
15
|
export const updateLinkTool = defineTool({
|
|
16
16
|
name: TOOL.applicationLinkUpdate,
|
|
17
17
|
title: "Run link action",
|
|
18
|
-
description:
|
|
18
|
+
description: 'Run a CUSTOM ACTION on a link (a service↔application connection) — the platform-team-defined operations its link specification exposes. Use it for "run the <action> on the <link>". Pass `link` (by name) and `action`; call WITHOUT `run:true` first to open the form, the user confirms by submitting. Not every link has custom actions. Renders the action panel. Not for creating (application_link_create) or deleting (application_link_delete) a link.',
|
|
19
19
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
20
20
|
widget: "service-action",
|
|
21
21
|
errorKey: "runAction.errorLabel",
|
|
@@ -16,7 +16,7 @@ import { appArg, requireApp } from "./shared.js";
|
|
|
16
16
|
export const updateServiceTool = defineTool({
|
|
17
17
|
name: TOOL.applicationServiceUpdate,
|
|
18
18
|
title: "Run service action",
|
|
19
|
-
description:
|
|
19
|
+
description: 'Run a CUSTOM ACTION on a provisioned service — the platform-team-defined operations a service exposes (e.g. a Postgres database\'s DML/DDL query runners). Use it for "run a query on <db>", "run the <action> on <service>". Pass `service` (by name) and `action`; call WITHOUT `run:true` first to open the form, the user confirms by submitting. A custom action is an explicit operation (NOT idempotent), so it never runs without that submit. Renders the action panel. Not for provisioning (application_service_create) or deleting (application_service_delete) a service.',
|
|
20
20
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
21
21
|
widget: "service-action",
|
|
22
22
|
errorKey: "runAction.errorLabel",
|
package/dist/ui.js
CHANGED
|
@@ -13,7 +13,7 @@ import { EXTENSION_ID, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY, registerAppRes
|
|
|
13
13
|
export { EXTENSION_ID, RESOURCE_MIME_TYPE };
|
|
14
14
|
/** Every widget this server can serve, by file name -> human title. */
|
|
15
15
|
export const WIDGETS = {
|
|
16
|
-
|
|
16
|
+
status: "Application panel",
|
|
17
17
|
"create-app": "Create application",
|
|
18
18
|
params: "Parameters",
|
|
19
19
|
logs: "Logs",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nullplatform/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "nullplatform from your code assistant — an MCP server that replaces the dashboard for the everyday developer journey",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "nullplatform",
|
|
@@ -41,14 +41,17 @@
|
|
|
41
41
|
"pretest": "node scripts/build-widgets.mjs",
|
|
42
42
|
"lint": "biome check .",
|
|
43
43
|
"lint:fix": "biome check --write .",
|
|
44
|
+
"eval": "tsx scripts/eval/run.ts",
|
|
44
45
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p src/widgets-react/tsconfig.json --noEmit"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
|
48
49
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
50
|
+
"pino": "^10.3.1",
|
|
49
51
|
"zod": "^3.23.8"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
54
|
+
"@anthropic-ai/sdk": "^0.105.0",
|
|
52
55
|
"@biomejs/biome": "2.4.16",
|
|
53
56
|
"@jsonforms/core": "^3.5.1",
|
|
54
57
|
"@jsonforms/react": "^3.5.1",
|
|
@@ -56,6 +59,7 @@
|
|
|
56
59
|
"@types/node": "^20.14.0",
|
|
57
60
|
"@types/react": "^19.2.17",
|
|
58
61
|
"@types/react-dom": "^19.2.3",
|
|
62
|
+
"dotenv": "^17.4.2",
|
|
59
63
|
"esbuild": "^0.28.0",
|
|
60
64
|
"happy-dom": "^20.10.3",
|
|
61
65
|
"preact": "^10.29.2",
|