@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 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-use1.stackablelabs.io`):**
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
- "MCP_API_BASE_URL": "https://mcp-dev.stackablelabs.io",
37
- "ADMIN_API_BASE_URL": "https://api-dev-use1.stackablelabs.io/admin"
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
- | `MCP_API_BASE_URL` | `https://mcp.stackablelabs.io` | MCP host root. OAuth discovery URL is derived as `${mcpBase}/.well-known/oauth-authorization-server`. |
69
- | `ADMIN_API_BASE_URL` | `https://api-use1.stackablelabs.io/admin` | Admin API endpoint for platform tool calls (`list_apps`, etc.). |
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": { "MCP_API_BASE_URL": "...", "ADMIN_API_BASE_URL": "..." }
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 DEFAULT_MCP_API_BASE_URL = "https://mcp.stackablelabs.io";
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 mcpApiBaseUrl = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL;
2924
- const mcpEndpointUrl = `${mcpApiBaseUrl}/app-extension`;
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 deriveClientId = async (clientName) => (await getDigest(clientName)).slice(0, 32);
5694
- var AUTH_DIR = join(homedir(), ".stackable");
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
- // src/server.ts
5741
- var MCP_CLIENT_NAME = STANDALONE_CLIENT.MCP;
5742
- var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
5743
- var DEFAULT_MCP_API_BASE_URL2 = "https://mcp.stackablelabs.io";
5744
- var textContent = (text) => ({ content: [{ type: "text", text }] });
5745
- var errorContent = (text) => ({ content: [{ type: "text", text }], isError: true });
5746
- var createMcpServer = (options = {}) => {
5747
- const server2 = new McpServer({
5748
- name: "stackable-extension-dev",
5749
- version: package_default.version
5750
- });
5751
- const getAuthToken = async () => {
5752
- if (options.authContext?.token) {
5753
- return options.authContext.token;
5754
- }
5755
- try {
5756
- return await getToken(STANDALONE_CLIENT_AUTH_FILE.MCP);
5757
- } catch {
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
- server2.registerTool("list_apps", {
5933
- description: "List available apps for development with IDs, names, and targets. Requires CLI auth."
5934
- }, async () => withAuth(() => callApi("/app-extension")));
5935
- server2.registerTool("list_extensions", {
5936
- description: "List extensions under an app with status, version, and bundle URL. Requires CLI auth.",
5937
- inputSchema: { appId: z.string().describe("App ID") }
5938
- }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions`)));
5939
- server2.registerTool("get_extension", {
5940
- description: "Get extension detail including manifest, permissions, and targets. Requires CLI auth.",
5941
- inputSchema: { appId: z.string().describe("App ID"), extensionId: z.string().describe("Extension ID") }
5942
- }, async ({ appId, extensionId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions/${extensionId}`)));
5943
- server2.registerTool("list_instances", {
5944
- description: "List instances under an app. Requires CLI auth.",
5945
- inputSchema: { appId: z.string().describe("App ID") }
5946
- }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/instances`)));
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 DEFAULT_MCP_API_BASE_URL = "https://mcp.stackablelabs.io";
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 mcpApiBaseUrl = process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_API_BASE_URL;
2917
- const mcpEndpointUrl = `${mcpApiBaseUrl}/app-extension`;
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-auth/src/constants.ts
5634
- var STANDALONE_CLIENT_DATA = {
5635
- CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
5636
- MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
5637
- };
5638
- var STANDALONE_CLIENT = Object.fromEntries(
5639
- Object.entries(STANDALONE_CLIENT_DATA).map(([k, v]) => [k, v.name])
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
- // src/server.ts
5690
- var MCP_CLIENT_NAME = STANDALONE_CLIENT.MCP;
5691
- var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
5692
- var DEFAULT_MCP_API_BASE_URL2 = "https://mcp.stackablelabs.io";
5693
- var textContent = (text) => ({ content: [{ type: "text", text }] });
5694
- var errorContent = (text) => ({ content: [{ type: "text", text }], isError: true });
5695
- var createMcpServer = (options = {}) => {
5696
- const server = new McpServer({
5697
- name: "stackable-extension-dev",
5698
- version: package_default.version
5699
- });
5700
- const getAuthToken = async () => {
5701
- if (options.authContext?.token) {
5702
- return options.authContext.token;
5703
- }
5704
- try {
5705
- return await getToken(STANDALONE_CLIENT_AUTH_FILE.MCP);
5706
- } catch {
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
- server.registerTool("list_apps", {
5882
- description: "List available apps for development with IDs, names, and targets. Requires CLI auth."
5883
- }, async () => withAuth(() => callApi("/app-extension")));
5884
- server.registerTool("list_extensions", {
5885
- description: "List extensions under an app with status, version, and bundle URL. Requires CLI auth.",
5886
- inputSchema: { appId: z.string().describe("App ID") }
5887
- }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions`)));
5888
- server.registerTool("get_extension", {
5889
- description: "Get extension detail including manifest, permissions, and targets. Requires CLI auth.",
5890
- inputSchema: { appId: z.string().describe("App ID"), extensionId: z.string().describe("Extension ID") }
5891
- }, async ({ appId, extensionId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions/${extensionId}`)));
5892
- server.registerTool("list_instances", {
5893
- description: "List instances under an app. Requires CLI auth.",
5894
- inputSchema: { appId: z.string().describe("App ID") }
5895
- }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/instances`)));
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackable-labs/mcp-app-extension",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mcp-app-extension": "./dist/index.js"