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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-configurator",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Set up, configure, audit, repair, back up, restore, and verify an Appilot app: provision the app, its domains and widget key, scaffold the host integration, and keep the content model correct. Uses the Appilot MCP server and the app-configurator skill.",
5
5
  "author": {
6
6
  "name": "Appilot",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-configurator",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Set up, configure, audit, repair, back up, restore, and verify an Appilot app: provision the app, its domains and widget key, scaffold the host integration, and keep the content model correct. Uses the Appilot MCP server and the app-configurator skill.",
5
5
  "author": {
6
6
  "name": "Appilot",
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 BetterKnow GmbH
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -29,7 +29,7 @@ not by preference.
29
29
  | Transport | Who launches it | Credential | Clients |
30
30
  |-----------|-----------------|------------|---------|
31
31
  | **stdio** (default) | The client, as a local child process | `APPILOT_PAT` in the process environment | Claude Code, Codex (CLI, IDE, app), Claude Desktop, Cursor, Antigravity |
32
- | **HTTP** (`--http`) | You, as a deployed service | The caller's own service token, collected once through OAuth | ChatGPT, claude.ai, and any remote MCP client |
32
+ | **HTTP** (`--http`) | You, as a deployed service | Account approval or a manually supplied service token through OAuth | ChatGPT, claude.ai, and any remote MCP client |
33
33
 
34
34
  ChatGPT and claude.ai cannot start a process on your machine, and neither offers
35
35
  a field for pasting an API key: they connect to a URL and authenticate with
@@ -41,7 +41,7 @@ OAuth. That is what HTTP mode is for. See
41
41
  | Env | Required | Meaning |
42
42
  |-----|----------|---------|
43
43
  | `APPILOT_BASE_URL` | yes | Backend URL, e.g. `https://api.appilot.space` or `http://localhost:6001` |
44
- | `APPILOT_PAT` | for writes / server echo | A scoped service token (`appilot_pat_…`), minted in the Backoffice (admin → service tokens). `config:read` to read/validate, `config:write` to apply. |
44
+ | `APPILOT_PAT` | for writes / server echo | A scoped service token (`appilot_pat_…`), minted in the Backoffice (Service tokens). `config:read` to read/validate, `config:write` to apply. |
45
45
  | `APPILOT_APP_ID` | optional | Default app id for tools that omit one |
46
46
  | `APPILOT_SOAK_STORAGE_STATE` | optional | Path to a Playwright `storageState` JSON for an authenticated site session (soak) |
47
47
 
@@ -55,17 +55,57 @@ because each caller brings theirs.
55
55
  | `APPILOT_MCP_OAUTH_SECRET` | yes | At least 32 characters (`openssl rand -base64 32`). Everything the authorization server issues is sealed with it, so rotating it disconnects every client. |
56
56
  | `PORT` | no | Defaults to 8080. Cloud Run supplies it. |
57
57
  | `APPILOT_MCP_ALLOWED_HOSTS` | no | Extra `Host` values to accept, comma separated, for a proxy or a platform-assigned hostname. The public origin is always accepted. |
58
+ | `APPILOT_MCP_DOCS_URL` | no | Where the protected-resource metadata points a client for documentation. Defaults to the public docs site. |
59
+ | `APPILOT_MCP_BACKOFFICE_URL` | no | The Backoffice origin consent screens link to, and the only origin account approval is accepted from. Must be HTTPS (localhost excepted). Defaults to `https://backoffice.appilot.space` when the instance is the Appilot cloud. |
60
+ | `APPILOT_MCP_HANDOFF_SECRET` | no | Enables account approval instead of pasting a service token. At least 32 characters, and it must match the backend's. Separate from the OAuth secret, and it requires `APPILOT_MCP_BACKOFFICE_URL`. |
58
61
 
59
62
  ## Tools
60
63
 
64
+ Twenty-five, in the order the server registers them. `test/toolSurface.test.ts`
65
+ holds this table, `mcpb/manifest.json` and the registered tools to one list.
66
+
61
67
  | Tool | Purpose |
62
68
  |------|---------|
63
- | `capabilities` | Version + migration level + schema versions of the connected instance (negotiate before configuring) |
69
+ | `capabilities` | Version, migration level and the closed vocabularies this instance accepts (negotiate before configuring) |
64
70
  | `read_config` | Normalized snapshot of an app's content model |
65
71
  | `validate_config` | Severity-ranked findings against the health contract (local + server echo) |
66
- | `update_action_plan` / `update_control` / `update_knowledge` | Guarded writes (server re-validates the trust boundary) |
72
+ | `entity_template` | A valid skeleton per entity kind, carrying the closed enums this instance accepts |
73
+ | `create_entity` / `update_entity` / `delete_entity` | Authoring across all eight kinds (server re-validates the trust boundary) |
74
+ | `validate_action_plan` | Check draft plan sections before writing them |
67
75
  | `export_config` / `import_config` | Whole-app ConfigBundle round trip, dry-run first, merge or replace |
76
+ | `whoami` | Which organization, which app and which scopes the credential reaches |
77
+ | `create_app` | Provision the app, its domains and a widget key, with the snippets to paste in |
78
+ | `list_apps` | What this organization has provisioned, with each domain's verification status |
79
+ | `list_widget_keys` | The keys already minted, by label and prefix. No raw key, no secret |
80
+ | `verify_domain` | Where a domain's DNS verification stands, and the TXT record it needs |
81
+ | `integration_snippet` | The script tag and boot call for an app that already exists. Pure, no scope |
82
+ | `plan_manifest` / `apply_manifest` | A whole tenant from one versioned file, plan before apply |
83
+ | `scaffold_integration` | The host application's source: identity relay, widget boot, a client action |
84
+ | `verify_integration` | Load the running page and report what is actually true |
68
85
  | `soak_selectors` | Headless-browser check that each control selector resolves on the live page (needs Playwright) |
86
+ | `inspect_page` | Read a page and rank locator candidates by whether they survive the next render |
87
+ | `scaffold_agent_first` | The artifacts one capability needs to be operable by the agent |
88
+ | `report_feedback` / `list_feedback` | Report a gap or a defect in Appilot, and see what this organization has already raised |
89
+
90
+ ## The command line
91
+
92
+ The package is an MCP server first, and a small CLI for the things a pipeline
93
+ needs without an MCP client in front of them.
94
+
95
+ ```bash
96
+ npx appilot-mcp --help
97
+ npx appilot-mcp --version
98
+ npx appilot-mcp install-skill --claude # or --codex, --cursor, --gemini
99
+ npx appilot-mcp plan app.appilot.json # preview, writes nothing
100
+ npx appilot-mcp apply app.appilot.json # apply the plan it just printed
101
+ ```
102
+
103
+ `plan` and `apply` read the same environment the stdio server does, and `apply`
104
+ runs the plan itself and passes its own token, so the preview cannot be skipped.
105
+ `install-skill` copies the bundled `app-configurator` skill into the client's
106
+ skills directory, which is what makes the skill reachable from a bare `npx`
107
+ install: inside the npm cache no client looks for it.
108
+
69
109
 
70
110
  ## Distribution artifacts
71
111
 
@@ -78,6 +118,8 @@ One package carries a tool-neutral implementation plus thin discovery adapters:
78
118
  | `.codex-plugin/plugin.json` | Codex plugin |
79
119
  | `.claude-plugin/plugin.json` | Claude Code plugin |
80
120
  | `mcpb/manifest.json` | Claude Desktop bundle manifest, packed by `pnpm build:mcpb` |
121
+ | `examples/app.appilot.json` | A complete app manifest to copy, held valid by `test/exampleManifest.test.ts` |
122
+ | `.claude-plugin/public-marketplace/` | The public marketplace repository, prepared and not published |
81
123
  | `Dockerfile` | The remote HTTP service |
82
124
  | `dist/index.bundle.js` | Self-contained Node.js MCP server, both transports |
83
125
 
@@ -108,10 +150,20 @@ task after installation so the new tool surface is loaded.
108
150
 
109
151
  ### Claude Code
110
152
 
111
- Add the repository marketplace and install `app-configurator@appilot`. Claude's
112
- plugin settings prompt for the backend URL, service token, and optional app ID.
113
- The token is declared sensitive so Claude stores it in secure credential
114
- storage.
153
+ ```
154
+ /plugin marketplace add /absolute/path/to/app-pilot
155
+ /plugin install app-configurator@appilot
156
+ ```
157
+
158
+ Claude's plugin settings then prompt for the backend URL, the service token and
159
+ an optional app id. The token is declared sensitive, so Claude stores it in
160
+ secure credential storage.
161
+
162
+ There is no published marketplace repository yet: `appilot/appilot-plugins` does
163
+ not exist on GitHub, so a `/plugin marketplace add appilot/appilot-plugins`
164
+ anywhere is wrong until someone publishes it. The tree that repository would
165
+ carry is prepared in `.claude-plugin/public-marketplace/`, along with the human
166
+ steps, and `pnpm build:marketplace` assembles it.
115
167
 
116
168
  ### Claude Desktop
117
169
 
@@ -134,7 +186,7 @@ Once one is running (see [Remote deployment](#remote-deployment)):
134
186
 
135
187
  - **ChatGPT**: Settings, Apps, Advanced settings, turn on Developer mode, then
136
188
  add a connector pointing at `https://<your-host>/mcp` with OAuth. The
137
- authorization step asks for a service token, once.
189
+ authorization step opens Appilot account approval, or manual token consent on older deployments.
138
190
  - **claude.ai**: Settings, Connectors, Add custom connector, same URL.
139
191
  - **Codex** can use the remote endpoint too, if you prefer one shared deployment
140
192
  over a local process: add the URL to `~/.codex/config.toml` and run
@@ -147,17 +199,21 @@ instance needs no public listener.
147
199
 
148
200
  ### Cursor
149
201
 
150
- Copy `skills/app-configurator` to `~/.cursor/skills/app-configurator`, then add
151
- the server to `~/.cursor/mcp.json`.
202
+ ```bash
203
+ npx -y appilot-mcp install-skill --cursor
204
+ ```
152
205
 
153
- ### Antigravity
206
+ Then add the server to `~/.cursor/mcp.json`.
154
207
 
155
- Copy `skills/app-configurator` to the applicable global skills directory:
208
+ ### Antigravity
156
209
 
157
- - Antigravity IDE: `~/.gemini/config/skills/app-configurator`
158
- - Antigravity CLI: `~/.gemini/antigravity-cli/skills/app-configurator`
210
+ ```bash
211
+ npx -y appilot-mcp install-skill --gemini
212
+ ```
159
213
 
160
- Register the stdio server in `~/.gemini/config/mcp_config.json`.
214
+ That writes `~/.gemini/config/skills/app-configurator`, which is where the IDE
215
+ looks. The CLI reads `~/.gemini/antigravity-cli/skills/`, so pass `--dir` for
216
+ that one. Register the stdio server in `~/.gemini/config/mcp_config.json`.
161
217
 
162
218
  ### Any stdio MCP client
163
219
 
@@ -179,16 +235,23 @@ Point the client at the built bundle:
179
235
  }
180
236
  ```
181
237
 
182
- For registry-backed distribution, replace `node` and the bundle path with
183
- `pnpm dlx appilot-mcp` after the package is published.
238
+ From npm, with no checkout at all, replace `node` and the bundle path with
239
+ `npx` and `-y appilot-mcp`, then install the skill the package ships:
240
+
241
+ ```bash
242
+ npx -y appilot-mcp install-skill --claude # or --codex, --cursor, --gemini
243
+ ```
244
+
245
+ Without that step the skill stays in the npm cache, where no client looks, and
246
+ the tools arrive with no procedure around them.
184
247
 
185
248
  Do not commit service tokens. Use the MCP client's secure secret storage or
186
249
  environment forwarding when available.
187
250
 
188
251
  ## Remote deployment
189
252
 
190
- HTTP mode is a small stateless web service. It stores nothing: no user table, no
191
- client registry, no token store. The OAuth authorization server seals its own
253
+ HTTP mode delegates identity and approval storage to the backend. It has no
254
+ local user table, client registry or raw token store. The OAuth authorization server seals its own
192
255
  state into the artifacts it issues (see `src/remote/tokens.ts`), so a second
193
256
  instance behaves exactly like the first and a cold start loses nothing.
194
257
 
@@ -200,7 +263,8 @@ What it serves:
200
263
  | `/.well-known/oauth-protected-resource/mcp` | Points a client at the authorization server |
201
264
  | `/.well-known/oauth-authorization-server` | Metadata, PKCE with S256, dynamic registration |
202
265
  | `/authorize`, `/token`, `/register` | The OAuth 2.1 flow |
203
- | `/consent` | The one page a person sees, where they paste a service token |
266
+ | `/consent` | Manual token verification and review |
267
+ | `/connect/callback` | Redeems account approval bound to the initiating browser |
204
268
  | `/health` (also `/healthz` off Cloud Run) | Liveness |
205
269
 
206
270
  Run it:
@@ -233,9 +297,9 @@ has to read before it can authenticate at all. The upload carries the
233
297
 
234
298
  ### How access works
235
299
 
236
- The service never sees an Appilot password and never mints a credential. A
237
- person creates a scoped service token in the Backoffice, pastes it once on the
238
- consent screen, and the service checks it against the instance
300
+ The service never sees an Appilot password. Account approval asks the backend
301
+ to create a scoped credential after the administrator reviews access. In the
302
+ manual flow, a person pastes a previously created token and MCP checks it against the instance
239
303
  (`GET /config/whoami`) before granting anything. The granted scopes are the
240
304
  intersection of what the client asked for and what that token actually carries,
241
305
  so a `config:read` token cannot be talked into write access. The token is then
@@ -271,3 +335,17 @@ Live DOM soak needs Playwright: add it with pnpm and install Chromium.
271
335
 
272
336
  See `docs/architecture/appilot-mcp.md` (server) and
273
337
  `docs/content-model/config-health-contract.md` (the contract it enforces).
338
+
339
+ ### Account-based remote approval
340
+
341
+ Remote deployments can reuse the Backoffice session for approval without asking
342
+ users to create or paste a token. Configure APPILOT_MCP_HANDOFF_SECRET on backend
343
+ and MCP, a trusted APPILOT_MCP_BACKOFFICE_URL on MCP, and the fixed
344
+ APPILOT_MCP_PUBLIC_URL on backend. Deploy migration system/025 and the Backoffice
345
+ approval page first. The cloud activation script is
346
+ `scripts/enable-session-approval.sh`. Keep this secret separate from the OAuth key.
347
+
348
+ Manual service-token consent remains available when session approval is not
349
+ configured, or with `manual=1` on the authorization request. Consent and errors
350
+ support English, Spanish and German. Administrators can filter connections,
351
+ renew expiry and revoke access in Backoffice Service tokens.
Binary file
package/dist/cli.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The command line around the MCP server.
3
+ *
4
+ * `npx appilot-mcp` starts the stdio server, which is what an MCP client wants
5
+ * and what nothing else does. Everything here exists because a person or a
6
+ * pipeline typed something else and got a server waiting silently on stdin:
7
+ * `--help` and `--version` did that, and so did every attempt to apply a
8
+ * manifest from CI, which had to script an MCP client to reach a plan and an
9
+ * apply that are two ordinary function calls.
10
+ *
11
+ * `install-skill` is the other half of the same problem. The npm tarball
12
+ * carries `skills/app-configurator`, and it lands in the npm cache, where no
13
+ * client looks. Copying it to the client's own skills directory is the step
14
+ * that was documented only as a marketplace command pointing at a repository
15
+ * that does not exist.
16
+ */
17
+ export declare const HELP = "appilot-mcp 0.3.0\n\nThe Appilot MCP server: read, validate, fix and provision an Appilot app.\n\nUSAGE\n appilot-mcp Start the MCP server on stdio (what a client launches)\n appilot-mcp --http Start the remote HTTP service instead\n appilot-mcp plan <file> Preview an appilot.app-manifest. Writes nothing\n appilot-mcp apply <file> Plan, then apply the same manifest\n appilot-mcp install-skill Copy the app-configurator skill to an agent client\n appilot-mcp --help | --version\n\nOPTIONS\n apply --mode merge|replace How the config half is imported. Default merge\n --allow-unhealthy Import a bundle the health gate would refuse\n install-skill --claude | --codex | --cursor | --gemini | --dir <path>\n\nENVIRONMENT\n APPILOT_BASE_URL Your instance, e.g. https://api.appilot.space or http://localhost:6001\n APPILOT_PAT A scoped service token from the Backoffice, under Service tokens\n APPILOT_APP_ID Optional default app id\n\nDocs: https://docs.appilot.space/docs/developers/configure-with-ai/overview";
18
+ /** Where each client looks for a skill. One directory per client, all under $HOME. */
19
+ export declare const SKILL_TARGETS: Record<string, string>;
20
+ /**
21
+ * The bundled skill directory, resolved from the running file.
22
+ *
23
+ * `dist/index.bundle.js` and `skills/` are siblings under the package root in
24
+ * the tarball and in the workspace, so one hop up is the answer in both. The
25
+ * fallback covers a deeper build output rather than guessing silently.
26
+ */
27
+ export declare function bundledSkillDir(from?: string): string | null;
28
+ /**
29
+ * Handle everything that is not "start a server".
30
+ *
31
+ * Returns null when the argv asks for a server, so the entry point stays the
32
+ * transport switch it was.
33
+ */
34
+ export declare function runCommand(argv: string[]): Promise<number | null>;
package/dist/cli.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The command line around the MCP server.
3
+ *
4
+ * `npx appilot-mcp` starts the stdio server, which is what an MCP client wants
5
+ * and what nothing else does. Everything here exists because a person or a
6
+ * pipeline typed something else and got a server waiting silently on stdin:
7
+ * `--help` and `--version` did that, and so did every attempt to apply a
8
+ * manifest from CI, which had to script an MCP client to reach a plan and an
9
+ * apply that are two ordinary function calls.
10
+ *
11
+ * `install-skill` is the other half of the same problem. The npm tarball
12
+ * carries `skills/app-configurator`, and it lands in the npm cache, where no
13
+ * client looks. Copying it to the client's own skills directory is the step
14
+ * that was documented only as a marketplace command pointing at a repository
15
+ * that does not exist.
16
+ */
17
+ import { cpSync, existsSync, readFileSync, mkdirSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { SERVER_VERSION } from './version.js';
22
+ import { loadConnection } from './config.js';
23
+ import { AppilotClient } from './client.js';
24
+ import { applyManifest, parseManifest, planManifest } from './manifest.js';
25
+ export const HELP = `appilot-mcp ${SERVER_VERSION}
26
+
27
+ The Appilot MCP server: read, validate, fix and provision an Appilot app.
28
+
29
+ USAGE
30
+ appilot-mcp Start the MCP server on stdio (what a client launches)
31
+ appilot-mcp --http Start the remote HTTP service instead
32
+ appilot-mcp plan <file> Preview an appilot.app-manifest. Writes nothing
33
+ appilot-mcp apply <file> Plan, then apply the same manifest
34
+ appilot-mcp install-skill Copy the app-configurator skill to an agent client
35
+ appilot-mcp --help | --version
36
+
37
+ OPTIONS
38
+ apply --mode merge|replace How the config half is imported. Default merge
39
+ --allow-unhealthy Import a bundle the health gate would refuse
40
+ install-skill --claude | --codex | --cursor | --gemini | --dir <path>
41
+
42
+ ENVIRONMENT
43
+ APPILOT_BASE_URL Your instance, e.g. https://api.appilot.space or http://localhost:6001
44
+ APPILOT_PAT A scoped service token from the Backoffice, under Service tokens
45
+ APPILOT_APP_ID Optional default app id
46
+
47
+ Docs: https://docs.appilot.space/docs/developers/configure-with-ai/overview`;
48
+ /** Where each client looks for a skill. One directory per client, all under $HOME. */
49
+ export const SKILL_TARGETS = {
50
+ claude: join('.claude', 'skills'),
51
+ codex: join('.codex', 'skills'),
52
+ cursor: join('.cursor', 'skills'),
53
+ gemini: join('.gemini', 'config', 'skills'),
54
+ };
55
+ /**
56
+ * The bundled skill directory, resolved from the running file.
57
+ *
58
+ * `dist/index.bundle.js` and `skills/` are siblings under the package root in
59
+ * the tarball and in the workspace, so one hop up is the answer in both. The
60
+ * fallback covers a deeper build output rather than guessing silently.
61
+ */
62
+ export function bundledSkillDir(from = fileURLToPath(import.meta.url)) {
63
+ const candidates = [
64
+ join(from, '..', '..', 'skills', 'app-configurator'),
65
+ join(from, '..', '..', '..', 'skills', 'app-configurator'),
66
+ ];
67
+ return candidates.find(candidate => existsSync(join(candidate, 'SKILL.md'))) ?? null;
68
+ }
69
+ function installSkill(args) {
70
+ const source = bundledSkillDir();
71
+ if (!source) {
72
+ process.stderr.write('[appilot-mcp] the bundled skill was not found next to this install. Reinstall the package (npm i -g appilot-mcp), or copy skills/app-configurator from the repository by hand.\n');
73
+ return 1;
74
+ }
75
+ const dirFlag = args.indexOf('--dir');
76
+ let destParent;
77
+ if (dirFlag !== -1) {
78
+ const explicit = args[dirFlag + 1];
79
+ if (!explicit) {
80
+ process.stderr.write('[appilot-mcp] --dir needs a path.\n');
81
+ return 2;
82
+ }
83
+ destParent = explicit;
84
+ }
85
+ else {
86
+ const client = Object.keys(SKILL_TARGETS).find(name => args.includes(`--${name}`));
87
+ if (!client) {
88
+ process.stderr.write(`[appilot-mcp] name the client: ${Object.keys(SKILL_TARGETS).map(c => `--${c}`).join(', ')}, or --dir <path>.\n`);
89
+ return 2;
90
+ }
91
+ destParent = join(homedir(), SKILL_TARGETS[client]);
92
+ }
93
+ const dest = join(destParent, 'app-configurator');
94
+ mkdirSync(destParent, { recursive: true });
95
+ cpSync(source, dest, { recursive: true });
96
+ process.stdout.write(`Installed the app-configurator skill to ${dest}\nStart a new session so the client loads it.\n`);
97
+ return 0;
98
+ }
99
+ function readManifestFile(path) {
100
+ if (!path)
101
+ throw new Error('Name the manifest file: appilot-mcp plan app.appilot.json');
102
+ if (!existsSync(path))
103
+ throw new Error(`No such file: ${path}`);
104
+ return JSON.parse(readFileSync(path, 'utf8'));
105
+ }
106
+ async function runPlan(args) {
107
+ const conn = loadConnection();
108
+ const client = new AppilotClient(conn);
109
+ const manifest = parseManifest(readManifestFile(args[0]));
110
+ const plan = await planManifest(client, manifest, id => {
111
+ const resolved = id ?? conn.defaultAppId;
112
+ return resolved != null && Number.isFinite(resolved) ? resolved : null;
113
+ });
114
+ process.stdout.write(JSON.stringify(plan, null, 2) + '\n');
115
+ return 0;
116
+ }
117
+ /**
118
+ * Apply runs its own plan and passes the token it produced.
119
+ *
120
+ * The token binds an apply to a preview of the same bytes, and a CLI that made
121
+ * the caller carry it between two commands would only teach them to store it.
122
+ * Planning here keeps the property (the manifest cannot change between the two)
123
+ * and prints the preview, so the run is still auditable in a CI log.
124
+ */
125
+ async function runApply(args) {
126
+ const conn = loadConnection();
127
+ const client = new AppilotClient(conn);
128
+ const manifest = parseManifest(readManifestFile(args[0]));
129
+ const modeFlag = args.indexOf('--mode');
130
+ const mode = modeFlag !== -1 ? args[modeFlag + 1] : undefined;
131
+ if (mode !== undefined && mode !== 'merge' && mode !== 'replace') {
132
+ throw new Error(`--mode is merge or replace, not ${mode}.`);
133
+ }
134
+ const plan = await planManifest(client, manifest, id => {
135
+ const resolved = id ?? conn.defaultAppId;
136
+ return resolved != null && Number.isFinite(resolved) ? resolved : null;
137
+ });
138
+ process.stderr.write(`[appilot-mcp] planned ${plan.planToken.slice(0, 12)}…\n`);
139
+ const result = await applyManifest(client, manifest, {
140
+ planToken: plan.planToken,
141
+ mode,
142
+ expectedCurrentHash: plan.config?.expectedCurrentHash,
143
+ allowUnhealthy: args.includes('--allow-unhealthy'),
144
+ });
145
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
146
+ return 0;
147
+ }
148
+ /**
149
+ * Handle everything that is not "start a server".
150
+ *
151
+ * Returns null when the argv asks for a server, so the entry point stays the
152
+ * transport switch it was.
153
+ */
154
+ export async function runCommand(argv) {
155
+ const [command, ...rest] = argv;
156
+ if (command === '--help' || command === '-h' || command === 'help') {
157
+ process.stdout.write(HELP + '\n');
158
+ return 0;
159
+ }
160
+ if (command === '--version' || command === '-v' || command === 'version') {
161
+ process.stdout.write(SERVER_VERSION + '\n');
162
+ return 0;
163
+ }
164
+ if (command === 'install-skill')
165
+ return installSkill(rest);
166
+ if (command === 'plan')
167
+ return runPlan(rest);
168
+ if (command === 'apply')
169
+ return runApply(rest);
170
+ return null;
171
+ }
package/dist/client.d.ts CHANGED
@@ -26,6 +26,13 @@ export interface Capabilities {
26
26
  bundleFormatVersion: string;
27
27
  maxImportBytes: number;
28
28
  };
29
+ /**
30
+ * The closed enums the write paths reject on, sourced from the instance so
31
+ * the published list and the validator are one list. Absent on instances
32
+ * that predate it, which is why every reader treats it as optional: an agent
33
+ * with no vocabulary guesses, and `css` is the guess it makes.
34
+ */
35
+ entityVocabularies?: Record<string, unknown>;
29
36
  }
30
37
  /** Mirrors the backend's ProvisioningService result. Secrets appear once. */
31
38
  export interface ProvisionAppResponse {
@@ -45,10 +52,19 @@ export interface ProvisionAppResponse {
45
52
  }>;
46
53
  widgetKey?: {
47
54
  id: number | null;
48
- action: 'created' | 'reused' | 'updated';
55
+ /**
56
+ * `blocked` is a real outcome, not an error: a LIVE key needs a verified
57
+ * domain, and the app and its domains are still provisioned without it.
58
+ * The union has to carry it or every caller reads a blocked key as a
59
+ * successful one.
60
+ */
61
+ action: 'created' | 'reused' | 'updated' | 'blocked';
49
62
  keyPrefix?: string;
50
63
  rawKey?: string;
51
64
  rawSecret?: string;
65
+ allowedDomains?: string[];
66
+ /** Present on `blocked`: what is missing and what to do about it. */
67
+ reason?: string;
52
68
  };
53
69
  integration: {
54
70
  apiUrl: string | null;
@@ -84,11 +100,53 @@ export interface ProvisionedAppSummary {
84
100
  verification_status: string;
85
101
  }>;
86
102
  }
103
+ /** A widget key as the org sees it back: no raw value, no secret, ever. */
104
+ export interface ProvisionedWidgetKey {
105
+ id: number;
106
+ name: string;
107
+ keyPrefix: string;
108
+ allowedDomains: string[];
109
+ isActive: boolean;
110
+ }
87
111
  export declare class AppilotApiError extends Error {
88
112
  readonly status: number;
89
113
  readonly body?: unknown;
90
114
  constructor(message: string, status: number, body?: unknown);
91
115
  }
116
+ /**
117
+ * The eight configurable entities, and where each one is written.
118
+ *
119
+ * Session templates and zones are here for the same reason as the rest: an app
120
+ * whose activities are conversations, or whose pages need landmark regions, was
121
+ * not authorable from a conversation at all, because the only writer that
122
+ * reached them was a whole-bundle import.
123
+ */
124
+ export declare const ENTITY_PATHS: {
125
+ readonly view: "/views";
126
+ readonly control: "/domain/controls";
127
+ readonly form: "/domain/forms";
128
+ readonly tool: "/domain/tools";
129
+ readonly zone: "/domain/zones";
130
+ readonly action_plan: "/domain/action-plans";
131
+ readonly knowledge: "/knowledge/content";
132
+ readonly session_template: "/domain/session-templates";
133
+ };
134
+ export type ConfigEntityKind = keyof typeof ENTITY_PATHS;
135
+ export declare const CONFIG_ENTITY_KINDS: ConfigEntityKind[];
136
+ /** What `POST /developer-reports` answers. The posture is the point of it. */
137
+ export interface DeveloperReportResponse {
138
+ report: {
139
+ id: string;
140
+ status: string;
141
+ occurrences: number;
142
+ kind: string;
143
+ title: string;
144
+ };
145
+ supportPosture: {
146
+ eligible: boolean;
147
+ message: string;
148
+ };
149
+ }
92
150
  export declare class AppilotClient {
93
151
  private readonly conn;
94
152
  constructor(conn: AppilotConnection);
@@ -103,24 +161,60 @@ export declare class AppilotClient {
103
161
  listForms(appId: number): Promise<Record<string, unknown>[]>;
104
162
  listViews(appId: number): Promise<Record<string, unknown>[]>;
105
163
  listKnowledge(appId: number): Promise<Record<string, unknown>[]>;
164
+ listTools(appId: number): Promise<Record<string, unknown>[]>;
165
+ listZones(appId: number): Promise<Record<string, unknown>[]>;
166
+ /**
167
+ * The app's domains: which hostnames it serves, the languages each declares,
168
+ * and whether the hostname is verified. On the config surface, so a read-only
169
+ * credential can resolve the locales the configuration is expected to cover.
170
+ */
171
+ listAppDomains(appId: number): Promise<Record<string, unknown>[]>;
172
+ private entityPath;
173
+ createEntity(kind: ConfigEntityKind, body: unknown): Promise<unknown>;
174
+ updateEntity(kind: ConfigEntityKind, id: string, body: unknown): Promise<unknown>;
175
+ deleteEntity(kind: ConfigEntityKind, id: string): Promise<unknown>;
106
176
  updateActionPlan(id: string, body: unknown): Promise<unknown>;
107
177
  updateControl(id: string, body: unknown): Promise<unknown>;
108
178
  createControl(body: unknown): Promise<unknown>;
109
179
  updateForm(id: string, body: unknown): Promise<unknown>;
110
180
  updateKnowledge(id: string, body: unknown): Promise<unknown>;
181
+ createDeveloperReport(body: unknown): Promise<DeveloperReportResponse>;
182
+ listDeveloperReports(query: string): Promise<unknown>;
111
183
  /** Server-side non-persisting plan validation (echoes the runtime trust boundary). */
112
184
  validatePlan(appId: number, sections: unknown, formValues: unknown): Promise<{
113
185
  ok: boolean;
114
186
  errors: string[];
115
187
  }>;
116
188
  listProvisioned(): Promise<ProvisionedAppSummary[]>;
189
+ /**
190
+ * The org's widget keys, prefixes only.
191
+ *
192
+ * A caller that cannot see the keys it already minted asks for another every
193
+ * run, and each one is a live credential.
194
+ */
195
+ listWidgetKeys(): Promise<ProvisionedWidgetKey[]>;
117
196
  provisionApp(body: ProvisionAppRequest): Promise<ProvisionAppResponse>;
118
197
  /**
119
198
  * Domain check: does this hostname resolve to a tenant? Public endpoint, so
120
199
  * it answers even for a read-only caller, which is what makes it usable as
121
200
  * the first probe of `verify_integration`.
201
+ *
202
+ * The endpoint takes a URL and derives the hostname itself; passing the bare
203
+ * hostname made the probe answer "URL is required" and report a warning about
204
+ * the tenant on every single run.
122
205
  */
123
206
  checkDomain(domain: string): Promise<Record<string, unknown>>;
207
+ /**
208
+ * The TXT record a domain needs, and where its verification stands.
209
+ *
210
+ * These two live on the apps router rather than under `/provision`, and that
211
+ * router authenticates an organization SESSION. A service token is refused
212
+ * there today, which is why `verify_domain` falls back to the provisioning
213
+ * dry run for the status and says plainly what it could not do.
214
+ */
215
+ domainVerification(domainId: number): Promise<Record<string, unknown>>;
216
+ /** Ask Appilot to look for the TXT record now. */
217
+ triggerDomainVerification(domainId: number): Promise<Record<string, unknown>>;
124
218
  /** Who this credential is: org, app narrowing, scopes. Never a secret. */
125
219
  whoami(): Promise<Record<string, unknown>>;
126
220
  exportConfig(appId: number): Promise<import('appilot-shared/config-bundle').ConfigBundle>;
@@ -133,8 +227,33 @@ export declare class AppilotClient {
133
227
  }): Promise<import('appilot-shared/config-bundle').ImportDiff>;
134
228
  /**
135
229
  * Read the app's content-model config and normalize it into a ConfigSnapshot.
136
- * `expectedLocales` defaults to de/en/es (Appilot's trilingual baseline) but
137
- * can be overridden per call once the domain's configured_languages are known.
230
+ *
231
+ * Two properties matter here and were both missing before.
232
+ *
233
+ * A failed read is a GAP, never an empty list. Swallowing the error made a
234
+ * refused knowledge read indistinguishable from an app with no knowledge, and
235
+ * every knowledge lint then passed on a configuration nobody had looked at.
236
+ *
237
+ * `expectedLocales` comes from the app's own domains when the caller does not
238
+ * say. The trilingual default is Appilot's baseline, not every customer's: a
239
+ * single-language app used to collect two medium i18n findings per entity for
240
+ * languages it had never claimed to support.
138
241
  */
139
242
  buildSnapshot(appId: number, expectedLocales?: string[]): Promise<ConfigSnapshot>;
243
+ /**
244
+ * Why an empty read should not be believed, or null when the credential is
245
+ * fine and the app genuinely has nothing.
246
+ *
247
+ * Only a 401 or a 403 counts. An instance too old to answer `whoami` at all
248
+ * says nothing about the token, and reporting a gap on that would be a
249
+ * confident false alarm on every on-premise deployment behind cloud.
250
+ */
251
+ private credentialRefusal;
252
+ /**
253
+ * Which locales this app's configuration is expected to cover: the union of
254
+ * its domains' configured languages. Falls back to the trilingual baseline
255
+ * when the instance cannot answer, which is the behaviour every caller had
256
+ * before and is still better than checking nothing.
257
+ */
258
+ private resolveLocales;
140
259
  }