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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/LICENSE +15 -0
- package/README.md +102 -24
- package/dist/appilot-configurator.mcpb +0 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +171 -0
- package/dist/client.d.ts +122 -3
- package/dist/client.js +306 -31
- package/dist/config.d.ts +14 -0
- package/dist/config.js +19 -0
- package/dist/contract/bundleSnapshot.js +8 -1
- package/dist/contract/healthContract.d.ts +1 -1
- package/dist/contract/healthContract.js +100 -10
- package/dist/contract/types.d.ts +37 -1
- package/dist/index.bundle.js +4487 -16520
- package/dist/index.js +7 -0
- package/dist/inspect.d.ts +88 -0
- package/dist/inspect.js +384 -0
- package/dist/manifest.d.ts +14 -2
- package/dist/manifest.js +31 -9
- package/dist/public-marketplace/.claude-plugin/marketplace.json +20 -0
- package/dist/public-marketplace/README.md +23 -0
- package/dist/public-marketplace/plugins/app-configurator/.claude-plugin/plugin.json +43 -0
- package/dist/public-marketplace/plugins/app-configurator/README.md +328 -0
- package/dist/public-marketplace/plugins/app-configurator/dist/index.bundle.js +57370 -0
- package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/SKILL.md +267 -0
- package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/agents/openai.yaml +13 -0
- package/dist/redaction.d.ts +18 -3
- package/dist/redaction.js +27 -3
- package/dist/remote/consent.d.ts +30 -20
- package/dist/remote/consent.js +114 -82
- package/dist/remote/consentMessages.d.ts +65 -0
- package/dist/remote/consentMessages.js +199 -0
- package/dist/remote/handoff.d.ts +10 -0
- package/dist/remote/handoff.js +44 -0
- package/dist/remote/httpServer.js +28 -5
- package/dist/remote/oauth.d.ts +39 -6
- package/dist/remote/oauth.js +281 -36
- package/dist/scaffold.d.ts +110 -1
- package/dist/scaffold.js +474 -39
- package/dist/server.js +425 -38
- package/dist/soak.js +21 -1
- package/dist/templates.d.ts +62 -0
- package/dist/templates.js +255 -0
- package/dist/verify.js +18 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/examples/app.appilot.json +212 -0
- package/mcpb/manifest.json +117 -15
- package/package.json +5 -3
- package/skills/app-configurator/SKILL.md +136 -25
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-configurator",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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 |
|
|
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 (
|
|
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
|
|
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
|
-
| `
|
|
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
|
-
|
|
112
|
-
plugin
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
202
|
+
```bash
|
|
203
|
+
npx -y appilot-mcp install-skill --cursor
|
|
204
|
+
```
|
|
152
205
|
|
|
153
|
-
|
|
206
|
+
Then add the server to `~/.cursor/mcp.json`.
|
|
154
207
|
|
|
155
|
-
|
|
208
|
+
### Antigravity
|
|
156
209
|
|
|
157
|
-
|
|
158
|
-
-
|
|
210
|
+
```bash
|
|
211
|
+
npx -y appilot-mcp install-skill --gemini
|
|
212
|
+
```
|
|
159
213
|
|
|
160
|
-
|
|
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
|
-
|
|
183
|
-
`
|
|
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
|
|
191
|
-
client registry
|
|
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` |
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
137
|
-
*
|
|
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
|
}
|