appilot-mcp 0.2.1 → 0.4.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 (47) 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 +133 -27
  5. package/dist/appilot-configurator.mcpb +0 -0
  6. package/dist/cli.d.ts +34 -0
  7. package/dist/cli.js +173 -0
  8. package/dist/client.d.ts +45 -1
  9. package/dist/client.js +74 -1
  10. package/dist/config.d.ts +21 -0
  11. package/dist/config.js +6 -0
  12. package/dist/contract/healthContract.js +31 -4
  13. package/dist/index.bundle.js +2111 -826
  14. package/dist/index.d.ts +7 -1
  15. package/dist/index.js +22 -4
  16. package/dist/manifest.d.ts +14 -2
  17. package/dist/manifest.js +31 -9
  18. package/dist/public-marketplace/.claude-plugin/marketplace.json +20 -0
  19. package/dist/public-marketplace/README.md +23 -0
  20. package/dist/public-marketplace/plugins/app-configurator/.claude-plugin/plugin.json +43 -0
  21. package/dist/public-marketplace/plugins/app-configurator/README.md +328 -0
  22. package/dist/public-marketplace/plugins/app-configurator/dist/index.bundle.js +57370 -0
  23. package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/SKILL.md +267 -0
  24. package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/agents/openai.yaml +13 -0
  25. package/dist/redaction.d.ts +51 -0
  26. package/dist/redaction.js +59 -0
  27. package/dist/remote/consent.d.ts +10 -2
  28. package/dist/remote/consent.js +16 -6
  29. package/dist/remote/consentMessages.d.ts +6 -1
  30. package/dist/remote/consentMessages.js +15 -6
  31. package/dist/remote/httpServer.d.ts +10 -0
  32. package/dist/remote/httpServer.js +126 -40
  33. package/dist/remote/oauth.d.ts +10 -1
  34. package/dist/remote/oauth.js +29 -11
  35. package/dist/scaffold.d.ts +68 -6
  36. package/dist/scaffold.js +424 -97
  37. package/dist/server.js +175 -18
  38. package/dist/userClient.d.ts +213 -0
  39. package/dist/userClient.js +400 -0
  40. package/dist/userServer.d.ts +47 -0
  41. package/dist/userServer.js +248 -0
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/examples/app.appilot.json +212 -0
  45. package/mcpb/manifest.json +117 -21
  46. package/package.json +5 -3
  47. package/skills/app-configurator/SKILL.md +61 -19
package/dist/index.d.ts CHANGED
@@ -2,7 +2,13 @@
2
2
  /**
3
3
  * Appilot MCP server entry point.
4
4
  *
5
- * Two transports, one tool surface (see server.ts):
5
+ * Two servers ship from this package. Appilot Studio (`server.ts`) writes an
6
+ * app's configuration, and is the default. Appilot (`userServer.ts`, started
7
+ * with `--runtime`) operates an already-configured app for the person using it.
8
+ * One MCP process serves one of them; the remote service serves both at once,
9
+ * on two paths.
10
+ *
11
+ * Two transports (see server.ts):
6
12
  *
7
13
  * stdio (default) The operator runs the process on their own machine or
8
14
  * inside their network, and it carries their credentials in
package/dist/index.js CHANGED
@@ -2,7 +2,13 @@
2
2
  /**
3
3
  * Appilot MCP server entry point.
4
4
  *
5
- * Two transports, one tool surface (see server.ts):
5
+ * Two servers ship from this package. Appilot Studio (`server.ts`) writes an
6
+ * app's configuration, and is the default. Appilot (`userServer.ts`, started
7
+ * with `--runtime`) operates an already-configured app for the person using it.
8
+ * One MCP process serves one of them; the remote service serves both at once,
9
+ * on two paths.
10
+ *
11
+ * Two transports (see server.ts):
6
12
  *
7
13
  * stdio (default) The operator runs the process on their own machine or
8
14
  * inside their network, and it carries their credentials in
@@ -18,20 +24,32 @@
18
24
  * Server: docs/architecture/appilot-mcp.md.
19
25
  */
