appilot-mcp 0.1.1 → 0.3.0

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.
Files changed (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/LICENSE +15 -0
  4. package/README.md +102 -24
  5. package/dist/appilot-configurator.mcpb +0 -0
  6. package/dist/cli.d.ts +34 -0
  7. package/dist/cli.js +171 -0
  8. package/dist/client.d.ts +122 -3
  9. package/dist/client.js +306 -31
  10. package/dist/config.d.ts +14 -0
  11. package/dist/config.js +19 -0
  12. package/dist/contract/bundleSnapshot.js +8 -1
  13. package/dist/contract/healthContract.d.ts +1 -1
  14. package/dist/contract/healthContract.js +100 -10
  15. package/dist/contract/types.d.ts +37 -1
  16. package/dist/index.bundle.js +4487 -16520
  17. package/dist/index.js +7 -0
  18. package/dist/inspect.d.ts +88 -0
  19. package/dist/inspect.js +384 -0
  20. package/dist/manifest.d.ts +14 -2
  21. package/dist/manifest.js +31 -9
  22. package/dist/public-marketplace/.claude-plugin/marketplace.json +20 -0
  23. package/dist/public-marketplace/README.md +23 -0
  24. package/dist/public-marketplace/plugins/app-configurator/.claude-plugin/plugin.json +43 -0
  25. package/dist/public-marketplace/plugins/app-configurator/README.md +328 -0
  26. package/dist/public-marketplace/plugins/app-configurator/dist/index.bundle.js +57370 -0
  27. package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/SKILL.md +267 -0
  28. package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/agents/openai.yaml +13 -0
  29. package/dist/redaction.d.ts +18 -3
  30. package/dist/redaction.js +27 -3
  31. package/dist/remote/consent.d.ts +30 -20
  32. package/dist/remote/consent.js +114 -82
  33. package/dist/remote/consentMessages.d.ts +65 -0
  34. package/dist/remote/consentMessages.js +199 -0
  35. package/dist/remote/handoff.d.ts +10 -0
  36. package/dist/remote/handoff.js +44 -0
  37. package/dist/remote/httpServer.js +28 -5
  38. package/dist/remote/oauth.d.ts +39 -6
  39. package/dist/remote/oauth.js +281 -36
  40. package/dist/scaffold.d.ts +110 -1
  41. package/dist/scaffold.js +474 -39
  42. package/dist/server.js +425 -38
  43. package/dist/soak.js +21 -1
  44. package/dist/templates.d.ts +62 -0
  45. package/dist/templates.js +255 -0
  46. package/dist/verify.js +18 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/examples/app.appilot.json +212 -0
  50. package/mcpb/manifest.json +117 -15
  51. package/package.json +5 -3
  52. package/skills/app-configurator/SKILL.md +136 -25
@@ -13,7 +13,8 @@
13
13
  */
14
14
  import { validateActionPlanMarkers } from 'appilot-shared/utils';
15
15
  import { IDENTIFIER_KINDS } from 'appilot-shared/validation';
16
- const VALID_KB_SCOPES = new Set(['app_global', 'domain_global', 'page']);
16
+ import { KNOWLEDGE_SCOPES } from 'appilot-shared/types';
17
+ const VALID_KB_SCOPES = new Set(KNOWLEDGE_SCOPES);
17
18
  /** Extract every `{{kind:id}}` marker (with or without a `[label](...)` wrapper). */
18
19
  function extractMarkers(step) {
19
20
  const rx = /\{\{([a-z_]+):([^}]+)\}\}/g;
@@ -30,6 +31,19 @@ const CREATE_INTENT = /\b(create|add|new|update|edit|erstell|anleg|neu|hinzu|cre
30
31
  function localizedValues(text) {
31
32
  return Object.values(text).filter((v) => typeof v === 'string');
32
33
  }
34
+ /**
35
+ * `form_values` as a record, whatever arrived.
36
+ *
37
+ * The snapshot type says it is one, and a hand-authored or scaffolded plan can
38
+ * still carry null or a string. Reading it defensively keeps a malformed bundle
39
+ * a finding rather than an exception thrown out of the whole audit.
40
+ */
41
+ function authoredFormValues(plan) {
42
+ const raw = plan.form_values;
43
+ return raw && typeof raw === 'object' && !Array.isArray(raw)
44
+ ? raw
45
+ : {};
46
+ }
33
47
  // ---------------------------------------------------------------------------
34
48
  // Lint 1 — marker trust boundary (reuse appilot-shared).
35
49
  // ---------------------------------------------------------------------------
@@ -51,7 +65,7 @@ function lintMarkers(snap) {
51
65
  });
52
66
  }
