@stackable-labs/mcp-app-extension 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/index.js +148 -112
- package/dist/server.js +143 -108
- 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
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
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
|
-
import fs4, { readFile, mkdir, writeFile, constants } from 'fs/promises';
|
|
7
5
|
import path, { join } from 'path';
|
|
6
|
+
import fs4, { readFile, mkdir, writeFile, constants } from 'fs/promises';
|
|
8
7
|
import os, { homedir } from 'os';
|
|
8
|
+
import { validatePatternFormat } from '@stackable-labs/lib-contracts';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { createServer } from 'http';
|
|
11
11
|
import process6 from 'process';
|
|
@@ -2912,7 +2912,7 @@ Both workflows produce the same output \u2014 a Stackable extension that runs in
|
|
|
2912
2912
|
};
|
|
2913
2913
|
|
|
2914
2914
|
// ../../sdk/extension/ai-docs/src/generators/ai-accelerated-development.ts
|
|
2915
|
-
var
|
|
2915
|
+
var DEFAULT_MCP_LABS_API_BASE_URL = "https://mcp.stackablelabs.io";
|
|
2916
2916
|
var generateAIAcceleratedDevelopment = () => {
|
|
2917
2917
|
const fm = frontmatter({
|
|
2918
2918
|
root: false,
|
|
@@ -2920,8 +2920,8 @@ var generateAIAcceleratedDevelopment = () => {
|
|
|
2920
2920
|
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
2921
|
globs: ["*"]
|
|
2922
2922
|
});
|
|
2923
|
-
const
|
|
2924
|
-
const mcpEndpointUrl = `${
|
|
2923
|
+
const mcpApiBaseUrl2 = process.env.MCP_LABS_API_BASE_URL ?? DEFAULT_MCP_LABS_API_BASE_URL;
|
|
2924
|
+
const mcpEndpointUrl = `${mcpApiBaseUrl2}/app-extension`;
|
|
2925
2925
|
return `${fm}
|
|
2926
2926
|
|
|
2927
2927
|
# AI-Accelerated Development
|
|
@@ -5678,23 +5678,25 @@ var getVerifier = () => getRandomBytes(32, "base64url");
|
|
|
5678
5678
|
|
|
5679
5679
|
// ../../lib/utils-auth/src/constants.ts
|
|
5680
5680
|
var STANDALONE_CLIENT_DATA = {
|
|
5681
|
-
|
|
5682
|
-
|
|
5681
|
+
STACKABLE_LABS: {
|
|
5682
|
+
CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
|
|
5683
|
+
MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
|
|
5684
|
+
},
|
|
5685
|
+
STACKABLE_OPS: {
|
|
5686
|
+
MCP: { name: "@stackable-ops/mcp", authFile: "mcp-ops-auth.json" }
|
|
5687
|
+
}
|
|
5683
5688
|
};
|
|
5684
|
-
var
|
|
5685
|
-
Object.entries(STANDALONE_CLIENT_DATA).map(([
|
|
5689
|
+
var projectField = (field) => Object.fromEntries(
|
|
5690
|
+
Object.entries(STANDALONE_CLIENT_DATA).map(([org, roles]) => [
|
|
5691
|
+
org,
|
|
5692
|
+
Object.fromEntries(Object.entries(roles).map(([role, entry]) => [role, entry[field]]))
|
|
5693
|
+
])
|
|
5686
5694
|
);
|
|
5687
|
-
var
|
|
5688
|
-
|
|
5689
|
-
);
|
|
5690
|
-
Object.values(STANDALONE_CLIENT);
|
|
5691
|
-
|
|
5692
|
-
// ../../lib/utils-auth/src/index.ts
|
|
5693
|
-
var deriveClientId = async (clientName) => (await getDigest(clientName)).slice(0, 32);
|
|
5695
|
+
var STANDALONE_CLIENT = projectField("name");
|
|
5696
|
+
var STANDALONE_CLIENT_AUTH_FILE = projectField("authFile");
|
|
5697
|
+
Object.values(STANDALONE_CLIENT_DATA).flatMap((roles) => Object.values(roles).map((entry) => entry.name));
|
|
5694
5698
|
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);
|
|
5699
|
+
var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.CLI);
|
|
5698
5700
|
var readAuthState = async (filename) => {
|
|
5699
5701
|
try {
|
|
5700
5702
|
const content = await readFile(resolveAuthFile(filename), "utf8");
|
|
@@ -5733,85 +5735,44 @@ var getToken = async (filename) => {
|
|
|
5733
5735
|
return state.token;
|
|
5734
5736
|
};
|
|
5735
5737
|
|
|
5738
|
+
// ../../lib/utils-auth/src/index.ts
|
|
5739
|
+
var deriveDcrClientId = async (clientName) => (await getDigest(clientName)).slice(0, 32);
|
|
5740
|
+
var deriveClientId = async (clientName) => deriveDcrClientId(clientName);
|
|
5741
|
+
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.CLI);
|
|
5742
|
+
var MCP_AUTH_FILE = join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP);
|
|
5743
|
+
|
|
5744
|
+
// ../../lib/utils-mcp/src/index.ts
|
|
5745
|
+
var textContent = (text) => ({
|
|
5746
|
+
content: [{ type: "text", text }]
|
|
5747
|
+
});
|
|
5748
|
+
var errorContent = (text) => ({
|
|
5749
|
+
isError: true,
|
|
5750
|
+
content: [{ type: "text", text }]
|
|
5751
|
+
});
|
|
5752
|
+
|
|
5736
5753
|
// package.json
|
|
5737
5754
|
var package_default = {
|
|
5738
5755
|
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
|
-
};
|
|
5756
|
+
var registerAppTools = (server2, ctx) => {
|
|
5757
|
+
server2.registerTool("list_apps", {
|
|
5758
|
+
description: "List available apps for development with IDs, names, and targets. Requires CLI auth."
|
|
5759
|
+
}, async () => ctx.withAuth(() => ctx.callApi("/app-extension")));
|
|
5760
|
+
server2.registerTool("list_extensions", {
|
|
5761
|
+
description: "List extensions under an app with status, version, and bundle URL. Requires CLI auth.",
|
|
5762
|
+
inputSchema: { appId: z.string().describe("App ID") }
|
|
5763
|
+
}, async ({ appId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/extensions`)));
|
|
5764
|
+
server2.registerTool("get_extension", {
|
|
5765
|
+
description: "Get extension detail including manifest, permissions, and targets. Requires CLI auth.",
|
|
5766
|
+
inputSchema: { appId: z.string().describe("App ID"), extensionId: z.string().describe("Extension ID") }
|
|
5767
|
+
}, async ({ appId, extensionId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/extensions/${extensionId}`)));
|
|
5768
|
+
server2.registerTool("list_instances", {
|
|
5769
|
+
description: "List instances under an app. Requires CLI auth.",
|
|
5770
|
+
inputSchema: { appId: z.string().describe("App ID") }
|
|
5771
|
+
}, async ({ appId }) => ctx.withAuth(() => ctx.callApi(`/app-extension/${appId}/instances`)));
|
|
5772
|
+
};
|
|
5773
|
+
var apps_default = registerAppTools;
|
|
5774
|
+
var registerSkillTools = (server2) => {
|
|
5804
5775
|
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
5776
|
server2.registerTool("list_skills", {
|
|
5816
5777
|
description: "List all available SDK skills with descriptions"
|
|
5817
5778
|
}, async () => textContent(JSON.stringify(registrySkills.map(getSkillMetadata), null, 2)));
|
|
@@ -5835,6 +5796,9 @@ var createMcpServer = (options = {}) => {
|
|
|
5835
5796
|
}
|
|
5836
5797
|
return errorContent('Provide either "id" or "query" parameter.');
|
|
5837
5798
|
});
|
|
5799
|
+
};
|
|
5800
|
+
var skills_default = registerSkillTools;
|
|
5801
|
+
var registerValidationTools = (server2) => {
|
|
5838
5802
|
server2.registerTool("validate_manifest", {
|
|
5839
5803
|
description: "Validate an extension manifest.json. Checks required fields, valid permissions, targets, and allowedDomains consistency with permissions.",
|
|
5840
5804
|
inputSchema: { manifestContent: z.string().describe("The full manifest.json content as a string") }
|
|
@@ -5929,21 +5893,93 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
5929
5893
|
return textContent(`Permission analysis:
|
|
5930
5894
|
${issues.map((i) => `- ${i}`).join("\n")}`);
|
|
5931
5895
|
});
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5896
|
+
};
|
|
5897
|
+
var validation_default = registerValidationTools;
|
|
5898
|
+
|
|
5899
|
+
// src/tools/index.ts
|
|
5900
|
+
var toolRegistrars = [
|
|
5901
|
+
skills_default,
|
|
5902
|
+
validation_default,
|
|
5903
|
+
apps_default
|
|
5904
|
+
];
|
|
5905
|
+
|
|
5906
|
+
// src/server.ts
|
|
5907
|
+
var MCP_CLIENT_NAME = STANDALONE_CLIENT.STACKABLE_LABS.MCP;
|
|
5908
|
+
var DEFAULT_ADMIN_LABS_API_URL = "https://api-x.stackablelabs.io/admin";
|
|
5909
|
+
var DEFAULT_MCP_LABS_API_BASE_URL2 = "https://mcp.stackablelabs.io";
|
|
5910
|
+
var adminApiBaseUrl = () => process.env.ADMIN_LABS_API_BASE_URL ?? process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_LABS_API_URL;
|
|
5911
|
+
var mcpApiBaseUrl = () => process.env.MCP_LABS_API_BASE_URL ?? process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_LABS_API_BASE_URL2;
|
|
5912
|
+
var createMcpServer = (options = {}) => {
|
|
5913
|
+
const server2 = new McpServer({
|
|
5914
|
+
name: "stackable-extension-dev",
|
|
5915
|
+
version: package_default.version
|
|
5916
|
+
});
|
|
5917
|
+
const getAuthToken = async () => {
|
|
5918
|
+
if (options.authContext?.token) {
|
|
5919
|
+
return options.authContext.token;
|
|
5920
|
+
}
|
|
5921
|
+
try {
|
|
5922
|
+
return await getToken(STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP);
|
|
5923
|
+
} catch {
|
|
5924
|
+
}
|
|
5925
|
+
if (!options.performOAuthFlow) {
|
|
5926
|
+
throw new Error(
|
|
5927
|
+
"No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
|
|
5928
|
+
);
|
|
5929
|
+
}
|
|
5930
|
+
return options.performOAuthFlow(`${mcpApiBaseUrl()}/.well-known/oauth-authorization-server`);
|
|
5931
|
+
};
|
|
5932
|
+
const authHeaders = (token) => ({
|
|
5933
|
+
authorization: `Bearer ${token}`,
|
|
5934
|
+
"content-type": "application/json",
|
|
5935
|
+
"x-client-name": MCP_CLIENT_NAME
|
|
5936
|
+
});
|
|
5937
|
+
const callApi = async (path2) => {
|
|
5938
|
+
const token = await getAuthToken();
|
|
5939
|
+
const adminBase = adminApiBaseUrl();
|
|
5940
|
+
const url = `${adminBase}${path2}`;
|
|
5941
|
+
let res;
|
|
5942
|
+
try {
|
|
5943
|
+
res = await fetch(url, { headers: authHeaders(token) });
|
|
5944
|
+
} catch (err) {
|
|
5945
|
+
return errorContent(
|
|
5946
|
+
`Network error calling ${url}: ${err.message}. Check ADMIN_LABS_API_BASE_URL env var (currently "${adminBase}").`
|
|
5947
|
+
);
|
|
5948
|
+
}
|
|
5949
|
+
if (!res.ok) {
|
|
5950
|
+
return errorContent(`API error: ${res.status} ${res.statusText} (${url})`);
|
|
5951
|
+
}
|
|
5952
|
+
const data = await res.json();
|
|
5953
|
+
return textContent(JSON.stringify(data, null, 2));
|
|
5954
|
+
};
|
|
5955
|
+
const withAuth = async (fn) => {
|
|
5956
|
+
try {
|
|
5957
|
+
return await fn();
|
|
5958
|
+
} catch (err) {
|
|
5959
|
+
const e = err;
|
|
5960
|
+
if (e.message.includes("fetch")) {
|
|
5961
|
+
return errorContent(
|
|
5962
|
+
`MCP auth flow failed: ${e.message}. Check MCP_LABS_API_BASE_URL env var (currently "${mcpApiBaseUrl()}"). To re-auth from scratch: rm ${MCP_AUTH_FILE}`
|
|
5963
|
+
);
|
|
5964
|
+
}
|
|
5965
|
+
return errorContent(e.message);
|
|
5966
|
+
}
|
|
5967
|
+
};
|
|
5968
|
+
const registrySkills = listSkills("registry");
|
|
5969
|
+
for (const skill of registrySkills) {
|
|
5970
|
+
server2.registerResource(
|
|
5971
|
+
skill.id,
|
|
5972
|
+
`sdk://${skill.id}`,
|
|
5973
|
+
{ description: skill.description, mimeType: "text/markdown" },
|
|
5974
|
+
async (uri) => ({
|
|
5975
|
+
contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
|
|
5976
|
+
})
|
|
5977
|
+
);
|
|
5978
|
+
}
|
|
5979
|
+
const ctx = { withAuth, callApi };
|
|
5980
|
+
for (const register of toolRegistrars) {
|
|
5981
|
+
register(server2, ctx);
|
|
5982
|
+
}
|
|
5947
5983
|
return server2;
|
|
5948
5984
|
};
|
|
5949
5985
|
var isDockerCached;
|
|
@@ -6437,7 +6473,7 @@ var registerClient = async (registrationEndpoint) => {
|
|
|
6437
6473
|
method: "POST",
|
|
6438
6474
|
headers: { "content-type": "application/json" },
|
|
6439
6475
|
body: JSON.stringify({
|
|
6440
|
-
client_name: STANDALONE_CLIENT.MCP,
|
|
6476
|
+
client_name: STANDALONE_CLIENT.STACKABLE_LABS.MCP,
|
|
6441
6477
|
redirect_uris: ["http://127.0.0.1/callback"]
|
|
6442
6478
|
})
|
|
6443
6479
|
});
|
|
@@ -6474,7 +6510,7 @@ var decodeJwtPayload2 = (token) => {
|
|
|
6474
6510
|
};
|
|
6475
6511
|
var performOAuthFlow = async (discoveryUrl) => {
|
|
6476
6512
|
const discovery = await fetchDiscovery(discoveryUrl);
|
|
6477
|
-
let clientId = await deriveClientId(STANDALONE_CLIENT.MCP);
|
|
6513
|
+
let clientId = await deriveClientId(STANDALONE_CLIENT.STACKABLE_LABS.MCP);
|
|
6478
6514
|
if (discovery.registration_endpoint) {
|
|
6479
6515
|
clientId = await registerClient(discovery.registration_endpoint);
|
|
6480
6516
|
}
|
|
@@ -6557,7 +6593,7 @@ var performOAuthFlow = async (discoveryUrl) => {
|
|
|
6557
6593
|
orgId: payload.orgId,
|
|
6558
6594
|
userId: payload.sub
|
|
6559
6595
|
},
|
|
6560
|
-
STANDALONE_CLIENT_AUTH_FILE.MCP
|
|
6596
|
+
STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP
|
|
6561
6597
|
);
|
|
6562
6598
|
return tokenResponse.access_token;
|
|
6563
6599
|
};
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
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
|
-
import { readFile } from 'fs/promises';
|
|
5
3
|
import { join } from 'path';
|
|
4
|
+
import { readFile } from 'fs/promises';
|
|
6
5
|
import { homedir } from 'os';
|
|
6
|
+
import { validatePatternFormat } from '@stackable-labs/lib-contracts';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
|
|
9
9
|
// src/server.ts
|
|
@@ -2905,7 +2905,7 @@ Both workflows produce the same output \u2014 a Stackable extension that runs in
|
|
|
2905
2905
|
};
|
|
2906
2906
|
|
|
2907
2907
|
// ../../sdk/extension/ai-docs/src/generators/ai-accelerated-development.ts
|
|
2908
|
-
var
|
|
2908
|
+
var DEFAULT_MCP_LABS_API_BASE_URL = "https://mcp.stackablelabs.io";
|
|
2909
2909
|
var generateAIAcceleratedDevelopment = () => {
|
|
2910
2910
|
const fm = frontmatter({
|
|
2911
2911
|
root: false,
|
|
@@ -2913,8 +2913,8 @@ var generateAIAcceleratedDevelopment = () => {
|
|
|
2913
2913
|
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
2914
|
globs: ["*"]
|
|
2915
2915
|
});
|
|
2916
|
-
const
|
|
2917
|
-
const mcpEndpointUrl = `${
|
|
2916
|
+
const mcpApiBaseUrl2 = process.env.MCP_LABS_API_BASE_URL ?? DEFAULT_MCP_LABS_API_BASE_URL;
|
|
2917
|
+
const mcpEndpointUrl = `${mcpApiBaseUrl2}/app-extension`;
|
|
2918
2918
|
return `${fm}
|
|
2919
2919
|
|
|
2920
2920
|
# AI-Accelerated Development
|
|
@@ -5632,22 +5632,25 @@ pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
|
|
|
5632
5632
|
|
|
5633
5633
|
// ../../lib/utils-auth/src/constants.ts
|
|
5634
5634
|
var STANDALONE_CLIENT_DATA = {
|
|
5635
|
-
|
|
5636
|
-
|
|
5635
|
+
STACKABLE_LABS: {
|
|
5636
|
+
CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
|
|
5637
|
+
MCP: { name: "@stackable-labs/mcp-app-extension", authFile: "mcp-auth.json" }
|
|
5638
|
+
},
|
|
5639
|
+
STACKABLE_OPS: {
|
|
5640
|
+
MCP: { name: "@stackable-ops/mcp", authFile: "mcp-ops-auth.json" }
|
|
5641
|
+
}
|
|
5637
5642
|
};
|
|
5638
|
-
var
|
|
5639
|
-
Object.entries(STANDALONE_CLIENT_DATA).map(([
|
|
5643
|
+
var projectField = (field) => Object.fromEntries(
|
|
5644
|
+
Object.entries(STANDALONE_CLIENT_DATA).map(([org, roles]) => [
|
|
5645
|
+
org,
|
|
5646
|
+
Object.fromEntries(Object.entries(roles).map(([role, entry]) => [role, entry[field]]))
|
|
5647
|
+
])
|
|
5640
5648
|
);
|
|
5641
|
-
var
|
|
5642
|
-
|
|
5643
|
-
);
|
|
5644
|
-
Object.values(STANDALONE_CLIENT);
|
|
5645
|
-
|
|
5646
|
-
// ../../lib/utils-auth/src/index.ts
|
|
5649
|
+
var STANDALONE_CLIENT = projectField("name");
|
|
5650
|
+
var STANDALONE_CLIENT_AUTH_FILE = projectField("authFile");
|
|
5651
|
+
Object.values(STANDALONE_CLIENT_DATA).flatMap((roles) => Object.values(roles).map((entry) => entry.name));
|
|
5647
5652
|
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);
|
|
5653
|
+
var resolveAuthFile = (filename) => join(AUTH_DIR, filename ?? STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.CLI);
|
|
5651
5654
|
var readAuthState = async (filename) => {
|
|
5652
5655
|
try {
|
|
5653
5656
|
const content = await readFile(resolveAuthFile(filename), "utf8");
|
|
@@ -5682,85 +5685,42 @@ var getToken = async (filename) => {
|
|
|
5682
5685
|
return state.token;
|
|
5683
5686
|
};
|
|
5684
5687
|
|
|
5688
|
+
// ../../lib/utils-auth/src/index.ts
|
|
5689
|
+
join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.CLI);
|
|
5690
|
+
var MCP_AUTH_FILE = join(AUTH_DIR, STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP);
|
|
5691
|
+
|
|
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
|
+
});
|
|
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,93 @@ ${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_CLIENT_NAME = STANDALONE_CLIENT.STACKABLE_LABS.MCP;
|
|
5856
|
+
var DEFAULT_ADMIN_LABS_API_URL = "https://api-x.stackablelabs.io/admin";
|
|
5857
|
+
var DEFAULT_MCP_LABS_API_BASE_URL2 = "https://mcp.stackablelabs.io";
|
|
5858
|
+
var adminApiBaseUrl = () => process.env.ADMIN_LABS_API_BASE_URL ?? process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_LABS_API_URL;
|
|
5859
|
+
var mcpApiBaseUrl = () => process.env.MCP_LABS_API_BASE_URL ?? process.env.MCP_API_BASE_URL ?? DEFAULT_MCP_LABS_API_BASE_URL2;
|
|
5860
|
+
var createMcpServer = (options = {}) => {
|
|
5861
|
+
const server = new McpServer({
|
|
5862
|
+
name: "stackable-extension-dev",
|
|
5863
|
+
version: package_default.version
|
|
5864
|
+
});
|
|
5865
|
+
const getAuthToken = async () => {
|
|
5866
|
+
if (options.authContext?.token) {
|
|
5867
|
+
return options.authContext.token;
|
|
5868
|
+
}
|
|
5869
|
+
try {
|
|
5870
|
+
return await getToken(STANDALONE_CLIENT_AUTH_FILE.STACKABLE_LABS.MCP);
|
|
5871
|
+
} catch {
|
|
5872
|
+
}
|
|
5873
|
+
if (!options.performOAuthFlow) {
|
|
5874
|
+
throw new Error(
|
|
5875
|
+
"No auth token available. Provide authContext (server) or performOAuthFlow (CLI)."
|
|
5876
|
+
);
|
|
5877
|
+
}
|
|
5878
|
+
return options.performOAuthFlow(`${mcpApiBaseUrl()}/.well-known/oauth-authorization-server`);
|
|
5879
|
+
};
|
|
5880
|
+
const authHeaders = (token) => ({
|
|
5881
|
+
authorization: `Bearer ${token}`,
|
|
5882
|
+
"content-type": "application/json",
|
|
5883
|
+
"x-client-name": MCP_CLIENT_NAME
|
|
5884
|
+
});
|
|
5885
|
+
const callApi = async (path) => {
|
|
5886
|
+
const token = await getAuthToken();
|
|
5887
|
+
const adminBase = adminApiBaseUrl();
|
|
5888
|
+
const url = `${adminBase}${path}`;
|
|
5889
|
+
let res;
|
|
5890
|
+
try {
|
|
5891
|
+
res = await fetch(url, { headers: authHeaders(token) });
|
|
5892
|
+
} catch (err) {
|
|
5893
|
+
return errorContent(
|
|
5894
|
+
`Network error calling ${url}: ${err.message}. Check ADMIN_LABS_API_BASE_URL env var (currently "${adminBase}").`
|
|
5895
|
+
);
|
|
5896
|
+
}
|
|
5897
|
+
if (!res.ok) {
|
|
5898
|
+
return errorContent(`API error: ${res.status} ${res.statusText} (${url})`);
|
|
5899
|
+
}
|
|
5900
|
+
const data = await res.json();
|
|
5901
|
+
return textContent(JSON.stringify(data, null, 2));
|
|
5902
|
+
};
|
|
5903
|
+
const withAuth = async (fn) => {
|
|
5904
|
+
try {
|
|
5905
|
+
return await fn();
|
|
5906
|
+
} catch (err) {
|
|
5907
|
+
const e = err;
|
|
5908
|
+
if (e.message.includes("fetch")) {
|
|
5909
|
+
return errorContent(
|
|
5910
|
+
`MCP auth flow failed: ${e.message}. Check MCP_LABS_API_BASE_URL env var (currently "${mcpApiBaseUrl()}"). To re-auth from scratch: rm ${MCP_AUTH_FILE}`
|
|
5911
|
+
);
|
|
5912
|
+
}
|
|
5913
|
+
return errorContent(e.message);
|
|
5914
|
+
}
|
|
5915
|
+
};
|
|
5916
|
+
const registrySkills = listSkills("registry");
|
|
5917
|
+
for (const skill of registrySkills) {
|
|
5918
|
+
server.registerResource(
|
|
5919
|
+
skill.id,
|
|
5920
|
+
`sdk://${skill.id}`,
|
|
5921
|
+
{ description: skill.description, mimeType: "text/markdown" },
|
|
5922
|
+
async (uri) => ({
|
|
5923
|
+
contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
|
|
5924
|
+
})
|
|
5925
|
+
);
|
|
5926
|
+
}
|
|
5927
|
+
const ctx = { withAuth, callApi };
|
|
5928
|
+
for (const register of toolRegistrars) {
|
|
5929
|
+
register(server, ctx);
|
|
5930
|
+
}
|
|
5896
5931
|
return server;
|
|
5897
5932
|
};
|
|
5898
5933
|
|