20
26
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
- import { loadConnection, loadRemoteConfig, resolveTransport, RemoteConfigError } from './config.js';
27
+ import { loadConnection, loadRemoteConfig, resolveServerSurface, resolveTransport, RemoteConfigError } from './config.js';
22
28
  import { createAppilotServer } from './server.js';
29
+ import { createAppilotRuntimeServer } from './userServer.js';
30
+ import { runCommand } from './cli.js';
23
31
  async function runStdio() {
24
32
  const conn = loadConnection();
25
- const server = createAppilotServer(conn);
33
+ const runtime = resolveServerSurface() === 'runtime';
34
+ const server = runtime ? createAppilotRuntimeServer(conn) : createAppilotServer(conn);
26
35
  await server.connect(new StdioServerTransport());
27
36
  // stderr is safe for logs; stdout is the MCP transport.
28
- process.stderr.write(`[appilot-mcp] connected · base=${conn.baseUrl ?? 'not-configured'} · token=${conn.token ? 'set' : 'none'}\n`);
37
+ const credential = runtime
38
+ ? `session=${conn.sessionToken ? 'set' : 'none'}`
39
+ : `token=${conn.token ? 'set' : 'none'}`;
40
+ process.stderr.write(`[appilot-mcp] connected · server=${runtime ? 'appilot (runtime)' : 'studio (config)'} · base=${conn.baseUrl ?? 'not-configured'} · ${credential}\n`);
29
41
  }
30
42
  async function runHttp() {
31
43
  const { startRemote } = await import('./remote/httpServer.js');
32
44
  await startRemote(loadRemoteConfig());
33
45
  }