53
67
  // form_values fields must belong to their form.
54
- for (const [formId, entry] of Object.entries(plan.form_values)) {
68
+ for (const [formId, entry] of Object.entries(authoredFormValues(plan))) {
55
69
  if (!formIds.has(formId)) {
56
70
  findings.push({
57
71
  severity: 'high', category: 'markers', entity: `action_plan:${plan.semantic_id}`,
@@ -59,8 +73,22 @@ function lintMarkers(snap) {
59
73
  });
60
74
  continue;
61
75
  }
76
+ // A malformed `form_values` entry is a FINDING, not a crash. The
77
+ // authored shape is `{ fields: [{ control_id, value }] }`, and a bundle
78
+ // carrying `{}` there used to take the whole contract down with
79
+ // "entry.fields is not iterable", so the one tool that could have named
80
+ // the defect answered with a stack trace instead.
81
+ const authored = entry;
82
+ if (!authored || !Array.isArray(authored.fields)) {
83
+ findings.push({
84
+ severity: 'high', category: 'markers', entity: `action_plan:${plan.semantic_id}`,
85
+ message: `form_values["${formId}"] is not in the authored shape { fields: [{ control_id, value }] }.`,
86
+ recommendation: 'Write an empty fields array when there are no authored defaults.',
87
+ });
88
+ continue;
89
+ }
62
90
  const fields = fieldsByForm.get(formId) ?? new Set();
63
- for (const f of entry.fields) {
91
+ for (const f of authored.fields) {
64
92
  if (!fields.has(f.control_id)) {
65
93
  findings.push({
66
94
  severity: 'high', category: 'markers', entity: `action_plan:${plan.semantic_id}`,
@@ -83,17 +111,34 @@ function lintActionability(snap) {
83
111
  const markers = planMarkers(plan);
84
112
  const hasFormStep = markers.some(m => m.kind === 'form');
85
113
  const hasSetValue = markers.some(m => m.kind === 'set_value' || m.kind === 'select');
86
- const hasFormValues = Object.keys(plan.form_values).length > 0;
114
+ const hasFormValues = Object.keys(authoredFormValues(plan)).length > 0;
87
115
  const entersValue = hasFormStep || hasSetValue || hasFormValues;
88
116
  const looksLikeCreate = localizedValues({ ...plan.name, ...plan.description }).some(v => CREATE_INTENT.test(v))
89
117
  || CREATE_INTENT.test(plan.semantic_id);
90
- if (!entersValue) {
118
+ // A plan that enters no value is not automatically inert. The defect
119
+ // this lint exists for is a plan that OPENS or FOCUSES something and
120
+ // stops, which is what "click the input, then nothing" looks like. A
121
+ // plan whose task is a decision, approve, reject, publish, archive,
122
+ // completes that task with the click itself, and there is no value to
123
+ // enter anywhere in it.
124
+ //
125
+ // So the test is what the clicks target. A click on a control that is
126
+ // not one of a form's own fields commits something. Only a plan with no
127
+ // commit at all, or one whose wording promises a create or an edit, is
128
+ // the defect.
129
+ const formFieldIds = new Set(snap.forms.flatMap(f => f.field_ids));
130
+ const commits = markers.some(m => m.kind === 'click' && !formFieldIds.has(m.id));
131
+ if (!entersValue && (looksLikeCreate || !commits)) {
91
132
  findings.push({
92
133
  severity: looksLikeCreate ? 'critical' : 'high',
93
134
  category: 'actionability',
94
135
  entity: `action_plan:${plan.semantic_id}`,
95
- message: 'Plan enters no value: it has no {{form:…}} step, no set_value step, and empty form_values, so it only opens/focuses an element and never completes the task.',
96
- recommendation: 'Add a {{form:…}} step (or a set_value step) to enter the value, then a click step on the submit control to confirm.',
136
+ message: looksLikeCreate
137
+ ? 'Plan enters no value: it has no {{form:…}} step, no set_value step, and empty form_values, so it opens an element and never completes the create or edit its description promises.'
138
+ : 'Plan neither enters a value nor clicks anything outside a form\'s own fields, so it only opens or focuses an element and never completes the task.',
139
+ recommendation: looksLikeCreate
140
+ ? 'Add a {{form:…}} step (or a set_value step) to enter the value, then a click step on the submit control to confirm.'
141
+ : 'Add the step that commits the task: a click on the control that confirms it, or a {{form:…}} step followed by a submit.',
97
142
  });
98
143
  }
99
144
  // Submit-without-fill: clicks a form's submit but never fills that form
@@ -105,7 +150,7 @@ function lintActionability(snap) {
105
150
  if (!clicksSubmit)
106
151
  continue;
107
152
  const fillsForm = markers.some(m => m.kind === 'form' && m.id === form.semantic_id)
108
- || (plan.form_values[form.semantic_id]?.fields?.length ?? 0) > 0;
153
+ || (authoredFormValues(plan)[form.semantic_id]?.fields?.length ?? 0) > 0;
109
154
  if (!fillsForm) {
110
155
  findings.push({
111
156
  severity: 'high', category: 'actionability', entity: `action_plan:${plan.semantic_id}`,
@@ -209,7 +254,7 @@ function lintKnowledge(snap) {
209
254
  findings.push({
210
255
  severity: 'high', category: 'kb-scope', entity,
211
256
  message: `Invalid knowledge scope "${kb.scope}".`,
212
- recommendation: 'Use one of app_global, domain_global, page.',
257
+ recommendation: `Use one of ${KNOWLEDGE_SCOPES.join(', ')}.`,
213
258
  });
214
259
  }
215
260
  const languages = new Set(kb.bodies.map(b => b.language));
@@ -274,14 +319,58 @@ function lintIdentifiers(snap) {
274
319
  lintIdentifier(z.semantic_id, `zone:${z.semantic_id}`, findings);
275
320
  for (const p of snap.actionPlans)
276
321
  lintIdentifier(p.semantic_id, `action_plan:${p.semantic_id}`, findings);
322
+ // A tool name is its own identifier kind: it reaches the model as a callable
323
+ // name, so it follows the tool_name rule rather than semantic_id. Medium, not
324
+ // high: an off-shape name still resolves at runtime, and a high finding would
325
+ // turn importing a working configuration that predates the rule into a 422.
326
+ for (const t of snap.tools) {
327
+ if (!IDENTIFIER_KINDS.tool_name.isValid(t.tool_name)) {
328
+ findings.push({
329
+ severity: 'medium', category: 'identifier', entity: `tool:${t.tool_name}`,
330
+ message: `Tool name "${t.tool_name}" is not a valid tool_name (${IDENTIFIER_KINDS.tool_name.rule}).`,
331
+ recommendation: `Rename it, for example ${IDENTIFIER_KINDS.tool_name.example}. The model calls the tool by this name.`,
332
+ });
333
+ }
334
+ }
277
335
  return findings;
278
336
  }
337
+ // ---------------------------------------------------------------------------
338
+ // Lint 7 — snapshot completeness.
339
+ //
340
+ // Reported as findings rather than thrown, because a partial snapshot is still
341
+ // worth auditing. What must not happen is a partial snapshot reading as a clean
342
+ // bill of health: every lint below an unreadable entity silently passes.
343
+ // ---------------------------------------------------------------------------
344
+ function lintGaps(snap) {
345
+ return (snap.gaps ?? []).map(g => ({
346
+ severity: 'high',
347
+ category: 'coverage',
348
+ entity: `snapshot:${g.entity}`,
349
+ message: `${g.entity} could not be read, so no ${g.entity} lint ran: ${g.reason}`,
350
+ recommendation: 'Fix the access problem and re-run. Treat every finding in this report as incomplete until this line is gone.',
351
+ }));
352
+ }
279
353
  const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
280
354
  /**
281
355
  * Run the full config health contract over a snapshot. Findings are returned
282
356
  * most-severe first, so the report reads like the manual audit.
283
357
  */
284
- export function runHealthContract(snap) {
358
+ export function runHealthContract(input) {
359
+ // Normalise once. A snapshot reaches this function from three producers (the
360
+ // live-API client, the bundle adapter, the backend import path) and every lint
361
+ // below iterates these arrays without a guard; one producer omitting one of
362
+ // them would otherwise crash the audit instead of reporting on it.
363
+ const snap = {
364
+ ...input,
365
+ expectedLocales: input.expectedLocales ?? [],
366
+ views: input.views ?? [],
367
+ controls: input.controls ?? [],
368
+ forms: input.forms ?? [],
369
+ tools: input.tools ?? [],
370
+ zones: input.zones ?? [],
371
+ actionPlans: input.actionPlans ?? [],
372
+ knowledge: input.knowledge ?? [],
373
+ };
285
374
  const findings = [
286
375
  ...lintMarkers(snap),
287
376
  ...lintActionability(snap),
@@ -289,6 +378,7 @@ export function runHealthContract(snap) {
289
378
  ...lintI18n(snap),
290
379
  ...lintKnowledge(snap),
291
380
  ...lintIdentifiers(snap),
381
+ ...lintGaps(snap),
292
382
  ].sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
293
383
  const counts = { critical: 0, high: 0, medium: 0, low: 0 };
294
384
  for (const f of findings)
@@ -7,7 +7,7 @@
7
7
  * same validator works online, offline (air-gapped), and in unit fixtures.
8
8
  */
9
9
  export type Severity = 'critical' | 'high' | 'medium' | 'low';
10
- export type FindingCategory = 'markers' | 'actionability' | 'selector-stability' | 'i18n-coverage' | 'kb-scope' | 'kb-procedure' | 'kb-hygiene' | 'identifier';
10
+ export type FindingCategory = 'markers' | 'actionability' | 'selector-stability' | 'i18n-coverage' | 'kb-scope' | 'kb-procedure' | 'kb-hygiene' | 'identifier' | 'coverage';
11
11
  export interface Finding {
12
12
  severity: Severity;
13
13
  category: FindingCategory;
@@ -21,6 +21,9 @@ export interface Finding {
21
21
  /** A per-locale text map, e.g. `{ de: 'Neuer Ordner', en: 'New folder' }`. */
22
22
  export type LocalizedText = Record<string, string | undefined>;
23
23
  export interface ControlSnapshot {
24
+ /** Row id, when the snapshot came from the live API. Absent from a bundle,
25
+ * which is keyed semantically. `update_control` needs it. */
26
+ id?: string;
24
27
  semantic_id: string;
25
28
  locator_type: string;
26
29
  locator: string;
@@ -34,6 +37,8 @@ export interface FormSnapshot {
34
37
  entry_control_id?: string | null;
35
38
  }
36
39
  export interface ActionPlanSnapshot {
40
+ /** Row id, when the snapshot came from the live API. See ControlSnapshot.id. */
41
+ id?: string;
37
42
  semantic_id: string;
38
43
  sections: Array<{
39
44
  view_path?: string;
@@ -52,9 +57,25 @@ export interface ActionPlanSnapshot {
52
57
  is_active?: boolean;
53
58
  }
54
59
  export interface ZoneSnapshot {
60
+ id?: string;
55
61
  semantic_id: string;
56
62
  }
63
+ /**
64
+ * A tool as the agent needs to reason about it: what it is called, what kind of
65
+ * runtime backs it, and whether it is bound to a view. Not the full definition;
66
+ * `export_config` carries that.
67
+ */
68
+ export interface ToolSnapshot {
69
+ id?: string;
70
+ tool_name: string;
71
+ /** `http_proxy`, `client_action`, … Empty when the instance does not say. */
72
+ kind: string;
73
+ view_path?: string | null;
74
+ is_active?: boolean;
75
+ }
57
76
  export interface KnowledgeSnapshot {
77
+ /** Row id on the live-API path (what `update_knowledge` takes), the
78
+ * translation group on the bundle path. */
58
79
  id: string | number;
59
80
  scope: string;
60
81
  /** Body text per language row (knowledge_content is one row per language). */
@@ -69,15 +90,30 @@ export interface ViewSnapshot {
69
90
  path: string;
70
91
  name: LocalizedText;
71
92
  }
93
+ /**
94
+ * An entity the snapshot could not read, and why.
95
+ *
96
+ * A snapshot with an empty `knowledge` array and a snapshot whose knowledge read
97
+ * was refused look identical, and the second one silently turns every knowledge
98
+ * lint into a pass. Recording the failure is what lets `read_config` and
99
+ * `validate_config` say "I could not see this" instead of "this is fine".
100
+ */
101
+ export interface SnapshotGap {
102
+ entity: 'views' | 'controls' | 'forms' | 'tools' | 'zones' | 'actionPlans' | 'knowledge';
103
+ reason: string;
104
+ }
72
105
  export interface ConfigSnapshot {
73
106
  /** Locales the config is expected to cover, e.g. ['de','en','es']. */
74
107
  expectedLocales: string[];
75
108
  views: ViewSnapshot[];
76
109
  controls: ControlSnapshot[];
77
110
  forms: FormSnapshot[];
111
+ tools: ToolSnapshot[];
78
112
  actionPlans: ActionPlanSnapshot[];
79
113
  zones: ZoneSnapshot[];
80
114
  knowledge: KnowledgeSnapshot[];
115
+ /** Entities that could not be read. Empty on a complete snapshot. */
116
+ gaps?: SnapshotGap[];
81
117
  }
82
118
  export interface HealthReport {
83
119
  ok: boolean;