@nullplatform/mcp 0.1.11 → 0.1.13

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 CHANGED
@@ -354,6 +354,8 @@ const english = {
354
354
  "createApp.errorLabel": "Couldn't create the application",
355
355
  // — playbook_get tool —
356
356
  "playbook.errorLabel": "Couldn't load the playbook",
357
+ "entityList.errorLabel": "Couldn't list that",
358
+ "entityGet.errorLabel": "Couldn't read that",
357
359
  // — application_scope_create tool —
358
360
  "createScope.whichType": "Which scope type for **{name}**?",
359
361
  "createScope.typeHint": "name a scope type for **{name}** to create it.",
@@ -720,6 +722,8 @@ const spanish = {
720
722
  "createApp.errorLabel": "No pude crear la aplicación",
721
723
  // — playbook_get tool —
722
724
  "playbook.errorLabel": "No pude cargar el playbook",
725
+ "entityList.errorLabel": "No pude listar eso",
726
+ "entityGet.errorLabel": "No pude leer eso",
723
727
  "createScope.whichType": "¿Qué tipo de scope para **{name}**?",
724
728
  "createScope.typeHint": "nombrá un tipo de scope para **{name}** para crearlo.",
725
729
  "createScope.noTypes": 'No hay tipos de scope disponibles — pasá type explícito (p. ej. type:"web_pool").',
@@ -1101,6 +1105,8 @@ const portuguese = {
1101
1105
  "createApp.errorLabel": "Não foi possível criar a aplicação",
1102
1106
  // — playbook_get tool —
1103
1107
  "playbook.errorLabel": "Não foi possível carregar o playbook",
1108
+ "entityList.errorLabel": "Não foi possível listar isso",
1109
+ "entityGet.errorLabel": "Não foi possível ler isso",
1104
1110
  // — application_scope_create tool —
1105
1111
  "createScope.whichType": "Qual tipo de scope para **{name}**?",
1106
1112
  "createScope.typeHint": "informe um tipo de scope para **{name}** para criá-lo.",
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Generic entity navigation. The platform's REST surface is uniform — every entity in the core tree
3
+ * lists as `GET /<entity>?<parent>_id=<id>` (returning `{results}`) and reads as `GET /<entity>/<id>`.
4
+ * One pair of accessors therefore resolves the whole hierarchy (organization → account → namespace →
5
+ * application → scope → deployment, plus build/release under application) without a per-entity function.
6
+ * (NRN-scoped resources — parameters/services/links/approvals — use a different addressing scheme and
7
+ * keep their own typed functions.) This is the typed seam the generic read tools call.
8
+ */
9
+ export async function listEntity(np, entity, query) {
10
+ const page = await np.get(`/${entity}`, query);
11
+ return Array.isArray(page) ? page : (page.results ?? []);
12
+ }
13
+ export async function getEntity(np, entity, id) {
14
+ return np.get(`/${entity}/${id}`);
15
+ }
1
16
  /** Traffic percentages the platform accepts (same marks the dashboard slider snaps to). */
2
17
  export const TRAFFIC_MARKS = [0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100];
3
18
  export const snapTraffic = (percent) => TRAFFIC_MARKS.reduce((closest, mark) => (Math.abs(mark - percent) < Math.abs(closest - percent) ? mark : closest), 0);
@@ -14,7 +14,9 @@ Every tool accepts \`language\`: ALWAYS set it to the language the user is conve
14
14
 
15
15
  Most tools render an interactive panel in clients that support it — apps, status, builds, releases, deployments, logs, metrics, parameters and approvals all appear as live UI. **When a tool's panel renders, that panel IS the answer: do not reproduce its data in your text reply.** The user already sees every row, status and value. NEVER print a markdown table of the same rows, re-list the items, or restate per-row status — duplicating the panel in text is the single most common mistake. Reply with AT MOST one short sentence — the one key takeaway or the next step — or nothing at all. A one-line insight the panel doesn't itself show is fine ("builds 3 and 5 were never released"); re-rendering the list as a table is not.
16
16
 
17
- When the user wants to create, scaffold, set up or import an application, call \`application_create\` right away. Its panel is an interactive FORM that collects the namespace, template and repository. INFER the best-fitting namespace and account from what the app is FOR — a demo → a demos/examples namespace, a backend service → a services namespace — and pass them as \`namespace\`/\`account\` (plus \`name\` if given) so the form opens pre-selected on your inference; the user can still change it in the form. Do NOT gather those details in conversation first, and while the form is on screen do NOT ask the user clarifying questions or use any question-asking tool for fields the form already covers (namespace, template, new-vs-import repository, monorepo path) the form is the input surface, and a separate "which namespace?" question on top of it is wrong. Just call \`application_create\` and let it drive; react only to what it reports back.
17
+ When the user wants to create, scaffold, set up or import an application, drive it through the \`application_create\` FORM its panel collects the namespace, template and repository. Two rules keep this clean. (1) Settle any genuinely STRUCTURAL question about WHAT to build — e.g. "is this one app or two, a frontend and an API?" in one short turn BEFORE opening any form, since each app gets its own form. (2) Never gather FORM-FIELD details (namespace, template, new-vs-import repository, monorepo path) in conversation: INFER the best-fitting namespace and account from what the app is FOR — a demo → a demos/examples namespace, a backend service → a services namespace — and pass them as \`namespace\`/\`account\` (plus \`name\` if given) so the form opens pre-selected on your inference; the user changes it in the form if they want. Once a form is on screen, NEVER ask a clarifying question or use any question-asking tool about a field it covers — above all NEVER ask "which namespace?". And NEVER open the form, ask a question, then re-open the form: that form question form flow is the single worst thing you can do here. Open it once, pre-filled, and let it drive; react only to what it reports back.
18
+
19
+ Creating a NEW app is the PLATFORM's job, never a local scaffold. nullplatform creates the GitHub repository from a template and provisions it — so for a new app you must NEVER run a generic brainstorm/design/implementation process, never \`git init\`, never create an empty repo, and never write code locally first. The flow is: settle what to build → \`application_create\` with a new repo and ONE OF THE PLATFORM'S TEMPLATES → the platform creates the repo on GitHub → THEN clone it, add code, push so CI builds. The available STACKS are exactly those templates for the chosen namespace — never invent or offer a stack of your own ("Next.js", "Express", …); the form lists the real templates, choose from those. And do NOT fire the rendering reads (\`application_list\`/\`application_get\`/\`application_build_list\`…) just to orient yourself before any of this — each renders a panel the user didn't ask for. To gather data for your OWN reasoning, navigate the entity tree headlessly with \`entity_list\` and \`entity_get\` — read-only, DATA-only, NO panel. \`entity_list\` lists a collection scoped to its parent (the tree is organization → account → namespace → application → {scope, build, release}; scope → deployment): e.g. \`entity_list entity:"application" parent_type:"namespace" parent_id:11\`, then \`entity_list entity:"build" parent_type:"application" parent_id:123\`. \`entity_get entity:"application" id:123\` reads one. Parameters, services, links and approvals list the same way under an application or scope (\`entity_list entity:"parameter" parent_type:"application" parent_id:123\`). That is how you learn where apps live, how they're named, and what's deployable without putting a list on the user's screen. Reserve the rendered reads (\`application_list\`/\`application_get\`/\`application_build_list\`/…) for when the user actually wants to SEE that data.
18
20
 
19
21
  Typical flows:
20
22
  - Ship: \`application_deployment_create\` (picks the latest build, cuts the release for you) → \`application_deployment_update percent:25/50/100\` → \`application_deployment_update action:"finalize"\`.
@@ -30,5 +30,7 @@ export const TOOL = {
30
30
  applicationParameterDelete: "application_parameter_delete",
31
31
  applicationApprovalList: "application_approval_list",
32
32
  organizationGet: "organization_get",
33
+ entityList: "entity_list",
34
+ entityGet: "entity_get",
33
35
  playbookGet: "playbook_get",
34
36
  };
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import { getEntity } from "../np/journey.js";
3
+ import { defineTool, reply } from "../tool.js";
4
+ import { TOOL } from "../tool-names.js";
5
+ /**
6
+ * Generic read-only, DATA-only GET — NO widget, so nothing renders. Reads ONE core entity by id
7
+ * (`GET /<entity>/<id>`), the drill-in companion to `entity_list`. The model uses it to inspect a
8
+ * specific node while navigating the tree, without putting a panel on the user's screen. The rendered
9
+ * reads (application_get/…) stay for when the user wants to SEE it. See CLAUDE.md (data-only read).
10
+ */
11
+ const ENTITIES = [
12
+ "organization",
13
+ "account",
14
+ "namespace",
15
+ "application",
16
+ "scope",
17
+ "build",
18
+ "release",
19
+ "deployment",
20
+ ];
21
+ export const entityGetTool = defineTool({
22
+ name: TOOL.entityGet,
23
+ title: "Read an entity (data)",
24
+ description: 'Read-only, DATA-only fetch of ONE core entity by id — returns structuredContent and renders NO panel. Pass `entity` and `id` (e.g. entity:"application" id:123). Use this to drill into a specific entity while navigating with entity_list; use the rendered reads (application_get/…) when the USER wants to SEE it. Entities: organization, account, namespace, application, scope, build, release, deployment. (parameters/services/links/approvals have their own tools.)',
25
+ annotations: { readOnlyHint: true },
26
+ // No widget on purpose: data-only. See CLAUDE.md (the data-only read pattern).
27
+ errorKey: "entityGet.errorLabel",
28
+ inputSchema: {
29
+ entity: z.enum(ENTITIES).describe("Which entity type to read."),
30
+ id: z.number().describe("The entity's id."),
31
+ },
32
+ async handler(args, context) {
33
+ const result = await getEntity(context.np, args.entity, args.id);
34
+ return reply(`${args.entity} #${args.id}.`, { entity: args.entity, result });
35
+ },
36
+ });
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+ import { getApplication, getScope, listApprovals, listEntity, listLinks, listParameters, listServices, } from "../np/journey.js";
3
+ import { defineTool, fail, reply } from "../tool.js";
4
+ import { TOOL } from "../tool-names.js";
5
+ /**
6
+ * Generic read-only, DATA-only LIST — NO widget, so nothing renders. One tool navigates the whole
7
+ * entity tree: pick an `entity` and the `parent_type`/`parent_id` it lives under. Two addressing
8
+ * schemes hide behind one uniform interface — the REST tree lists as `GET /<entity>?<parent>_id=<id>`,
9
+ * and the NRN-scoped resources (parameter/service/link/approval) list by the parent's NRN (resolved
10
+ * from `parent_id`, or passed directly as `nrn`). Each dispatches to its existing typed function in
11
+ * journey.ts (tools never touch NpClient directly). The model uses it to traverse the org and gather
12
+ * context without a panel; pair it with `entity_get` to drill into one. This is the data-only read
13
+ * pattern (see CLAUDE.md).
14
+ */
15
+ /** REST-addressed collections — listed by `<parent_type>_id`. */
16
+ const REST_ENTITIES = [
17
+ "account",
18
+ "namespace",
19
+ "application",
20
+ "scope",
21
+ "build",
22
+ "release",
23
+ "deployment",
24
+ ];
25
+ /** NRN-addressed collections — listed by the parent's NRN (resolved from parent_id, or given as nrn). */
26
+ const NRN_ENTITIES = ["parameter", "service", "link", "approval"];
27
+ const ENTITIES = [...REST_ENTITIES, ...NRN_ENTITIES];
28
+ /** The parents a collection can hang under — its id (REST) or its NRN (nrn-scoped) scopes the list. */
29
+ const PARENTS = ["organization", "account", "namespace", "application", "scope"];
30
+ const NRN_ENTITY_SET = new Set(NRN_ENTITIES);
31
+ /** Resolve the NRN that scopes an nrn-addressed collection: the model's `nrn`, or its parent's. */
32
+ async function resolveNrn(context, args) {
33
+ if (args.nrn)
34
+ return args.nrn;
35
+ if (args.parent_id === undefined)
36
+ return undefined;
37
+ if (args.parent_type === "application")
38
+ return (await getApplication(context.np, args.parent_id)).nrn;
39
+ if (args.parent_type === "scope")
40
+ return (await getScope(context.np, args.parent_id)).nrn;
41
+ return undefined;
42
+ }
43
+ /** List an nrn-scoped resource via its existing typed function (a different addressing scheme to REST). */
44
+ async function listByNrn(context, entity, nrn) {
45
+ switch (entity) {
46
+ case "parameter":
47
+ return (await listParameters(context.np, nrn)) ?? [];
48
+ case "service":
49
+ return listServices(context.bff, nrn);
50
+ case "link":
51
+ return listLinks(context.np, nrn);
52
+ case "approval":
53
+ return listApprovals(context.bff, nrn);
54
+ default:
55
+ return [];
56
+ }
57
+ }
58
+ export const entityListTool = defineTool({
59
+ name: TOOL.entityList,
60
+ title: "List entities (data)",
61
+ description: 'Read-only, DATA-only list of an entity scoped to its parent — returns structuredContent and renders NO panel, so you can navigate the org and gather context without putting a widget on the user\'s screen. 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:"build" 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. Use this to reason for yourself; use the rendered reads (application_list/application_build_list/…) when the USER wants to SEE the data. For one entity by id use entity_get.',
62
+ annotations: { readOnlyHint: true },
63
+ // No widget on purpose: data-only navigation. See CLAUDE.md (the data-only read pattern).
64
+ errorKey: "entityList.errorLabel",
65
+ inputSchema: {
66
+ entity: z
67
+ .enum(ENTITIES)
68
+ .describe("Collection to list. REST tree: account, namespace, application, scope, build, release, deployment. NRN-scoped: parameter, service, link, approval."),
69
+ parent_type: z
70
+ .enum(PARENTS)
71
+ .optional()
72
+ .describe("The parent the collection lives under. REST: its `<parent_type>_id` scopes the list (required except `account`). NRN-scoped: application|scope, used to resolve the NRN."),
73
+ parent_id: z.number().optional().describe("The parent entity's id."),
74
+ nrn: z
75
+ .string()
76
+ .optional()
77
+ .describe("For nrn-scoped entities (parameter/service/link/approval): the scoping NRN directly, if you have it; otherwise pass parent_type + parent_id and it's resolved for you."),
78
+ limit: z.number().optional().describe("Max rows (default 100) — REST collections only."),
79
+ },
80
+ async handler(args, context) {
81
+ if (NRN_ENTITY_SET.has(args.entity)) {
82
+ const nrn = await resolveNrn(context, args);
83
+ if (!nrn) {
84
+ return fail(`Listing ${args.entity} needs an NRN — pass nrn, or parent_type:"application"|"scope" + parent_id.`);
85
+ }
86
+ const nrnResults = await listByNrn(context, args.entity, nrn);
87
+ return reply(`${nrnResults.length} ${args.entity}(s).`, { entity: args.entity, results: nrnResults });
88
+ }
89
+ const query = { limit: args.limit ?? 100 };
90
+ if (args.parent_type && args.parent_id !== undefined) {
91
+ query[`${args.parent_type}_id`] = args.parent_id;
92
+ }
93
+ else if (args.entity === "account") {
94
+ query.organization_id = (await context.org.getSkeleton()).orgId;
95
+ }
96
+ const results = await listEntity(context.np, args.entity, query);
97
+ return reply(`${results.length} ${args.entity}(s).`, { entity: args.entity, results });
98
+ },
99
+ });
@@ -10,6 +10,8 @@ import { deleteParamTool } from "./delete-param.js";
10
10
  import { deleteServiceTool } from "./delete-service.js";
11
11
  import { deployTool } from "./deploy.js";
12
12
  import { deploymentsTool } from "./deployments.js";
13
+ import { entityGetTool } from "./entity-get.js";
14
+ import { entityListTool } from "./entity-list.js";
13
15
  import { findAppsTool } from "./find-apps.js";
14
16
  import { logsTool } from "./logs.js";
15
17
  import { metricsTool } from "./metrics.js";
@@ -45,6 +47,8 @@ export const tools = [
45
47
  createAppTool,
46
48
  createScopeTool,
47
49
  approvalsTool,
50
+ entityListTool,
51
+ entityGetTool,
48
52
  servicesTool,
49
53
  createServiceTool,
50
54
  updateServiceTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullplatform/mcp",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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",