34
46
  async function main() {
47
+ // A subcommand answers and exits. Without this, `appilot-mcp --help` started
48
+ // the stdio server and waited on stdin, which reads as a hang.
49
+ const handled = await runCommand(process.argv.slice(2));
50
+ if (handled !== null) {
51
+ process.exit(handled);
52
+ }
35
53
  if (resolveTransport() === 'http') {
36
54
  await runHttp();
37
55
  return;
@@ -81,12 +81,24 @@ export declare function planManifest(client: AppilotClient, manifest: AppManifes
81
81
  * the token is unobtainable without the plan call and changes with the
82
82
  * manifest.
83
83
  */
84
- export declare function applyManifest(client: AppilotClient, manifest: AppManifest, options: {
84
+ export interface ApplyManifestOptions {
85
85
  planToken: string;
86
86
  mode?: 'merge' | 'replace';
87
87
  expectedCurrentHash?: string;
88
88
  allowUnhealthy?: boolean;
89
- }): Promise<{
89
+ /**
90
+ * The refusal to raise when the caller may not provision, or null/undefined
91
+ * when it may.
92
+ *
93
+ * Set by the tool layer from the connection's grant. The apply then proceeds
94
+ * whenever the provisioning half changes nothing, which is the ordinary case
95
+ * in CI: the app, its domains and its keys were created once by a person and
96
+ * every run after that only syncs configuration. Demanding provision:write
97
+ * for that run contradicted the reason the two scopes are separate.
98
+ */
99
+ provisionRefusal?: string | null;
100
+ }
101
+ export declare function applyManifest(client: AppilotClient, manifest: AppManifest, options: ApplyManifestOptions): Promise<{
90
102
  provisioning: ProvisionAppResponse;
91
103
  config?: unknown;
92
104
  notes: string[];
package/dist/manifest.js CHANGED
@@ -106,21 +106,43 @@ export async function planManifest(client, manifest, resolveAppId) {
106
106
  }
107
107
  return { planToken: manifestDigest(manifest), provisioning, config, notes };
108
108
  }
109
- /**
110
- * Apply a previously planned manifest.
111
- *
112
- * `planToken` must match the manifest being applied. This is the structural
113
- * version of "always dry-run first": the guidance cannot be skipped, because
114
- * the token is unobtainable without the plan call and changes with the
115
- * manifest.
116
- */
109
+ /** What the provisioning half of this manifest would change, in plain words. */
110
+ function provisioningChanges(preview) {
111
+ const changes = [];
112
+ if (preview.app.action !== 'reused')
113
+ changes.push(`the app "${preview.app.name}" (${preview.app.action})`);
114
+ for (const domain of preview.domains) {
115
+ if (domain.action !== 'reused')
116
+ changes.push(`the domain ${domain.domain} (${domain.action})`);
117
+ }
118
+ if (preview.widgetKey && preview.widgetKey.action !== 'reused') {
119
+ changes.push(`a widget key (${preview.widgetKey.action})`);
120
+ }
121
+ return changes;
122
+ }
117
123
  export async function applyManifest(client, manifest, options) {
118
124
  const expected = manifestDigest(manifest);
119
125
  if (options.planToken !== expected) {
120
126
  throw new Error('planToken does not match this manifest. Run plan_manifest on the exact manifest you intend to apply, then pass the planToken it returns. A mismatch means the manifest changed after it was previewed.');
121
127
  }
122
128
  const notes = [];
123
- const provisioning = await client.provisionApp(provisionRequest(manifest, false));
129
+ // Ask what provisioning would do before deciding whether it may. The dry run
130
+ // is the same call with `dryRun: true`, so this costs one request and turns
131
+ // "this manifest needs provision:write" from a property of the tool into a
132
+ // property of the manifest in front of it.
133
+ const preview = await client.provisionApp(provisionRequest(manifest, true));
134
+ const changes = provisioningChanges(preview);
135
+ if (changes.length > 0 && options.provisionRefusal) {
136
+ throw new Error(`${options.provisionRefusal}\nThis manifest would change ${changes.join(', ')}, so the apply cannot proceed without it. A manifest whose app, domains and keys already exist applies with config:write alone.`);
137
+ }
138
+ let provisioning;
139
+ if (changes.length === 0) {
140
+ provisioning = preview;
141
+ notes.push('Provisioning wrote nothing: the app, its domains and its keys already matched the manifest, so this apply needed only config:write. The provisioning block below is the preview, which is why it reports dryRun true.');
142
+ }
143
+ else {
144
+ provisioning = await client.provisionApp(provisionRequest(manifest, false));
145
+ }
124
146
  let config;
125
147
  if (manifest.config) {
126
148
  const appId = provisioning.app.id;
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "appilot",
3
+ "owner": {
4
+ "name": "Appilot",
5
+ "url": "https://appilot.space"
6
+ },
7
+ "metadata": {
8
+ "description": "Appilot agent tooling: skills and MCP servers that configure and operate Appilot apps.",
9
+ "version": "0.3.0"
10
+ },
11
+ "plugins": [
12
+ {
13
+ "name": "app-configurator",
14
+ "source": "./plugins/app-configurator",
15
+ "description": "Set up, configure, audit, repair, back up, restore and verify an Appilot app: provision the app, its domains and its widget key, scaffold the host integration, and keep the content model correct against the config health contract. Bundles the app-configurator skill and the Appilot MCP server (cloud or on-premise).",
16
+ "version": "0.3.0",
17
+ "strict": false
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,23 @@
1
+ # Appilot plugins
2
+
3
+ The Claude Code marketplace for the Appilot **app-configurator** plugin: the
4
+ `app-configurator` skill plus the Appilot MCP server, which reads, validates,
5
+ fixes and provisions an Appilot app.
6
+
7
+ ```
8
+ /plugin marketplace add appilot/appilot-plugins
9
+ /plugin install app-configurator@appilot
10
+ ```
11
+
12
+ The plugin asks for your instance URL and a scoped service token from the
13
+ Backoffice, under Service tokens.
14
+
15
+ Other ways in, for clients that are not Claude Code: `npx -y appilot-mcp` plus
16
+ `appilot-mcp install-skill --codex` (or `--cursor`, `--gemini`), the hosted
17
+ endpoint at https://mcp.appilot.space/mcp for ChatGPT and claude.ai, and the
18
+ Claude Desktop bundle at https://downloads.appilot.space/claude-desktop.
19
+
20
+ Docs: https://docs.appilot.space/docs/developers/configure-with-ai/overview
21
+
22
+ Built from appilot-mcp 0.3.0. Do not edit here; this tree is generated by
23
+ `pnpm -C packages/tools/appilot-mcp build:marketplace` in the Appilot monorepo.
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "app-configurator",
3
+ "version": "0.3.0",
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
+ "author": {
6
+ "name": "Appilot",
7
+ "url": "https://appilot.space"
8
+ },
9
+ "homepage": "https://docs.appilot.space/docs/developers/configure-with-ai/overview",
10
+ "skills": "./skills/",
11
+ "mcpServers": {
12
+ "appilot": {
13
+ "command": "node",
14
+ "args": [
15
+ "${CLAUDE_PLUGIN_ROOT}/dist/index.bundle.js"
16
+ ],
17
+ "env": {
18
+ "APPILOT_BASE_URL": "${user_config.base_url}",
19
+ "APPILOT_PAT": "${user_config.pat}",
20
+ "APPILOT_APP_ID": "${user_config.app_id}"
21
+ }
22
+ }
23
+ },
24
+ "userConfig": {
25
+ "base_url": {
26
+ "type": "string",
27
+ "title": "Appilot backend URL",
28
+ "description": "Cloud or on-premise Appilot backend URL.",
29
+ "required": true
30
+ },
31
+ "pat": {
32
+ "type": "string",
33
+ "title": "Appilot service token",
34
+ "description": "Scoped appilot_pat service token. config:read for audits, config:write for changes, provision:write to create apps, domains, and widget keys.",
35
+ "sensitive": true
36
+ },
37
+ "app_id": {
38
+ "type": "string",
39
+ "title": "Default Appilot app ID",
40
+ "description": "Optional default app ID used when a tool call does not provide one."
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,328 @@
1
+ # appilot-mcp
2
+
3
+ MCP server that lets an AI agent **read, validate, fix, and soak** an Appilot
4
+ app's content-model configuration (Views, Controls, Forms, Action Plans,
5
+ Knowledge, Zones, Tools) against the **config health contract**.
6
+
7
+ It is the tool surface behind the `app-configurator` skill. The skill is the
8
+ instructions; this server is what actually touches your instance.
9
+
10
+ ## Why
11
+
12
+ Hand-authored app config drifts silently: a plan that opens an input but never
13
+ submits, an auto-generated selector like `#nc-vue-30`, half-translated labels, a
14
+ knowledge article that is really a procedure. This server encodes the contract
15
+ those failures violate and checks it locally (no round-trip, air-gapped-safe)
16
+ and against the live instance.
17
+
18
+ ## Cloud or on-premise
19
+
20
+ Endpoint-agnostic by design. The same binary talks to a cloud tenant or an
21
+ on-premise / sovereign instance purely by pointing `APPILOT_BASE_URL` at it. The
22
+ static gate runs locally from `appilot-shared`, so it works with no network.
23
+
24
+ ## Two transports, one tool surface
25
+
26
+ The same tools serve two shapes, and which one you need is decided by the client,
27
+ not by preference.
28
+
29
+ | Transport | Who launches it | Credential | Clients |
30
+ |-----------|-----------------|------------|---------|
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 | Account approval or a manually supplied service token through OAuth | ChatGPT, claude.ai, and any remote MCP client |
33
+
34
+ ChatGPT and claude.ai cannot start a process on your machine, and neither offers
35
+ a field for pasting an API key: they connect to a URL and authenticate with
36
+ OAuth. That is what HTTP mode is for. See
37
+ [Remote deployment](#remote-deployment).
38
+
39
+ ## Configuration
40
+
41
+ | Env | Required | Meaning |
42
+ |-----|----------|---------|
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 (Service tokens). `config:read` to read/validate, `config:write` to apply. |
45
+ | `APPILOT_APP_ID` | optional | Default app id for tools that omit one |
46
+ | `APPILOT_SOAK_STORAGE_STATE` | optional | Path to a Playwright `storageState` JSON for an authenticated site session (soak) |
47
+
48
+ HTTP mode reads a different profile. It holds no Appilot credential of its own,
49
+ because each caller brings theirs.
50
+
51
+ | Env | Required | Meaning |
52
+ |-----|----------|---------|
53
+ | `APPILOT_BASE_URL` | yes | The one instance this deployment fronts. Never client-supplied: a caller-chosen URL would make the service an SSRF relay. |
54
+ | `APPILOT_MCP_PUBLIC_URL` | yes | The public origin clients reach, e.g. `https://mcp.appilot.space`. OAuth metadata and redirects are absolute. |
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
+ | `PORT` | no | Defaults to 8080. Cloud Run supplies it. |
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`. |
61
+
62
+ ## Tools
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
+
67
+ | Tool | Purpose |
68
+ |------|---------|
69
+ | `capabilities` | Version, migration level and the closed vocabularies this instance accepts (negotiate before configuring) |
70
+ | `read_config` | Normalized snapshot of an app's content model |
71
+ | `validate_config` | Severity-ranked findings against the health contract (local + server echo) |
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 |
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 |
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
+
109
+
110
+ ## Distribution artifacts
111
+
112
+ One package carries a tool-neutral implementation plus thin discovery adapters:
113
+
114
+ | Artifact | Consumers |
115
+ |----------|-----------|
116
+ | `skills/app-configurator/SKILL.md` | Open Agent Skills clients, including Codex, Claude Code, Cursor, and Antigravity |
117
+ | `.mcp.json` | Codex plugin MCP declaration |
118
+ | `.codex-plugin/plugin.json` | Codex plugin |
119
+ | `.claude-plugin/plugin.json` | Claude Code plugin |
120
+ | `mcpb/manifest.json` | Claude Desktop bundle manifest, packed by `pnpm build:mcpb` |
121
+ | `Dockerfile` | The remote HTTP service |
122
+ | `dist/index.bundle.js` | Self-contained Node.js MCP server, both transports |
123
+
124
+ `pnpm pack` runs the build and includes all of these artifacts in the package.
125
+ The server can initialize and expose its tools before connection settings are
126
+ provided. Live tool calls return a clear configuration error until
127
+ `APPILOT_BASE_URL` is set.
128
+
129
+ ## Install from this repository
130
+
131
+ Build the self-contained server first:
132
+
133
+ ```bash
134
+ pnpm -C packages/tools/appilot-mcp build
135
+ ```
136
+
137
+ ### Codex
138
+
139
+ The repository is a Codex marketplace:
140
+
141
+ ```bash
142
+ codex plugin marketplace add /absolute/path/to/app-pilot
143
+ codex plugin add app-configurator@appilot
144
+ ```
145
+
146
+ The plugin installs both the skill and MCP server globally. Start a new Codex
147
+ task after installation so the new tool surface is loaded.
148
+
149
+ ### Claude Code
150
+
151
+ Add the repository marketplace and install `app-configurator@appilot`. Claude's
152
+ plugin settings prompt for the backend URL, service token, and optional app ID.
153
+ The token is declared sensitive so Claude stores it in secure credential
154
+ storage.
155
+
156
+ ### Claude Desktop
157
+
158
+ Build the double-clickable bundle and open it:
159
+
160
+ ```bash
161
+ pnpm -C packages/tools/appilot-mcp build
162
+ pnpm -C packages/tools/appilot-mcp build:mcpb
163
+ open packages/tools/appilot-mcp/dist/appilot-configurator.mcpb
164
+ ```
165
+
166
+ Desktop asks for the backend URL and the service token, storing the token in the
167
+ OS keychain. The bundle carries the server and its manifest and nothing else, so
168
+ it installs on a machine with no Node toolchain and no network.
169
+
170
+ ### ChatGPT and claude.ai
171
+
172
+ Neither can launch a local process, so both connect to a deployed HTTP endpoint.
173
+ Once one is running (see [Remote deployment](#remote-deployment)):
174
+
175
+ - **ChatGPT**: Settings, Apps, Advanced settings, turn on Developer mode, then
176
+ add a connector pointing at `https://<your-host>/mcp` with OAuth. The
177
+ authorization step opens Appilot account approval, or manual token consent on older deployments.
178
+ - **claude.ai**: Settings, Connectors, Add custom connector, same URL.
179
+ - **Codex** can use the remote endpoint too, if you prefer one shared deployment
180
+ over a local process: add the URL to `~/.codex/config.toml` and run
181
+ `codex mcp login appilot`.
182
+
183
+ To reach a private instance from ChatGPT without exposing an endpoint at all,
184
+ run OpenAI's Secure MCP Tunnel against the local stdio server instead. The
185
+ tunnel client makes an outbound connection from inside your network, so the
186
+ instance needs no public listener.
187
+
188
+ ### Cursor
189
+
190
+ Copy `skills/app-configurator` to `~/.cursor/skills/app-configurator`, then add
191
+ the server to `~/.cursor/mcp.json`.
192
+
193
+ ### Antigravity
194
+
195
+ Copy `skills/app-configurator` to the applicable global skills directory:
196
+
197
+ - Antigravity IDE: `~/.gemini/config/skills/app-configurator`
198
+ - Antigravity CLI: `~/.gemini/antigravity-cli/skills/app-configurator`
199
+
200
+ Register the stdio server in `~/.gemini/config/mcp_config.json`.
201
+
202
+ ### Any stdio MCP client
203
+
204
+ Point the client at the built bundle:
205
+
206
+ ```json
207
+ {
208
+ "mcpServers": {
209
+ "appilot": {
210
+ "command": "node",
211
+ "args": ["/absolute/path/to/appilot-mcp/dist/index.bundle.js"],
212
+ "env": {
213
+ "APPILOT_BASE_URL": "https://api.appilot.space",
214
+ "APPILOT_PAT": "appilot_pat_...",
215
+ "APPILOT_APP_ID": "123"
216
+ }
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ For registry-backed distribution, replace `node` and the bundle path with
223
+ `pnpm dlx appilot-mcp` after the package is published.
224
+
225
+ Do not commit service tokens. Use the MCP client's secure secret storage or
226
+ environment forwarding when available.
227
+
228
+ ## Remote deployment
229
+
230
+ HTTP mode delegates identity and approval storage to the backend. It has no
231
+ local user table, client registry or raw token store. The OAuth authorization server seals its own
232
+ state into the artifacts it issues (see `src/remote/tokens.ts`), so a second
233
+ instance behaves exactly like the first and a cold start loses nothing.
234
+
235
+ What it serves:
236
+
237
+ | Path | Purpose |
238
+ |------|---------|
239
+ | `POST /mcp` | The MCP endpoint, Streamable HTTP, bearer required |
240
+ | `/.well-known/oauth-protected-resource/mcp` | Points a client at the authorization server |
241
+ | `/.well-known/oauth-authorization-server` | Metadata, PKCE with S256, dynamic registration |
242
+ | `/authorize`, `/token`, `/register` | The OAuth 2.1 flow |
243
+ | `/consent` | Manual token verification and review |
244
+ | `/connect/callback` | Redeems account approval bound to the initiating browser |
245
+ | `/health` (also `/healthz` off Cloud Run) | Liveness |
246
+
247
+ Run it:
248
+
249
+ ```bash
250
+ pnpm -C packages/tools/appilot-mcp build
251
+ docker build -t appilot-mcp packages/tools/appilot-mcp
252
+ docker run -p 8080:8080 \
253
+ -e APPILOT_BASE_URL=https://api.appilot.space \
254
+ -e APPILOT_MCP_PUBLIC_URL=https://mcp.appilot.space \
255
+ -e APPILOT_MCP_OAUTH_SECRET="$(openssl rand -base64 32)" \
256
+ appilot-mcp
257
+ ```
258
+
259
+ On Cloud Run, which is where the backend already runs, use the script rather
260
+ than a hand-typed `gcloud` line: it creates the signing secret, grants the
261
+ runtime service account access to it, and handles the two-pass problem where the
262
+ service URL only exists after the first deploy while `APPILOT_MCP_PUBLIC_URL`
263
+ has to name that exact origin.
264
+
265
+ ```bash
266
+ packages/tools/appilot-mcp/scripts/deploy-cloud-run.sh
267
+ ```
268
+
269
+ It deploys unauthenticated on purpose: the service does its own OAuth, and
270
+ platform-level IAM in front of it would block the discovery endpoints a client
271
+ has to read before it can authenticate at all. The upload carries the
272
+ `Dockerfile` and the one bundle, which is what `.gcloudignore` is for. Mapping
273
+ `mcp.appilot.space` onto the service is the last step, and the script prints it.
274
+
275
+ ### How access works
276
+
277
+ The service never sees an Appilot password. Account approval asks the backend
278
+ to create a scoped credential after the administrator reviews access. In the
279
+ manual flow, a person pastes a previously created token and MCP checks it against the instance
280
+ (`GET /config/whoami`) before granting anything. The granted scopes are the
281
+ intersection of what the client asked for and what that token actually carries,
282
+ so a `config:read` token cannot be talked into write access. The token is then
283
+ sealed inside the OAuth tokens the client holds; the client never sees it.
284
+
285
+ Revocation happens where the token was minted: revoke it in the Backoffice and
286
+ every connection built on it stops working on the next call. Rotating
287
+ `APPILOT_MCP_OAUTH_SECRET` invalidates every issued token at once.
288
+
289
+ The soak tool is deliberately unavailable in this mode: driving a real browser
290
+ belongs on the operator's machine over stdio, not in a shared web service.
291
+
292
+ ## Develop
293
+
294
+ ```bash
295
+ pnpm -C packages/tools/appilot-mcp build # tsc + the self-contained bundle
296
+ pnpm -C packages/tools/appilot-mcp build:mcpb # the Claude Desktop bundle
297
+ pnpm -C packages/tools/appilot-mcp test # vitest: contract, OAuth, HTTP end to end
298
+ pnpm -C packages/tools/appilot-mcp typecheck
299
+ ```
300
+
301
+ To exercise HTTP mode locally, point it at a dev backend and use a localhost
302
+ public origin, which is the one case where plaintext is accepted:
303
+
304
+ ```bash
305
+ APPILOT_BASE_URL=http://localhost:6001 \
306
+ APPILOT_MCP_PUBLIC_URL=http://localhost:8080 \
307
+ APPILOT_MCP_OAUTH_SECRET="$(openssl rand -base64 32)" \
308
+ node packages/tools/appilot-mcp/dist/index.bundle.js --http
309
+ ```
310
+
311
+ Live DOM soak needs Playwright: add it with pnpm and install Chromium.
312
+
313
+ See `docs/architecture/appilot-mcp.md` (server) and
314
+ `docs/content-model/config-health-contract.md` (the contract it enforces).
315
+
316
+ ### Account-based remote approval
317
+
318
+ Remote deployments can reuse the Backoffice session for approval without asking
319
+ users to create or paste a token. Configure APPILOT_MCP_HANDOFF_SECRET on backend
320
+ and MCP, a trusted APPILOT_MCP_BACKOFFICE_URL on MCP, and the fixed
321
+ APPILOT_MCP_PUBLIC_URL on backend. Deploy migration system/025 and the Backoffice
322
+ approval page first. The cloud activation script is
323
+ `scripts/enable-session-approval.sh`. Keep this secret separate from the OAuth key.
324
+
325
+ Manual service-token consent remains available when session approval is not
326
+ configured, or with `manual=1` on the authorization request. Consent and errors
327
+ support English, Spanish and German. Administrators can filter connections,
328
+ renew expiry and revoke access in Backoffice Service tokens.