@stackable-labs/mcp-app-extension 0.23.0 → 1.0.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/dist/index.js +1406 -78
- package/dist/server.js +735 -71
- package/package.json +1 -1
- package/dist/auth-DWIWIJNN.js +0 -629
- package/dist/auth-WHLYYSCQ.js +0 -630
- package/dist/chunk-DCPV7HMV.js +0 -108
- package/dist/chunk-HOOVB46Z.js +0 -107
package/dist/index.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { STANDALONE_CLIENT, MCP_AUTH_FILE, getToken, STANDALONE_CLIENT_AUTH_FILE } from './chunk-DCPV7HMV.js';
|
|
3
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
-
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, PERMISSIONS, CAPABILITY_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
|
|
4
|
+
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
|
|
5
|
+
import fs4, { readFile, mkdir, writeFile, constants } from 'fs/promises';
|
|
6
|
+
import path, { join } from 'path';
|
|
7
|
+
import os, { homedir } from 'os';
|
|
6
8
|
import { z } from 'zod';
|
|
9
|
+
import { createServer } from 'http';
|
|
10
|
+
import process6 from 'process';
|
|
11
|
+
import { Buffer as Buffer$1 } from 'buffer';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
import childProcess, { execFile } from 'child_process';
|
|
15
|
+
import fs3 from 'fs';
|
|
7
16
|
|
|
8
17
|
// ../../sdk/extension/ai-docs/src/generated/template-content.ts
|
|
9
18
|
var PATTERN_SECTIONS = [
|
|
@@ -532,6 +541,8 @@ const result = await capabilities.data.fetch('https://api.example.com/orders', {
|
|
|
532
541
|
- For optional secret fields, the entire header is omitted if the value is not configured
|
|
533
542
|
- Declare secret fields in your \`manifest.json\` \`settingsSchema\` with \`"secret": true\`
|
|
534
543
|
|
|
544
|
+
> See [Instance Settings](./instance-settings) for the full schema-declaration + storage-mode story, including which field types accept \`secret: true\`.
|
|
545
|
+
|
|
535
546
|
## context.read \u2014 Read Host Context
|
|
536
547
|
Read host-provided context (customer ID, email, extension settings, etc.).
|
|
537
548
|
- **Permission required:** \`context:read\`
|
|
@@ -1150,6 +1161,202 @@ const context = await capabilities.context.read()
|
|
|
1150
1161
|
\`\`\`
|
|
1151
1162
|
`;
|
|
1152
1163
|
};
|
|
1164
|
+
|
|
1165
|
+
// ../../sdk/extension/ai-docs/src/generators/instance-settings.ts
|
|
1166
|
+
var SECRET_ALLOWED = ["text", "textarea", "email"];
|
|
1167
|
+
var generateInstanceSettings = () => {
|
|
1168
|
+
const fm = frontmatter({
|
|
1169
|
+
root: false,
|
|
1170
|
+
targets: ["*"],
|
|
1171
|
+
description: "Instance Settings: declaring per-Instance configuration in your extension manifest, the install-time admin form, and reading both regular and secure values from extension code.",
|
|
1172
|
+
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
|
|
1173
|
+
});
|
|
1174
|
+
return `${fm}
|
|
1175
|
+
|
|
1176
|
+
# Instance Settings
|
|
1177
|
+
|
|
1178
|
+
Instance Settings are the per-Instance configuration values your extension needs in
|
|
1179
|
+
order to do its job \u2014 API keys, account identifiers, environment toggles, feature
|
|
1180
|
+
flags. You declare them once in your extension's \`manifest.json\`; whoever installs
|
|
1181
|
+
your extension fills them in from the admin dashboard at install time; your code
|
|
1182
|
+
reads the values at runtime.
|
|
1183
|
+
|
|
1184
|
+
There are two flavors:
|
|
1185
|
+
|
|
1186
|
+
- **Regular** values live in plaintext and are readable from your extension code.
|
|
1187
|
+
Use them for non-sensitive configuration the UI needs to branch on.
|
|
1188
|
+
- **Secure** values are encrypted at rest and **never** leave the server. Use them
|
|
1189
|
+
for credentials. Your code references them as a placeholder; the proxy
|
|
1190
|
+
substitutes the real value server-side when it makes the outbound request.
|
|
1191
|
+
|
|
1192
|
+
## 1. Declare your settings in \`manifest.json\`
|
|
1193
|
+
|
|
1194
|
+
Add a \`settingsSchema\` array to \`packages/extension/public/manifest.json\`. Each
|
|
1195
|
+
entry describes one input on the install-time admin form:
|
|
1196
|
+
|
|
1197
|
+
\`\`\`json
|
|
1198
|
+
{
|
|
1199
|
+
"name": "My Extension",
|
|
1200
|
+
"version": "1.0.0",
|
|
1201
|
+
"targets": ["slot.content"],
|
|
1202
|
+
"permissions": ["context:read", "data:fetch"],
|
|
1203
|
+
"allowedDomains": ["api.example.com"],
|
|
1204
|
+
"settingsSchema": [
|
|
1205
|
+
{
|
|
1206
|
+
"identifier": "apiKey",
|
|
1207
|
+
"label": "API Key",
|
|
1208
|
+
"type": "text",
|
|
1209
|
+
"secret": true,
|
|
1210
|
+
"required": true,
|
|
1211
|
+
"description": "Your account API key. Stored encrypted; injected server-side into outbound request headers."
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
"identifier": "environmentType",
|
|
1215
|
+
"label": "Environment",
|
|
1216
|
+
"type": "select",
|
|
1217
|
+
"options": [
|
|
1218
|
+
{ "label": "Production", "value": "prod" },
|
|
1219
|
+
{ "label": "Sandbox", "value": "sandbox" }
|
|
1220
|
+
]
|
|
1221
|
+
}
|
|
1222
|
+
]
|
|
1223
|
+
}
|
|
1224
|
+
\`\`\`
|
|
1225
|
+
|
|
1226
|
+
That's the entire authoring step. The next time someone installs (or re-syncs)
|
|
1227
|
+
your extension, the admin dashboard renders a form from this schema on the
|
|
1228
|
+
Instance settings page. Regular fields show inline values; secret fields show a
|
|
1229
|
+
masked input that accepts a value once and displays \`\u2022\u2022\u2022\u2022\` after save (the
|
|
1230
|
+
cleartext value is never returned by the API).
|
|
1231
|
+
|
|
1232
|
+
### Field types
|
|
1233
|
+
|
|
1234
|
+
| Type | Renders as | Accepts \`secret: true\` |
|
|
1235
|
+
|------|------------|--------------------------|
|
|
1236
|
+
| \`text\` | Single-line text input | Yes |
|
|
1237
|
+
| \`textarea\` | Multi-line text input | Yes |
|
|
1238
|
+
| \`email\` | Email input with format validation | Yes |
|
|
1239
|
+
| \`number\` | Numeric input (\`min\` / \`max\` / \`step\`) | No |
|
|
1240
|
+
| \`select\` | Dropdown (use \`allowMultiple\` for multi-select) | No |
|
|
1241
|
+
| \`radio\` | Radio button group | No |
|
|
1242
|
+
| \`toggle\` | On/off switch | No |
|
|
1243
|
+
| \`tags\` | Tag list / multi-string entry | No |
|
|
1244
|
+
|
|
1245
|
+
The \`secret: true\` flag is only valid on \`${SECRET_ALLOWED.join("`, `")}\` types.
|
|
1246
|
+
|
|
1247
|
+
All fields share these optional properties: \`required\`, \`placeholder\`,
|
|
1248
|
+
\`description\` (help text shown below the input), and \`dependsOn\` (conditional
|
|
1249
|
+
visibility \u2014 show this field only when another field has a matching value).
|
|
1250
|
+
|
|
1251
|
+
## 2. The install-time admin form
|
|
1252
|
+
|
|
1253
|
+
When someone installs your extension into one of their Instances, the admin
|
|
1254
|
+
dashboard reads your \`settingsSchema\` and generates a form. Each field becomes
|
|
1255
|
+
an input; \`required\` fields block save; \`description\` text becomes inline help;
|
|
1256
|
+
\`dependsOn\` rules show or hide fields based on other field values.
|
|
1257
|
+
|
|
1258
|
+
Each Instance gets its own copy of these values, so the same extension can run
|
|
1259
|
+
side-by-side on multiple Instances with completely different credentials and
|
|
1260
|
+
configuration. The installer can edit values later from the same Instance
|
|
1261
|
+
settings page \u2014 your code always sees the current values.
|
|
1262
|
+
|
|
1263
|
+
## 3. Read regular values: \`useSettings()\` (or \`useContextData()\`)
|
|
1264
|
+
|
|
1265
|
+
Regular (non-secret) values are exposed to your extension code via the
|
|
1266
|
+
\`useSettings()\` hook, keyed by each field's \`identifier\`. This requires the
|
|
1267
|
+
\`context:read\` permission.
|
|
1268
|
+
|
|
1269
|
+
\`\`\`tsx
|
|
1270
|
+
import { Surface, ui, useSettings } from '@stackable-labs/sdk-extension-react'
|
|
1271
|
+
|
|
1272
|
+
export const Content = () => {
|
|
1273
|
+
const settings = useSettings()
|
|
1274
|
+
const environmentType = (settings.environmentType as string) || 'prod'
|
|
1275
|
+
|
|
1276
|
+
return (
|
|
1277
|
+
<Surface id="slot.content">
|
|
1278
|
+
<ui.Card>
|
|
1279
|
+
<ui.CardContent>
|
|
1280
|
+
<ui.Text>Looking up orders\u2026</ui.Text>
|
|
1281
|
+
{environmentType === 'sandbox' && (
|
|
1282
|
+
<ui.Text className="text-xs opacity-50">Sandbox Mode</ui.Text>
|
|
1283
|
+
)}
|
|
1284
|
+
</ui.CardContent>
|
|
1285
|
+
</ui.Card>
|
|
1286
|
+
</Surface>
|
|
1287
|
+
)
|
|
1288
|
+
}
|
|
1289
|
+
\`\`\`
|
|
1290
|
+
|
|
1291
|
+
If you need settings alongside other context data (customer, locale, etc.), they
|
|
1292
|
+
also come back from \`useContextData()\`:
|
|
1293
|
+
|
|
1294
|
+
\`\`\`tsx
|
|
1295
|
+
import { useContextData } from '@stackable-labs/sdk-extension-react'
|
|
1296
|
+
|
|
1297
|
+
const { loading, settings, customerId } = useContextData()
|
|
1298
|
+
\`\`\`
|
|
1299
|
+
|
|
1300
|
+
> **Note:** Secret fields are **never** present in \`useSettings()\` or on
|
|
1301
|
+
> \`ctx.settings\`. Reading \`settings.apiKey\` for a \`secret: true\` field returns
|
|
1302
|
+
> \`undefined\`, even when a value is configured. If you find yourself reaching
|
|
1303
|
+
> for a secret value in extension code, you almost certainly want the
|
|
1304
|
+
> \`data.fetch\` placeholder pattern below instead.
|
|
1305
|
+
|
|
1306
|
+
## 4. Use secure values: \`{{settings.<identifier>}}\` in \`data.fetch\`
|
|
1307
|
+
|
|
1308
|
+
Secure values are decrypted only by the proxy Lambda that handles \`data.fetch\` \u2014
|
|
1309
|
+
at substitution time, per-request, then discarded. Your code references them as
|
|
1310
|
+
template placeholders in **header values**:
|
|
1311
|
+
|
|
1312
|
+
\`\`\`tsx
|
|
1313
|
+
import { useCapabilities, useSettings } from '@stackable-labs/sdk-extension-react'
|
|
1314
|
+
|
|
1315
|
+
const capabilities = useCapabilities()
|
|
1316
|
+
const settings = useSettings()
|
|
1317
|
+
const environmentType = (settings.environmentType as string) || 'prod'
|
|
1318
|
+
|
|
1319
|
+
const result = await capabilities.data.fetch('https://api.example.com/orders', {
|
|
1320
|
+
method: 'GET',
|
|
1321
|
+
headers: {
|
|
1322
|
+
// environmentType is a regular value \u2014 interpolate it normally
|
|
1323
|
+
'X-Environment': environmentType,
|
|
1324
|
+
// apiKey is secret \u2014 the proxy substitutes it server-side; the cleartext
|
|
1325
|
+
// value never enters this code path
|
|
1326
|
+
'X-Api-Key': '{{settings.apiKey}}',
|
|
1327
|
+
},
|
|
1328
|
+
})
|
|
1329
|
+
if (!result.ok) throw new Error(\`Request failed: \${result.status}\`)
|
|
1330
|
+
\`\`\`
|
|
1331
|
+
|
|
1332
|
+
Placeholders are only resolved in **header values** \u2014 not URLs, not header
|
|
1333
|
+
names, not the request body. For \`required: true\` secret fields the proxy
|
|
1334
|
+
returns \`400\` when the value is not configured; for optional secret fields
|
|
1335
|
+
the entire header is omitted.
|
|
1336
|
+
|
|
1337
|
+
## Regular vs secure at a glance
|
|
1338
|
+
|
|
1339
|
+
| | Regular | Secure (\`secret: true\`) |
|
|
1340
|
+
|--|---------|--------------------------|
|
|
1341
|
+
| **Stored** | Plaintext | Encrypted with a per-Instance derived key |
|
|
1342
|
+
| **Readable from extension code?** | Yes \u2014 \`useSettings().<identifier>\` | **Never** \u2014 secrets are not serialized to the browser or sandbox |
|
|
1343
|
+
| **How to use the value** | Read at render time and use freely | Reference as a \`{{settings.<identifier>}}\` placeholder in \`data.fetch\` headers \u2014 the proxy substitutes server-side |
|
|
1344
|
+
| **Returned by admin API after save?** | Yes (the value is shown back in the form) | No (the form shows \`\u2022\u2022\u2022\u2022\`; the cleartext value is unrecoverable) |
|
|
1345
|
+
|
|
1346
|
+
## When in doubt, mark it \`secret: true\`
|
|
1347
|
+
|
|
1348
|
+
The cost is negligible \u2014 one encryption per write, one decryption per outbound
|
|
1349
|
+
request. The blast radius of an accidental log line, screenshot, or sandbox
|
|
1350
|
+
exfiltration drops to zero. Secrets on one Instance also can't be used to
|
|
1351
|
+
decrypt secrets on any other Instance, even when both Instances run the same
|
|
1352
|
+
extension \u2014 encryption keys are derived per-Instance.
|
|
1353
|
+
|
|
1354
|
+
If a value is sensitive at all (API keys, OAuth tokens, signing secrets, webhook
|
|
1355
|
+
shared secrets, personal access tokens), declare the field \`secret: true\` and
|
|
1356
|
+
reach for it via the \`data.fetch\` placeholder pattern. Save \`useSettings()\` for
|
|
1357
|
+
the values your UI legitimately needs to branch on.
|
|
1358
|
+
`;
|
|
1359
|
+
};
|
|
1153
1360
|
var generatePermissions = () => {
|
|
1154
1361
|
const fm = frontmatter({
|
|
1155
1362
|
root: false,
|
|
@@ -1374,10 +1581,10 @@ var generatePatterns = () => {
|
|
|
1374
1581
|
description: "Code patterns: store navigation, API wrapper, surface composition",
|
|
1375
1582
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
1376
1583
|
});
|
|
1377
|
-
const sections = PATTERN_SECTIONS.map(({ path, title, code }) => `## ${title}
|
|
1584
|
+
const sections = PATTERN_SECTIONS.map(({ path: path2, title, code }) => `## ${title}
|
|
1378
1585
|
|
|
1379
1586
|
\`\`\`tsx
|
|
1380
|
-
// src/${
|
|
1587
|
+
// src/${path2}
|
|
1381
1588
|
${code}
|
|
1382
1589
|
\`\`\``).join("\n\n");
|
|
1383
1590
|
const body = `# Code Patterns
|
|
@@ -1397,10 +1604,10 @@ var generateRecipes = () => {
|
|
|
1397
1604
|
description: "Reference code and recipes for common extension development tasks",
|
|
1398
1605
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
|
|
1399
1606
|
});
|
|
1400
|
-
const references = RECIPE_SECTIONS.map(({ path, title, code }) => `## Reference: ${title}
|
|
1607
|
+
const references = RECIPE_SECTIONS.map(({ path: path2, title, code }) => `## Reference: ${title}
|
|
1401
1608
|
|
|
1402
1609
|
\`\`\`tsx
|
|
1403
|
-
// src/${
|
|
1610
|
+
// src/${path2}
|
|
1404
1611
|
${code}
|
|
1405
1612
|
\`\`\``).join("\n\n");
|
|
1406
1613
|
const body = `# Recipes
|
|
@@ -1540,28 +1747,42 @@ export const Header = () => {
|
|
|
1540
1747
|
- **Use ScrollArea** \u2014 wrap content surfaces in \`<ui.ScrollArea>\` for overflow handling
|
|
1541
1748
|
`;
|
|
1542
1749
|
};
|
|
1543
|
-
|
|
1544
|
-
// ../../sdk/extension/ai-docs/src/cli-commands.ts
|
|
1545
1750
|
var DLX = "pnpm --config.dlx-cache-max-age=0 dlx";
|
|
1546
1751
|
var CLI = {
|
|
1547
1752
|
/** Scaffold a new extension project */
|
|
1548
|
-
create: (name = "<
|
|
1753
|
+
create: (name = "<extension-name>") => `${DLX} @stackable-labs/create-extension ${name}`,
|
|
1549
1754
|
/** Start dev servers with hot reload */
|
|
1550
1755
|
dev: `${DLX} @stackable-labs/cli-app-extension@latest dev`,
|
|
1551
|
-
/** Deploy the extension */
|
|
1756
|
+
/** Deploy the extension (future) */
|
|
1552
1757
|
deploy: `${DLX} @stackable-labs/cli-app-extension@latest deploy`,
|
|
1553
|
-
/** Validate the extension for common errors */
|
|
1758
|
+
/** Validate the extension for common errors (coming soon) */
|
|
1554
1759
|
validate: `${DLX} @stackable-labs/cli-app-extension@latest validate`,
|
|
1555
1760
|
/** Scaffold from an existing extension */
|
|
1556
1761
|
scaffold: `${DLX} @stackable-labs/cli-app-extension@latest scaffold`,
|
|
1557
1762
|
/** Update an existing extension */
|
|
1558
|
-
update: `${DLX} @stackable-labs/cli-app-extension@latest update
|
|
1763
|
+
update: `${DLX} @stackable-labs/cli-app-extension@latest update`,
|
|
1764
|
+
/** Manage CLI authentication (browser-based OAuth) */
|
|
1765
|
+
auth: {
|
|
1766
|
+
login: `${DLX} @stackable-labs/cli-app-extension@latest auth login`,
|
|
1767
|
+
logout: `${DLX} @stackable-labs/cli-app-extension@latest auth logout`,
|
|
1768
|
+
status: `${DLX} @stackable-labs/cli-app-extension@latest auth status`
|
|
1769
|
+
},
|
|
1770
|
+
/** AI editor configuration tools (Skills + MCP) */
|
|
1771
|
+
ai: {
|
|
1772
|
+
/** Download AI editor config files (Stackable Skills) into your project */
|
|
1773
|
+
scaffold: `${DLX} @stackable-labs/cli-app-extension@latest ai scaffold`,
|
|
1774
|
+
/** Add Stackable MCP server config to your project */
|
|
1775
|
+
mcp: `${DLX} @stackable-labs/cli-app-extension@latest ai mcp`
|
|
1776
|
+
}
|
|
1559
1777
|
};
|
|
1560
|
-
var
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1778
|
+
var TEMPLATE_FLAVOR_META = {
|
|
1779
|
+
minimal: { label: "Minimal", description: "Bare minimum \u2014 single surface, hello-world component" },
|
|
1780
|
+
starter: { label: "Starter", description: "Common patterns \u2014 store, api helpers, menu (default)" },
|
|
1781
|
+
"kitchen-sink": { label: "Kitchen Sink", description: "Everything \u2014 every component, capability, surface, and hook" }
|
|
1782
|
+
};
|
|
1783
|
+
var TEMPLATE_FLAVOR_DETAILS = TEMPLATE_FLAVORS.map(
|
|
1784
|
+
(name) => ({ name, ...TEMPLATE_FLAVOR_META[name] })
|
|
1785
|
+
);
|
|
1565
1786
|
|
|
1566
1787
|
// ../../sdk/extension/ai-docs/src/generators/quick-start.ts
|
|
1567
1788
|
var generateQuickStart = () => {
|
|
@@ -1577,6 +1798,21 @@ var generateQuickStart = () => {
|
|
|
1577
1798
|
|
|
1578
1799
|
Create, develop, and deploy your first Stackable extension in minutes.
|
|
1579
1800
|
|
|
1801
|
+
## Choosing your path
|
|
1802
|
+
|
|
1803
|
+
Stackable supports two authoring paths. Both produce the same kind of extension and ship to the same marketplace.
|
|
1804
|
+
|
|
1805
|
+
| | AI Extension Studio | CLI (this guide) |
|
|
1806
|
+
|---|---|---|
|
|
1807
|
+
| **Best for** | Prototyping, learning, quick iterations | Production extensions, team workflows |
|
|
1808
|
+
| **Code structure** | Single file | Multi-file (surfaces/, components/, lib/) |
|
|
1809
|
+
| **Preview** | Built-in live preview | Local dev server + Cloudflare tunnel |
|
|
1810
|
+
| **AI assistance** | Sidekick chat + smart insertion | Skills, MCP server, and Claude Code plugin (see [AI-Accelerated Development](/docs/reference/ai-accelerated-development)) |
|
|
1811
|
+
| **Version control** | Auto-saved to cloud | Git-based |
|
|
1812
|
+
| **Deployment** | Link to extension + deploy | CLI deploy command |
|
|
1813
|
+
|
|
1814
|
+
Both produce the same output \u2014 a Stackable extension that runs in the host application via the same Remote DOM pipeline. **This guide covers the [CLI](/docs/reference/cli-reference) path.** For Studio, see [AI Extension Studio](/docs/reference/extension-studio).
|
|
1815
|
+
|
|
1580
1816
|
## Prerequisites
|
|
1581
1817
|
|
|
1582
1818
|
- **Node.js** 22 or later
|
|
@@ -1715,21 +1951,11 @@ export const Content = () => {
|
|
|
1715
1951
|
}
|
|
1716
1952
|
\`\`\`
|
|
1717
1953
|
|
|
1718
|
-
## 6.
|
|
1719
|
-
|
|
1720
|
-
Before deploying, validate your extension:
|
|
1721
|
-
|
|
1722
|
-
\`\`\`bash
|
|
1723
|
-
${CLI.validate}
|
|
1724
|
-
\`\`\`
|
|
1725
|
-
|
|
1726
|
-
This checks manifest structure, permission usage, surface targets, and import patterns.
|
|
1954
|
+
## 6. Submit for review
|
|
1727
1955
|
|
|
1728
|
-
When ready,
|
|
1956
|
+
When your extension is ready, open it in the [Stackable admin dashboard](https://admin.stackablelabs.com), fill in the marketplace listing (icon, screenshots, description, tagline, support links), and submit for review. Most reviews complete within a few business days.
|
|
1729
1957
|
|
|
1730
|
-
|
|
1731
|
-
${CLI.deploy}
|
|
1732
|
-
\`\`\`
|
|
1958
|
+
> *CLI \`validate\` and \`deploy\` commands are on the roadmap (see [CLI Reference](/docs/reference/cli-reference#validate-coming-soon)). Today, manifests are validated server-side during submission and via the \`validate_manifest\` MCP tool.*
|
|
1733
1959
|
|
|
1734
1960
|
## Next Steps
|
|
1735
1961
|
|
|
@@ -1748,7 +1974,7 @@ var generateCliReference = () => {
|
|
|
1748
1974
|
description: "CLI commands for creating, developing, and deploying Stackable extensions",
|
|
1749
1975
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
1750
1976
|
});
|
|
1751
|
-
const templateRows =
|
|
1977
|
+
const templateRows = TEMPLATE_FLAVOR_DETAILS.map((t) => `| \`${t.name}\` | ${t.description} |`).join("\n");
|
|
1752
1978
|
return `${fm}
|
|
1753
1979
|
|
|
1754
1980
|
# CLI Reference
|
|
@@ -1767,7 +1993,7 @@ ${CLI.create()}
|
|
|
1767
1993
|
|
|
1768
1994
|
| Argument | Description |
|
|
1769
1995
|
|----------|-------------|
|
|
1770
|
-
| \`
|
|
1996
|
+
| \`extension-name\` | Name for your extension (saved as the manifest's \`name\`; kebab-cased to derive the directory and extension ID) |
|
|
1771
1997
|
|
|
1772
1998
|
**Options:**
|
|
1773
1999
|
|
|
@@ -1828,9 +2054,11 @@ Append this to your deployed host app URL to load your local extension instead
|
|
|
1828
2054
|
of the production bundle. The override is browser-session only \u2014 no DB changes,
|
|
1829
2055
|
no shared state. Each developer gets isolated overrides.
|
|
1830
2056
|
|
|
1831
|
-
## validate
|
|
2057
|
+
## validate *(coming soon)*
|
|
1832
2058
|
|
|
1833
|
-
|
|
2059
|
+
> *NOTE: manifests are validated server-side during the marketplace submission flow, in the AI Studio, or can be validated using the \`validate_manifest\` MCP tool \u2014 see [AI-Accelerated Development](/docs/reference/ai-accelerated-development#live-mcp-server).*
|
|
2060
|
+
|
|
2061
|
+
When shipped, the \`validate\` command will check your extension for common errors locally before submission:
|
|
1834
2062
|
|
|
1835
2063
|
\`\`\`bash
|
|
1836
2064
|
${CLI.validate}
|
|
@@ -1851,23 +2079,21 @@ ${CLI.validate}
|
|
|
1851
2079
|
- \`0\` \u2014 all checks pass
|
|
1852
2080
|
- \`1\` \u2014 errors found (must fix before deploying)
|
|
1853
2081
|
|
|
1854
|
-
## deploy
|
|
2082
|
+
## deploy *(future)*
|
|
2083
|
+
|
|
2084
|
+
> *This command is on the roadmap. Currently build, deployment and hosting are the responsibility of the extension developer with your hosted Bundle URL being updated via the admin dashboard or via CLI.*
|
|
1855
2085
|
|
|
1856
|
-
|
|
2086
|
+
When shipped, the \`deploy\` command will package and ship your extension straight from the terminal:
|
|
1857
2087
|
|
|
1858
2088
|
\`\`\`bash
|
|
1859
2089
|
${CLI.deploy}
|
|
1860
2090
|
\`\`\`
|
|
1861
2091
|
|
|
1862
|
-
**
|
|
1863
|
-
1.
|
|
1864
|
-
2.
|
|
1865
|
-
3.
|
|
1866
|
-
4.
|
|
1867
|
-
|
|
1868
|
-
**Prerequisites:**
|
|
1869
|
-
- All validation checks must pass
|
|
1870
|
-
- You must be authenticated (follow the prompts if not)
|
|
2092
|
+
**Planned behavior:**
|
|
2093
|
+
1. Run validation checks (same as \`validate\`)
|
|
2094
|
+
2. Build the extension for production
|
|
2095
|
+
3. Package the build output
|
|
2096
|
+
4. Upload to the Stackable extension registry
|
|
1871
2097
|
|
|
1872
2098
|
## scaffold
|
|
1873
2099
|
|
|
@@ -1904,6 +2130,62 @@ ${CLI.update} [extensionId]
|
|
|
1904
2130
|
| \`--bundle-url <url>\` | New bundle URL |
|
|
1905
2131
|
| \`--enabled <bool>\` | Enable/disable extension |
|
|
1906
2132
|
| \`--dir <path>\` | Project root (default: cwd) |
|
|
2133
|
+
|
|
2134
|
+
## auth
|
|
2135
|
+
|
|
2136
|
+
Manage CLI authentication. Subcommands open a browser-based OAuth flow against the Stackable platform and store the resulting credentials locally.
|
|
2137
|
+
|
|
2138
|
+
**Subcommands:**
|
|
2139
|
+
|
|
2140
|
+
| Subcommand | Description |
|
|
2141
|
+
|------------|-------------|
|
|
2142
|
+
| \`login\` | Authenticate with Stackable via browser-based OAuth |
|
|
2143
|
+
| \`logout\` | Clear stored CLI credentials |
|
|
2144
|
+
| \`status\` | Show current authentication status (signed-in user, org, token expiry) |
|
|
2145
|
+
|
|
2146
|
+
**Examples:**
|
|
2147
|
+
|
|
2148
|
+
\`\`\`bash
|
|
2149
|
+
# First-time setup
|
|
2150
|
+
${CLI.auth.login}
|
|
2151
|
+
|
|
2152
|
+
# Check who's signed in
|
|
2153
|
+
${CLI.auth.status}
|
|
2154
|
+
|
|
2155
|
+
# Sign out
|
|
2156
|
+
${CLI.auth.logout}
|
|
2157
|
+
\`\`\`
|
|
2158
|
+
|
|
2159
|
+
## ai
|
|
2160
|
+
|
|
2161
|
+
Manage AI editor configuration in your Extension project. Subcommands write Stackable's [AI tooling](/docs/reference/ai-accelerated-development) (Skills + MCP server config) into your project.
|
|
2162
|
+
|
|
2163
|
+
**Subcommands:**
|
|
2164
|
+
|
|
2165
|
+
| Subcommand | Description |
|
|
2166
|
+
|------------|-------------|
|
|
2167
|
+
| \`scaffold\` | Download Stackable Skills into your project's \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, etc. |
|
|
2168
|
+
| \`mcp\` | Add Stackable MCP server config to your project so any MCP-compatible AI client can connect |
|
|
2169
|
+
|
|
2170
|
+
**Options:**
|
|
2171
|
+
|
|
2172
|
+
| Flag | Description | Applies to |
|
|
2173
|
+
|------|-------------|------------|
|
|
2174
|
+
| \`--version <version>\` | AI docs version (semver or \`latest\`, default: \`latest\`) | both |
|
|
2175
|
+
| \`--dir <path>\` | Project root directory (default: cwd) | \`mcp\` only |
|
|
2176
|
+
|
|
2177
|
+
**Examples:**
|
|
2178
|
+
|
|
2179
|
+
\`\`\`bash
|
|
2180
|
+
# Add the latest Stackable Skills to your project
|
|
2181
|
+
${CLI.ai.scaffold}
|
|
2182
|
+
|
|
2183
|
+
# Pin a specific version
|
|
2184
|
+
${CLI.ai.scaffold} --version 0.2.0
|
|
2185
|
+
|
|
2186
|
+
# Configure your project to talk to the Stackable MCP server
|
|
2187
|
+
${CLI.ai.mcp}
|
|
2188
|
+
\`\`\`
|
|
1907
2189
|
`;
|
|
1908
2190
|
};
|
|
1909
2191
|
|
|
@@ -2062,29 +2344,29 @@ var generateExtensionStudio = () => {
|
|
|
2062
2344
|
const fm = frontmatter({
|
|
2063
2345
|
root: false,
|
|
2064
2346
|
targets: ["*"],
|
|
2065
|
-
description: "Extension Studio: in-browser builder with AI assistant, component palette, and live preview",
|
|
2347
|
+
description: "AI Extension Studio: in-browser builder with AI assistant, component palette, and live preview",
|
|
2066
2348
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
2067
2349
|
});
|
|
2068
2350
|
return `${fm}
|
|
2069
2351
|
|
|
2070
|
-
# Extension Studio
|
|
2352
|
+
# AI Extension Studio
|
|
2071
2353
|
|
|
2072
|
-
Extension Studio is an in-browser development environment for building Stackable
|
|
2354
|
+
Stackable's AI Extension Studio is an in-browser development environment for building Stackable
|
|
2073
2355
|
extensions without local tooling. It runs inside the admin dashboard and provides
|
|
2074
2356
|
a code editor, live preview, component palette, and an AI assistant \u2014 everything
|
|
2075
2357
|
needed to create, iterate on, and deploy extensions from the browser.
|
|
2076
2358
|
|
|
2077
2359
|
## Layout
|
|
2078
2360
|
|
|
2079
|
-
Studio is organized as a 3-pane workspace:
|
|
2361
|
+
"Studio" is organized as a 3-pane workspace:
|
|
2080
2362
|
|
|
2081
2363
|
| Pane | Position | Purpose |
|
|
2082
2364
|
|------|----------|---------|
|
|
2083
2365
|
| **Shelf** | Left (collapsible) | Component palette, surface picker, capability list |
|
|
2084
|
-
| **Stage** | Center | Code editor
|
|
2366
|
+
| **Stage** | Center | Code editor or live preview \u2014 toggle between modes |
|
|
2085
2367
|
| **Sidekick** | Right (collapsible, resizable) | AI chat assistant for code generation and SDK guidance |
|
|
2086
2368
|
|
|
2087
|
-
## The Shelf
|
|
2369
|
+
## The "Shelf"
|
|
2088
2370
|
|
|
2089
2371
|
The Shelf is a collapsible sidebar with three sections:
|
|
2090
2372
|
|
|
@@ -2109,7 +2391,7 @@ The SDK capabilities your extension can use: \`data.query\`, \`data.fetch\`,
|
|
|
2109
2391
|
\`events:identity\`, \`events:messaging\`, and \`events:activity\`. Clicking a
|
|
2110
2392
|
capability adds the permission to your manifest and AI-inserts the hook usage.
|
|
2111
2393
|
|
|
2112
|
-
## The Stage
|
|
2394
|
+
## The "Stage"
|
|
2113
2395
|
|
|
2114
2396
|
The center pane switches between two modes:
|
|
2115
2397
|
|
|
@@ -2124,7 +2406,7 @@ Changes in Code mode recompile automatically via in-browser esbuild and update
|
|
|
2124
2406
|
the preview. The toolbar shows the current status: Ready, Saving, Compiling,
|
|
2125
2407
|
Thinking (AI), or Error.
|
|
2126
2408
|
|
|
2127
|
-
## The Sidekick
|
|
2409
|
+
## The "Sidekick"
|
|
2128
2410
|
|
|
2129
2411
|
The Sidekick is an AI chat assistant that understands the Stackable Extension SDK.
|
|
2130
2412
|
It can:
|
|
@@ -2164,18 +2446,114 @@ Studio toolbar.
|
|
|
2164
2446
|
|
|
2165
2447
|
## Studio vs CLI
|
|
2166
2448
|
|
|
2167
|
-
|
|
2449
|
+
Comparing approaches? See [Choosing your path](/docs/guides/quick-start#choosing-your-path) in the Quick Start guide for the full Studio vs CLI comparison.
|
|
2450
|
+
|
|
2451
|
+
Both workflows produce the same output \u2014 a Stackable extension that runs in the host application via the same Remote DOM pipeline. Start in Studio to prototype, then scaffold to CLI when you need the full development workflow.
|
|
2452
|
+
`;
|
|
2453
|
+
};
|
|
2454
|
+
|
|
2455
|
+
// ../../sdk/extension/ai-docs/src/generators/ai-accelerated-development.ts
|
|
2456
|
+
var generateAIAcceleratedDevelopment = () => {
|
|
2457
|
+
const fm = frontmatter({
|
|
2458
|
+
root: false,
|
|
2459
|
+
targets: ["*"],
|
|
2460
|
+
description: "AI-Accelerated Development: AI Extension Studio, Agent Skills, the live MCP server, and the Claude Code plugin \u2014 and how to choose between them.",
|
|
2461
|
+
globs: ["*"]
|
|
2462
|
+
});
|
|
2463
|
+
return `${fm}
|
|
2464
|
+
|
|
2465
|
+
# AI-Accelerated Development
|
|
2466
|
+
|
|
2467
|
+
Stackable's AI-native messaging experience platform makes building extensions really simple - and incredibly fast. Four complementary surfaces (Studio, Agent Skills, MCP Server, and Claude Code Plugin) take you from possibility to production in minutes.
|
|
2468
|
+
|
|
2469
|
+
| Surface | Best for | Install |
|
|
2168
2470
|
|---|---|---|
|
|
2169
|
-
| **
|
|
2170
|
-
| **
|
|
2171
|
-
| **
|
|
2172
|
-
| **
|
|
2173
|
-
|
|
2174
|
-
|
|
2471
|
+
| **AI Extension Studio** | First extensions, prototyping, in-browser builds | Open the admin dashboard |
|
|
2472
|
+
| **Agent Skills** | Any AI coding assistant | \`pnpm dlx skills add stackable-labs/skills\` (auto-bundled in scaffolds) |
|
|
2473
|
+
| **Live MCP Server** | AI clients that speak MCP | Add to your client's MCP config |
|
|
2474
|
+
| **Claude Code Plugin** | Claude Code users (bundles Skills + MCP) | \`/plugin marketplace add stackable-labs/claude-plugins\` |
|
|
2475
|
+
|
|
2476
|
+
---
|
|
2477
|
+
|
|
2478
|
+
## AI Extension Studio
|
|
2479
|
+
|
|
2480
|
+
AI Extension Studio is more than just another AI App builder ([Lovable](https://lovable.dev), [v0](https://v0.dev), [Bolt](https://bolt.new), [Replit](https://replit.com), etc.). It's an in-browser companion trained specifically to conceptualize and build experiences for the Stackable framework that are secure, look great, and are **production ready in minutes!**
|
|
2481
|
+
|
|
2482
|
+
The 3-pane workspace (component palette, live preview, agentic chat) takes you from idea to shipped code in moments. The agentic chat ("Sidekick") writes and modifies code, updates your manifest, looks up SDK reference on demand, and suggests next steps as you work. No installs, no Node version to manage, no tunnel \u2014 open the admin dashboard and you're building.
|
|
2483
|
+
|
|
2484
|
+
**Best for:** first extensions, prototyping, non-developers, and quick experiments.
|
|
2485
|
+
|
|
2486
|
+
**When to move to the CLI:** Studio produces a simple single-file project. When you need multi-file structure, custom dependencies, your own build pipeline, or git-based version control, you can transition seamlessly. Either download your Studio project as a ZIP from the export menu, or pull it directly via the CLI. Either path produces a fully-structured local project ready for production build and deployment.
|
|
2487
|
+
|
|
2488
|
+
For full Studio details, see [AI Extension Studio](/docs/reference/extension-studio).
|
|
2489
|
+
|
|
2490
|
+
## AI Agentic Skills
|
|
2175
2491
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2492
|
+
[Agent Skills](https://agentskills.io) is an open standard for publishing machine-readable knowledge that AI coding assistants can pull on demand instead of crawling your docs. Stackable publishes the core SDK concepts (surfaces, capabilities, components, hooks, patterns, recipes, and more) as Agent Skills, so any compatible assistant has just-in-time access to exactly the surface area it needs.
|
|
2493
|
+
|
|
2494
|
+
### How to install
|
|
2495
|
+
|
|
2496
|
+
- **Bundled automatically when you scaffold** \u2014 \`@stackable-labs/cli-app-extension create\` writes the Stackable Skills into your project's \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, and equivalent directories. No extra setup.
|
|
2497
|
+
- **Standalone install** \u2014 for an existing project, or to update to the latest skills, run:
|
|
2498
|
+
\`\`\`bash
|
|
2499
|
+
pnpm dlx skills add stackable-labs/skills
|
|
2500
|
+
\`\`\`
|
|
2501
|
+
This is powered by [Vercel's open \`skills\` CLI](https://github.com/vercel-labs/skills).
|
|
2502
|
+
|
|
2503
|
+
### Compatible assistants
|
|
2504
|
+
|
|
2505
|
+
Agent Skills work with any tool that supports the format \u2014 Claude Code, Cursor, Windsurf, Codex, VS Code (with MCP-aware extensions), Continue, Zed, and 40+ others listed on agentskills.io. Stackable's Claude Code plugin (below) bundles + auto-updates these skills for Claude Code users.
|
|
2506
|
+
|
|
2507
|
+
## Live MCP Server
|
|
2508
|
+
|
|
2509
|
+
The Stackable platform exposes a hosted MCP (Model Context Protocol) server that any compatible AI client can connect to for live access to your account's apps, extensions, and platform metadata.
|
|
2510
|
+
|
|
2511
|
+
**Endpoint:** \`https://api-use1.stackablelabs.io/mcp/app-extension\`
|
|
2512
|
+
|
|
2513
|
+
### Available tools (snapshot)
|
|
2514
|
+
|
|
2515
|
+
| Tool | What it does |
|
|
2516
|
+
|---|---|
|
|
2517
|
+
| \`list_apps\` | List apps in your account |
|
|
2518
|
+
| \`list_extensions\` | List extensions for a given app |
|
|
2519
|
+
| \`list_instances\` | List deployed instances of an extension |
|
|
2520
|
+
| \`list_skills\` | List available SDK skills |
|
|
2521
|
+
| \`lookup_skill\` | Fetch a specific SDK skill's content on demand |
|
|
2522
|
+
| \`get_extension\` | Fetch full details for a single extension |
|
|
2523
|
+
| \`validate_manifest\` | Validate an extension manifest against the platform |
|
|
2524
|
+
| \`validate_permissions\` | Validate manifest permissions |
|
|
2525
|
+
|
|
2526
|
+
### Compatible clients
|
|
2527
|
+
|
|
2528
|
+
[Claude Desktop](https://claude.ai/download), [VS Code](https://code.visualstudio.com), [Cursor](https://cursor.com), [Codex](https://developers.openai.com/codex), [Antigravity](https://antigravity.google), [Windsurf](https://windsurf.com), and any other MCP-compatible AI tool. Each client has its own way to register MCP servers \u2014 see your client's docs for the exact config syntax.
|
|
2529
|
+
|
|
2530
|
+
Authentication uses standard OAuth2 \u2014 your client will prompt you to sign in on first use.
|
|
2531
|
+
|
|
2532
|
+
## Claude Code Plugin
|
|
2533
|
+
|
|
2534
|
+
For Claude Code users, the Stackable Claude Code plugin bundles the Skills and pre-configures the MCP server in one install.
|
|
2535
|
+
|
|
2536
|
+
**Install:**
|
|
2537
|
+
\`\`\`bash
|
|
2538
|
+
# Add marketplace
|
|
2539
|
+
/plugin marketplace add stackable-labs/claude-plugins
|
|
2540
|
+
|
|
2541
|
+
# Install plugin
|
|
2542
|
+
/plugin install stackable-extension-dev@stackable-claude-plugins
|
|
2543
|
+
\`\`\`
|
|
2544
|
+
|
|
2545
|
+
**What it includes:**
|
|
2546
|
+
- Latest Stackable Agent Skills (auto-updated)
|
|
2547
|
+
- The Stackable MCP server pre-configured
|
|
2548
|
+
- Slash commands and workflows tailored for extension development
|
|
2549
|
+
|
|
2550
|
+
After install, Claude Code can scaffold, validate, and inspect your Stackable extensions directly from your editor without any extra config.
|
|
2551
|
+
|
|
2552
|
+
## Choosing your tools
|
|
2553
|
+
|
|
2554
|
+
- **Just starting?** Open [AI Extension Studio](/docs/reference/extension-studio) in the admin dashboard. No installs, no decisions \u2014 get started on your first extension right in the browser.
|
|
2555
|
+
- **Want more control, or preparing for release?** Use the [CLI](/docs/guides/quick-start) for a full local TypeScript project: multi-file structure, your own dependencies, git-based version control, and your own build pipeline.
|
|
2556
|
+
- **Building from scratch with your own AI workflow?** Wire up your editor with [Agent Skills](#ai-agentic-skills), the [Live MCP Server](#live-mcp-server), or both \u2014 or use the [Claude Code Plugin](#claude-code-plugin) for Claude Code users, which auto-bundles Skills and pre-configures the MCP server in one install.
|
|
2179
2557
|
`;
|
|
2180
2558
|
};
|
|
2181
2559
|
var generateProjectStructure = () => {
|
|
@@ -3014,12 +3392,23 @@ var generateValidateExtensionCommand = () => {
|
|
|
3014
3392
|
description: "Validate this extension for common errors before deploying (manifest, permissions, surfaces, imports)",
|
|
3015
3393
|
targets: ["*"]
|
|
3016
3394
|
});
|
|
3395
|
+
const eventHookBullets = Object.keys(EVENT_HOOK_PERMISSION_MAP).map((hook) => `- \`${hook}\` \u2192 needs \`${EVENT_HOOK_PERMISSION_MAP[hook]}\` permission (also check manifest \`events\` array has matching entries)`).join("\n");
|
|
3017
3396
|
return `${fm}
|
|
3018
3397
|
|
|
3019
3398
|
# Validate Extension
|
|
3020
3399
|
|
|
3021
3400
|
Check this extension for common errors before deploying. Run through each check
|
|
3022
|
-
and
|
|
3401
|
+
and ensure no issues found before you submit.
|
|
3402
|
+
|
|
3403
|
+
> **Tip \u2014 let the platform validate for you.** The hosted Stackable MCP server
|
|
3404
|
+
> exposes a \`validate_manifest\` tool that runs the same server-side checks the
|
|
3405
|
+
> marketplace submission flow uses. If your AI client is connected to the
|
|
3406
|
+
> Stackable MCP server (see [AI-Accelerated Development](/docs/reference/ai-accelerated-development#live-mcp-server)),
|
|
3407
|
+
> just ask it to validate \`packages/extension/public/manifest.json\` for you \u2014
|
|
3408
|
+
> it returns structured errors and warnings without you having to walk the
|
|
3409
|
+
> checklist below by hand. The manual steps remain useful for code-level checks
|
|
3410
|
+
> the manifest validator can't see (permission usage, surface wiring, sandbox
|
|
3411
|
+
> compliance).
|
|
3023
3412
|
|
|
3024
3413
|
## 1. Manifest validation
|
|
3025
3414
|
Read \`packages/extension/public/manifest.json\` and verify:
|
|
@@ -3039,9 +3428,7 @@ Scan all \`.tsx\` files in \`packages/extension/src/\` for capability usage:
|
|
|
3039
3428
|
- \`capabilities.actions.toast\` \u2192 needs \`actions:toast\` permission
|
|
3040
3429
|
- \`capabilities.actions.invoke\` \u2192 needs \`actions:invoke\` permission
|
|
3041
3430
|
- \`useExtendIdentity\` \u2192 needs \`extend:identity\` permission
|
|
3042
|
-
|
|
3043
|
-
- \`useMessagingEvent\` \u2192 needs \`events:messaging\` permission (also check manifest \`events\` array has matching entries)
|
|
3044
|
-
- \`useActivityEvent\` \u2192 needs \`events:activity\` permission (also check manifest \`events\` array has matching entries)
|
|
3431
|
+
${eventHookBullets}
|
|
3045
3432
|
|
|
3046
3433
|
Report:
|
|
3047
3434
|
- **Missing permissions:** capabilities used in code but not declared in manifest
|
|
@@ -3061,6 +3448,202 @@ Verify that:
|
|
|
3061
3448
|
## 5. Summary
|
|
3062
3449
|
Print a summary: total issues found, categorized by severity (error vs warning).
|
|
3063
3450
|
Errors must be fixed before deploying. Warnings are recommendations.
|
|
3451
|
+
|
|
3452
|
+
## Next: deploy
|
|
3453
|
+
|
|
3454
|
+
Once validation passes, build the production bundle, host it, and register the
|
|
3455
|
+
Bundle URL with Stackable. See the [Deploy guide](/docs/guides/deploy).
|
|
3456
|
+
`;
|
|
3457
|
+
};
|
|
3458
|
+
|
|
3459
|
+
// ../../sdk/extension/ai-docs/src/commands/deploy-extension.ts
|
|
3460
|
+
var generateDeployExtensionCommand = () => {
|
|
3461
|
+
const fm = frontmatter({
|
|
3462
|
+
description: "Build, host, and register the production Bundle URL for this Stackable extension",
|
|
3463
|
+
targets: ["*"]
|
|
3464
|
+
});
|
|
3465
|
+
return `${fm}
|
|
3466
|
+
|
|
3467
|
+
# Deploy
|
|
3468
|
+
|
|
3469
|
+
Stackable hosts the marketplace, the proxy, and the runtime \u2014 but **you host
|
|
3470
|
+
your extension's bundle/runtime**. Deployment is three steps: build for production,
|
|
3471
|
+
upload to a host of your choice, then point your extension's **Bundle URL** at
|
|
3472
|
+
the result.
|
|
3473
|
+
|
|
3474
|
+
## 1. Build the production bundle
|
|
3475
|
+
|
|
3476
|
+
From the project root:
|
|
3477
|
+
|
|
3478
|
+
\`\`\`bash
|
|
3479
|
+
pnpm build
|
|
3480
|
+
\`\`\`
|
|
3481
|
+
|
|
3482
|
+
This runs Vite in production mode and writes the static bundle to
|
|
3483
|
+
\`packages/extension/dist/\` \u2014 a small set of JS/CSS files plus your
|
|
3484
|
+
\`manifest.json\`. That's the artifact you ship.
|
|
3485
|
+
|
|
3486
|
+
> **Tip:** Validate the build before you upload it. Run through the
|
|
3487
|
+
> [Validate guide](/docs/guides/validate) (or ask your AI client to call the
|
|
3488
|
+
> \`validate_manifest\` MCP tool) so manifest typos and missing permissions are
|
|
3489
|
+
> caught before they reach the marketplace.
|
|
3490
|
+
|
|
3491
|
+
## 2. Upload to a hosting provider
|
|
3492
|
+
|
|
3493
|
+
Stackable doesn't care where the bundle lives \u2014 only that it's served over
|
|
3494
|
+
HTTPS with permissive CORS so the host runtime can fetch it. Anything that
|
|
3495
|
+
serves static files works. The most common options:
|
|
3496
|
+
|
|
3497
|
+
| Provider | Notes |
|
|
3498
|
+
|----------|-------|
|
|
3499
|
+
| **Netlify** | Drop \`packages/extension/dist/\` into a site, or wire up Git auto-deploys. CORS is already permissive. |
|
|
3500
|
+
| **Vercel** | Same idea \u2014 link the repo and let Vercel build/deploy on push. Set the build output to \`packages/extension/dist\`. |
|
|
3501
|
+
| **Cloudflare Pages** | Connect the repo, set the output directory, deploys land on the edge globally. |
|
|
3502
|
+
| **AWS S3 + CloudFront** | Sync \`dist/\` to an S3 bucket, front it with a CloudFront distribution. Make sure the CloudFront response headers include \`Access-Control-Allow-Origin: *\` (or your host origin). |
|
|
3503
|
+
| **Your own CDN / object storage** | Anything that returns \`200 OK\` with the right \`Content-Type\` and CORS headers will work. |
|
|
3504
|
+
|
|
3505
|
+
Whichever you pick, the only requirements are:
|
|
3506
|
+
|
|
3507
|
+
- Bundle is reachable over **HTTPS**
|
|
3508
|
+
- CORS allows the host application's origin (most providers do this by default; S3+CloudFront is the one that usually needs explicit configuration)
|
|
3509
|
+
- The URL is **stable** \u2014 every Instance running your extension fetches from this URL on load
|
|
3510
|
+
|
|
3511
|
+
> **Need help with setup or deployment?** Reach out at [developers@stackablelabs.com](mailto:developers@stackablelabs.com) or open an issue / discussion on [GitHub](https://github.com/stackable-labs).
|
|
3512
|
+
|
|
3513
|
+
## 3. Register your Bundle URL with Stackable
|
|
3514
|
+
|
|
3515
|
+
Once your extension bundle is deployed/live, tell Stackable where to find it. You have two options:
|
|
3516
|
+
|
|
3517
|
+
- **Admin dashboard** \u2014 open your extension at [admin.stackablelabs.com](https://admin.stackablelabs.com), edit the **Bundle URL** field, and save. Existing Instances pick up the new bundle the next time they load.
|
|
3518
|
+
- **CLI** \u2014 from the project directory:
|
|
3519
|
+
|
|
3520
|
+
\`\`\`bash
|
|
3521
|
+
${CLI.update} --bundle-url https://your-host.example.com/manifest.json
|
|
3522
|
+
\`\`\`
|
|
3523
|
+
|
|
3524
|
+
Or, with an explicit extension ID (handy for CI/CD):
|
|
3525
|
+
|
|
3526
|
+
\`\`\`bash
|
|
3527
|
+
${CLI.update} ext_xxx --bundle-url https://your-host.example.com/manifest.json
|
|
3528
|
+
\`\`\`
|
|
3529
|
+
|
|
3530
|
+
Point the URL at your bundle's \`manifest.json\` \u2014 that's the entrypoint the
|
|
3531
|
+
runtime fetches first; everything else is loaded relative to it.
|
|
3532
|
+
|
|
3533
|
+
## 4. Roll out and iterate
|
|
3534
|
+
|
|
3535
|
+
That's the entire deploy loop. Subsequent updates follow the same path: bump
|
|
3536
|
+
\`version\` in \`manifest.json\`, run \`pnpm build\`, re-upload, and (only if the
|
|
3537
|
+
URL changed) re-register. Most teams wire steps 1\u20133 into a single CI/CD job
|
|
3538
|
+
that runs on every merge to \`main\`.
|
|
3539
|
+
|
|
3540
|
+
## Next: list it on the Marketplace
|
|
3541
|
+
|
|
3542
|
+
Deploying makes your extension **runnable** at a stable URL. To make it
|
|
3543
|
+
**installable** by other organizations, you also need to fill in the
|
|
3544
|
+
marketplace listing (icon, screenshots, description, visibility) and submit
|
|
3545
|
+
for review. See [Marketplace Listing](/docs/reference/marketplace-listing).
|
|
3546
|
+
`;
|
|
3547
|
+
};
|
|
3548
|
+
|
|
3549
|
+
// ../../sdk/extension/ai-docs/src/commands/marketplace-listing.ts
|
|
3550
|
+
var generateMarketplaceListing = () => {
|
|
3551
|
+
const fm = frontmatter({
|
|
3552
|
+
description: "Fill in your marketplace listing, choose visibility, and submit your Stackable extension for review and approval",
|
|
3553
|
+
targets: ["*"]
|
|
3554
|
+
});
|
|
3555
|
+
return `${fm}
|
|
3556
|
+
|
|
3557
|
+
# Listing & Submission
|
|
3558
|
+
|
|
3559
|
+
Once your extension is built, hosted, and registered with a Bundle URL (see
|
|
3560
|
+
[Deploy](/docs/guides/deploy)), the last step is to make it discoverable and
|
|
3561
|
+
installable. That happens entirely in the [admin dashboard](https://admin.stackablelabs.com)
|
|
3562
|
+
\u2014 you fill in your marketplace listing, pick a visibility mode, and submit for
|
|
3563
|
+
review.
|
|
3564
|
+
|
|
3565
|
+
## 1. Fill in the listing fields
|
|
3566
|
+
|
|
3567
|
+
Open your extension in the admin dashboard and complete the **Listing** tab.
|
|
3568
|
+
Every field below is part of the marketplace metadata that prospective
|
|
3569
|
+
installers see when browsing or evaluating your extension.
|
|
3570
|
+
|
|
3571
|
+
| Field | Required | What it's for |
|
|
3572
|
+
|-------|----------|---------------|
|
|
3573
|
+
| **Icon** | Yes | Square image rendered on browse cards and the detail page. Square aspect ratio, transparent background recommended. |
|
|
3574
|
+
| **Short description** | Yes | One-line summary used on browse cards and search results. Keep it under ~120 characters. |
|
|
3575
|
+
| **Long description** | Yes | Markdown body for the extension detail page. Cover what the extension does, the problem it solves, and any setup requirements. Screenshots and links allowed. |
|
|
3576
|
+
| **Screenshots** | Recommended | Image gallery shown on the detail page. Pick views that demonstrate the extension actually working in a host application. |
|
|
3577
|
+
| **Categories** | Yes | One or more curated categories from the platform taxonomy (e.g., commerce, support, analytics). Drives marketplace browse filters. |
|
|
3578
|
+
| **Tags** | Optional | Freeform searchable tags. Used for keyword search across the marketplace. |
|
|
3579
|
+
| **Publisher** | Auto | Pulled from your organization profile \u2014 the org name shown as the listing's publisher. Update your org profile in the dashboard if this needs to change. |
|
|
3580
|
+
|
|
3581
|
+
> **Tip:** The icon and screenshots are what make a listing feel real. A
|
|
3582
|
+
> placeholder icon and no screenshots is the single biggest reason listings
|
|
3583
|
+
> get a "needs more info" review response.
|
|
3584
|
+
|
|
3585
|
+
## 2. Choose a visibility mode
|
|
3586
|
+
|
|
3587
|
+
Every listing has a **visibility** setting that controls who can install it:
|
|
3588
|
+
|
|
3589
|
+
| Visibility | Who can find and install it |
|
|
3590
|
+
|------------|------------------------------|
|
|
3591
|
+
| **Public** | Anyone browsing the marketplace. Goes through full marketplace review. Best for extensions you want available to everyone. |
|
|
3592
|
+
| **Protected** | Only organizations you explicitly add to the **allowed orgs** list. The listing is hidden from public browse; people you've allowed see it in their marketplace as if it were public. Best for private/internal extensions, paid customers, or staged rollouts. |
|
|
3593
|
+
|
|
3594
|
+
Visibility can be changed later from the same Listing tab. Switching from
|
|
3595
|
+
**Protected** \u2192 **Public** triggers a fresh marketplace review.
|
|
3596
|
+
|
|
3597
|
+
## 3. Submit for review
|
|
3598
|
+
|
|
3599
|
+
Click **Submit for review**. The platform runs a two-stage review:
|
|
3600
|
+
|
|
3601
|
+
### Stage 1 \u2014 Automated Bundle scan
|
|
3602
|
+
|
|
3603
|
+
Runs immediately on submit. The platform fetches your bundle from the
|
|
3604
|
+
registered Bundle URL and runs deterministic checks:
|
|
3605
|
+
|
|
3606
|
+
- Bundle reachable, valid \`manifest.json\`, sane bundle size
|
|
3607
|
+
- Declared permissions match observed capability usage
|
|
3608
|
+
- Declared targets match the surfaces your code actually registers
|
|
3609
|
+
- Declared \`allowedDomains\` match the endpoints \`data.fetch\` actually hits
|
|
3610
|
+
- No use of forbidden browser globals (\`document\`, \`window.location\`, etc.)
|
|
3611
|
+
|
|
3612
|
+
Findings come back categorized as **error**, **warning**, or **info**. Errors
|
|
3613
|
+
must be fixed before the listing can move forward. Warnings don't block
|
|
3614
|
+
submission but factor into the review.
|
|
3615
|
+
|
|
3616
|
+
### Stage 2 \u2014 Advanced review + Stackable approval
|
|
3617
|
+
|
|
3618
|
+
If Stage 1 passes, the platform runs an advanced review of your bundle \u2014
|
|
3619
|
+
framework analysis, permission surface, network behavior, and any
|
|
3620
|
+
security-relevant findings get scored and assessed against your listing
|
|
3621
|
+
content. From there, Stackable will either:
|
|
3622
|
+
|
|
3623
|
+
- **Approve** \u2014 your listing moves to **Published** and becomes installable.
|
|
3624
|
+
- **Request changes** \u2014 you'll see structured feedback on the listing page. Address it, re-deploy if needed, and resubmit.
|
|
3625
|
+
|
|
3626
|
+
Most reviews complete within a few business days. You'll get an email when
|
|
3627
|
+
the status changes; the listing's review status is also visible in the admin
|
|
3628
|
+
dashboard at all times.
|
|
3629
|
+
|
|
3630
|
+
## Review status lifecycle
|
|
3631
|
+
|
|
3632
|
+
| Status | Meaning |
|
|
3633
|
+
|--------|---------|
|
|
3634
|
+
| **Draft** | Listing is being edited; not submitted. |
|
|
3635
|
+
| **Pending review** | Submitted; waiting on Stage 1 + Stage 2 + Stackable approval. |
|
|
3636
|
+
| **Rejected** | Stackable requested changes. Address the feedback and resubmit. |
|
|
3637
|
+
| **Published** | Live in the marketplace. Installable per your visibility setting. |
|
|
3638
|
+
| **Suspended** | Pulled from the marketplace by Stackable (security issue, ToS violation, etc.). Contact support. |
|
|
3639
|
+
|
|
3640
|
+
## After publishing
|
|
3641
|
+
|
|
3642
|
+
Re-deploys to the same Bundle URL don't require re-submission \u2014 your existing
|
|
3643
|
+
Instances pick up the new bundle automatically. **Listing edits** (description,
|
|
3644
|
+
screenshots, categories, visibility) and **major version bumps** do go through
|
|
3645
|
+
review again. Patch updates that don't change permissions, targets, or
|
|
3646
|
+
\`allowedDomains\` typically clear review quickly.
|
|
3064
3647
|
`;
|
|
3065
3648
|
};
|
|
3066
3649
|
|
|
@@ -3154,6 +3737,12 @@ var SKILLS = [
|
|
|
3154
3737
|
type: "knowledge",
|
|
3155
3738
|
content: () => generateStylingAndTheming()
|
|
3156
3739
|
},
|
|
3740
|
+
{
|
|
3741
|
+
id: "instance-settings",
|
|
3742
|
+
description: "Instance Settings: declaring settingsSchema in your extension manifest, the install-time admin form, and reading regular values via useSettings() vs secure values via {{settings.x}} placeholders in data.fetch. Use when adding per-Instance configuration to an extension.",
|
|
3743
|
+
type: "knowledge",
|
|
3744
|
+
content: () => generateInstanceSettings()
|
|
3745
|
+
},
|
|
3157
3746
|
// ── Docs-only knowledge skills ──────────────────────────────
|
|
3158
3747
|
{
|
|
3159
3748
|
id: "quick-start",
|
|
@@ -3183,6 +3772,13 @@ var SKILLS = [
|
|
|
3183
3772
|
scopes: ["docs"],
|
|
3184
3773
|
content: () => generateExtensionStudio()
|
|
3185
3774
|
},
|
|
3775
|
+
{
|
|
3776
|
+
id: "ai-accelerated-development",
|
|
3777
|
+
description: "AI-Accelerated Development: AI Extension Studio, Agent Skills, the live MCP server, and the Claude Code plugin \u2014 and how to choose between them.",
|
|
3778
|
+
type: "knowledge",
|
|
3779
|
+
scopes: ["docs"],
|
|
3780
|
+
content: () => generateAIAcceleratedDevelopment()
|
|
3781
|
+
},
|
|
3186
3782
|
// ── Cookbook skills (docs-only) ────────────────────────────────
|
|
3187
3783
|
{
|
|
3188
3784
|
id: "cookbook-structural",
|
|
@@ -3205,6 +3801,13 @@ var SKILLS = [
|
|
|
3205
3801
|
scopes: ["docs"],
|
|
3206
3802
|
content: () => generateCookbookEvents()
|
|
3207
3803
|
},
|
|
3804
|
+
{
|
|
3805
|
+
id: "marketplace-listing",
|
|
3806
|
+
description: "Marketplace listing reference: the listing fields (icon, screenshots, description, categories), Public vs Protected visibility, and the two-stage review process (bundle scan + security review + approval). Use when publishing a deployed extension to the marketplace.",
|
|
3807
|
+
type: "knowledge",
|
|
3808
|
+
scopes: ["docs"],
|
|
3809
|
+
content: () => generateMarketplaceListing()
|
|
3810
|
+
},
|
|
3208
3811
|
// ── Action skills ─────────────────────────────────────────────
|
|
3209
3812
|
{
|
|
3210
3813
|
id: "add-surface",
|
|
@@ -3229,6 +3832,15 @@ var SKILLS = [
|
|
|
3229
3832
|
description: "Validate this extension for common errors before deploying: manifest, permissions, surfaces, imports. Use before publishing or when debugging issues.",
|
|
3230
3833
|
type: "action",
|
|
3231
3834
|
content: () => generateValidateExtensionCommand()
|
|
3835
|
+
},
|
|
3836
|
+
{
|
|
3837
|
+
id: "deploy",
|
|
3838
|
+
description: "Build the production bundle, host it on Netlify / Vercel / Cloudflare Pages / S3+CloudFront, and register your Bundle URL with Stackable via the admin dashboard or CLI. Use when shipping an extension after validation passes.",
|
|
3839
|
+
type: "action",
|
|
3840
|
+
// TODO: T3 #24 — remove `scopes: ['docs']` once `cli deploy` is implemented so Studio's
|
|
3841
|
+
// AI assistant in the future should be able invoke the deploy flow (not just describe it).
|
|
3842
|
+
scopes: ["docs"],
|
|
3843
|
+
content: () => generateDeployExtensionCommand()
|
|
3232
3844
|
}
|
|
3233
3845
|
];
|
|
3234
3846
|
|
|
@@ -3830,6 +4442,108 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
|
|
|
3830
4442
|
pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
|
|
3831
4443
|
pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
|
|
3832
4444
|
|
|
4445
|
+
// ../../lib/contracts/src/custom.ts
|
|
4446
|
+
var CUSTOM_ROLE = {
|
|
4447
|
+
SUPER_ADMIN: "super_admin"
|
|
4448
|
+
};
|
|
4449
|
+
new Set(Object.values(CUSTOM_ROLE));
|
|
4450
|
+
|
|
4451
|
+
// ../../lib/utils-js/src/crypto.ts
|
|
4452
|
+
var getCrypto = () => {
|
|
4453
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
4454
|
+
return globalThis.crypto;
|
|
4455
|
+
}
|
|
4456
|
+
throw new Error("Web Crypto API not available \u2014 requires Node.js 22+ or a modern browser");
|
|
4457
|
+
};
|
|
4458
|
+
var getSubtle = () => {
|
|
4459
|
+
const crypto = getCrypto();
|
|
4460
|
+
if (crypto.subtle) {
|
|
4461
|
+
return crypto.subtle;
|
|
4462
|
+
}
|
|
4463
|
+
throw new Error("SubtleCrypto not available \u2014 requires a secure context (HTTPS) or Node.js 22+");
|
|
4464
|
+
};
|
|
4465
|
+
var encodeBytes = (bytes, encoding) => {
|
|
4466
|
+
if (encoding === "hex") {
|
|
4467
|
+
return [...bytes].map((b2) => b2.toString(16).padStart(2, "0")).join("");
|
|
4468
|
+
}
|
|
4469
|
+
const base64 = btoa(String.fromCharCode(...bytes));
|
|
4470
|
+
if (encoding === "base64url") {
|
|
4471
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4472
|
+
}
|
|
4473
|
+
return base64;
|
|
4474
|
+
};
|
|
4475
|
+
var getRandomBytes = (length, encoding = "base64") => {
|
|
4476
|
+
const bytes = new Uint8Array(length);
|
|
4477
|
+
getCrypto().getRandomValues(bytes);
|
|
4478
|
+
return encodeBytes(bytes, encoding);
|
|
4479
|
+
};
|
|
4480
|
+
var getDigest = async (input, encoding = "hex") => {
|
|
4481
|
+
const digest = await getSubtle().digest(
|
|
4482
|
+
"SHA-256",
|
|
4483
|
+
new TextEncoder().encode(input)
|
|
4484
|
+
);
|
|
4485
|
+
return encodeBytes(new Uint8Array(digest), encoding);
|
|
4486
|
+
};
|
|
4487
|
+
var getNonce = () => getRandomBytes(16, "base64url");
|
|
4488
|
+
var getVerifier = () => getRandomBytes(32, "base64url");
|
|
4489
|
+
|
|
4490
|
+
// ../../lib/utils-auth/src/constants.ts
|
|
4491
|
+
var STANDALONE_CLIENT_DATA = {
|
|
4492
|
+
CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
|
|
4493
|
+
MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
|
|
4494
|
+
};
|
|
4495
|
+
var STANDALONE_CLIENT = Object.fromEntries(
|
|
4496
|
+
Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.name])
|
|
4497
|
+
);
|
|
4498
|
+
var STANDALONE_CLIENT_AUTH_FILE = Object.fromEntries(
|
|
4499
|
+
Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.authFile])
|
|
4500
|
+
);
|
|
4501
|
+
Object.values(STANDALONE_CLIENT);
|
|
4502
|
+
|
|
4503
|
+
// ../../lib/utils-auth/src/index.ts
|
|
4504
|
+
var deriveClientId = async (clientName) => (await getDigest(clientName)).slice(0, 32);
|
|
4505
|
+
var AUTH_DIR = join(homedir(), ".stackable");
|
|
4506
|
+
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.CLI);
|
|
4507
|
+
var MCP_AUTH_FILE = join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.MCP);
|
|
4508
|
+
var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.CLI);
|
|
4509
|
+
var readAuthState = async (filename) => {
|
|
4510
|
+
try {
|
|
4511
|
+
const content = await readFile(resolveAuthFile(filename), "utf8");
|
|
4512
|
+
return JSON.parse(content);
|
|
4513
|
+
} catch {
|
|
4514
|
+
return null;
|
|
4515
|
+
}
|
|
4516
|
+
};
|
|
4517
|
+
var writeAuthState = async (state, filename) => {
|
|
4518
|
+
await mkdir(AUTH_DIR, { recursive: true, mode: 448 });
|
|
4519
|
+
await writeFile(resolveAuthFile(filename), JSON.stringify(state, null, 2), { mode: 384 });
|
|
4520
|
+
};
|
|
4521
|
+
var decodeJwtPayload = (token) => {
|
|
4522
|
+
try {
|
|
4523
|
+
const [, payload] = token.split(".");
|
|
4524
|
+
if (!payload) {
|
|
4525
|
+
return null;
|
|
4526
|
+
}
|
|
4527
|
+
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
4528
|
+
return JSON.parse(json);
|
|
4529
|
+
} catch {
|
|
4530
|
+
return null;
|
|
4531
|
+
}
|
|
4532
|
+
};
|
|
4533
|
+
var getToken = async (filename) => {
|
|
4534
|
+
const state = await readAuthState(filename);
|
|
4535
|
+
if (!state) {
|
|
4536
|
+
throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
|
|
4537
|
+
}
|
|
4538
|
+
const payload = decodeJwtPayload(state.token);
|
|
4539
|
+
if (payload?.exp && typeof payload.exp === "number") {
|
|
4540
|
+
if (Date.now() >= payload.exp * 1e3) {
|
|
4541
|
+
throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
return state.token;
|
|
4545
|
+
};
|
|
4546
|
+
|
|
3833
4547
|
// package.json
|
|
3834
4548
|
var package_default = {
|
|
3835
4549
|
version: "0.0.0"};
|
|
@@ -3853,19 +4567,23 @@ var createMcpServer = (options = {}) => {
|
|
|
3853
4567
|
return await getToken(STANDALONE_CLIENT_AUTH_FILE.MCP);
|
|
3854
4568
|
} catch {
|
|
3855
4569
|
}
|
|
3856
|
-
|
|
4570
|
+
if (!options.performOAuthFlow) {
|
|
4571
|
+
throw new Error(
|
|
4572
|
+
"No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
|
|
4573
|
+
);
|
|
4574
|
+
}
|
|
3857
4575
|
const MCP_API_BASE_URL = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL;
|
|
3858
|
-
return performOAuthFlow(`${MCP_API_BASE_URL}/.well-known/oauth-authorization-server`);
|
|
4576
|
+
return options.performOAuthFlow(`${MCP_API_BASE_URL}/.well-known/oauth-authorization-server`);
|
|
3859
4577
|
};
|
|
3860
4578
|
const authHeaders = (token) => ({
|
|
3861
4579
|
authorization: `Bearer ${token}`,
|
|
3862
4580
|
"content-type": "application/json",
|
|
3863
4581
|
"x-client-name": MCP_CLIENT_NAME
|
|
3864
4582
|
});
|
|
3865
|
-
const callApi = async (
|
|
4583
|
+
const callApi = async (path2) => {
|
|
3866
4584
|
const token = await getAuthToken();
|
|
3867
4585
|
const ADMIN_API_BASE_URL = process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
|
|
3868
|
-
const url = `${ADMIN_API_BASE_URL}${
|
|
4586
|
+
const url = `${ADMIN_API_BASE_URL}${path2}`;
|
|
3869
4587
|
let res;
|
|
3870
4588
|
try {
|
|
3871
4589
|
res = await fetch(url, { headers: authHeaders(token) });
|
|
@@ -3992,12 +4710,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
3992
4710
|
usedPermissions.add(permission);
|
|
3993
4711
|
}
|
|
3994
4712
|
}
|
|
3995
|
-
const
|
|
3996
|
-
useIdentityEvent: "events:identity",
|
|
3997
|
-
useMessagingEvent: "events:messaging",
|
|
3998
|
-
useActivityEvent: "events:activity"
|
|
3999
|
-
};
|
|
4000
|
-
for (const [hook, permission] of Object.entries(eventHookMap)) {
|
|
4713
|
+
for (const [hook, permission] of Object.entries(EVENT_HOOK_PERMISSION_MAP)) {
|
|
4001
4714
|
if (allSource.includes(hook)) {
|
|
4002
4715
|
usedPermissions.add(permission);
|
|
4003
4716
|
}
|
|
@@ -4036,8 +4749,623 @@ ${issues.map((i) => `- ${i}`).join("\n")}`);
|
|
|
4036
4749
|
}, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/instances`)));
|
|
4037
4750
|
return server2;
|
|
4038
4751
|
};
|
|
4752
|
+
var isDockerCached;
|
|
4753
|
+
function hasDockerEnv() {
|
|
4754
|
+
try {
|
|
4755
|
+
fs3.statSync("/.dockerenv");
|
|
4756
|
+
return true;
|
|
4757
|
+
} catch {
|
|
4758
|
+
return false;
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
function hasDockerCGroup() {
|
|
4762
|
+
try {
|
|
4763
|
+
return fs3.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
4764
|
+
} catch {
|
|
4765
|
+
return false;
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
function isDocker() {
|
|
4769
|
+
if (isDockerCached === void 0) {
|
|
4770
|
+
isDockerCached = hasDockerEnv() || hasDockerCGroup();
|
|
4771
|
+
}
|
|
4772
|
+
return isDockerCached;
|
|
4773
|
+
}
|
|
4774
|
+
|
|
4775
|
+
// ../../../node_modules/.pnpm/is-inside-container@1.0.0/node_modules/is-inside-container/index.js
|
|
4776
|
+
var cachedResult;
|
|
4777
|
+
var hasContainerEnv = () => {
|
|
4778
|
+
try {
|
|
4779
|
+
fs3.statSync("/run/.containerenv");
|
|
4780
|
+
return true;
|
|
4781
|
+
} catch {
|
|
4782
|
+
return false;
|
|
4783
|
+
}
|
|
4784
|
+
};
|
|
4785
|
+
function isInsideContainer() {
|
|
4786
|
+
if (cachedResult === void 0) {
|
|
4787
|
+
cachedResult = hasContainerEnv() || isDocker();
|
|
4788
|
+
}
|
|
4789
|
+
return cachedResult;
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
// ../../../node_modules/.pnpm/is-wsl@3.1.1/node_modules/is-wsl/index.js
|
|
4793
|
+
var isWsl = () => {
|
|
4794
|
+
if (process6.platform !== "linux") {
|
|
4795
|
+
return false;
|
|
4796
|
+
}
|
|
4797
|
+
if (os.release().toLowerCase().includes("microsoft")) {
|
|
4798
|
+
if (isInsideContainer()) {
|
|
4799
|
+
return false;
|
|
4800
|
+
}
|
|
4801
|
+
return true;
|
|
4802
|
+
}
|
|
4803
|
+
try {
|
|
4804
|
+
if (fs3.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
|
|
4805
|
+
return !isInsideContainer();
|
|
4806
|
+
}
|
|
4807
|
+
} catch {
|
|
4808
|
+
}
|
|
4809
|
+
if (fs3.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs3.existsSync("/run/WSL")) {
|
|
4810
|
+
return !isInsideContainer();
|
|
4811
|
+
}
|
|
4812
|
+
return false;
|
|
4813
|
+
};
|
|
4814
|
+
var is_wsl_default = process6.env.__IS_WSL_TEST__ ? isWsl : isWsl();
|
|
4815
|
+
|
|
4816
|
+
// ../../../node_modules/.pnpm/wsl-utils@0.1.0/node_modules/wsl-utils/index.js
|
|
4817
|
+
var wslDrivesMountPoint = /* @__PURE__ */ (() => {
|
|
4818
|
+
const defaultMountPoint = "/mnt/";
|
|
4819
|
+
let mountPoint;
|
|
4820
|
+
return async function() {
|
|
4821
|
+
if (mountPoint) {
|
|
4822
|
+
return mountPoint;
|
|
4823
|
+
}
|
|
4824
|
+
const configFilePath = "/etc/wsl.conf";
|
|
4825
|
+
let isConfigFileExists = false;
|
|
4826
|
+
try {
|
|
4827
|
+
await fs4.access(configFilePath, constants.F_OK);
|
|
4828
|
+
isConfigFileExists = true;
|
|
4829
|
+
} catch {
|
|
4830
|
+
}
|
|
4831
|
+
if (!isConfigFileExists) {
|
|
4832
|
+
return defaultMountPoint;
|
|
4833
|
+
}
|
|
4834
|
+
const configContent = await fs4.readFile(configFilePath, { encoding: "utf8" });
|
|
4835
|
+
const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
|
|
4836
|
+
if (!configMountPoint) {
|
|
4837
|
+
return defaultMountPoint;
|
|
4838
|
+
}
|
|
4839
|
+
mountPoint = configMountPoint.groups.mountPoint.trim();
|
|
4840
|
+
mountPoint = mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`;
|
|
4841
|
+
return mountPoint;
|
|
4842
|
+
};
|
|
4843
|
+
})();
|
|
4844
|
+
var powerShellPathFromWsl = async () => {
|
|
4845
|
+
const mountPoint = await wslDrivesMountPoint();
|
|
4846
|
+
return `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`;
|
|
4847
|
+
};
|
|
4848
|
+
var powerShellPath = async () => {
|
|
4849
|
+
if (is_wsl_default) {
|
|
4850
|
+
return powerShellPathFromWsl();
|
|
4851
|
+
}
|
|
4852
|
+
return `${process6.env.SYSTEMROOT || process6.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
4853
|
+
};
|
|
4854
|
+
|
|
4855
|
+
// ../../../node_modules/.pnpm/define-lazy-prop@3.0.0/node_modules/define-lazy-prop/index.js
|
|
4856
|
+
function defineLazyProperty(object, propertyName, valueGetter) {
|
|
4857
|
+
const define = (value) => Object.defineProperty(object, propertyName, { value, enumerable: true, writable: true });
|
|
4858
|
+
Object.defineProperty(object, propertyName, {
|
|
4859
|
+
configurable: true,
|
|
4860
|
+
enumerable: true,
|
|
4861
|
+
get() {
|
|
4862
|
+
const result = valueGetter();
|
|
4863
|
+
define(result);
|
|
4864
|
+
return result;
|
|
4865
|
+
},
|
|
4866
|
+
set(value) {
|
|
4867
|
+
define(value);
|
|
4868
|
+
}
|
|
4869
|
+
});
|
|
4870
|
+
return object;
|
|
4871
|
+
}
|
|
4872
|
+
var execFileAsync = promisify(execFile);
|
|
4873
|
+
async function defaultBrowserId() {
|
|
4874
|
+
if (process6.platform !== "darwin") {
|
|
4875
|
+
throw new Error("macOS only");
|
|
4876
|
+
}
|
|
4877
|
+
const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
|
|
4878
|
+
const match = /LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(stdout);
|
|
4879
|
+
const browserId = match?.groups.id ?? "com.apple.Safari";
|
|
4880
|
+
if (browserId === "com.apple.safari") {
|
|
4881
|
+
return "com.apple.Safari";
|
|
4882
|
+
}
|
|
4883
|
+
return browserId;
|
|
4884
|
+
}
|
|
4885
|
+
var execFileAsync2 = promisify(execFile);
|
|
4886
|
+
async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
|
|
4887
|
+
if (process6.platform !== "darwin") {
|
|
4888
|
+
throw new Error("macOS only");
|
|
4889
|
+
}
|
|
4890
|
+
const outputArguments = humanReadableOutput ? [] : ["-ss"];
|
|
4891
|
+
const execOptions = {};
|
|
4892
|
+
if (signal) {
|
|
4893
|
+
execOptions.signal = signal;
|
|
4894
|
+
}
|
|
4895
|
+
const { stdout } = await execFileAsync2("osascript", ["-e", script, outputArguments], execOptions);
|
|
4896
|
+
return stdout.trim();
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// ../../../node_modules/.pnpm/bundle-name@4.1.0/node_modules/bundle-name/index.js
|
|
4900
|
+
async function bundleName(bundleId) {
|
|
4901
|
+
return runAppleScript(`tell application "Finder" to set app_path to application file id "${bundleId}" as string
|
|
4902
|
+
tell application "System Events" to get value of property list item "CFBundleName" of property list file (app_path & ":Contents:Info.plist")`);
|
|
4903
|
+
}
|
|
4904
|
+
var execFileAsync3 = promisify(execFile);
|
|
4905
|
+
var windowsBrowserProgIds = {
|
|
4906
|
+
MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
|
|
4907
|
+
// The missing `L` is correct.
|
|
4908
|
+
MSEdgeBHTML: { name: "Edge Beta", id: "com.microsoft.edge.beta" },
|
|
4909
|
+
MSEdgeDHTML: { name: "Edge Dev", id: "com.microsoft.edge.dev" },
|
|
4910
|
+
AppXq0fevzme2pys62n3e0fbqa7peapykr8v: { name: "Edge", id: "com.microsoft.edge.old" },
|
|
4911
|
+
ChromeHTML: { name: "Chrome", id: "com.google.chrome" },
|
|
4912
|
+
ChromeBHTML: { name: "Chrome Beta", id: "com.google.chrome.beta" },
|
|
4913
|
+
ChromeDHTML: { name: "Chrome Dev", id: "com.google.chrome.dev" },
|
|
4914
|
+
ChromiumHTM: { name: "Chromium", id: "org.chromium.Chromium" },
|
|
4915
|
+
BraveHTML: { name: "Brave", id: "com.brave.Browser" },
|
|
4916
|
+
BraveBHTML: { name: "Brave Beta", id: "com.brave.Browser.beta" },
|
|
4917
|
+
BraveDHTML: { name: "Brave Dev", id: "com.brave.Browser.dev" },
|
|
4918
|
+
BraveSSHTM: { name: "Brave Nightly", id: "com.brave.Browser.nightly" },
|
|
4919
|
+
FirefoxURL: { name: "Firefox", id: "org.mozilla.firefox" },
|
|
4920
|
+
OperaStable: { name: "Opera", id: "com.operasoftware.Opera" },
|
|
4921
|
+
VivaldiHTM: { name: "Vivaldi", id: "com.vivaldi.Vivaldi" },
|
|
4922
|
+
"IE.HTTP": { name: "Internet Explorer", id: "com.microsoft.ie" }
|
|
4923
|
+
};
|
|
4924
|
+
new Map(Object.entries(windowsBrowserProgIds));
|
|
4925
|
+
var UnknownBrowserError = class extends Error {
|
|
4926
|
+
};
|
|
4927
|
+
async function defaultBrowser(_execFileAsync = execFileAsync3) {
|
|
4928
|
+
const { stdout } = await _execFileAsync("reg", [
|
|
4929
|
+
"QUERY",
|
|
4930
|
+
" HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
|
|
4931
|
+
"/v",
|
|
4932
|
+
"ProgId"
|
|
4933
|
+
]);
|
|
4934
|
+
const match = /ProgId\s*REG_SZ\s*(?<id>\S+)/.exec(stdout);
|
|
4935
|
+
if (!match) {
|
|
4936
|
+
throw new UnknownBrowserError(`Cannot find Windows browser in stdout: ${JSON.stringify(stdout)}`);
|
|
4937
|
+
}
|
|
4938
|
+
const { id } = match.groups;
|
|
4939
|
+
const dotIndex = id.lastIndexOf(".");
|
|
4940
|
+
const hyphenIndex = id.lastIndexOf("-");
|
|
4941
|
+
const baseIdByDot = dotIndex === -1 ? void 0 : id.slice(0, dotIndex);
|
|
4942
|
+
const baseIdByHyphen = hyphenIndex === -1 ? void 0 : id.slice(0, hyphenIndex);
|
|
4943
|
+
return windowsBrowserProgIds[id] ?? windowsBrowserProgIds[baseIdByDot] ?? windowsBrowserProgIds[baseIdByHyphen] ?? { name: id, id };
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
// ../../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/index.js
|
|
4947
|
+
var execFileAsync4 = promisify(execFile);
|
|
4948
|
+
var titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
|
|
4949
|
+
async function defaultBrowser2() {
|
|
4950
|
+
if (process6.platform === "darwin") {
|
|
4951
|
+
const id = await defaultBrowserId();
|
|
4952
|
+
const name = await bundleName(id);
|
|
4953
|
+
return { name, id };
|
|
4954
|
+
}
|
|
4955
|
+
if (process6.platform === "linux") {
|
|
4956
|
+
const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
4957
|
+
const id = stdout.trim();
|
|
4958
|
+
const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
|
|
4959
|
+
return { name, id };
|
|
4960
|
+
}
|
|
4961
|
+
if (process6.platform === "win32") {
|
|
4962
|
+
return defaultBrowser();
|
|
4963
|
+
}
|
|
4964
|
+
throw new Error("Only macOS, Linux, and Windows are supported");
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
// ../../../node_modules/.pnpm/open@10.2.0/node_modules/open/index.js
|
|
4968
|
+
var execFile5 = promisify(childProcess.execFile);
|
|
4969
|
+
var __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
4970
|
+
var localXdgOpenPath = path.join(__dirname$1, "xdg-open");
|
|
4971
|
+
var { platform, arch } = process6;
|
|
4972
|
+
async function getWindowsDefaultBrowserFromWsl() {
|
|
4973
|
+
const powershellPath = await powerShellPath();
|
|
4974
|
+
const rawCommand = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
|
|
4975
|
+
const encodedCommand = Buffer$1.from(rawCommand, "utf16le").toString("base64");
|
|
4976
|
+
const { stdout } = await execFile5(
|
|
4977
|
+
powershellPath,
|
|
4978
|
+
[
|
|
4979
|
+
"-NoProfile",
|
|
4980
|
+
"-NonInteractive",
|
|
4981
|
+
"-ExecutionPolicy",
|
|
4982
|
+
"Bypass",
|
|
4983
|
+
"-EncodedCommand",
|
|
4984
|
+
encodedCommand
|
|
4985
|
+
],
|
|
4986
|
+
{ encoding: "utf8" }
|
|
4987
|
+
);
|
|
4988
|
+
const progId = stdout.trim();
|
|
4989
|
+
const browserMap = {
|
|
4990
|
+
ChromeHTML: "com.google.chrome",
|
|
4991
|
+
BraveHTML: "com.brave.Browser",
|
|
4992
|
+
MSEdgeHTM: "com.microsoft.edge",
|
|
4993
|
+
FirefoxURL: "org.mozilla.firefox"
|
|
4994
|
+
};
|
|
4995
|
+
return browserMap[progId] ? { id: browserMap[progId] } : {};
|
|
4996
|
+
}
|
|
4997
|
+
var pTryEach = async (array, mapper) => {
|
|
4998
|
+
let latestError;
|
|
4999
|
+
for (const item of array) {
|
|
5000
|
+
try {
|
|
5001
|
+
return await mapper(item);
|
|
5002
|
+
} catch (error) {
|
|
5003
|
+
latestError = error;
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
throw latestError;
|
|
5007
|
+
};
|
|
5008
|
+
var baseOpen = async (options) => {
|
|
5009
|
+
options = {
|
|
5010
|
+
wait: false,
|
|
5011
|
+
background: false,
|
|
5012
|
+
newInstance: false,
|
|
5013
|
+
allowNonzeroExitCode: false,
|
|
5014
|
+
...options
|
|
5015
|
+
};
|
|
5016
|
+
if (Array.isArray(options.app)) {
|
|
5017
|
+
return pTryEach(options.app, (singleApp) => baseOpen({
|
|
5018
|
+
...options,
|
|
5019
|
+
app: singleApp
|
|
5020
|
+
}));
|
|
5021
|
+
}
|
|
5022
|
+
let { name: app, arguments: appArguments = [] } = options.app ?? {};
|
|
5023
|
+
appArguments = [...appArguments];
|
|
5024
|
+
if (Array.isArray(app)) {
|
|
5025
|
+
return pTryEach(app, (appName) => baseOpen({
|
|
5026
|
+
...options,
|
|
5027
|
+
app: {
|
|
5028
|
+
name: appName,
|
|
5029
|
+
arguments: appArguments
|
|
5030
|
+
}
|
|
5031
|
+
}));
|
|
5032
|
+
}
|
|
5033
|
+
if (app === "browser" || app === "browserPrivate") {
|
|
5034
|
+
const ids = {
|
|
5035
|
+
"com.google.chrome": "chrome",
|
|
5036
|
+
"google-chrome.desktop": "chrome",
|
|
5037
|
+
"com.brave.Browser": "brave",
|
|
5038
|
+
"org.mozilla.firefox": "firefox",
|
|
5039
|
+
"firefox.desktop": "firefox",
|
|
5040
|
+
"com.microsoft.msedge": "edge",
|
|
5041
|
+
"com.microsoft.edge": "edge",
|
|
5042
|
+
"com.microsoft.edgemac": "edge",
|
|
5043
|
+
"microsoft-edge.desktop": "edge"
|
|
5044
|
+
};
|
|
5045
|
+
const flags = {
|
|
5046
|
+
chrome: "--incognito",
|
|
5047
|
+
brave: "--incognito",
|
|
5048
|
+
firefox: "--private-window",
|
|
5049
|
+
edge: "--inPrivate"
|
|
5050
|
+
};
|
|
5051
|
+
const browser = is_wsl_default ? await getWindowsDefaultBrowserFromWsl() : await defaultBrowser2();
|
|
5052
|
+
if (browser.id in ids) {
|
|
5053
|
+
const browserName = ids[browser.id];
|
|
5054
|
+
if (app === "browserPrivate") {
|
|
5055
|
+
appArguments.push(flags[browserName]);
|
|
5056
|
+
}
|
|
5057
|
+
return baseOpen({
|
|
5058
|
+
...options,
|
|
5059
|
+
app: {
|
|
5060
|
+
name: apps[browserName],
|
|
5061
|
+
arguments: appArguments
|
|
5062
|
+
}
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
throw new Error(`${browser.name} is not supported as a default browser`);
|
|
5066
|
+
}
|
|
5067
|
+
let command;
|
|
5068
|
+
const cliArguments = [];
|
|
5069
|
+
const childProcessOptions = {};
|
|
5070
|
+
if (platform === "darwin") {
|
|
5071
|
+
command = "open";
|
|
5072
|
+
if (options.wait) {
|
|
5073
|
+
cliArguments.push("--wait-apps");
|
|
5074
|
+
}
|
|
5075
|
+
if (options.background) {
|
|
5076
|
+
cliArguments.push("--background");
|
|
5077
|
+
}
|
|
5078
|
+
if (options.newInstance) {
|
|
5079
|
+
cliArguments.push("--new");
|
|
5080
|
+
}
|
|
5081
|
+
if (app) {
|
|
5082
|
+
cliArguments.push("-a", app);
|
|
5083
|
+
}
|
|
5084
|
+
} else if (platform === "win32" || is_wsl_default && !isInsideContainer() && !app) {
|
|
5085
|
+
command = await powerShellPath();
|
|
5086
|
+
cliArguments.push(
|
|
5087
|
+
"-NoProfile",
|
|
5088
|
+
"-NonInteractive",
|
|
5089
|
+
"-ExecutionPolicy",
|
|
5090
|
+
"Bypass",
|
|
5091
|
+
"-EncodedCommand"
|
|
5092
|
+
);
|
|
5093
|
+
if (!is_wsl_default) {
|
|
5094
|
+
childProcessOptions.windowsVerbatimArguments = true;
|
|
5095
|
+
}
|
|
5096
|
+
const encodedArguments = ["Start"];
|
|
5097
|
+
if (options.wait) {
|
|
5098
|
+
encodedArguments.push("-Wait");
|
|
5099
|
+
}
|
|
5100
|
+
if (app) {
|
|
5101
|
+
encodedArguments.push(`"\`"${app}\`""`);
|
|
5102
|
+
if (options.target) {
|
|
5103
|
+
appArguments.push(options.target);
|
|
5104
|
+
}
|
|
5105
|
+
} else if (options.target) {
|
|
5106
|
+
encodedArguments.push(`"${options.target}"`);
|
|
5107
|
+
}
|
|
5108
|
+
if (appArguments.length > 0) {
|
|
5109
|
+
appArguments = appArguments.map((argument) => `"\`"${argument}\`""`);
|
|
5110
|
+
encodedArguments.push("-ArgumentList", appArguments.join(","));
|
|
5111
|
+
}
|
|
5112
|
+
options.target = Buffer$1.from(encodedArguments.join(" "), "utf16le").toString("base64");
|
|
5113
|
+
} else {
|
|
5114
|
+
if (app) {
|
|
5115
|
+
command = app;
|
|
5116
|
+
} else {
|
|
5117
|
+
const isBundled = !__dirname$1 || __dirname$1 === "/";
|
|
5118
|
+
let exeLocalXdgOpen = false;
|
|
5119
|
+
try {
|
|
5120
|
+
await fs4.access(localXdgOpenPath, constants.X_OK);
|
|
5121
|
+
exeLocalXdgOpen = true;
|
|
5122
|
+
} catch {
|
|
5123
|
+
}
|
|
5124
|
+
const useSystemXdgOpen = process6.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
|
|
5125
|
+
command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
|
|
5126
|
+
}
|
|
5127
|
+
if (appArguments.length > 0) {
|
|
5128
|
+
cliArguments.push(...appArguments);
|
|
5129
|
+
}
|
|
5130
|
+
if (!options.wait) {
|
|
5131
|
+
childProcessOptions.stdio = "ignore";
|
|
5132
|
+
childProcessOptions.detached = true;
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
if (platform === "darwin" && appArguments.length > 0) {
|
|
5136
|
+
cliArguments.push("--args", ...appArguments);
|
|
5137
|
+
}
|
|
5138
|
+
if (options.target) {
|
|
5139
|
+
cliArguments.push(options.target);
|
|
5140
|
+
}
|
|
5141
|
+
const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
|
|
5142
|
+
if (options.wait) {
|
|
5143
|
+
return new Promise((resolve, reject) => {
|
|
5144
|
+
subprocess.once("error", reject);
|
|
5145
|
+
subprocess.once("close", (exitCode) => {
|
|
5146
|
+
if (!options.allowNonzeroExitCode && exitCode > 0) {
|
|
5147
|
+
reject(new Error(`Exited with code ${exitCode}`));
|
|
5148
|
+
return;
|
|
5149
|
+
}
|
|
5150
|
+
resolve(subprocess);
|
|
5151
|
+
});
|
|
5152
|
+
});
|
|
5153
|
+
}
|
|
5154
|
+
subprocess.unref();
|
|
5155
|
+
return subprocess;
|
|
5156
|
+
};
|
|
5157
|
+
var open = (target, options) => {
|
|
5158
|
+
if (typeof target !== "string") {
|
|
5159
|
+
throw new TypeError("Expected a `target`");
|
|
5160
|
+
}
|
|
5161
|
+
return baseOpen({
|
|
5162
|
+
...options,
|
|
5163
|
+
target
|
|
5164
|
+
});
|
|
5165
|
+
};
|
|
5166
|
+
function detectArchBinary(binary) {
|
|
5167
|
+
if (typeof binary === "string" || Array.isArray(binary)) {
|
|
5168
|
+
return binary;
|
|
5169
|
+
}
|
|
5170
|
+
const { [arch]: archBinary } = binary;
|
|
5171
|
+
if (!archBinary) {
|
|
5172
|
+
throw new Error(`${arch} is not supported`);
|
|
5173
|
+
}
|
|
5174
|
+
return archBinary;
|
|
5175
|
+
}
|
|
5176
|
+
function detectPlatformBinary({ [platform]: platformBinary }, { wsl }) {
|
|
5177
|
+
if (wsl && is_wsl_default) {
|
|
5178
|
+
return detectArchBinary(wsl);
|
|
5179
|
+
}
|
|
5180
|
+
if (!platformBinary) {
|
|
5181
|
+
throw new Error(`${platform} is not supported`);
|
|
5182
|
+
}
|
|
5183
|
+
return detectArchBinary(platformBinary);
|
|
5184
|
+
}
|
|
5185
|
+
var apps = {};
|
|
5186
|
+
defineLazyProperty(apps, "chrome", () => detectPlatformBinary({
|
|
5187
|
+
darwin: "google chrome",
|
|
5188
|
+
win32: "chrome",
|
|
5189
|
+
linux: ["google-chrome", "google-chrome-stable", "chromium"]
|
|
5190
|
+
}, {
|
|
5191
|
+
wsl: {
|
|
5192
|
+
ia32: "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
5193
|
+
x64: ["/mnt/c/Program Files/Google/Chrome/Application/chrome.exe", "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"]
|
|
5194
|
+
}
|
|
5195
|
+
}));
|
|
5196
|
+
defineLazyProperty(apps, "brave", () => detectPlatformBinary({
|
|
5197
|
+
darwin: "brave browser",
|
|
5198
|
+
win32: "brave",
|
|
5199
|
+
linux: ["brave-browser", "brave"]
|
|
5200
|
+
}, {
|
|
5201
|
+
wsl: {
|
|
5202
|
+
ia32: "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
|
|
5203
|
+
x64: ["/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe"]
|
|
5204
|
+
}
|
|
5205
|
+
}));
|
|
5206
|
+
defineLazyProperty(apps, "firefox", () => detectPlatformBinary({
|
|
5207
|
+
darwin: "firefox",
|
|
5208
|
+
win32: String.raw`C:\Program Files\Mozilla Firefox\firefox.exe`,
|
|
5209
|
+
linux: "firefox"
|
|
5210
|
+
}, {
|
|
5211
|
+
wsl: "/mnt/c/Program Files/Mozilla Firefox/firefox.exe"
|
|
5212
|
+
}));
|
|
5213
|
+
defineLazyProperty(apps, "edge", () => detectPlatformBinary({
|
|
5214
|
+
darwin: "microsoft edge",
|
|
5215
|
+
win32: "msedge",
|
|
5216
|
+
linux: ["microsoft-edge", "microsoft-edge-dev"]
|
|
5217
|
+
}, {
|
|
5218
|
+
wsl: "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
|
5219
|
+
}));
|
|
5220
|
+
defineLazyProperty(apps, "browser", () => "browser");
|
|
5221
|
+
defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
5222
|
+
var open_default = open;
|
|
5223
|
+
|
|
5224
|
+
// src/auth.ts
|
|
5225
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5226
|
+
var generatePKCE = async () => {
|
|
5227
|
+
const codeVerifier = getVerifier();
|
|
5228
|
+
const codeChallenge = await getDigest(codeVerifier, "base64url");
|
|
5229
|
+
return { codeVerifier, codeChallenge };
|
|
5230
|
+
};
|
|
5231
|
+
var fetchDiscovery = async (discoveryUrl) => {
|
|
5232
|
+
const res = await fetch(discoveryUrl);
|
|
5233
|
+
if (!res.ok) {
|
|
5234
|
+
throw new Error(`Failed to fetch OAuth discovery: ${res.status} ${res.statusText}`);
|
|
5235
|
+
}
|
|
5236
|
+
return res.json();
|
|
5237
|
+
};
|
|
5238
|
+
var registerClient = async (registrationEndpoint) => {
|
|
5239
|
+
const res = await fetch(registrationEndpoint, {
|
|
5240
|
+
method: "POST",
|
|
5241
|
+
headers: { "content-type": "application/json" },
|
|
5242
|
+
body: JSON.stringify({
|
|
5243
|
+
client_name: STANDALONE_CLIENT.MCP,
|
|
5244
|
+
redirect_uris: ["http://127.0.0.1/callback"]
|
|
5245
|
+
})
|
|
5246
|
+
});
|
|
5247
|
+
if (!res.ok) {
|
|
5248
|
+
throw new Error(`Client registration failed: ${res.status}`);
|
|
5249
|
+
}
|
|
5250
|
+
const data = await res.json();
|
|
5251
|
+
return data.client_id;
|
|
5252
|
+
};
|
|
5253
|
+
var exchangeCodeForToken = async (tokenEndpoint, code, codeVerifier, clientId, redirectUri) => {
|
|
5254
|
+
const res = await fetch(tokenEndpoint, {
|
|
5255
|
+
method: "POST",
|
|
5256
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
5257
|
+
body: new URLSearchParams({
|
|
5258
|
+
grant_type: "authorization_code",
|
|
5259
|
+
code,
|
|
5260
|
+
code_verifier: codeVerifier,
|
|
5261
|
+
client_id: clientId,
|
|
5262
|
+
redirect_uri: redirectUri
|
|
5263
|
+
}).toString()
|
|
5264
|
+
});
|
|
5265
|
+
if (!res.ok) {
|
|
5266
|
+
const error = await res.json().catch(() => ({}));
|
|
5267
|
+
throw new Error(error.error_description ?? error.error ?? `Token exchange failed: ${res.status}`);
|
|
5268
|
+
}
|
|
5269
|
+
return res.json();
|
|
5270
|
+
};
|
|
5271
|
+
var decodeJwtPayload2 = (token) => {
|
|
5272
|
+
const [, payload] = token.split(".");
|
|
5273
|
+
if (!payload) {
|
|
5274
|
+
throw new Error("Invalid JWT format");
|
|
5275
|
+
}
|
|
5276
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
5277
|
+
};
|
|
5278
|
+
var performOAuthFlow = async (discoveryUrl) => {
|
|
5279
|
+
const discovery = await fetchDiscovery(discoveryUrl);
|
|
5280
|
+
let clientId = await deriveClientId(STANDALONE_CLIENT.MCP);
|
|
5281
|
+
if (discovery.registration_endpoint) {
|
|
5282
|
+
clientId = await registerClient(discovery.registration_endpoint);
|
|
5283
|
+
}
|
|
5284
|
+
const { codeVerifier, codeChallenge } = await generatePKCE();
|
|
5285
|
+
const state = getNonce();
|
|
5286
|
+
let resolveCode;
|
|
5287
|
+
let rejectCode;
|
|
5288
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
5289
|
+
resolveCode = resolve;
|
|
5290
|
+
rejectCode = reject;
|
|
5291
|
+
});
|
|
5292
|
+
let server2;
|
|
5293
|
+
server2 = createServer((req, res) => {
|
|
5294
|
+
const url = new URL(req.url, "http://localhost");
|
|
5295
|
+
if (url.pathname === "/callback") {
|
|
5296
|
+
const error = url.searchParams.get("error");
|
|
5297
|
+
if (error) {
|
|
5298
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
5299
|
+
res.end("<html><body><h2>Authentication failed</h2><p>You can close this tab.</p></body></html>");
|
|
5300
|
+
rejectCode(new Error(url.searchParams.get("error_description") ?? error));
|
|
5301
|
+
return;
|
|
5302
|
+
}
|
|
5303
|
+
const code2 = url.searchParams.get("code");
|
|
5304
|
+
const returnedState = url.searchParams.get("state");
|
|
5305
|
+
if (!code2) {
|
|
5306
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
5307
|
+
res.end("Missing authorization code");
|
|
5308
|
+
return;
|
|
5309
|
+
}
|
|
5310
|
+
if (returnedState !== state) {
|
|
5311
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
5312
|
+
res.end("State mismatch");
|
|
5313
|
+
rejectCode(new Error("OAuth state mismatch \u2014 possible CSRF attack"));
|
|
5314
|
+
return;
|
|
5315
|
+
}
|
|
5316
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
5317
|
+
res.end("<html><body><h2>Authenticated</h2><p>You can return to your terminal.</p></body></html>");
|
|
5318
|
+
resolveCode(code2);
|
|
5319
|
+
} else {
|
|
5320
|
+
res.writeHead(404);
|
|
5321
|
+
res.end();
|
|
5322
|
+
}
|
|
5323
|
+
});
|
|
5324
|
+
await new Promise((r) => server2.listen(0, "127.0.0.1", r));
|
|
5325
|
+
const port = server2.address().port;
|
|
5326
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
5327
|
+
const authUrl = new URL(discovery.authorization_endpoint);
|
|
5328
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
5329
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
5330
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
5331
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
5332
|
+
authUrl.searchParams.set("state", state);
|
|
5333
|
+
authUrl.searchParams.set("response_type", "code");
|
|
5334
|
+
await open_default(authUrl.toString());
|
|
5335
|
+
const timeout = setTimeout(() => {
|
|
5336
|
+
server2.close();
|
|
5337
|
+
rejectCode(new Error("Authentication timed out. Please try again."));
|
|
5338
|
+
}, LOGIN_TIMEOUT_MS);
|
|
5339
|
+
let code;
|
|
5340
|
+
try {
|
|
5341
|
+
code = await codePromise;
|
|
5342
|
+
} catch (err) {
|
|
5343
|
+
clearTimeout(timeout);
|
|
5344
|
+
server2.close();
|
|
5345
|
+
throw err;
|
|
5346
|
+
}
|
|
5347
|
+
clearTimeout(timeout);
|
|
5348
|
+
server2.close();
|
|
5349
|
+
const tokenResponse = await exchangeCodeForToken(
|
|
5350
|
+
discovery.token_endpoint,
|
|
5351
|
+
code,
|
|
5352
|
+
codeVerifier,
|
|
5353
|
+
clientId,
|
|
5354
|
+
redirectUri
|
|
5355
|
+
);
|
|
5356
|
+
const payload = decodeJwtPayload2(tokenResponse.access_token);
|
|
5357
|
+
await writeAuthState(
|
|
5358
|
+
{
|
|
5359
|
+
token: tokenResponse.access_token,
|
|
5360
|
+
orgId: payload.orgId,
|
|
5361
|
+
userId: payload.sub
|
|
5362
|
+
},
|
|
5363
|
+
STANDALONE_CLIENT_AUTH_FILE.MCP
|
|
5364
|
+
);
|
|
5365
|
+
return tokenResponse.access_token;
|
|
5366
|
+
};
|
|
4039
5367
|
|
|
4040
5368
|
// src/index.ts
|
|
4041
|
-
var server = createMcpServer();
|
|
5369
|
+
var server = createMcpServer({ performOAuthFlow });
|
|
4042
5370
|
var transport = new StdioServerTransport();
|
|
4043
5371
|
await server.connect(transport);
|