@stackable-labs/mcp-app-extension 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/index.js +192 -153
- package/dist/server.js +182 -146
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Both transports expose the same tool surface: `list_apps`, `list_extensions`, `g
|
|
|
15
15
|
|
|
16
16
|
Drop into your project root (or `~/.claude.json` for user-scoped setup). Both entries can coexist — Claude Code routes tool calls per-server.
|
|
17
17
|
|
|
18
|
-
**Dev (against deployed `api-dev
|
|
18
|
+
**Dev (against deployed `api-x-dev.stackablelabs.io`):**
|
|
19
19
|
|
|
20
20
|
```json
|
|
21
21
|
{
|
|
@@ -33,8 +33,8 @@ Drop into your project root (or `~/.claude.json` for user-scoped setup). Both en
|
|
|
33
33
|
"@stackable-labs/mcp-app-extension@latest"
|
|
34
34
|
],
|
|
35
35
|
"env": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"MCP_LABS_API_BASE_URL": "https://mcp-dev.stackablelabs.io",
|
|
37
|
+
"ADMIN_LABS_API_BASE_URL": "https://api-x-dev.stackablelabs.io/admin"
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -65,8 +65,10 @@ Both optional — defaults point at prod. Override per-stage as needed.
|
|
|
65
65
|
|
|
66
66
|
| Env var | Default | Purpose |
|
|
67
67
|
|---------|---------|---------|
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
68
|
+
| `MCP_LABS_API_BASE_URL` | `https://mcp.stackablelabs.io` | MCP host root. OAuth discovery URL is derived as `${mcpBase}/.well-known/oauth-authorization-server`. |
|
|
69
|
+
| `ADMIN_LABS_API_BASE_URL` | `https://api-x.stackablelabs.io/admin` | Admin API endpoint for platform tool calls (`list_apps`, etc.). |
|
|
70
|
+
|
|
71
|
+
> **Renamed (2026-07):** `MCP_API_BASE_URL` → `MCP_LABS_API_BASE_URL`, `ADMIN_API_BASE_URL` → `ADMIN_LABS_API_BASE_URL`. The old names still work as a deprecated fallback — update your config; they'll be removed in a later release.
|
|
70
72
|
|
|
71
73
|
HTTP transport reads URLs from the deployed Lambda's env (set via CDK + `packages/infra/cdk/.env.{stage}` — not client-controlled).
|
|
72
74
|
|
|
@@ -87,7 +89,7 @@ To test a local build via stdio, point the `.mcp.json` `command`/`args` at the b
|
|
|
87
89
|
```json
|
|
88
90
|
{
|
|
89
91
|
"command": "node",
|
|
90
|
-
"args": ["/absolute/path/to/packages/mcp/app-extension/dist/index.js"],
|
|
91
|
-
"env": { "
|
|
92
|
+
"args": ["/absolute/path/to/packages/mcp/labs-app-extension/dist/index.js"],
|
|
93
|
+
"env": { "MCP_LABS_API_BASE_URL": "...", "ADMIN_LABS_API_BASE_URL": "..." }
|
|
92
94
|
}
|
|
93
95
|
```
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
-
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, EVENT_HOOK_PERMISSION_MAP, MESSAGING_SEND_EXAMPLES } from '@stackable-labs/sdk-extension-contracts';
|
|
5
|
-
import { validatePatternFormat } from '@stackable-labs/lib-contracts';
|
|
6
4
|
import fs4, { readFile, mkdir, writeFile, constants } from 'fs/promises';
|
|
7
5
|
import path, { join } from 'path';
|
|
8
6
|
import os, { homedir } from 'os';
|
|
7
|
+
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, EVENT_HOOK_PERMISSION_MAP, MESSAGING_SEND_EXAMPLES } from '@stackable-labs/sdk-extension-contracts';
|
|
9
8
|
import { z } from 'zod';
|
|
9
|
+
import { validatePatternFormat } from '@stackable-labs/lib-contracts';
|
|
10
10
|
import { createServer } from 'http';
|
|
11
11
|
import process6 from 'process';
|
|
12
12
|
import { Buffer as Buffer$1 } from 'buffer';
|
|
@@ -15,6 +15,69 @@ import { promisify } from 'util';
|
|
|
15
15
|
import childProcess, { execFile } from 'child_process';
|
|
16
16
|
import fs3 from 'fs';
|
|
17
17
|
|
|
18
|
+
// ../../lib/utils-auth/src/constants.ts
|
|
19
|
+
var STANDALONE_CLIENT_DATA = {
|
|
20
|
+
STACKABLE_LABS: {
|
|
21
|
+
CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
|
|
22
|
+
MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
|
|
23
|
+
},
|
|
24
|
+
STACKABLE_OPS: {
|
|
25
|
+
MCP: { name: "@stackable-ops/mcp", authFile: "mcp-ops-auth.json" }
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var projectField = (field) => Object.fromEntries(
|
|
29
|
+
Object.entries(STANDALONE_CLIENT_DATA).map(([org, roles]) => [
|
|
30
|
+
org,
|
|
31
|
+
Object.fromEntries(Object.entries(roles).map(([role, entry]) => [role, entry[field]]))
|
|
32
|
+
])
|
|
33
|
+
);
|
|
34
|
+
var STANDALONE_CLIENT = projectField("name");
|
|
35
|
+
var STANDALONE_CLIENT_AUTH_FILE = projectField("authFile");
|
|
36
|
+
Object.values(STANDALONE_CLIENT_DATA).flatMap((roles) => Object.values(roles).map((entry) => entry.name));
|
|
37
|
+
|
|
38
|
+
// ../../lib/utils-auth/src/cli/shared.ts
|
|
39
|
+
var AUTH_DIR = join(homedir(), ".stackable");
|
|
40
|
+
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.CLI);
|
|
41
|
+
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP);
|
|
42
|
+
var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.CLI);
|
|
43
|
+
var readAuthState = async (filename) => {
|
|
44
|
+
try {
|
|
45
|
+
const content = await readFile(resolveAuthFile(filename), "utf8");
|
|
46
|
+
return JSON.parse(content);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var writeAuthState = async (state, filename) => {
|
|
52
|
+
await mkdir(AUTH_DIR, { recursive: true, mode: 448 });
|
|
53
|
+
await writeFile(resolveAuthFile(filename), JSON.stringify(state, null, 2), { mode: 384 });
|
|
54
|
+
};
|
|
55
|
+
var decodeJwtPayload = (token) => {
|
|
56
|
+
try {
|
|
57
|
+
const [, payload] = token.split(".");
|
|
58
|
+
if (!payload) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
62
|
+
return JSON.parse(json);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var getToken = async (filename) => {
|
|
68
|
+
const state = await readAuthState(filename);
|
|
69
|
+
if (!state) {
|
|
70
|
+
throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
|
|
71
|
+
}
|
|
72
|
+
const payload = decodeJwtPayload(state.token);
|
|
73
|
+
if (payload?.exp && typeof payload.exp === "number") {
|
|
74
|
+
if (Date.now() >= payload.exp * 1e3) {
|
|
75
|
+
throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return state.token;
|
|
79
|
+
};
|
|
80
|
+
|
|
18
81
|
// ../../sdk/extension/ai-docs/src/generated/template-content.ts
|
|
19
82
|
var PATTERN_SECTIONS = [
|
|
20
83
|
{
|
|
@@ -2912,7 +2975,7 @@ Both workflows produce the same output \u2014 a Stackable extension that runs in
|
|
|
2912
2975
|
};
|
|
2913
2976
|
|
|
2914
2977
|
// ../../sdk/extension/ai-docs/src/generators/ai-accelerated-development.ts
|
|
2915
|
-
var
|
|
2978
|
+
var DEFAULT_MCP_LABS_API_BASE_URL = "https://mcp.stackablelabs.io";
|
|
2916
2979
|
var generateAIAcceleratedDevelopment = () => {
|
|
2917
2980
|
const fm = frontmatter({
|
|
2918
2981
|
root: false,
|
|
@@ -2920,8 +2983,8 @@ var generateAIAcceleratedDevelopment = () => {
|
|
|
2920
2983
|
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.",
|
|
2921
2984
|
globs: ["*"]
|
|
2922
2985
|
});
|
|
2923
|
-
const
|
|
2924
|
-
const mcpEndpointUrl = `${
|
|
2986
|
+
const mcpApiBaseUrl2 = process.env.MCP_LABS_API_BASE_URL ?? DEFAULT_MCP_LABS_API_BASE_URL;
|
|
2987
|
+
const mcpEndpointUrl = `${mcpApiBaseUrl2}/app-extension`;
|
|
2925
2988
|
return `${fm}
|
|
2926
2989
|
|
|
2927
2990
|
# AI-Accelerated Development
|
|
@@ -5637,6 +5700,15 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
|
|
|
5637
5700
|
pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
|
|
5638
5701
|
pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
|
|
5639
5702
|
|
|
5703
|
+
// ../../lib/utils-mcp/src/index.ts
|
|
5704
|
+
var textContent = (text) => ({
|
|
5705
|
+
content: [{ type: "text", text }]
|
|
5706
|
+
});
|
|
5707
|
+
var errorContent = (text) => ({
|
|
5708
|
+
isError: true,
|
|
5709
|
+
content: [{ type: "text", text }]
|
|
5710
|
+
});
|
|
5711
|
+
|
|
5640
5712
|
// ../../lib/utils-js/src/crypto.ts
|
|
5641
5713
|
var getCrypto = () => {
|
|
5642
5714
|
if (typeof globalThis !== "undefined" && globalThis.crypto) {
|
|
@@ -5676,142 +5748,33 @@ var getDigest = async (input, encoding = "hex") => {
|
|
|
5676
5748
|
var getNonce = () => getRandomBytes(16, "base64url");
|
|
5677
5749
|
var getVerifier = () => getRandomBytes(32, "base64url");
|
|
5678
5750
|
|
|
5679
|
-
// ../../lib/utils-auth/src/constants.ts
|
|
5680
|
-
var STANDALONE_CLIENT_DATA = {
|
|
5681
|
-
CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
|
|
5682
|
-
MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
|
|
5683
|
-
};
|
|
5684
|
-
var STANDALONE_CLIENT = Object.fromEntries(
|
|
5685
|
-
Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.name])
|
|
5686
|
-
);
|
|
5687
|
-
var STANDALONE_CLIENT_AUTH_FILE = Object.fromEntries(
|
|
5688
|
-
Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.authFile])
|
|
5689
|
-
);
|
|
5690
|
-
Object.values(STANDALONE_CLIENT);
|
|
5691
|
-
|
|
5692
5751
|
// ../../lib/utils-auth/src/index.ts
|
|
5693
|
-
var
|
|
5694
|
-
var
|
|
5695
|
-
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.CLI);
|
|
5696
|
-
var MCP_AUTH_FILE = join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.MCP);
|
|
5697
|
-
var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.CLI);
|
|
5698
|
-
var readAuthState = async (filename) => {
|
|
5699
|
-
try {
|
|
5700
|
-
const content = await readFile(resolveAuthFile(filename), "utf8");
|
|
5701
|
-
return JSON.parse(content);
|
|
5702
|
-
} catch {
|
|
5703
|
-
return null;
|
|
5704
|
-
}
|
|
5705
|
-
};
|
|
5706
|
-
var writeAuthState = async (state, filename) => {
|
|
5707
|
-
await mkdir(AUTH_DIR, { recursive: true, mode: 448 });
|
|
5708
|
-
await writeFile(resolveAuthFile(filename), JSON.stringify(state, null, 2), { mode: 384 });
|
|
5709
|
-
};
|
|
5710
|
-
var decodeJwtPayload = (token) => {
|
|
5711
|
-
try {
|
|
5712
|
-
const [, payload] = token.split(".");
|
|
5713
|
-
if (!payload) {
|
|
5714
|
-
return null;
|
|
5715
|
-
}
|
|
5716
|
-
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
5717
|
-
return JSON.parse(json);
|
|
5718
|
-
} catch {
|
|
5719
|
-
return null;
|
|
5720
|
-
}
|
|
5721
|
-
};
|
|
5722
|
-
var getToken = async (filename) => {
|
|
5723
|
-
const state = await readAuthState(filename);
|
|
5724
|
-
if (!state) {
|
|
5725
|
-
throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
|
|
5726
|
-
}
|
|
5727
|
-
const payload = decodeJwtPayload(state.token);
|
|
5728
|
-
if (payload?.exp && typeof payload.exp === "number") {
|
|
5729
|
-
if (Date.now() >= payload.exp * 1e3) {
|
|
5730
|
-
throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
|
|
5731
|
-
}
|
|
5732
|
-
}
|
|
5733
|
-
return state.token;
|
|
5734
|
-
};
|
|
5752
|
+
var deriveDcrClientId = async (clientName) => (await getDigest(clientName)).slice(0, 32);
|
|
5753
|
+
var deriveClientId = async (clientName) => deriveDcrClientId(clientName);
|
|
5735
5754
|
|
|
5736
5755
|
// package.json
|
|
5737
5756
|
var package_default = {
|
|
5738
5757
|
version: "0.0.0"};
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
});
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
}
|
|
5759
|
-
if (!options.performOAuthFlow) {
|
|
5760
|
-
throw new Error(
|
|
5761
|
-
"No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
|
|
5762
|
-
);
|
|
5763
|
-
}
|
|
5764
|
-
const MCP_API_BASE_URL = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL2;
|
|
5765
|
-
return options.performOAuthFlow(`${MCP_API_BASE_URL}/.well-known/oauth-authorization-server`);
|
|
5766
|
-
};
|
|
5767
|
-
const authHeaders = (token) => ({
|
|
5768
|
-
authorization: `Bearer ${token}`,
|
|
5769
|
-
"content-type": "application/json",
|
|
5770
|
-
"x-client-name": MCP_CLIENT_NAME
|
|
5771
|
-
});
|
|
5772
|
-
const callApi = async (path2) => {
|
|
5773
|
-
const token = await getAuthToken();
|
|
5774
|
-
const ADMIN_API_BASE_URL = process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
|
|
5775
|
-
const url = `${ADMIN_API_BASE_URL}${path2}`;
|
|
5776
|
-
let res;
|
|
5777
|
-
try {
|
|
5778
|
-
res = await fetch(url, { headers: authHeaders(token) });
|
|
5779
|
-
} catch (err) {
|
|
5780
|
-
return errorContent(
|
|
5781
|
-
`Network error calling ${url}: ${err.message}. Check ADMIN_API_BASE_URL env var (currently "${ADMIN_API_BASE_URL}").`
|
|
5782
|
-
);
|
|
5783
|
-
}
|
|
5784
|
-
if (!res.ok) {
|
|
5785
|
-
return errorContent(`API error: ${res.status} ${res.statusText} (${url})`);
|
|
5786
|
-
}
|
|
5787
|
-
const data = await res.json();
|
|
5788
|
-
return textContent(JSON.stringify(data, null, 2));
|
|
5789
|
-
};
|
|
5790
|
-
const withAuth = async (fn) => {
|
|
5791
|
-
try {
|
|
5792
|
-
return await fn();
|
|
5793
|
-
} catch (err) {
|
|
5794
|
-
const e = err;
|
|
5795
|
-
if (e.message.includes("fetch")) {
|
|
5796
|
-
const MCP_API_BASE_URL = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL2;
|
|
5797
|
-
return errorContent(
|
|
5798
|
-
`MCP auth flow failed: ${e.message}. Check MCP_API_BASE_URL env var (currently "${MCP_API_BASE_URL}"). To re-auth from scratch: rm ${MCP_AUTH_FILE}`
|
|
5799
|
-
);
|
|
5800
|
-
}
|
|
5801
|
-
return errorContent(e.message);
|
|
5802
|
-
}
|
|
5803
|
-
};
|
|
5758
|
+
var registerAppTools = (server2, ctx) => {
|
|
5759
|
+
server2.registerTool("list_apps", {
|
|
5760
|
+
description: "List available apps for development with IDs, names, and targets. Requires CLI auth."
|
|
5761
|
+
}, async () => ctx.withAuth(() => ctx.callApi("/app-extension")));
|
|
5762
|
+
server2.registerTool("list_extensions", {
|
|
5763
|
+
description: "List extensions under an app with status, version, and bundle URL. Requires CLI auth.",
|
|
5764
|
+
inputSchema: { appId: z.string().describe("App ID") }
|
|
5765
|
+
}, async ({ appId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/extensions`)));
|
|
5766
|
+
server2.registerTool("get_extension", {
|
|
5767
|
+
description: "Get extension detail including manifest, permissions, and targets. Requires CLI auth.",
|
|
5768
|
+
inputSchema: { appId: z.string().describe("App ID"), extensionId: z.string().describe("Extension ID") }
|
|
5769
|
+
}, async ({ appId, extensionId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/extensions/${extensionId}`)));
|
|
5770
|
+
server2.registerTool("list_instances", {
|
|
5771
|
+
description: "List instances under an app. Requires CLI auth.",
|
|
5772
|
+
inputSchema: { appId: z.string().describe("App ID") }
|
|
5773
|
+
}, async ({ appId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/instances`)));
|
|
5774
|
+
};
|
|
5775
|
+
var apps_default = registerAppTools;
|
|
5776
|
+
var registerSkillTools = (server2) => {
|
|
5804
5777
|
const registrySkills = listSkills("registry");
|
|
5805
|
-
for (const skill of registrySkills) {
|
|
5806
|
-
server2.registerResource(
|
|
5807
|
-
skill.id,
|
|
5808
|
-
`sdk://${skill.id}`,
|
|
5809
|
-
{ description: skill.description, mimeType: "text/markdown" },
|
|
5810
|
-
async (uri) => ({
|
|
5811
|
-
contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
|
|
5812
|
-
})
|
|
5813
|
-
);
|
|
5814
|
-
}
|
|
5815
5778
|
server2.registerTool("list_skills", {
|
|
5816
5779
|
description: "List all available SDK skills with descriptions"
|
|
5817
5780
|
}, async () => textContent(JSON.stringify(registrySkills.map(getSkillMetadata), null, 2)));
|
|
@@ -5835,6 +5798,9 @@ var createMcpServer = (options = {}) => {
|
|
|
5835
5798
|
}
|
|
5836
5799
|
return errorContent('Provide either "id" or "query" parameter.');
|
|
5837
5800
|
});
|
|
5801
|
+
};
|
|
5802
|
+
var skills_default = registerSkillTools;
|
|
5803
|
+
var registerValidationTools = (server2) => {
|
|
5838
5804
|
server2.registerTool("validate_manifest", {
|
|
5839
5805
|
description: "Validate an extension manifest.json. Checks required fields, valid permissions, targets, and allowedDomains consistency with permissions.",
|
|
5840
5806
|
inputSchema: { manifestContent: z.string().describe("The full manifest.json content as a string") }
|
|
@@ -5929,21 +5895,94 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
5929
5895
|
return textContent(`Permission analysis:
|
|
5930
5896
|
${issues.map((i) => `- ${i}`).join("\n")}`);
|
|
5931
5897
|
});
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5898
|
+
};
|
|
5899
|
+
var validation_default = registerValidationTools;
|
|
5900
|
+
|
|
5901
|
+
// src/tools/index.ts
|
|
5902
|
+
var toolRegistrars = [
|
|
5903
|
+
skills_default,
|
|
5904
|
+
validation_default,
|
|
5905
|
+
apps_default
|
|
5906
|
+
];
|
|
5907
|
+
|
|
5908
|
+
// src/server.ts
|
|
5909
|
+
var MCP_LABS_AUTH_FILE2 = STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP;
|
|
5910
|
+
var MCP_LABS_CLIENT_NAME = STANDALONE_CLIENT.STACKABLE_LABS.MCP;
|
|
5911
|
+
var DEFAULT_MCP_LABS_API_BASE_URL2 = "https://mcp.stackablelabs.io";
|
|
5912
|
+
var DEFAULT_ADMIN_LABS_API_URL = "https://api-x.stackablelabs.io/admin";
|
|
5913
|
+
var adminApiBaseUrl = () => process.env.ADMIN_LABS_API_BASE_URL ?? DEFAULT_ADMIN_LABS_API_URL;
|
|
5914
|
+
var mcpApiBaseUrl = () => process.env.MCP_LABS_API_BASE_URL ?? DEFAULT_MCP_LABS_API_BASE_URL2;
|
|
5915
|
+
var createMcpServer = (options = {}) => {
|
|
5916
|
+
const server2 = new McpServer({
|
|
5917
|
+
name: "stackable-extension-dev",
|
|
5918
|
+
version: package_default.version
|
|
5919
|
+
});
|
|
5920
|
+
const getAuthToken = async () => {
|
|
5921
|
+
if (options.authContext?.token) {
|
|
5922
|
+
return options.authContext.token;
|
|
5923
|
+
}
|
|
5924
|
+
try {
|
|
5925
|
+
return await getToken(MCP_LABS_AUTH_FILE2);
|
|
5926
|
+
} catch {
|
|
5927
|
+
}
|
|
5928
|
+
if (!options.performOAuthFlow) {
|
|
5929
|
+
throw new Error(
|
|
5930
|
+
"No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
|
|
5931
|
+
);
|
|
5932
|
+
}
|
|
5933
|
+
return options.performOAuthFlow(`${mcpApiBaseUrl()}/.well-known/oauth-authorization-server`);
|
|
5934
|
+
};
|
|
5935
|
+
const authHeaders = (token) => ({
|
|
5936
|
+
authorization: `Bearer ${token}`,
|
|
5937
|
+
"content-type": "application/json",
|
|
5938
|
+
"x-client-name": MCP_LABS_CLIENT_NAME
|
|
5939
|
+
});
|
|
5940
|
+
const callApi = async (path2) => {
|
|
5941
|
+
const token = await getAuthToken();
|
|
5942
|
+
const adminBase = adminApiBaseUrl();
|
|
5943
|
+
const url = `${adminBase}${path2}`;
|
|
5944
|
+
let res;
|
|
5945
|
+
try {
|
|
5946
|
+
res = await fetch(url, { headers: authHeaders(token) });
|
|
5947
|
+
} catch (err) {
|
|
5948
|
+
return errorContent(
|
|
5949
|
+
`Network error calling ${url}: ${err.message}. Check ADMIN_LABS_API_BASE_URL env var (currently "${adminBase}").`
|
|
5950
|
+
);
|
|
5951
|
+
}
|
|
5952
|
+
if (!res.ok) {
|
|
5953
|
+
return errorContent(`API error: ${res.status} ${res.statusText} (${url})`);
|
|
5954
|
+
}
|
|
5955
|
+
const data = await res.json();
|
|
5956
|
+
return textContent(JSON.stringify(data, null, 2));
|
|
5957
|
+
};
|
|
5958
|
+
const withAuth = async (fn) => {
|
|
5959
|
+
try {
|
|
5960
|
+
return await fn();
|
|
5961
|
+
} catch (err) {
|
|
5962
|
+
const e = err;
|
|
5963
|
+
if (e.message.includes("fetch")) {
|
|
5964
|
+
return errorContent(
|
|
5965
|
+
`MCP auth flow failed: ${e.message}. Check MCP_LABS_API_BASE_URL env var (currently "${mcpApiBaseUrl()}"). To re-auth from scratch: rm ${MCP_LABS_AUTH_FILE2}`
|
|
5966
|
+
);
|
|
5967
|
+
}
|
|
5968
|
+
return errorContent(e.message);
|
|
5969
|
+
}
|
|
5970
|
+
};
|
|
5971
|
+
const registrySkills = listSkills("registry");
|
|
5972
|
+
for (const skill of registrySkills) {
|
|
5973
|
+
server2.registerResource(
|
|
5974
|
+
skill.id,
|
|
5975
|
+
`sdk://${skill.id}`,
|
|
5976
|
+
{ description: skill.description, mimeType: "text/markdown" },
|
|
5977
|
+
async (uri) => ({
|
|
5978
|
+
contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
|
|
5979
|
+
})
|
|
5980
|
+
);
|
|
5981
|
+
}
|
|
5982
|
+
const ctx = { withAuth, callApi };
|
|
5983
|
+
for (const register of toolRegistrars) {
|
|
5984
|
+
register(server2, ctx);
|
|
5985
|
+
}
|
|
5947
5986
|
return server2;
|
|
5948
5987
|
};
|
|
5949
5988
|
var isDockerCached;
|
|
@@ -6437,7 +6476,7 @@ var registerClient = async (registrationEndpoint) => {
|
|
|
6437
6476
|
method: "POST",
|
|
6438
6477
|
headers: { "content-type": "application/json" },
|
|
6439
6478
|
body: JSON.stringify({
|
|
6440
|
-
client_name: STANDALONE_CLIENT.MCP,
|
|
6479
|
+
client_name: STANDALONE_CLIENT.STACKABLE_LABS.MCP,
|
|
6441
6480
|
redirect_uris: ["http://127.0.0.1/callback"]
|
|
6442
6481
|
})
|
|
6443
6482
|
});
|
|
@@ -6474,7 +6513,7 @@ var decodeJwtPayload2 = (token) => {
|
|
|
6474
6513
|
};
|
|
6475
6514
|
var performOAuthFlow = async (discoveryUrl) => {
|
|
6476
6515
|
const discovery = await fetchDiscovery(discoveryUrl);
|
|
6477
|
-
let clientId = await deriveClientId(STANDALONE_CLIENT.MCP);
|
|
6516
|
+
let clientId = await deriveClientId(STANDALONE_CLIENT.STACKABLE_LABS.MCP);
|
|
6478
6517
|
if (discovery.registration_endpoint) {
|
|
6479
6518
|
clientId = await registerClient(discovery.registration_endpoint);
|
|
6480
6519
|
}
|
|
@@ -6557,7 +6596,7 @@ var performOAuthFlow = async (discoveryUrl) => {
|
|
|
6557
6596
|
orgId: payload.orgId,
|
|
6558
6597
|
userId: payload.sub
|
|
6559
6598
|
},
|
|
6560
|
-
STANDALONE_CLIENT_AUTH_FILE.MCP
|
|
6599
|
+
STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP
|
|
6561
6600
|
);
|
|
6562
6601
|
return tokenResponse.access_token;
|
|
6563
6602
|
};
|
package/dist/server.js
CHANGED
|
@@ -1,13 +1,72 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, EVENT_HOOK_PERMISSION_MAP, MESSAGING_SEND_EXAMPLES } from '@stackable-labs/sdk-extension-contracts';
|
|
3
|
-
import { validatePatternFormat } from '@stackable-labs/lib-contracts';
|
|
4
2
|
import { readFile } from 'fs/promises';
|
|
5
3
|
import { join } from 'path';
|
|
6
4
|
import { homedir } from 'os';
|
|
5
|
+
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, EVENT_HOOK_PERMISSION_MAP, MESSAGING_SEND_EXAMPLES } from '@stackable-labs/sdk-extension-contracts';
|
|
7
6
|
import { z } from 'zod';
|
|
7
|
+
import { validatePatternFormat } from '@stackable-labs/lib-contracts';
|
|
8
8
|
|
|
9
9
|
// src/server.ts
|
|
10
10
|
|
|
11
|
+
// ../../lib/utils-auth/src/constants.ts
|
|
12
|
+
var STANDALONE_CLIENT_DATA = {
|
|
13
|
+
STACKABLE_LABS: {
|
|
14
|
+
CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
|
|
15
|
+
MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
|
|
16
|
+
},
|
|
17
|
+
STACKABLE_OPS: {
|
|
18
|
+
MCP: { name: "@stackable-ops/mcp", authFile: "mcp-ops-auth.json" }
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var projectField = (field) => Object.fromEntries(
|
|
22
|
+
Object.entries(STANDALONE_CLIENT_DATA).map(([org, roles]) => [
|
|
23
|
+
org,
|
|
24
|
+
Object.fromEntries(Object.entries(roles).map(([role, entry]) => [role, entry[field]]))
|
|
25
|
+
])
|
|
26
|
+
);
|
|
27
|
+
var STANDALONE_CLIENT = projectField("name");
|
|
28
|
+
var STANDALONE_CLIENT_AUTH_FILE = projectField("authFile");
|
|
29
|
+
Object.values(STANDALONE_CLIENT_DATA).flatMap((roles) => Object.values(roles).map((entry) => entry.name));
|
|
30
|
+
|
|
31
|
+
// ../../lib/utils-auth/src/cli/shared.ts
|
|
32
|
+
var AUTH_DIR = join(homedir(), ".stackable");
|
|
33
|
+
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.CLI);
|
|
34
|
+
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP);
|
|
35
|
+
var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.CLI);
|
|
36
|
+
var readAuthState = async (filename) => {
|
|
37
|
+
try {
|
|
38
|
+
const content = await readFile(resolveAuthFile(filename), "utf8");
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var decodeJwtPayload = (token) => {
|
|
45
|
+
try {
|
|
46
|
+
const [, payload] = token.split(".");
|
|
47
|
+
if (!payload) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
51
|
+
return JSON.parse(json);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var getToken = async (filename) => {
|
|
57
|
+
const state = await readAuthState(filename);
|
|
58
|
+
if (!state) {
|
|
59
|
+
throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
|
|
60
|
+
}
|
|
61
|
+
const payload = decodeJwtPayload(state.token);
|
|
62
|
+
if (payload?.exp && typeof payload.exp === "number") {
|
|
63
|
+
if (Date.now() >= payload.exp * 1e3) {
|
|
64
|
+
throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return state.token;
|
|
68
|
+
};
|
|
69
|
+
|
|
11
70
|
// ../../sdk/extension/ai-docs/src/generated/template-content.ts
|
|
12
71
|
var PATTERN_SECTIONS = [
|
|
13
72
|
{
|
|
@@ -2905,7 +2964,7 @@ Both workflows produce the same output \u2014 a Stackable extension that runs in
|
|
|
2905
2964
|
};
|
|
2906
2965
|
|
|
2907
2966
|
// ../../sdk/extension/ai-docs/src/generators/ai-accelerated-development.ts
|
|
2908
|
-
var
|
|
2967
|
+
var DEFAULT_MCP_LABS_API_BASE_URL = "https://mcp.stackablelabs.io";
|
|
2909
2968
|
var generateAIAcceleratedDevelopment = () => {
|
|
2910
2969
|
const fm = frontmatter({
|
|
2911
2970
|
root: false,
|
|
@@ -2913,8 +2972,8 @@ var generateAIAcceleratedDevelopment = () => {
|
|
|
2913
2972
|
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.",
|
|
2914
2973
|
globs: ["*"]
|
|
2915
2974
|
});
|
|
2916
|
-
const
|
|
2917
|
-
const mcpEndpointUrl = `${
|
|
2975
|
+
const mcpApiBaseUrl2 = process.env.MCP_LABS_API_BASE_URL ?? DEFAULT_MCP_LABS_API_BASE_URL;
|
|
2976
|
+
const mcpEndpointUrl = `${mcpApiBaseUrl2}/app-extension`;
|
|
2918
2977
|
return `${fm}
|
|
2919
2978
|
|
|
2920
2979
|
# AI-Accelerated Development
|
|
@@ -5630,137 +5689,38 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
|
|
|
5630
5689
|
pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
|
|
5631
5690
|
pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
|
|
5632
5691
|
|
|
5633
|
-
// ../../lib/utils-
|
|
5634
|
-
var
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
);
|
|
5641
|
-
var STANDALONE_CLIENT_AUTH_FILE = Object.fromEntries(
|
|
5642
|
-
Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.authFile])
|
|
5643
|
-
);
|
|
5644
|
-
Object.values(STANDALONE_CLIENT);
|
|
5645
|
-
|
|
5646
|
-
// ../../lib/utils-auth/src/index.ts
|
|
5647
|
-
var AUTH_DIR = join(homedir(), ".stackable");
|
|
5648
|
-
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.CLI);
|
|
5649
|
-
var MCP_AUTH_FILE = join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.MCP);
|
|
5650
|
-
var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.CLI);
|
|
5651
|
-
var readAuthState = async (filename) => {
|
|
5652
|
-
try {
|
|
5653
|
-
const content = await readFile(resolveAuthFile(filename), "utf8");
|
|
5654
|
-
return JSON.parse(content);
|
|
5655
|
-
} catch {
|
|
5656
|
-
return null;
|
|
5657
|
-
}
|
|
5658
|
-
};
|
|
5659
|
-
var decodeJwtPayload = (token) => {
|
|
5660
|
-
try {
|
|
5661
|
-
const [, payload] = token.split(".");
|
|
5662
|
-
if (!payload) {
|
|
5663
|
-
return null;
|
|
5664
|
-
}
|
|
5665
|
-
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
5666
|
-
return JSON.parse(json);
|
|
5667
|
-
} catch {
|
|
5668
|
-
return null;
|
|
5669
|
-
}
|
|
5670
|
-
};
|
|
5671
|
-
var getToken = async (filename) => {
|
|
5672
|
-
const state = await readAuthState(filename);
|
|
5673
|
-
if (!state) {
|
|
5674
|
-
throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
|
|
5675
|
-
}
|
|
5676
|
-
const payload = decodeJwtPayload(state.token);
|
|
5677
|
-
if (payload?.exp && typeof payload.exp === "number") {
|
|
5678
|
-
if (Date.now() >= payload.exp * 1e3) {
|
|
5679
|
-
throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
return state.token;
|
|
5683
|
-
};
|
|
5692
|
+
// ../../lib/utils-mcp/src/index.ts
|
|
5693
|
+
var textContent = (text) => ({
|
|
5694
|
+
content: [{ type: "text", text }]
|
|
5695
|
+
});
|
|
5696
|
+
var errorContent = (text) => ({
|
|
5697
|
+
isError: true,
|
|
5698
|
+
content: [{ type: "text", text }]
|
|
5699
|
+
});
|
|
5684
5700
|
|
|
5685
5701
|
// package.json
|
|
5686
5702
|
var package_default = {
|
|
5687
5703
|
version: "0.0.0"};
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
});
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
}
|
|
5708
|
-
if (!options.performOAuthFlow) {
|
|
5709
|
-
throw new Error(
|
|
5710
|
-
"No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
|
|
5711
|
-
);
|
|
5712
|
-
}
|
|
5713
|
-
const MCP_API_BASE_URL = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL2;
|
|
5714
|
-
return options.performOAuthFlow(`${MCP_API_BASE_URL}/.well-known/oauth-authorization-server`);
|
|
5715
|
-
};
|
|
5716
|
-
const authHeaders = (token) => ({
|
|
5717
|
-
authorization: `Bearer ${token}`,
|
|
5718
|
-
"content-type": "application/json",
|
|
5719
|
-
"x-client-name": MCP_CLIENT_NAME
|
|
5720
|
-
});
|
|
5721
|
-
const callApi = async (path) => {
|
|
5722
|
-
const token = await getAuthToken();
|
|
5723
|
-
const ADMIN_API_BASE_URL = process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
|
|
5724
|
-
const url = `${ADMIN_API_BASE_URL}${path}`;
|
|
5725
|
-
let res;
|
|
5726
|
-
try {
|
|
5727
|
-
res = await fetch(url, { headers: authHeaders(token) });
|
|
5728
|
-
} catch (err) {
|
|
5729
|
-
return errorContent(
|
|
5730
|
-
`Network error calling ${url}: ${err.message}. Check ADMIN_API_BASE_URL env var (currently "${ADMIN_API_BASE_URL}").`
|
|
5731
|
-
);
|
|
5732
|
-
}
|
|
5733
|
-
if (!res.ok) {
|
|
5734
|
-
return errorContent(`API error: ${res.status} ${res.statusText} (${url})`);
|
|
5735
|
-
}
|
|
5736
|
-
const data = await res.json();
|
|
5737
|
-
return textContent(JSON.stringify(data, null, 2));
|
|
5738
|
-
};
|
|
5739
|
-
const withAuth = async (fn) => {
|
|
5740
|
-
try {
|
|
5741
|
-
return await fn();
|
|
5742
|
-
} catch (err) {
|
|
5743
|
-
const e = err;
|
|
5744
|
-
if (e.message.includes("fetch")) {
|
|
5745
|
-
const MCP_API_BASE_URL = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL2;
|
|
5746
|
-
return errorContent(
|
|
5747
|
-
`MCP auth flow failed: ${e.message}. Check MCP_API_BASE_URL env var (currently "${MCP_API_BASE_URL}"). To re-auth from scratch: rm ${MCP_AUTH_FILE}`
|
|
5748
|
-
);
|
|
5749
|
-
}
|
|
5750
|
-
return errorContent(e.message);
|
|
5751
|
-
}
|
|
5752
|
-
};
|
|
5704
|
+
var registerAppTools = (server, ctx) => {
|
|
5705
|
+
server.registerTool("list_apps", {
|
|
5706
|
+
description: "List available apps for development with IDs, names, and targets. Requires CLI auth."
|
|
5707
|
+
}, async () => ctx.withAuth(() => ctx.callApi("/app-extension")));
|
|
5708
|
+
server.registerTool("list_extensions", {
|
|
5709
|
+
description: "List extensions under an app with status, version, and bundle URL. Requires CLI auth.",
|
|
5710
|
+
inputSchema: { appId: z.string().describe("App ID") }
|
|
5711
|
+
}, async ({ appId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/extensions`)));
|
|
5712
|
+
server.registerTool("get_extension", {
|
|
5713
|
+
description: "Get extension detail including manifest, permissions, and targets. Requires CLI auth.",
|
|
5714
|
+
inputSchema: { appId: z.string().describe("App ID"), extensionId: z.string().describe("Extension ID") }
|
|
5715
|
+
}, async ({ appId, extensionId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/extensions/${extensionId}`)));
|
|
5716
|
+
server.registerTool("list_instances", {
|
|
5717
|
+
description: "List instances under an app. Requires CLI auth.",
|
|
5718
|
+
inputSchema: { appId: z.string().describe("App ID") }
|
|
5719
|
+
}, async ({ appId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/instances`)));
|
|
5720
|
+
};
|
|
5721
|
+
var apps_default = registerAppTools;
|
|
5722
|
+
var registerSkillTools = (server) => {
|
|
5753
5723
|
const registrySkills = listSkills("registry");
|
|
5754
|
-
for (const skill of registrySkills) {
|
|
5755
|
-
server.registerResource(
|
|
5756
|
-
skill.id,
|
|
5757
|
-
`sdk://${skill.id}`,
|
|
5758
|
-
{ description: skill.description, mimeType: "text/markdown" },
|
|
5759
|
-
async (uri) => ({
|
|
5760
|
-
contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
|
|
5761
|
-
})
|
|
5762
|
-
);
|
|
5763
|
-
}
|
|
5764
5724
|
server.registerTool("list_skills", {
|
|
5765
5725
|
description: "List all available SDK skills with descriptions"
|
|
5766
5726
|
}, async () => textContent(JSON.stringify(registrySkills.map(getSkillMetadata), null, 2)));
|
|
@@ -5784,6 +5744,9 @@ var createMcpServer = (options = {}) => {
|
|
|
5784
5744
|
}
|
|
5785
5745
|
return errorContent('Provide either "id" or "query" parameter.');
|
|
5786
5746
|
});
|
|
5747
|
+
};
|
|
5748
|
+
var skills_default = registerSkillTools;
|
|
5749
|
+
var registerValidationTools = (server) => {
|
|
5787
5750
|
server.registerTool("validate_manifest", {
|
|
5788
5751
|
description: "Validate an extension manifest.json. Checks required fields, valid permissions, targets, and allowedDomains consistency with permissions.",
|
|
5789
5752
|
inputSchema: { manifestContent: z.string().describe("The full manifest.json content as a string") }
|
|
@@ -5878,21 +5841,94 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
5878
5841
|
return textContent(`Permission analysis:
|
|
5879
5842
|
${issues.map((i) => `- ${i}`).join("\n")}`);
|
|
5880
5843
|
});
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5844
|
+
};
|
|
5845
|
+
var validation_default = registerValidationTools;
|
|
5846
|
+
|
|
5847
|
+
// src/tools/index.ts
|
|
5848
|
+
var toolRegistrars = [
|
|
5849
|
+
skills_default,
|
|
5850
|
+
validation_default,
|
|
5851
|
+
apps_default
|
|
5852
|
+
];
|
|
5853
|
+
|
|
5854
|
+
// src/server.ts
|
|
5855
|
+
var MCP_LABS_AUTH_FILE2 = STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP;
|
|
5856
|
+
var MCP_LABS_CLIENT_NAME = STANDALONE_CLIENT.STACKABLE_LABS.MCP;
|
|
5857
|
+
var DEFAULT_MCP_LABS_API_BASE_URL2 = "https://mcp.stackablelabs.io";
|
|
5858
|
+
var DEFAULT_ADMIN_LABS_API_URL = "https://api-x.stackablelabs.io/admin";
|
|
5859
|
+
var adminApiBaseUrl = () => process.env.ADMIN_LABS_API_BASE_URL ?? DEFAULT_ADMIN_LABS_API_URL;
|
|
5860
|
+
var mcpApiBaseUrl = () => process.env.MCP_LABS_API_BASE_URL ?? DEFAULT_MCP_LABS_API_BASE_URL2;
|
|
5861
|
+
var createMcpServer = (options = {}) => {
|
|
5862
|
+
const server = new McpServer({
|
|
5863
|
+
name: "stackable-extension-dev",
|
|
5864
|
+
version: package_default.version
|
|
5865
|
+
});
|
|
5866
|
+
const getAuthToken = async () => {
|
|
5867
|
+
if (options.authContext?.token) {
|
|
5868
|
+
return options.authContext.token;
|
|
5869
|
+
}
|
|
5870
|
+
try {
|
|
5871
|
+
return await getToken(MCP_LABS_AUTH_FILE2);
|
|
5872
|
+
} catch {
|
|
5873
|
+
}
|
|
5874
|
+
if (!options.performOAuthFlow) {
|
|
5875
|
+
throw new Error(
|
|
5876
|
+
"No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
|
|
5877
|
+
);
|
|
5878
|
+
}
|
|
5879
|
+
return options.performOAuthFlow(`${mcpApiBaseUrl()}/.well-known/oauth-authorization-server`);
|
|
5880
|
+
};
|
|
5881
|
+
const authHeaders = (token) => ({
|
|
5882
|
+
authorization: `Bearer ${token}`,
|
|
5883
|
+
"content-type": "application/json",
|
|
5884
|
+
"x-client-name": MCP_LABS_CLIENT_NAME
|
|
5885
|
+
});
|
|
5886
|
+
const callApi = async (path) => {
|
|
5887
|
+
const token = await getAuthToken();
|
|
5888
|
+
const adminBase = adminApiBaseUrl();
|
|
5889
|
+
const url = `${adminBase}${path}`;
|
|
5890
|
+
let res;
|
|
5891
|
+
try {
|
|
5892
|
+
res = await fetch(url, { headers: authHeaders(token) });
|
|
5893
|
+
} catch (err) {
|
|
5894
|
+
return errorContent(
|
|
5895
|
+
`Network error calling ${url}: ${err.message}. Check ADMIN_LABS_API_BASE_URL env var (currently "${adminBase}").`
|
|
5896
|
+
);
|
|
5897
|
+
}
|
|
5898
|
+
if (!res.ok) {
|
|
5899
|
+
return errorContent(`API error: ${res.status} ${res.statusText} (${url})`);
|
|
5900
|
+
}
|
|
5901
|
+
const data = await res.json();
|
|
5902
|
+
return textContent(JSON.stringify(data, null, 2));
|
|
5903
|
+
};
|
|
5904
|
+
const withAuth = async (fn) => {
|
|
5905
|
+
try {
|
|
5906
|
+
return await fn();
|
|
5907
|
+
} catch (err) {
|
|
5908
|
+
const e = err;
|
|
5909
|
+
if (e.message.includes("fetch")) {
|
|
5910
|
+
return errorContent(
|
|
5911
|
+
`MCP auth flow failed: ${e.message}. Check MCP_LABS_API_BASE_URL env var (currently "${mcpApiBaseUrl()}"). To re-auth from scratch: rm ${MCP_LABS_AUTH_FILE2}`
|
|
5912
|
+
);
|
|
5913
|
+
}
|
|
5914
|
+
return errorContent(e.message);
|
|
5915
|
+
}
|
|
5916
|
+
};
|
|
5917
|
+
const registrySkills = listSkills("registry");
|
|
5918
|
+
for (const skill of registrySkills) {
|
|
5919
|
+
server.registerResource(
|
|
5920
|
+
skill.id,
|
|
5921
|
+
`sdk://${skill.id}`,
|
|
5922
|
+
{ description: skill.description, mimeType: "text/markdown" },
|
|
5923
|
+
async (uri) => ({
|
|
5924
|
+
contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
|
|
5925
|
+
})
|
|
5926
|
+
);
|
|
5927
|
+
}
|
|
5928
|
+
const ctx = { withAuth, callApi };
|
|
5929
|
+
for (const register of toolRegistrars) {
|
|
5930
|
+
register(server, ctx);
|
|
5931
|
+
}
|
|
5896
5932
|
return server;
|
|
5897
5933
|
};
|
|
5898
5934
|
|