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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/LICENSE +15 -0
- package/README.md +133 -27
- package/dist/appilot-configurator.mcpb +0 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +173 -0
- package/dist/client.d.ts +45 -1
- package/dist/client.js +74 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.js +6 -0
- package/dist/contract/healthContract.js +31 -4
- package/dist/index.bundle.js +2111 -826
- package/dist/index.d.ts +7 -1
- package/dist/index.js +22 -4
- 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 +51 -0
- package/dist/redaction.js +59 -0
- package/dist/remote/consent.d.ts +10 -2
- package/dist/remote/consent.js +16 -6
- package/dist/remote/consentMessages.d.ts +6 -1
- package/dist/remote/consentMessages.js +15 -6
- package/dist/remote/httpServer.d.ts +10 -0
- package/dist/remote/httpServer.js +126 -40
- package/dist/remote/oauth.d.ts +10 -1
- package/dist/remote/oauth.js +29 -11
- package/dist/scaffold.d.ts +68 -6
- package/dist/scaffold.js +424 -97
- package/dist/server.js +175 -18
- package/dist/userClient.d.ts +213 -0
- package/dist/userClient.js +400 -0
- package/dist/userServer.d.ts +47 -0
- package/dist/userServer.js +248 -0
- 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 -21
- package/package.json +5 -3
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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
|
|
84
|
+
export interface ApplyManifestOptions {
|
|
85
85
|
planToken: string;
|
|
86
86
|
mode?: 'merge' | 'replace';
|
|
87
87
|
expectedCurrentHash?: string;
|
|
88
88
|
allowUnhealthy?: boolean;
|
|
89
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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.
|