@nullplatform/mcp 0.1.5 → 0.1.7
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/i18n.js +206 -22
- package/dist/md.js +14 -4
- package/dist/np/client.js +3 -0
- package/dist/np/journey.js +236 -24
- package/dist/surfaces/developer.js +1 -1
- package/dist/tool-names.js +7 -0
- package/dist/tools/action-flow.js +94 -0
- package/dist/tools/approvals.js +2 -2
- package/dist/tools/builds.js +23 -6
- package/dist/tools/create-link.js +163 -0
- package/dist/tools/create-service.js +149 -0
- package/dist/tools/delete-flow.js +30 -0
- package/dist/tools/delete-link.js +95 -0
- package/dist/tools/delete-param.js +76 -0
- package/dist/tools/delete-service.js +108 -0
- package/dist/tools/deployments.js +2 -2
- package/dist/tools/index.js +14 -0
- package/dist/tools/logs.js +2 -2
- package/dist/tools/metrics.js +4 -1
- package/dist/tools/overview.js +2 -2
- package/dist/tools/params.js +78 -17
- package/dist/tools/playbook.js +2 -1
- package/dist/tools/releases.js +2 -2
- package/dist/tools/services.js +2 -2
- package/dist/tools/set-params.js +61 -11
- package/dist/tools/shared.js +4 -0
- package/dist/tools/update-link.js +87 -0
- package/dist/tools/update-service.js +92 -0
- package/dist/ui.js +4 -1
- package/package.json +3 -1
- package/skills/starting-a-new-app/SKILL.md +71 -0
- package/widgets-dist/approvals.html +257 -53
- package/widgets-dist/builds.html +261 -57
- package/widgets-dist/create-app.html +252 -48
- package/widgets-dist/deployments.html +253 -49
- package/widgets-dist/find-apps.html +251 -47
- package/widgets-dist/logs.html +256 -52
- package/widgets-dist/manifest.json +16 -13
- package/widgets-dist/metrics.html +261 -57
- package/widgets-dist/np-panel.html +257 -53
- package/widgets-dist/overview.html +261 -57
- package/widgets-dist/params.html +257 -53
- package/widgets-dist/releases.html +259 -55
- package/widgets-dist/service-action.html +1118 -0
- package/widgets-dist/service-create.html +1117 -0
- package/widgets-dist/{playbook.html → service-delete.html} +258 -58
- package/widgets-dist/service-link.html +1117 -0
- package/widgets-dist/services.html +255 -51
package/dist/tools/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { approvalsTool } from "./approvals.js";
|
|
2
2
|
import { buildsTool } from "./builds.js";
|
|
3
3
|
import { createAppTool } from "./create-app.js";
|
|
4
|
+
import { createLinkTool } from "./create-link.js";
|
|
4
5
|
import { createReleaseTool } from "./create-release.js";
|
|
5
6
|
import { createScopeTool } from "./create-scope.js";
|
|
7
|
+
import { createServiceTool } from "./create-service.js";
|
|
8
|
+
import { deleteLinkTool } from "./delete-link.js";
|
|
9
|
+
import { deleteParamTool } from "./delete-param.js";
|
|
10
|
+
import { deleteServiceTool } from "./delete-service.js";
|
|
6
11
|
import { deployTool } from "./deploy.js";
|
|
7
12
|
import { deploymentsTool } from "./deployments.js";
|
|
8
13
|
import { findAppsTool } from "./find-apps.js";
|
|
@@ -16,6 +21,8 @@ import { servicesTool } from "./services.js";
|
|
|
16
21
|
import { setParamsTool } from "./set-params.js";
|
|
17
22
|
import { statusTool } from "./status.js";
|
|
18
23
|
import { trafficTool } from "./traffic.js";
|
|
24
|
+
import { updateLinkTool } from "./update-link.js";
|
|
25
|
+
import { updateServiceTool } from "./update-service.js";
|
|
19
26
|
/**
|
|
20
27
|
* The tool registry — the single extension point. To add a tool: create
|
|
21
28
|
* src/tools/<name>.ts exporting a defineTool({...}) and list it here. Its widget
|
|
@@ -39,5 +46,12 @@ export const tools = [
|
|
|
39
46
|
createScopeTool,
|
|
40
47
|
approvalsTool,
|
|
41
48
|
servicesTool,
|
|
49
|
+
createServiceTool,
|
|
50
|
+
updateServiceTool,
|
|
51
|
+
deleteServiceTool,
|
|
52
|
+
createLinkTool,
|
|
53
|
+
updateLinkTool,
|
|
54
|
+
deleteLinkTool,
|
|
55
|
+
deleteParamTool,
|
|
42
56
|
playbookGetTool,
|
|
43
57
|
];
|
package/dist/tools/logs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { translate } from "../i18n.js";
|
|
2
|
+
import { plural, translate } from "../i18n.js";
|
|
3
3
|
import { dashboardLink, linkLine, next } from "../md.js";
|
|
4
4
|
import { listScopes, readLogs } from "../np/journey.js";
|
|
5
5
|
import { defineTool, fail, reply } from "../tool.js";
|
|
@@ -71,7 +71,7 @@ export const logsTool = defineTool({
|
|
|
71
71
|
return `${prefix}${(entry.message ?? "").trimEnd()}`;
|
|
72
72
|
})
|
|
73
73
|
.join("\n");
|
|
74
|
-
const markdown = `${
|
|
74
|
+
const markdown = `${plural(entries.length, "logs.lastLines.one", "logs.lastLines.many", { app: app.name, scope: scopeSuffix })}\n\n\`\`\`\n${body}\n\`\`\``;
|
|
75
75
|
return reply(markdown, {
|
|
76
76
|
count: entries.length,
|
|
77
77
|
next_page_token: page.next_page_token ?? null,
|
package/dist/tools/metrics.js
CHANGED
|
@@ -34,6 +34,8 @@ export const metricsTool = defineTool({
|
|
|
34
34
|
if ("out" in picked)
|
|
35
35
|
return picked.out;
|
|
36
36
|
const scope = picked.scope;
|
|
37
|
+
// The full scope list rides along so the widget can offer a scope switcher (same as logs).
|
|
38
|
+
const scopeList = scopes.map((candidate) => ({ name: candidate.name, status: candidate.status }));
|
|
37
39
|
const window = args.window ?? "3h";
|
|
38
40
|
const series = await readGoldenMetrics(context.bff, {
|
|
39
41
|
application_id: app.id,
|
|
@@ -45,7 +47,7 @@ export const metricsTool = defineTool({
|
|
|
45
47
|
if (withData.length === 0) {
|
|
46
48
|
const orgSlug = await context.org.organizationSlug();
|
|
47
49
|
return reply(translate("metrics.empty", { app: app.name, scope: scope.name, window }) +
|
|
48
|
-
linkLine(translate("metrics.openInDashboard"), dashboardLink(orgSlug, scope.nrn)), { app: `#${app.id}`, scope: scope.name, window, series: [] });
|
|
50
|
+
linkLine(translate("metrics.openInDashboard"), dashboardLink(orgSlug, scope.nrn)), { app: `#${app.id}`, app_name: app.name, scope: scope.name, scopes: scopeList, window, series: [] });
|
|
49
51
|
}
|
|
50
52
|
const markdown = [
|
|
51
53
|
translate("metrics.title", { app: app.name, scope: scope.name, window }),
|
|
@@ -68,6 +70,7 @@ export const metricsTool = defineTool({
|
|
|
68
70
|
app: `#${app.id}`,
|
|
69
71
|
app_name: app.name,
|
|
70
72
|
scope: scope.name,
|
|
73
|
+
scopes: scopeList,
|
|
71
74
|
window,
|
|
72
75
|
series: series.map((metric) => ({
|
|
73
76
|
id: metric.id,
|
package/dist/tools/overview.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { translate } from "../i18n.js";
|
|
2
|
+
import { plural, translate } from "../i18n.js";
|
|
3
3
|
import { ago, glyph, next, table } from "../md.js";
|
|
4
4
|
import { pmap } from "../np/context.js";
|
|
5
5
|
import { isDeploymentTerminal, listScopeDeployments, listScopes } from "../np/journey.js";
|
|
@@ -66,7 +66,7 @@ export const overviewTool = defineTool({
|
|
|
66
66
|
});
|
|
67
67
|
function renderOverview(args) {
|
|
68
68
|
const { active, trouble, scanned, truncated } = args;
|
|
69
|
-
const lines = [
|
|
69
|
+
const lines = [plural(scanned, "overview.title.one", "overview.title.many")];
|
|
70
70
|
if (truncated)
|
|
71
71
|
lines.push(translate("overview.truncated", { count: MAX_APPS }));
|
|
72
72
|
lines.push("");
|
package/dist/tools/params.js
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { plural, translate } from "../i18n.js";
|
|
2
2
|
import { dashboardLink, linkLine, next, table } from "../md.js";
|
|
3
|
-
import { listParameters } from "../np/journey.js";
|
|
3
|
+
import { listParameters, listScopes } from "../np/journey.js";
|
|
4
4
|
import { defineTool, fail, reply } from "../tool.js";
|
|
5
5
|
import { TOOL } from "../tool-names.js";
|
|
6
|
-
import { appArg, offsetArg, pageOf, requireApp } from "./shared.js";
|
|
6
|
+
import { appArg, dimsLabel, offsetArg, pageOf, pickScope, requireApp, scopeArg } from "./shared.js";
|
|
7
|
+
/** Where a value applies, in words: the base (Application), a dimension set, or a named scope. */
|
|
8
|
+
function contextLabel(value, scopeNameById) {
|
|
9
|
+
if (value.scope_id) {
|
|
10
|
+
return translate("params.ctxScope", { scope: scopeNameById.get(value.scope_id) ?? `#${value.scope_id}` });
|
|
11
|
+
}
|
|
12
|
+
if (value.dimensions)
|
|
13
|
+
return dimsLabel(value.dimensions);
|
|
14
|
+
return translate("params.ctxApplication");
|
|
15
|
+
}
|
|
16
|
+
/** The full value set of a parameter, summarized as "context: value" pairs for the text table. */
|
|
17
|
+
function valueSummary(parameter, scopeNameById) {
|
|
18
|
+
if (!parameter.values.length)
|
|
19
|
+
return translate("params.unset");
|
|
20
|
+
return parameter.values
|
|
21
|
+
.map((value) => `${contextLabel(value, scopeNameById)}: ${value.value ?? translate("params.unset")}`)
|
|
22
|
+
.join(" · ");
|
|
23
|
+
}
|
|
7
24
|
export const paramsTool = defineTool({
|
|
8
25
|
name: TOOL.applicationParameterList,
|
|
9
26
|
title: "Parameters",
|
|
10
|
-
description: "List an application's configuration parameters (env vars / files; secret values are masked). Use application_parameter_create to add or change them.",
|
|
27
|
+
description: "List an application's configuration parameters and every value they hold across scopes and dimensions (env vars / files; secret values are masked). Pass `scope` to resolve the single effective value per parameter for that scope. Use application_parameter_create to add or change them.",
|
|
11
28
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12
29
|
widget: "params",
|
|
13
30
|
errorKey: "params.errorLabel",
|
|
14
|
-
inputSchema: { app: appArg, offset: offsetArg },
|
|
31
|
+
inputSchema: { app: appArg, scope: scopeArg, offset: offsetArg },
|
|
15
32
|
async handler(args, context) {
|
|
16
33
|
const resolved = await requireApp(context, args);
|
|
17
34
|
if ("out" in resolved)
|
|
@@ -19,12 +36,32 @@ export const paramsTool = defineTool({
|
|
|
19
36
|
const app = resolved.app;
|
|
20
37
|
if (!app.nrn)
|
|
21
38
|
return fail(translate("resolve.noNrn", { app: app.name }));
|
|
22
|
-
const [
|
|
23
|
-
|
|
39
|
+
const [scopes, orgSlug] = await Promise.all([
|
|
40
|
+
listScopes(context.np, app.id),
|
|
24
41
|
context.org.organizationSlug(),
|
|
25
42
|
]);
|
|
43
|
+
const scopeNameById = new Map(scopes.map((scope) => [scope.id, scope.name]));
|
|
26
44
|
const dashboard = dashboardLink(orgSlug, app.nrn);
|
|
27
|
-
|
|
45
|
+
// scopes ride along so the widget can build its scope picker AND derive the by-dimension targets
|
|
46
|
+
const appRef = {
|
|
47
|
+
app: `#${app.id}`,
|
|
48
|
+
app_name: app.name,
|
|
49
|
+
scopes: scopes.map((scope) => ({ id: scope.id, name: scope.name, dimensions: scope.dimensions ?? {} })),
|
|
50
|
+
};
|
|
51
|
+
// With a scope, read at the scope NRN so the platform collapses each parameter to its single
|
|
52
|
+
// effective value (scope value › most-specific dimension match › app default).
|
|
53
|
+
let resolvedScope;
|
|
54
|
+
let readNrn = app.nrn;
|
|
55
|
+
if (args.scope) {
|
|
56
|
+
const picked = pickScope(scopes, args.scope);
|
|
57
|
+
if ("out" in picked)
|
|
58
|
+
return picked.out;
|
|
59
|
+
if (!picked.scope.nrn)
|
|
60
|
+
return fail(translate("resolve.noNrn", { app: picked.scope.name }));
|
|
61
|
+
resolvedScope = { id: picked.scope.id, name: picked.scope.name };
|
|
62
|
+
readNrn = picked.scope.nrn;
|
|
63
|
+
}
|
|
64
|
+
const parameters = await listParameters(context.np, readNrn, { offset: args.offset });
|
|
28
65
|
if (parameters === undefined) {
|
|
29
66
|
return reply(translate("params.unavailable") + linkLine(translate("params.viewInDashboard"), dashboard), { available: false, ...appRef });
|
|
30
67
|
}
|
|
@@ -35,6 +72,35 @@ export const paramsTool = defineTool({
|
|
|
35
72
|
...appRef,
|
|
36
73
|
});
|
|
37
74
|
}
|
|
75
|
+
const payload = {
|
|
76
|
+
available: true,
|
|
77
|
+
params: parameters,
|
|
78
|
+
page: pageOf(parameters.length, 100, args.offset ?? 0),
|
|
79
|
+
...(resolvedScope ? { resolved_scope: resolvedScope } : {}),
|
|
80
|
+
...appRef,
|
|
81
|
+
};
|
|
82
|
+
if (resolvedScope) {
|
|
83
|
+
const markdown = [
|
|
84
|
+
translate("params.effectiveFor", { app: app.name, scope: resolvedScope.name }),
|
|
85
|
+
"",
|
|
86
|
+
table([
|
|
87
|
+
translate("header.name"),
|
|
88
|
+
translate("header.variable"),
|
|
89
|
+
translate("header.value"),
|
|
90
|
+
translate("header.source"),
|
|
91
|
+
], parameters.map((parameter) => {
|
|
92
|
+
const effective = parameter.values[0];
|
|
93
|
+
return [
|
|
94
|
+
parameter.name,
|
|
95
|
+
parameter.variable ?? parameter.destination_path ?? "",
|
|
96
|
+
effective?.value ?? translate("params.unset"),
|
|
97
|
+
effective ? contextLabel(effective, scopeNameById) : "—",
|
|
98
|
+
];
|
|
99
|
+
})),
|
|
100
|
+
next(translate("params.applyHint")),
|
|
101
|
+
].join("\n");
|
|
102
|
+
return reply(markdown, payload);
|
|
103
|
+
}
|
|
38
104
|
const markdown = [
|
|
39
105
|
plural(parameters.length, "params.count.one", "params.count.many", { app: app.name }),
|
|
40
106
|
"",
|
|
@@ -43,21 +109,16 @@ export const paramsTool = defineTool({
|
|
|
43
109
|
translate("header.variable"),
|
|
44
110
|
translate("header.type"),
|
|
45
111
|
translate("header.secret"),
|
|
46
|
-
translate("header.
|
|
112
|
+
translate("header.values"),
|
|
47
113
|
], parameters.map((parameter) => [
|
|
48
114
|
parameter.name,
|
|
49
|
-
parameter.variable ?? "",
|
|
50
|
-
parameter.type ?? "
|
|
115
|
+
parameter.variable ?? parameter.destination_path ?? "",
|
|
116
|
+
parameter.type ?? "environment",
|
|
51
117
|
parameter.secret ? "🔒" : "",
|
|
52
|
-
parameter
|
|
118
|
+
valueSummary(parameter, scopeNameById),
|
|
53
119
|
])),
|
|
54
120
|
next(translate("params.applyHint")),
|
|
55
121
|
].join("\n");
|
|
56
|
-
return reply(markdown,
|
|
57
|
-
available: true,
|
|
58
|
-
params: parameters,
|
|
59
|
-
page: pageOf(parameters.length, 100, args.offset ?? 0),
|
|
60
|
-
...appRef,
|
|
61
|
-
});
|
|
122
|
+
return reply(markdown, payload);
|
|
62
123
|
},
|
|
63
124
|
});
|
package/dist/tools/playbook.js
CHANGED
|
@@ -16,7 +16,8 @@ export const playbookGetTool = defineTool({
|
|
|
16
16
|
title: "Operating playbooks",
|
|
17
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
|
-
widget:
|
|
19
|
+
// No widget: a playbook is model-facing methodology prose, not an interactive entity — it
|
|
20
|
+
// renders as text (the markdown below) for the model to read and act on. See CLAUDE.md.
|
|
20
21
|
errorKey: "playbook.errorLabel",
|
|
21
22
|
inputSchema: {
|
|
22
23
|
name: z.string().optional().describe('Playbook name, e.g. "deploying-safely". Omit to list them.'),
|
package/dist/tools/releases.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { translate } from "../i18n.js";
|
|
2
|
+
import { plural, translate } from "../i18n.js";
|
|
3
3
|
import { ago, glyph, next, table } from "../md.js";
|
|
4
4
|
import { listReleases } from "../np/journey.js";
|
|
5
5
|
import { defineTool, reply } from "../tool.js";
|
|
@@ -34,7 +34,7 @@ export const releasesTool = defineTool({
|
|
|
34
34
|
return reply(translate("releaseList.none", { app: app.name }) + next(translate("releaseList.noneHint")), { app: `#${app.id}`, app_name: app.name, releases: [] });
|
|
35
35
|
}
|
|
36
36
|
const markdown = [
|
|
37
|
-
|
|
37
|
+
plural(releases.length, "releaseList.title.one", "releaseList.title.many", { app: app.name }),
|
|
38
38
|
"",
|
|
39
39
|
table([
|
|
40
40
|
translate("header.version"),
|
package/dist/tools/services.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { translate } from "../i18n.js";
|
|
1
|
+
import { plural, translate } from "../i18n.js";
|
|
2
2
|
import { dashboardLink, glyph, linkLine, next, table } from "../md.js";
|
|
3
3
|
import { listServiceSpecifications, listServices } from "../np/journey.js";
|
|
4
4
|
import { defineTool, fail, reply } from "../tool.js";
|
|
@@ -37,7 +37,7 @@ export const servicesTool = defineTool({
|
|
|
37
37
|
sections.push(translate("services.none", { app: app.name }));
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
40
|
-
sections.push(
|
|
40
|
+
sections.push(plural(services.length, "services.attached.one", "services.attached.many", { app: app.name }));
|
|
41
41
|
sections.push("");
|
|
42
42
|
sections.push(table([translate("header.name"), translate("services.specification"), translate("header.status")], services.map((service) => [
|
|
43
43
|
service.name,
|
package/dist/tools/set-params.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { translate } from "../i18n.js";
|
|
2
|
+
import { plural, translate } from "../i18n.js";
|
|
3
3
|
import { next } from "../md.js";
|
|
4
|
-
import { listParameters, setParameters } from "../np/journey.js";
|
|
4
|
+
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
|
-
import { appArg, requireApp } from "./shared.js";
|
|
7
|
+
import { appArg, pickScope, requireApp } from "./shared.js";
|
|
8
8
|
export const setParamsTool = defineTool({
|
|
9
9
|
name: TOOL.applicationParameterCreate,
|
|
10
10
|
title: "Set parameters",
|
|
11
|
-
description: "Create or update application configuration parameters (environment variables or files; mark secrets). Values apply on the NEXT deploy.",
|
|
11
|
+
description: "Create or update application configuration parameters (environment variables or files; mark secrets). Target a value at the application (default — every scope), by dimension (applies to any matching scope), or pinned to one scope. Values apply on the NEXT deploy.",
|
|
12
12
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
13
13
|
widget: "params",
|
|
14
14
|
errorKey: "setParams.errorLabel",
|
|
@@ -18,8 +18,19 @@ export const setParamsTool = defineTool({
|
|
|
18
18
|
.array(z.object({
|
|
19
19
|
name: z.string().describe("Variable name, e.g. DATABASE_URL"),
|
|
20
20
|
value: z.string(),
|
|
21
|
-
secret: z
|
|
21
|
+
secret: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Mask the value — only honored when first creating the parameter (secrecy is immutable after)"),
|
|
22
25
|
type: z.enum(["ENVIRONMENT", "FILE"]).optional().describe("Default ENVIRONMENT"),
|
|
26
|
+
scope: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Pin this value to ONE scope (name, "#id", or "key=value") — the last-resort target. Do not combine with dimensions.'),
|
|
30
|
+
dimensions: z
|
|
31
|
+
.record(z.string())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Target by dimension, e.g. {"environment":"production"} — the value then applies to any scope whose dimensions match. Do not combine with scope.'),
|
|
23
34
|
}))
|
|
24
35
|
.min(1),
|
|
25
36
|
},
|
|
@@ -30,23 +41,57 @@ export const setParamsTool = defineTool({
|
|
|
30
41
|
const app = resolved.app;
|
|
31
42
|
if (!app.nrn)
|
|
32
43
|
return fail(translate("resolve.noNrn", { app: app.name }));
|
|
44
|
+
// A scope NRN already locates the value; dimensions only apply at the application NRN. The
|
|
45
|
+
// platform rejects scope-NRN + dimensions (SCOPE_NRN_DIMENSIONS_AMBIGUITY) — catch it early.
|
|
46
|
+
const conflict = args.params.find((param) => param.scope && param.dimensions && Object.keys(param.dimensions).length);
|
|
47
|
+
if (conflict)
|
|
48
|
+
return fail(translate("setParams.scopeDimConflict", { name: conflict.name }));
|
|
49
|
+
// One scope fetch — used both to resolve scope refs to NRNs and to feed the widget's picker.
|
|
50
|
+
const scopes = await listScopes(context.np, app.id).catch(() => []);
|
|
51
|
+
const inputs = [];
|
|
52
|
+
for (const param of args.params) {
|
|
53
|
+
let nrn;
|
|
54
|
+
if (param.scope) {
|
|
55
|
+
const picked = pickScope(scopes, param.scope);
|
|
56
|
+
if ("out" in picked)
|
|
57
|
+
return picked.out;
|
|
58
|
+
if (!picked.scope.nrn)
|
|
59
|
+
return fail(translate("resolve.noNrn", { app: picked.scope.name }));
|
|
60
|
+
nrn = picked.scope.nrn;
|
|
61
|
+
}
|
|
62
|
+
inputs.push({
|
|
63
|
+
name: param.name,
|
|
64
|
+
value: param.value,
|
|
65
|
+
secret: param.secret,
|
|
66
|
+
type: param.type,
|
|
67
|
+
nrn,
|
|
68
|
+
dimensions: param.dimensions,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
33
71
|
try {
|
|
34
|
-
const result = await setParameters(context.np, app.nrn,
|
|
35
|
-
const names = args.params.map((
|
|
72
|
+
const result = await setParameters(context.np, app.nrn, inputs);
|
|
73
|
+
const names = args.params.map((param) => `\`${param.name}\``).join(", ");
|
|
36
74
|
const total = result.created + result.updated;
|
|
37
75
|
// Honest about upsert: say how many were new vs changed so a retry reads as "0 new".
|
|
38
76
|
const summary = result.updated === 0
|
|
39
|
-
?
|
|
77
|
+
? plural(result.created, "setParams.created.one", "setParams.created.many", {
|
|
78
|
+
app: app.name,
|
|
79
|
+
names,
|
|
80
|
+
})
|
|
40
81
|
: result.created === 0
|
|
41
|
-
?
|
|
82
|
+
? plural(result.updated, "setParams.updated.one", "setParams.updated.many", {
|
|
83
|
+
app: app.name,
|
|
84
|
+
names,
|
|
85
|
+
})
|
|
42
86
|
: translate("setParams.mixed", {
|
|
43
87
|
created: result.created,
|
|
44
88
|
updated: result.updated,
|
|
89
|
+
total,
|
|
45
90
|
app: app.name,
|
|
46
91
|
names,
|
|
47
92
|
});
|
|
48
|
-
// Mirror the params widget's shape so the SAME panel renders the resulting set with its
|
|
49
|
-
// add
|
|
93
|
+
// Mirror the params widget's shape so the SAME panel renders the resulting set (with its
|
|
94
|
+
// scope picker + add affordance) after the write; the markdown stays the plain confirmation.
|
|
50
95
|
const parameters = await listParameters(context.np, app.nrn).catch(() => undefined);
|
|
51
96
|
return reply(summary + next(translate("setParams.applyHint")), {
|
|
52
97
|
created: result.created,
|
|
@@ -54,6 +99,11 @@ export const setParamsTool = defineTool({
|
|
|
54
99
|
total,
|
|
55
100
|
app: `#${app.id}`,
|
|
56
101
|
app_name: app.name,
|
|
102
|
+
scopes: scopes.map((scope) => ({
|
|
103
|
+
id: scope.id,
|
|
104
|
+
name: scope.name,
|
|
105
|
+
dimensions: scope.dimensions ?? {},
|
|
106
|
+
})),
|
|
57
107
|
...(parameters === undefined ? { available: false } : { available: true, params: parameters }),
|
|
58
108
|
});
|
|
59
109
|
}
|
package/dist/tools/shared.js
CHANGED
|
@@ -15,6 +15,10 @@ export const offsetArg = z
|
|
|
15
15
|
.number()
|
|
16
16
|
.optional()
|
|
17
17
|
.describe("Skip this many items to page through a long list (use page.next_offset from a prior call).");
|
|
18
|
+
export const scopeArg = z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Scope name, "#id", or a dimension match like "environment=production". Omit to list every value across all scopes and dimensions.');
|
|
18
22
|
/**
|
|
19
23
|
* Pagination token for a list reply. No list endpoint returns a total, so the boundary is the
|
|
20
24
|
* standard offset heuristic: a full page means there is likely more. The widget/model pages by
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { translate } from "../i18n.js";
|
|
3
|
+
import { dashboardLink, next, table } from "../md.js";
|
|
4
|
+
import { createLinkAction, getLinkAction, linkActionSpecs, listLinks } from "../np/journey.js";
|
|
5
|
+
import { defineTool, fail, reply } from "../tool.js";
|
|
6
|
+
import { TOOL } from "../tool-names.js";
|
|
7
|
+
import { runActionFlow } from "./action-flow.js";
|
|
8
|
+
import { appArg, requireApp } from "./shared.js";
|
|
9
|
+
/**
|
|
10
|
+
* Run a custom action on a link — the platform-team-defined operations a link specification exposes
|
|
11
|
+
* under "Run action". Not every link spec has custom actions (many only have create/delete). Creating
|
|
12
|
+
* a link is `application_link_create`, removing it is `application_link_delete`. Form-first, same as
|
|
13
|
+
* the service variant.
|
|
14
|
+
*/
|
|
15
|
+
export const updateLinkTool = defineTool({
|
|
16
|
+
name: TOOL.applicationLinkUpdate,
|
|
17
|
+
title: "Run link action",
|
|
18
|
+
description: "Run a custom action on a link (a service↔application connection) — the platform-team-defined operations its link specification exposes. Pass `link` to choose which one (by name) and `action` to choose which action; call WITHOUT `run:true` first to open the form. Not every link has custom actions.",
|
|
19
|
+
annotations: { destructiveHint: false, openWorldHint: true },
|
|
20
|
+
widget: "service-action",
|
|
21
|
+
errorKey: "runAction.errorLabel",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
app: appArg,
|
|
24
|
+
link: z.string().optional().describe("Which link to operate, by name (or the link id)"),
|
|
25
|
+
action: z.string().optional().describe("Which custom action to run, by name (or its slug)"),
|
|
26
|
+
parameters: z
|
|
27
|
+
.record(z.unknown())
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Values for the action parameters (per the action spec's schema shown in the form)"),
|
|
30
|
+
run: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Set true to actually run the action (the form's submit sets it). Omit to return the form."),
|
|
34
|
+
},
|
|
35
|
+
async handler(args, context) {
|
|
36
|
+
const resolved = await requireApp(context, args);
|
|
37
|
+
if ("out" in resolved)
|
|
38
|
+
return resolved.out;
|
|
39
|
+
const app = resolved.app;
|
|
40
|
+
if (!app.nrn)
|
|
41
|
+
return fail(translate("resolve.noNrn", { app: app.name }));
|
|
42
|
+
const orgSlug = await context.org.organizationSlug();
|
|
43
|
+
const dashboard = dashboardLink(orgSlug, app.nrn);
|
|
44
|
+
// The app's links — pick which one to operate.
|
|
45
|
+
const links = (await listLinks(context.np, app.nrn)).filter((link) => link.status !== "failed");
|
|
46
|
+
if (links.length === 0) {
|
|
47
|
+
return fail(translate("runAction.noLinks", { app: app.name }) + next(translate("runAction.noLinksHint")));
|
|
48
|
+
}
|
|
49
|
+
const wanted = args.link?.toLowerCase();
|
|
50
|
+
const matched = wanted
|
|
51
|
+
? links.filter((link) => link.name.toLowerCase().includes(wanted) || link.id === args.link)
|
|
52
|
+
: [];
|
|
53
|
+
if (matched.length !== 1) {
|
|
54
|
+
const list = matched.length > 1 ? matched : links;
|
|
55
|
+
const md = `${translate(matched.length > 1 ? "runAction.linkAmbiguous" : "runAction.pickLink", {
|
|
56
|
+
app: app.name,
|
|
57
|
+
})}\n\n${table([translate("header.name"), translate("header.status")], list.map((link) => [link.name, link.status]))}${next(translate("runAction.pickLinkHint"))}`;
|
|
58
|
+
return reply(md, {
|
|
59
|
+
mode: "pick-target",
|
|
60
|
+
entity: "link",
|
|
61
|
+
tool: TOOL.applicationLinkUpdate,
|
|
62
|
+
app: `#${app.id}`,
|
|
63
|
+
app_name: app.name,
|
|
64
|
+
targets: list,
|
|
65
|
+
dashboard,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const link = matched[0];
|
|
69
|
+
const specs = link.specification_id
|
|
70
|
+
? (await linkActionSpecs(context.np, link.specification_id)).filter((spec) => spec.type === "custom")
|
|
71
|
+
: [];
|
|
72
|
+
const config = {
|
|
73
|
+
tool: TOOL.applicationLinkUpdate,
|
|
74
|
+
entity: "link",
|
|
75
|
+
appRef: `#${app.id}`,
|
|
76
|
+
appName: app.name,
|
|
77
|
+
entityId: link.id,
|
|
78
|
+
entityName: link.name,
|
|
79
|
+
entityStatus: link.status,
|
|
80
|
+
specs,
|
|
81
|
+
dashboard,
|
|
82
|
+
run: (input) => createLinkAction(context.np, link.id, input),
|
|
83
|
+
poll: (actionId) => getLinkAction(context.np, link.id, actionId),
|
|
84
|
+
};
|
|
85
|
+
return runActionFlow(config, args);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { translate } from "../i18n.js";
|
|
3
|
+
import { dashboardLink, next, table } from "../md.js";
|
|
4
|
+
import { createServiceAction, getServiceAction, listAppServices, serviceActionSpecs } from "../np/journey.js";
|
|
5
|
+
import { defineTool, fail, reply } from "../tool.js";
|
|
6
|
+
import { TOOL } from "../tool-names.js";
|
|
7
|
+
import { runActionFlow } from "./action-flow.js";
|
|
8
|
+
import { appArg, requireApp } from "./shared.js";
|
|
9
|
+
/**
|
|
10
|
+
* Run a custom action on a provisioned service — the platform-team-defined ad-hoc operations the
|
|
11
|
+
* dashboard surfaces under "Run action" (e.g. a Postgres "Run DML Query" / "Run DDL Query"). These
|
|
12
|
+
* are the action specs of `type: custom`; provisioning (`create`) is `application_service_create`,
|
|
13
|
+
* and deprovisioning (`delete`) is `application_service_delete`. Form-first: nothing runs until the
|
|
14
|
+
* user submits, because a custom action is an explicit, non-idempotent operation.
|
|
15
|
+
*/
|
|
16
|
+
export const updateServiceTool = defineTool({
|
|
17
|
+
name: TOOL.applicationServiceUpdate,
|
|
18
|
+
title: "Run service action",
|
|
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). Pass `service` to choose which one (by name) and `action` to choose which action; call WITHOUT `run:true` first to open the form, the user confirms by submitting it. A custom action is an explicit operation (not idempotent), so it never runs without that submit.",
|
|
20
|
+
annotations: { destructiveHint: false, openWorldHint: true },
|
|
21
|
+
widget: "service-action",
|
|
22
|
+
errorKey: "runAction.errorLabel",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
app: appArg,
|
|
25
|
+
service: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Which provisioned service to operate, by name (or the service id)"),
|
|
29
|
+
action: z.string().optional().describe("Which custom action to run, by name (or its slug)"),
|
|
30
|
+
parameters: z
|
|
31
|
+
.record(z.unknown())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Values for the action parameters (per the action spec's schema shown in the form)"),
|
|
34
|
+
run: z
|
|
35
|
+
.boolean()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Set true to actually run the action (the form's submit sets it). Omit to return the form."),
|
|
38
|
+
},
|
|
39
|
+
async handler(args, context) {
|
|
40
|
+
const resolved = await requireApp(context, args);
|
|
41
|
+
if ("out" in resolved)
|
|
42
|
+
return resolved.out;
|
|
43
|
+
const app = resolved.app;
|
|
44
|
+
if (!app.nrn)
|
|
45
|
+
return fail(translate("resolve.noNrn", { app: app.name }));
|
|
46
|
+
const orgSlug = await context.org.organizationSlug();
|
|
47
|
+
const dashboard = dashboardLink(orgSlug, app.nrn);
|
|
48
|
+
// The app's provisioned services — pick which one to operate.
|
|
49
|
+
const services = (await listAppServices(context.np, app.nrn)).filter((service) => service.status !== "failed");
|
|
50
|
+
if (services.length === 0) {
|
|
51
|
+
return fail(translate("runAction.noServices", { app: app.name }) + next(translate("runAction.noServicesHint")));
|
|
52
|
+
}
|
|
53
|
+
const wanted = args.service?.toLowerCase();
|
|
54
|
+
const matched = wanted
|
|
55
|
+
? services.filter((service) => service.name.toLowerCase().includes(wanted) || service.id === args.service)
|
|
56
|
+
: [];
|
|
57
|
+
if (matched.length !== 1) {
|
|
58
|
+
const list = matched.length > 1 ? matched : services;
|
|
59
|
+
const md = `${translate(matched.length > 1 ? "runAction.serviceAmbiguous" : "runAction.pickService", {
|
|
60
|
+
app: app.name,
|
|
61
|
+
})}\n\n${table([translate("header.name"), translate("header.status")], list.map((service) => [service.name, service.status]))}${next(translate("runAction.pickServiceHint"))}`;
|
|
62
|
+
return reply(md, {
|
|
63
|
+
mode: "pick-target",
|
|
64
|
+
entity: "service",
|
|
65
|
+
tool: TOOL.applicationServiceUpdate,
|
|
66
|
+
app: `#${app.id}`,
|
|
67
|
+
app_name: app.name,
|
|
68
|
+
targets: list,
|
|
69
|
+
dashboard,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const service = matched[0];
|
|
73
|
+
// Only `custom` action specs are user-runnable (create/update/delete drive the lifecycle).
|
|
74
|
+
const specs = service.specification_id
|
|
75
|
+
? (await serviceActionSpecs(context.np, service.specification_id)).filter((spec) => spec.type === "custom")
|
|
76
|
+
: [];
|
|
77
|
+
const config = {
|
|
78
|
+
tool: TOOL.applicationServiceUpdate,
|
|
79
|
+
entity: "service",
|
|
80
|
+
appRef: `#${app.id}`,
|
|
81
|
+
appName: app.name,
|
|
82
|
+
entityId: service.id,
|
|
83
|
+
entityName: service.name,
|
|
84
|
+
entityStatus: service.status,
|
|
85
|
+
specs,
|
|
86
|
+
dashboard,
|
|
87
|
+
run: (input) => createServiceAction(context.np, service.id, input),
|
|
88
|
+
poll: (actionId) => getServiceAction(context.np, service.id, actionId),
|
|
89
|
+
};
|
|
90
|
+
return runActionFlow(config, args);
|
|
91
|
+
},
|
|
92
|
+
});
|
package/dist/ui.js
CHANGED
|
@@ -25,7 +25,10 @@ export const WIDGETS = {
|
|
|
25
25
|
approvals: "Approvals",
|
|
26
26
|
overview: "Organization overview",
|
|
27
27
|
services: "Services & dependencies",
|
|
28
|
-
|
|
28
|
+
"service-create": "Provision service",
|
|
29
|
+
"service-link": "Link service",
|
|
30
|
+
"service-action": "Run action",
|
|
31
|
+
"service-delete": "Delete service",
|
|
29
32
|
};
|
|
30
33
|
/** Did this session's client negotiate the MCP Apps extension? (Knowable after initialize.) */
|
|
31
34
|
export function uiNegotiated(server) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nullplatform/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@biomejs/biome": "2.4.16",
|
|
53
|
+
"@jsonforms/core": "^3.5.1",
|
|
54
|
+
"@jsonforms/react": "^3.5.1",
|
|
53
55
|
"@testing-library/react": "^16.3.2",
|
|
54
56
|
"@types/node": "^20.14.0",
|
|
55
57
|
"@types/react": "^19.2.17",
|