appilot-mcp 0.2.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 +74 -16
- 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 +45 -1
- package/dist/client.js +74 -1
- package/dist/contract/healthContract.js +31 -4
- package/dist/index.bundle.js +820 -142
- package/dist/index.js +7 -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/remote/consent.d.ts +10 -2
- package/dist/remote/consent.js +15 -6
- package/dist/remote/consentMessages.d.ts +4 -1
- package/dist/remote/consentMessages.js +9 -6
- package/dist/remote/httpServer.js +2 -2
- package/dist/remote/oauth.js +9 -9
- package/dist/scaffold.d.ts +68 -6
- package/dist/scaffold.js +424 -97
- package/dist/server.js +175 -18
- 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/server.js
CHANGED
|
@@ -19,7 +19,7 @@ import { inspectPage } from './inspect.js';
|
|
|
19
19
|
import { runHealthContract } from './contract/healthContract.js';
|
|
20
20
|
import { soakSelectors } from './soak.js';
|
|
21
21
|
import { applyManifest, parseManifest, planManifest } from './manifest.js';
|
|
22
|
-
import { scaffoldIntegration, scaffoldAgentFirst } from './scaffold.js';
|
|
22
|
+
import { SCAFFOLD_FRAMEWORKS, scaffoldIntegration, scaffoldAgentFirst, integrationSnippet } from './scaffold.js';
|
|
23
23
|
import { verifyIntegration } from './verify.js';
|
|
24
24
|
import { redactForTransport, refuseSecretOverRemote } from './redaction.js';
|
|
25
25
|
/**
|
|
@@ -43,7 +43,9 @@ const DEFAULT_WIDGET_SCRIPT_URL = 'https://cdn.appilot.space/widget/v1/appilot.e
|
|
|
43
43
|
*/
|
|
44
44
|
const SERVER_INSTRUCTIONS = `Audit, extend and fix an Appilot app's content-model configuration, and build a new capability so the agent can operate it.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
For a new app, work in this order: whoami, then create_app (on bare localhost pass isTest, which mints a wk_test_ key), then scaffold_integration for the host's framework, then verify_integration against the running page, then configure the content model.
|
|
47
|
+
|
|
48
|
+
To audit an existing one, work in this order: capabilities (what this instance supports, and the closed vocabularies its entities accept; on-premise trails cloud), read_config, validate_config, then report the findings to the user in plain language, ranked critical to low, each with its concrete fix. Do not paste raw tool output at them.
|
|
47
49
|
|
|
48
50
|
If read_config returns a gaps array, an entity could not be read. Say so and stop treating that entity as empty: every lint over it silently passed.
|
|
49
51
|
|
|
@@ -58,6 +60,20 @@ Three things to get right. A widget secret belongs in the server environment and
|
|
|
58
60
|
If something is missing or broken in Appilot itself, report_feedback records it. Say plainly what it does: the report is read, and a reply is part of a support plan rather than something promised here. Never put configuration contents, knowledge bodies or secrets in a report, and show the user the exact text first.
|
|
59
61
|
|
|
60
62
|
Writing needs a config:write service token, reading needs config:read, provisioning needs provision:write, reporting needs feedback:write. A scope refusal means the user should reconnect with a token carrying that scope, not that you should find another route.`;
|
|
63
|
+
/**
|
|
64
|
+
* Where each scope comes from, appended to the refusal.
|
|
65
|
+
*
|
|
66
|
+
* A refusal that names the missing scope and stops leaves the reader one
|
|
67
|
+
* question short of acting: who can widen it. For provisioning the answer is
|
|
68
|
+
* not "you", which is exactly the case the configurator meets, so the refusal
|
|
69
|
+
* says who to ask and what the preset is called on the screen they will open.
|
|
70
|
+
*/
|
|
71
|
+
const HOW_TO_GRANT = {
|
|
72
|
+
'config:read': 'config:read is on every Service tokens preset in the Backoffice, "Inspect only" included.',
|
|
73
|
+
'config:write': 'config:write comes from the Backoffice Service tokens preset "Edit configuration".',
|
|
74
|
+
'provision:write': 'provision:write is granted by an organization administrator in the Backoffice, under Service tokens with the "Set up integrations" preset, or through the account approval screen. A configurator who is not an administrator cannot mint it and has to ask one.',
|
|
75
|
+
'feedback:write': 'feedback:write is a Backoffice Service tokens option, and it is the only scope that sends anything out of the organization.',
|
|
76
|
+
};
|
|
61
77
|
export function createAppilotServer(conn) {
|
|
62
78
|
const client = new AppilotClient(conn);
|
|
63
79
|
function text(value) {
|
|
@@ -85,7 +101,8 @@ export function createAppilotServer(conn) {
|
|
|
85
101
|
if (!granted || granted.includes(scope))
|
|
86
102
|
return null;
|
|
87
103
|
return errorText(new Error(`This connection was granted ${granted.length ? granted.join(', ') : 'no scopes'}, which does not include ${scope}. ` +
|
|
88
|
-
'Reconnect and approve that scope, using a service token that carries it.'
|
|
104
|
+
'Reconnect and approve that scope, using a service token that carries it. ' +
|
|
105
|
+
HOW_TO_GRANT[scope]));
|
|
89
106
|
}
|
|
90
107
|
function resolveAppId(appId) {
|
|
91
108
|
const id = appId ?? conn.defaultAppId;
|
|
@@ -365,16 +382,17 @@ export function createAppilotServer(conn) {
|
|
|
365
382
|
});
|
|
366
383
|
server.registerTool('create_app', {
|
|
367
384
|
title: 'Create an app, its domains, and a widget key',
|
|
368
|
-
description: 'Provision an Appilot app in one call: the app, the domains it runs on, and optionally a widget key, plus the exact script tag and boot snippet to paste into the host application. Idempotent: re-running converges on the existing app rather than creating a second one. Pass dryRun to preview. The widget key and its secret are returned EXACTLY ONCE, at creation; store the secret in the host backend only. Requires a provision:write service token.',
|
|
385
|
+
description: 'Provision an Appilot app in one call: the app, the domains it runs on, and optionally a widget key, plus the exact script tag and boot snippet to paste into the host application. Idempotent: re-running converges on the existing app rather than creating a second one. Pass dryRun to preview. The widget key and its secret are returned EXACTLY ONCE, at creation; store the secret in the host backend only. Working on your own machine: do NOT pass localhost or 127.0.0.1 as a domain, because they name every developer\'s machine and are refused. Either run the app on a hostname that resolves to 127.0.0.1 (myapp.lvh.me) and register THAT, which gives the turn full app context and needs no key, or pass isTest true with no domains for a loopback-bound wk_test_ key, which gives the tenant and the user and no app context. Requires a provision:write service token.',
|
|
369
386
|
inputSchema: {
|
|
370
387
|
name: z.string().min(1),
|
|
371
388
|
description: z.string().optional(),
|
|
372
389
|
domains: z.array(z.string()).optional(),
|
|
373
390
|
widgetKeyName: z.string().optional(),
|
|
374
|
-
|
|
391
|
+
isTest: z.boolean().optional(),
|
|
392
|
+
allowedDomains: z.array(z.string()).optional(),
|
|
375
393
|
dryRun: z.boolean().optional(),
|
|
376
394
|
},
|
|
377
|
-
}, async ({ name, description, domains, widgetKeyName,
|
|
395
|
+
}, async ({ name, description, domains, widgetKeyName, isTest, allowedDomains, dryRun }) => {
|
|
378
396
|
const refusal = scopeRefusal('provision:write');
|
|
379
397
|
if (refusal)
|
|
380
398
|
return refusal;
|
|
@@ -382,8 +400,8 @@ export function createAppilotServer(conn) {
|
|
|
382
400
|
const result = await client.provisionApp({
|
|
383
401
|
app: { name, description },
|
|
384
402
|
domains: (domains ?? []).map(domain => ({ domain })),
|
|
385
|
-
widgetKey: widgetKeyName ||
|
|
386
|
-
? { name: widgetKeyName, isTest
|
|
403
|
+
widgetKey: widgetKeyName || isTest !== undefined || allowedDomains
|
|
404
|
+
? { name: widgetKeyName, isTest, allowedDomains }
|
|
387
405
|
: undefined,
|
|
388
406
|
dryRun: dryRun === true,
|
|
389
407
|
});
|
|
@@ -393,9 +411,135 @@ export function createAppilotServer(conn) {
|
|
|
393
411
|
return errorText(err);
|
|
394
412
|
}
|
|
395
413
|
});
|
|
414
|
+
server.registerTool('list_apps', {
|
|
415
|
+
title: 'List the apps this organization has provisioned',
|
|
416
|
+
description: 'The apps this credential reaches, with each app\'s registered domains and their verification status. Call it before create_app so "which apps do I have" has an answer, and to find the app id and the domain id every other provisioning tool takes. Carries no secret. Reads the provisioning surface, so it needs a provision:write service token even though it writes nothing.',
|
|
417
|
+
inputSchema: {},
|
|
418
|
+
}, async () => {
|
|
419
|
+
const refusal = scopeRefusal('provision:write');
|
|
420
|
+
if (refusal)
|
|
421
|
+
return refusal;
|
|
422
|
+
try {
|
|
423
|
+
return text(await client.listProvisioned());
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
return errorText(err);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
server.registerTool('list_widget_keys', {
|
|
430
|
+
title: 'List the widget keys this organization holds',
|
|
431
|
+
description: 'The organization\'s widget keys by label, prefix, allowed domains and active state. No raw key and no secret: both are shown exactly once, at creation. Call it before create_app so a re-run recognises the key it already minted instead of asking for another, because every extra key is another live credential. Requires a provision:write service token.',
|
|
432
|
+
inputSchema: {},
|
|
433
|
+
}, async () => {
|
|
434
|
+
const refusal = scopeRefusal('provision:write');
|
|
435
|
+
if (refusal)
|
|
436
|
+
return refusal;
|
|
437
|
+
try {
|
|
438
|
+
return text(await client.listWidgetKeys());
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
return errorText(err);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
server.registerTool('verify_domain', {
|
|
445
|
+
title: 'Check or trigger a domain\'s DNS verification',
|
|
446
|
+
description: 'Where a domain\'s verification stands, and the exact TXT record it needs. Pass trigger to ask Appilot to look for the record now. A live widget key needs a verified domain, and this is what closes that loop: create_app reports the key as blocked and names the record, and this reports whether the record is visible yet. Requires a provision:write service token.',
|
|
447
|
+
inputSchema: {
|
|
448
|
+
domain: z.string().min(1),
|
|
449
|
+
appId: z.number().int().optional(),
|
|
450
|
+
trigger: z.boolean().optional(),
|
|
451
|
+
},
|
|
452
|
+
}, async ({ domain, appId, trigger }) => {
|
|
453
|
+
const refusal = scopeRefusal('provision:write');
|
|
454
|
+
if (refusal)
|
|
455
|
+
return refusal;
|
|
456
|
+
try {
|
|
457
|
+
const wanted = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/[/:].*$/, '');
|
|
458
|
+
const provisioned = await client.listProvisioned();
|
|
459
|
+
const scoped = appId != null ? provisioned.filter(a => Number(a.id) === appId) : provisioned;
|
|
460
|
+
const owner = scoped.find(a => a.domains.some(d => String(d.domain).toLowerCase() === wanted));
|
|
461
|
+
const row = owner?.domains.find(d => String(d.domain).toLowerCase() === wanted);
|
|
462
|
+
if (!owner || !row) {
|
|
463
|
+
return errorText(new Error(`${wanted} is not a registered domain of ${appId != null ? `app ${appId}` : 'any app in this organization'}. ` +
|
|
464
|
+
'list_apps shows what is registered, and create_app adds a domain. A loopback name (localhost, 127.0.0.1) is never registrable: run the app on a hostname that resolves to 127.0.0.1 instead.'));
|
|
465
|
+
}
|
|
466
|
+
const result = {
|
|
467
|
+
app: { id: owner.id, name: owner.name },
|
|
468
|
+
domain: row.domain,
|
|
469
|
+
domainId: row.id,
|
|
470
|
+
verificationStatus: row.verification_status,
|
|
471
|
+
};
|
|
472
|
+
// The TXT record lives on the apps router, which authenticates an
|
|
473
|
+
// organization session rather than a service token. When that is
|
|
474
|
+
// refused, the provisioning dry run answers the same question, so the
|
|
475
|
+
// tool degrades to a second source instead of to nothing.
|
|
476
|
+
try {
|
|
477
|
+
result.record = await client.domainVerification(Number(row.id));
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
try {
|
|
481
|
+
const preview = await client.provisionApp({
|
|
482
|
+
app: { name: owner.name },
|
|
483
|
+
domains: [{ domain: row.domain }],
|
|
484
|
+
dryRun: true,
|
|
485
|
+
});
|
|
486
|
+
const previewed = preview.domains.find(d => d.domain.toLowerCase() === wanted);
|
|
487
|
+
if (previewed?.dns_record_name) {
|
|
488
|
+
result.record = {
|
|
489
|
+
dns_record_name: previewed.dns_record_name,
|
|
490
|
+
dns_record_value: previewed.dns_record_value,
|
|
491
|
+
verification_status: previewed.verification_status,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
result.recordUnavailable = err instanceof Error ? err.message : String(err);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (trigger && row.verification_status !== 'verified') {
|
|
500
|
+
try {
|
|
501
|
+
result.checked = await client.triggerDomainVerification(Number(row.id));
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
result.triggerRefused =
|
|
505
|
+
`${err instanceof Error ? err.message : String(err)} ` +
|
|
506
|
+
'Triggering the check authenticates an organization session rather than a service token today, so run it from the Backoffice under Domains once the TXT record is published.';
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return text(result);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
return errorText(err);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
server.registerTool('integration_snippet', {
|
|
516
|
+
title: 'Get the script tag and boot call for an app that already exists',
|
|
517
|
+
description: 'The two forms of the widget boot for an app you already provisioned: the no-build script tag, and the bundled bootAppilotWidget call reading the key from the framework\'s public variable. Use it instead of re-running create_app, which is a provisioning write, when all you lost was the snippet. It composes the answer locally, writes nothing, and needs no scope. Pass the publishable widget key to have it appear in the script tag; the widget SECRET never belongs here.',
|
|
518
|
+
inputSchema: {
|
|
519
|
+
appId: z.number().int().optional(),
|
|
520
|
+
widgetKey: z.string().optional(),
|
|
521
|
+
widgetScriptUrl: z.string().optional(),
|
|
522
|
+
framework: z.enum(SCAFFOLD_FRAMEWORKS).optional(),
|
|
523
|
+
},
|
|
524
|
+
}, async ({ appId, widgetKey, widgetScriptUrl, framework }) => {
|
|
525
|
+
try {
|
|
526
|
+
return text({
|
|
527
|
+
appId: appId ?? conn.defaultAppId ?? null,
|
|
528
|
+
...integrationSnippet({
|
|
529
|
+
widgetScriptUrl: widgetScriptUrl ?? DEFAULT_WIDGET_SCRIPT_URL,
|
|
530
|
+
apiUrl: conn.baseUrl || null,
|
|
531
|
+
widgetKey: widgetKey ?? null,
|
|
532
|
+
framework,
|
|
533
|
+
}),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
return errorText(err);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
396
540
|
server.registerTool('plan_manifest', {
|
|
397
541
|
title: 'Preview an app manifest (writes nothing)',
|
|
398
|
-
description: 'Diff an appilot.app-manifest against the live instance and return what would change: provisioning actions per app/domain/key, the config-bundle import diff, and the health findings over the resulting state. Writes nothing. Returns a planToken that apply_manifest requires, so an apply always follows a preview of the exact same manifest. Keep the manifest in the repository under version control.',
|
|
542
|
+
description: 'Diff an appilot.app-manifest against the live instance and return what would change: provisioning actions per app/domain/key, the config-bundle import diff, and the health findings over the resulting state. Writes nothing. Returns a planToken that apply_manifest requires, so an apply always follows a preview of the exact same manifest. Keep the manifest in the repository under version control. A complete example ships with this package at examples/app.appilot.json and is also in the docs. Which half is revisioned: the CONFIG half gets a pre_restore revision on every commit, so a wrong import is undone by restoring it; the PROVISIONING half is not revisioned, so a domain or a key it creates is undone by hand.',
|
|
399
543
|
inputSchema: { manifest: z.union([z.record(z.any()), z.string()]) },
|
|
400
544
|
}, async ({ manifest }) => {
|
|
401
545
|
const refusal = scopeRefusal('config:read');
|
|
@@ -415,7 +559,7 @@ export function createAppilotServer(conn) {
|
|
|
415
559
|
});
|
|
416
560
|
server.registerTool('apply_manifest', {
|
|
417
561
|
title: 'Apply a previously planned app manifest',
|
|
418
|
-
description: 'Provision and configure an app from an appilot.app-manifest. Requires the planToken returned by plan_manifest for the SAME manifest: a mismatch means the manifest changed after it was previewed, and the apply is refused. Pass expectedCurrentHash from the plan so a concurrent config edit is a 409 rather than a silent overwrite. mode=replace makes the config match the bundle exactly, including deletions.
|
|
562
|
+
description: 'Provision and configure an app from an appilot.app-manifest. Requires the planToken returned by plan_manifest for the SAME manifest: a mismatch means the manifest changed after it was previewed, and the apply is refused. Pass expectedCurrentHash from the plan so a concurrent config edit is a 409 rather than a silent overwrite. mode=replace makes the config match the bundle exactly, including deletions. Needs config:write when the manifest carries a config bundle, and provision:write only when the provisioning half would actually change something: a manifest whose app, domains and keys already exist applies with config:write alone.',
|
|
419
563
|
inputSchema: {
|
|
420
564
|
manifest: z.union([z.record(z.any()), z.string()]),
|
|
421
565
|
planToken: z.string(),
|
|
@@ -424,17 +568,26 @@ export function createAppilotServer(conn) {
|
|
|
424
568
|
allowUnhealthy: z.boolean().optional(),
|
|
425
569
|
},
|
|
426
570
|
}, async ({ manifest, planToken, mode, expectedCurrentHash, allowUnhealthy }) => {
|
|
427
|
-
// A manifest apply provisions AND writes configuration, so it needs both.
|
|
428
|
-
const refusal = scopeRefusal('provision:write') ?? scopeRefusal('config:write');
|
|
429
|
-
if (refusal)
|
|
430
|
-
return refusal;
|
|
431
571
|
try {
|
|
432
572
|
const parsed = parseManifest(manifest);
|
|
573
|
+
// The config half always writes when the manifest carries one. The
|
|
574
|
+
// provisioning half is decided by the manifest rather than by the
|
|
575
|
+
// tool: applyManifest previews it and refuses only when it would
|
|
576
|
+
// actually change an app, a domain or a key. A CI token holding
|
|
577
|
+
// config:write alone keeps content in sync on an app a person
|
|
578
|
+
// already provisioned, which is the reason the scopes are separate.
|
|
579
|
+
if (parsed.config) {
|
|
580
|
+
const configRefusal = scopeRefusal('config:write');
|
|
581
|
+
if (configRefusal)
|
|
582
|
+
return configRefusal;
|
|
583
|
+
}
|
|
584
|
+
const provisionRefusal = scopeRefusal('provision:write');
|
|
433
585
|
const result = await applyManifest(client, parsed, {
|
|
434
586
|
planToken,
|
|
435
587
|
mode,
|
|
436
588
|
expectedCurrentHash,
|
|
437
589
|
allowUnhealthy,
|
|
590
|
+
provisionRefusal: provisionRefusal ? provisionRefusal.content[0].text : null,
|
|
438
591
|
});
|
|
439
592
|
return text({ ...result, provisioning: redactForTransport(result.provisioning, conn.transport) });
|
|
440
593
|
}
|
|
@@ -444,9 +597,9 @@ export function createAppilotServer(conn) {
|
|
|
444
597
|
});
|
|
445
598
|
server.registerTool('scaffold_integration', {
|
|
446
599
|
title: 'Generate the host application integration code',
|
|
447
|
-
description: 'Return the source a host application needs: the identity relay for its backend (the one security-critical piece, built on appilot-server), the widget boot call, and a client-action example. Returns file CONTENTS for you to write into the repository; this server never touches the filesystem. Pick the framework that matches the host.',
|
|
600
|
+
description: 'Return the source a host application needs: the identity relay for its backend (the one security-critical piece, built on appilot-server), the widget boot call, and a client-action example. Returns file CONTENTS for you to write into the repository; this server never touches the filesystem. Pick the framework that matches the host, or `other` when the backend is not Node (Django, Rails, PHP), which returns the raw exchange as curl plus a Python and a Ruby handler. The notes carry what local development needs, and the answer differs between a hostname you registered and bare localhost.',
|
|
448
601
|
inputSchema: {
|
|
449
|
-
framework: z.enum(
|
|
602
|
+
framework: z.enum(SCAFFOLD_FRAMEWORKS),
|
|
450
603
|
widgetKey: z.string().optional(),
|
|
451
604
|
widgetScriptUrl: z.string().optional(),
|
|
452
605
|
idNamespace: z.string().optional(),
|
|
@@ -522,15 +675,17 @@ export function createAppilotServer(conn) {
|
|
|
522
675
|
});
|
|
523
676
|
server.registerTool('scaffold_agent_first', {
|
|
524
677
|
title: 'Scaffold one capability so the agent can operate it',
|
|
525
|
-
description: 'Return the four artifacts a capability needs to be agent-operable, agreeing with each other: the HTTP-proxy tool, the client action when the operation belongs in the page, the Action Plan that is the procedure, and the knowledge article that carries the meaning and not the steps. Also returns the order to create them in, which matters because a form cannot name controls that do not exist yet. `endpoint.path` is a path on the host origin, not an absolute URL. Use it when building a new agent-first app or making an existing feature reachable through the assistant.',
|
|
678
|
+
description: 'Return the four artifacts a capability needs to be agent-operable, agreeing with each other: the HTTP-proxy tool, the client action when the operation belongs in the page, the Action Plan that is the procedure, and the knowledge article that carries the meaning and not the steps. Also returns the order to create them in, which matters because a form cannot name controls that do not exist yet. `endpoint.path` is a path on the host origin, not an absolute URL. Pass `shape`: `create` is the open, fill and submit plan, `navigate` is one step to a screen, and `read` gets NO plan at all, because a plan whose only step opens a screen does nothing. Pass `clientSide` when the operation belongs in the page, and no HTTP-proxy tool is emitted. Use it when building a new agent-first app or making an existing feature reachable through the assistant.',
|
|
526
679
|
inputSchema: {
|
|
527
680
|
capability: z.string().min(1),
|
|
528
681
|
slug: z.string().min(1),
|
|
529
682
|
appId: z.number().int().optional(),
|
|
530
683
|
endpoint: z.object({ method: z.string(), path: z.string() }).optional(),
|
|
531
684
|
clientSide: z.boolean().optional(),
|
|
685
|
+
shape: z.enum(['create', 'navigate', 'read']).optional(),
|
|
686
|
+
viewPath: z.string().optional(),
|
|
532
687
|
},
|
|
533
|
-
}, async ({ capability, slug, appId, endpoint, clientSide }) => {
|
|
688
|
+
}, async ({ capability, slug, appId, endpoint, clientSide, shape, viewPath }) => {
|
|
534
689
|
try {
|
|
535
690
|
return text(scaffoldAgentFirst({
|
|
536
691
|
capability,
|
|
@@ -538,6 +693,8 @@ export function createAppilotServer(conn) {
|
|
|
538
693
|
appId: appId ?? conn.defaultAppId ?? null,
|
|
539
694
|
endpoint: endpoint ?? null,
|
|
540
695
|
clientSide,
|
|
696
|
+
shape,
|
|
697
|
+
viewPath,
|
|
541
698
|
}));
|
|
542
699
|
}
|
|
543
700
|
catch (err) {
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "appilot.app-manifest",
|
|
3
|
+
"formatVersion": "1.0",
|
|
4
|
+
"app": {
|
|
5
|
+
"name": "Acme Booking",
|
|
6
|
+
"description": "The example manifest that ships with appilot-mcp. Copy it, change the names, keep the shape."
|
|
7
|
+
},
|
|
8
|
+
"domains": [
|
|
9
|
+
{ "domain": "app.acme.com", "default_language": "en", "configured_languages": ["en", "de"] }
|
|
10
|
+
],
|
|
11
|
+
"widgetKeys": [
|
|
12
|
+
{ "name": "production", "allowedDomains": ["app.acme.com"] }
|
|
13
|
+
],
|
|
14
|
+
"config": {
|
|
15
|
+
"kind": "appilot.config-bundle",
|
|
16
|
+
"formatVersion": "1.0",
|
|
17
|
+
"exportedAt": "2026-09-07T00:00:00.000Z",
|
|
18
|
+
"source": { "appId": 0, "appSlug": "acme-booking", "orgId": null },
|
|
19
|
+
"locales": ["en", "de"],
|
|
20
|
+
"entities": {
|
|
21
|
+
"views": [
|
|
22
|
+
{
|
|
23
|
+
"domain": "app.acme.com",
|
|
24
|
+
"path": "bookings",
|
|
25
|
+
"slug": "bookings",
|
|
26
|
+
"entry_path": "/bookings",
|
|
27
|
+
"is_active": true,
|
|
28
|
+
"source_locale": "en",
|
|
29
|
+
"translations": {
|
|
30
|
+
"en": { "name": "Bookings", "description": "The list of bookings and the form that creates one." },
|
|
31
|
+
"de": { "name": "Buchungen", "description": "Die Liste der Buchungen und das Formular, das eine anlegt." }
|
|
32
|
+
},
|
|
33
|
+
"detection_rules": [
|
|
34
|
+
{ "priority": 10, "type": "path_prefix", "config": { "value": "/bookings" }, "is_active": true }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"controls": [
|
|
39
|
+
{
|
|
40
|
+
"semantic_id": "btn-create-booking",
|
|
41
|
+
"locator": "[data-testid=\"new-booking\"]",
|
|
42
|
+
"locator_type": "class_text",
|
|
43
|
+
"llm_hint": "Opens the booking form.",
|
|
44
|
+
"view_path": "bookings",
|
|
45
|
+
"scope": "view",
|
|
46
|
+
"prerequisites": null,
|
|
47
|
+
"is_active": true,
|
|
48
|
+
"form": null,
|
|
49
|
+
"source_locale": "en",
|
|
50
|
+
"translations": {
|
|
51
|
+
"en": { "label": "New booking", "description": "Opens the form that creates a booking." },
|
|
52
|
+
"de": { "label": "Neue Buchung", "description": "Öffnet das Formular, das eine Buchung anlegt." }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"semantic_id": "field-booking-date",
|
|
57
|
+
"locator": "booking-date",
|
|
58
|
+
"locator_type": "id",
|
|
59
|
+
"llm_hint": "The day the booking is for.",
|
|
60
|
+
"view_path": "bookings",
|
|
61
|
+
"scope": "view",
|
|
62
|
+
"prerequisites": null,
|
|
63
|
+
"is_active": true,
|
|
64
|
+
"form": { "form": "form-create-booking", "order": 1 },
|
|
65
|
+
"source_locale": "en",
|
|
66
|
+
"translations": {
|
|
67
|
+
"en": { "label": "Date", "description": "The day the booking is for." },
|
|
68
|
+
"de": { "label": "Datum", "description": "Der Tag, für den gebucht wird." }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"semantic_id": "submit-create-booking",
|
|
73
|
+
"locator": "[data-testid=\"save-booking\"]",
|
|
74
|
+
"locator_type": "class_text",
|
|
75
|
+
"llm_hint": "Saves the booking.",
|
|
76
|
+
"view_path": "bookings",
|
|
77
|
+
"scope": "view",
|
|
78
|
+
"prerequisites": null,
|
|
79
|
+
"is_active": true,
|
|
80
|
+
"form": null,
|
|
81
|
+
"source_locale": "en",
|
|
82
|
+
"translations": {
|
|
83
|
+
"en": { "label": "Save", "description": "Saves the booking." },
|
|
84
|
+
"de": { "label": "Speichern", "description": "Speichert die Buchung." }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"forms": [
|
|
89
|
+
{
|
|
90
|
+
"semantic_id": "form-create-booking",
|
|
91
|
+
"domain": "app.acme.com",
|
|
92
|
+
"llm_hint": "Creates one booking.",
|
|
93
|
+
"view_paths": ["bookings"],
|
|
94
|
+
"required_fields": ["field-booking-date"],
|
|
95
|
+
"submit_control": "submit-create-booking",
|
|
96
|
+
"entry_control": "btn-create-booking",
|
|
97
|
+
"visibility_rules": null,
|
|
98
|
+
"field_defaults": null,
|
|
99
|
+
"prerequisites": null,
|
|
100
|
+
"is_active": true,
|
|
101
|
+
"source_locale": "en",
|
|
102
|
+
"translations": {
|
|
103
|
+
"en": { "label": "New booking", "description": "Creates one booking." },
|
|
104
|
+
"de": { "label": "Neue Buchung", "description": "Legt eine Buchung an." }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"tools": [
|
|
109
|
+
{
|
|
110
|
+
"tool_name": "create_booking",
|
|
111
|
+
"view": null,
|
|
112
|
+
"parameters": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": { "date": { "type": "string", "description": "ISO date the booking is for." } },
|
|
115
|
+
"required": ["date"]
|
|
116
|
+
},
|
|
117
|
+
"runtime_spec": {
|
|
118
|
+
"kind": "http_proxy",
|
|
119
|
+
"method": "POST",
|
|
120
|
+
"path_template": "/api/bookings",
|
|
121
|
+
"body_template": { "date": "{{date}}" }
|
|
122
|
+
},
|
|
123
|
+
"auth_header_name": "Authorization",
|
|
124
|
+
"source_locale": "en",
|
|
125
|
+
"translations": {
|
|
126
|
+
"en": { "title": "Create a booking", "description": "Use when the user wants to create a booking." },
|
|
127
|
+
"de": { "title": "Buchung anlegen", "description": "Verwenden, wenn die Person eine Buchung anlegen möchte." }
|
|
128
|
+
},
|
|
129
|
+
"parameter_descriptions": {
|
|
130
|
+
"en": { "date": "ISO date the booking is for." },
|
|
131
|
+
"de": { "date": "ISO-Datum, für das gebucht wird." }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"zones": [],
|
|
136
|
+
"action_plans": [
|
|
137
|
+
{
|
|
138
|
+
"semantic_id": "plan-create-booking",
|
|
139
|
+
"sections": [
|
|
140
|
+
{
|
|
141
|
+
"view_path": "bookings",
|
|
142
|
+
"steps": [
|
|
143
|
+
"Open the form with [New booking]({{click:btn-create-booking}}).",
|
|
144
|
+
"Fill in [the booking form]({{form:form-create-booking}}).",
|
|
145
|
+
"Submit it with [Save]({{click:submit-create-booking}})."
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
"form_values": { "form-create-booking": { "fields": [] } },
|
|
150
|
+
"provenance": "registry",
|
|
151
|
+
"is_active": true,
|
|
152
|
+
"source_locale": "en",
|
|
153
|
+
"translations": {
|
|
154
|
+
"en": {
|
|
155
|
+
"name": "Create a booking",
|
|
156
|
+
"description": "Use when the user wants to create a booking.",
|
|
157
|
+
"step_narratives": null
|
|
158
|
+
},
|
|
159
|
+
"de": {
|
|
160
|
+
"name": "Buchung anlegen",
|
|
161
|
+
"description": "Verwenden, wenn die Person eine Buchung anlegen möchte.",
|
|
162
|
+
"step_narratives": [[
|
|
163
|
+
"Öffne das Formular mit [Neue Buchung]({{click:btn-create-booking}}).",
|
|
164
|
+
"Fülle [das Buchungsformular]({{form:form-create-booking}}) aus.",
|
|
165
|
+
"Sende es mit [Speichern]({{click:submit-create-booking}}) ab."
|
|
166
|
+
]]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
"knowledge_content": [
|
|
172
|
+
{
|
|
173
|
+
"group": "booking-rules",
|
|
174
|
+
"scope": "app_global",
|
|
175
|
+
"url_pattern": null,
|
|
176
|
+
"domain": "app.acme.com",
|
|
177
|
+
"visibility": "internal",
|
|
178
|
+
"source": "manual",
|
|
179
|
+
"is_active": true,
|
|
180
|
+
"source_locale": "en",
|
|
181
|
+
"bodies": [
|
|
182
|
+
{
|
|
183
|
+
"language": "en",
|
|
184
|
+
"title": "What a booking is",
|
|
185
|
+
"description": "The rules and vocabulary behind a booking.",
|
|
186
|
+
"notes": null,
|
|
187
|
+
"general_info": "A booking reserves one resource for one day. Bookings are made by the account holder and cannot start in the past. A booking that has already started can be cancelled but not moved.\n\nThe procedure for creating one lives in the plan-create-booking action plan, not here."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"language": "de",
|
|
191
|
+
"title": "Was eine Buchung ist",
|
|
192
|
+
"description": "Die Regeln und Begriffe hinter einer Buchung.",
|
|
193
|
+
"notes": null,
|
|
194
|
+
"general_info": "Eine Buchung reserviert eine Ressource für einen Tag. Buchungen legt die Kontoinhaberin an, und sie können nicht in der Vergangenheit beginnen. Eine bereits begonnene Buchung lässt sich stornieren, aber nicht verschieben.\n\nDas Vorgehen zum Anlegen steht im Action Plan plan-create-booking, nicht hier."
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"views": [{ "domain": "app.acme.com", "path": "bookings", "priority": 10 }]
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
"session_templates": []
|
|
201
|
+
},
|
|
202
|
+
"secretRefs": [
|
|
203
|
+
{
|
|
204
|
+
"entity": "tools",
|
|
205
|
+
"id": "create_booking",
|
|
206
|
+
"secret": "tool_auth:create_booking",
|
|
207
|
+
"status": "needs-reconnect",
|
|
208
|
+
"action": "Open the tool in the Backoffice and paste the credential. Secrets never travel in a bundle."
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
}
|