@stackable-labs/mcp-app-extension 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +191 -182
  2. package/dist/server.js +3985 -0
  3. package/package.json +11 -1
package/dist/index.js CHANGED
@@ -1,9 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from 'fs/promises';
3
- import { join } from 'path';
4
- import { homedir } from 'os';
5
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
4
  import { IDENTITY_EVENTS, ACTIVITY_EVENTS, SURFACE_TARGETS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
8
5
  import { z } from 'zod';
9
6
 
@@ -3780,200 +3777,212 @@ var STANDALONE_CLIENT = {
3780
3777
  var package_default = {
3781
3778
  version: "0.0.0"};
3782
3779
 
3783
- // src/index.ts
3780
+ // src/server.ts
3784
3781
  var MCP_CLIENT_NAME = STANDALONE_CLIENT.MCP;
3785
3782
  var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
3786
- var getAuthToken = async () => {
3787
- const authFile = join(homedir(), ".stackable", "auth.json");
3788
- let state;
3789
- try {
3790
- const content = await readFile(authFile, "utf8");
3791
- state = JSON.parse(content);
3792
- } catch {
3793
- throw new Error("Not authenticated. Run 'stackable-app-extension auth login' first.");
3794
- }
3795
- const [, payload] = state.token.split(".");
3796
- if (payload) {
3783
+ var textContent = (text) => ({ content: [{ type: "text", text }] });
3784
+ var errorContent = (text) => ({ content: [{ type: "text", text }], isError: true });
3785
+ var createMcpServer = (options = {}) => {
3786
+ const server2 = new McpServer({
3787
+ name: "stackable-extension-dev",
3788
+ version: package_default.version
3789
+ });
3790
+ const getAuthToken = async () => {
3791
+ if (options.authContext?.token) {
3792
+ return options.authContext.token;
3793
+ }
3794
+ const { readFile } = await import('fs/promises');
3795
+ const { join } = await import('path');
3796
+ const { homedir } = await import('os');
3797
+ const authFile = join(homedir(), ".stackable", "auth.json");
3798
+ let state;
3797
3799
  try {
3798
- const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
3799
- if (decoded.exp && Date.now() >= decoded.exp * 1e3) {
3800
- throw new Error("Session expired. Run 'stackable-app-extension auth login' to re-authenticate.");
3800
+ const content = await readFile(authFile, "utf8");
3801
+ state = JSON.parse(content);
3802
+ } catch {
3803
+ throw new Error("Not authenticated. Run 'stackable-app-extension auth login' first.");
3804
+ }
3805
+ const [, payload] = state.token.split(".");
3806
+ if (payload) {
3807
+ try {
3808
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
3809
+ if (decoded.exp && Date.now() >= decoded.exp * 1e3) {
3810
+ throw new Error("Session expired. Run 'stackable-app-extension auth login' to re-authenticate.");
3811
+ }
3812
+ } catch (err) {
3813
+ if (err instanceof Error && err.message.includes("expired")) {
3814
+ throw err;
3815
+ }
3801
3816
  }
3817
+ }
3818
+ return state.token;
3819
+ };
3820
+ const authHeaders = (token) => ({
3821
+ authorization: `Bearer ${token}`,
3822
+ "content-type": "application/json",
3823
+ "x-client-name": MCP_CLIENT_NAME
3824
+ });
3825
+ const callApi = async (path) => {
3826
+ const token = await getAuthToken();
3827
+ const baseUrl = process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
3828
+ const res = await fetch(`${baseUrl}${path}`, { headers: authHeaders(token) });
3829
+ if (!res.ok) {
3830
+ return errorContent(`API error: ${res.status} ${res.statusText}`);
3831
+ }
3832
+ const data = await res.json();
3833
+ return textContent(JSON.stringify(data, null, 2));
3834
+ };
3835
+ const withAuth = async (fn) => {
3836
+ try {
3837
+ return await fn();
3802
3838
  } catch (err) {
3803
- if (err instanceof Error && err.message.includes("expired")) {
3804
- throw err;
3805
- }
3839
+ return errorContent(err.message);
3806
3840
  }
3841
+ };
3842
+ const registrySkills = listSkills("registry");
3843
+ for (const skill of registrySkills) {
3844
+ server2.registerResource(
3845
+ skill.id,
3846
+ `sdk://${skill.id}`,
3847
+ { description: skill.description, mimeType: "text/markdown" },
3848
+ async (uri) => ({
3849
+ contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
3850
+ })
3851
+ );
3807
3852
  }
3808
- return state.token;
3809
- };
3810
- var authHeaders = (token) => ({
3811
- authorization: `Bearer ${token}`,
3812
- "content-type": "application/json",
3813
- "x-client-name": MCP_CLIENT_NAME
3814
- });
3815
- var textContent = (text) => ({ content: [{ type: "text", text }] });
3816
- var errorContent = (text) => ({ content: [{ type: "text", text }], isError: true });
3817
- var callApi = async (path) => {
3818
- const token = await getAuthToken();
3819
- const baseUrl = process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
3820
- const res = await fetch(`${baseUrl}${path}`, { headers: authHeaders(token) });
3821
- if (!res.ok) {
3822
- return errorContent(`API error: ${res.status} ${res.statusText}`);
3823
- }
3824
- const data = await res.json();
3825
- return textContent(JSON.stringify(data, null, 2));
3826
- };
3827
- var withAuth = async (fn) => {
3828
- try {
3829
- return await fn();
3830
- } catch (err) {
3831
- return errorContent(err.message);
3832
- }
3833
- };
3834
- var server = new McpServer({
3835
- name: "stackable-extension-dev",
3836
- version: package_default.version
3837
- });
3838
- var registrySkills = listSkills("registry");
3839
- for (const skill of registrySkills) {
3840
- server.registerResource(
3841
- skill.id,
3842
- `sdk://${skill.id}`,
3843
- { description: skill.description, mimeType: "text/markdown" },
3844
- async (uri) => ({
3845
- contents: [{ uri: uri.href, text: getSkillBody(skill), mimeType: "text/markdown" }]
3846
- })
3847
- );
3848
- }
3849
- server.registerTool("list_skills", {
3850
- description: "List all available SDK skills with descriptions"
3851
- }, async () => textContent(JSON.stringify(registrySkills.map(getSkillMetadata), null, 2)));
3852
- server.registerTool("lookup_skill", {
3853
- description: "Look up a specific SDK skill by ID or search by keyword. Returns full skill content for ID lookup, or matching skill metadata for keyword search.",
3854
- inputSchema: { id: z.string().optional().describe('Exact skill ID (e.g. "capabilities", "patterns")'), query: z.string().optional().describe("Search keywords to find relevant skills") }
3855
- }, async ({ id, query }) => {
3856
- if (id) {
3857
- const body = lookupSkill(registrySkills, id);
3858
- if (!body) {
3859
- return errorContent(`No skill found with ID "${id}". Use list_skills to see available IDs.`);
3853
+ server2.registerTool("list_skills", {
3854
+ description: "List all available SDK skills with descriptions"
3855
+ }, async () => textContent(JSON.stringify(registrySkills.map(getSkillMetadata), null, 2)));
3856
+ server2.registerTool("lookup_skill", {
3857
+ description: "Look up a specific SDK skill by ID or search by keyword. Returns full skill content for ID lookup, or matching skill metadata for keyword search.",
3858
+ inputSchema: { id: z.string().optional().describe('Exact skill ID (e.g. "capabilities", "patterns")'), query: z.string().optional().describe("Search keywords to find relevant skills") }
3859
+ }, async ({ id, query }) => {
3860
+ if (id) {
3861
+ const body = lookupSkill(registrySkills, id);
3862
+ if (!body) {
3863
+ return errorContent(`No skill found with ID "${id}". Use list_skills to see available IDs.`);
3864
+ }
3865
+ return textContent(body);
3860
3866
  }
3861
- return textContent(body);
3862
- }
3863
- if (query) {
3864
- const matches = findRelevantSkills(registrySkills, query);
3865
- if (matches.length === 0) {
3866
- return errorContent(`No skills matched "${query}". Use list_skills to see available skills.`);
3867
+ if (query) {
3868
+ const matches = findRelevantSkills(registrySkills, query);
3869
+ if (matches.length === 0) {
3870
+ return errorContent(`No skills matched "${query}". Use list_skills to see available skills.`);
3871
+ }
3872
+ return textContent(JSON.stringify(matches.map(getSkillMetadata), null, 2));
3867
3873
  }
3868
- return textContent(JSON.stringify(matches.map(getSkillMetadata), null, 2));
3869
- }
3870
- return errorContent('Provide either "id" or "query" parameter.');
3871
- });
3872
- server.registerTool("validate_manifest", {
3873
- description: "Validate an extension manifest.json. Checks required fields, valid permissions, targets, and allowedDomains consistency with permissions.",
3874
- inputSchema: { manifestContent: z.string().describe("The full manifest.json content as a string") }
3875
- }, async ({ manifestContent }) => {
3876
- const errors = [];
3877
- let manifest;
3878
- try {
3879
- manifest = JSON.parse(manifestContent);
3880
- } catch {
3881
- return errorContent("Invalid JSON: could not parse manifest content.");
3882
- }
3883
- if (!manifest.name || typeof manifest.name !== "string") {
3884
- errors.push('Missing or invalid "name" field (must be a non-empty string).');
3885
- }
3886
- if (!manifest.version || typeof manifest.version !== "string") {
3887
- errors.push('Missing or invalid "version" field (must be a non-empty string).');
3888
- }
3889
- if (!Array.isArray(manifest.targets) || manifest.targets.length === 0) {
3890
- errors.push('Missing or empty "targets" array. At least one surface target is required.');
3891
- }
3892
- if (!Array.isArray(manifest.permissions)) {
3893
- errors.push('Missing "permissions" array.');
3894
- } else {
3895
- const validPermissions = new Set(PERMISSIONS);
3896
- for (const perm of manifest.permissions) {
3897
- if (!validPermissions.has(perm)) {
3898
- errors.push(`Invalid permission "${perm}". Valid permissions: ${PERMISSIONS.join(", ")}`);
3874
+ return errorContent('Provide either "id" or "query" parameter.');
3875
+ });
3876
+ server2.registerTool("validate_manifest", {
3877
+ description: "Validate an extension manifest.json. Checks required fields, valid permissions, targets, and allowedDomains consistency with permissions.",
3878
+ inputSchema: { manifestContent: z.string().describe("The full manifest.json content as a string") }
3879
+ }, async ({ manifestContent }) => {
3880
+ const errors = [];
3881
+ let manifest;
3882
+ try {
3883
+ manifest = JSON.parse(manifestContent);
3884
+ } catch {
3885
+ return errorContent("Invalid JSON: could not parse manifest content.");
3886
+ }
3887
+ if (!manifest.name || typeof manifest.name !== "string") {
3888
+ errors.push('Missing or invalid "name" field (must be a non-empty string).');
3889
+ }
3890
+ if (!manifest.version || typeof manifest.version !== "string") {
3891
+ errors.push('Missing or invalid "version" field (must be a non-empty string).');
3892
+ }
3893
+ if (!Array.isArray(manifest.targets) || manifest.targets.length === 0) {
3894
+ errors.push('Missing or empty "targets" array. At least one surface target is required.');
3895
+ }
3896
+ if (!Array.isArray(manifest.permissions)) {
3897
+ errors.push('Missing "permissions" array.');
3898
+ } else {
3899
+ const validPermissions = new Set(PERMISSIONS);
3900
+ for (const perm of manifest.permissions) {
3901
+ if (!validPermissions.has(perm)) {
3902
+ errors.push(`Invalid permission "${perm}". Valid permissions: ${PERMISSIONS.join(", ")}`);
3903
+ }
3899
3904
  }
3900
3905
  }
3901
- }
3902
- if (!Array.isArray(manifest.allowedDomains)) {
3903
- errors.push('Missing "allowedDomains" array. Use an empty array if no external API calls are needed.');
3904
- }
3905
- const permissions = manifest.permissions ?? [];
3906
- if (permissions.includes("data:fetch") && Array.isArray(manifest.allowedDomains) && manifest.allowedDomains.length === 0) {
3907
- errors.push('"data:fetch" permission declared but "allowedDomains" is empty. Add the domains your extension needs to fetch from.');
3908
- }
3909
- if (errors.length === 0) {
3910
- return textContent("Manifest is valid.");
3911
- }
3912
- return errorContent(`Manifest validation failed:
3906
+ if (!Array.isArray(manifest.allowedDomains)) {
3907
+ errors.push('Missing "allowedDomains" array. Use an empty array if no external API calls are needed.');
3908
+ }
3909
+ const permissions = manifest.permissions ?? [];
3910
+ if (permissions.includes("data:fetch") && Array.isArray(manifest.allowedDomains) && manifest.allowedDomains.length === 0) {
3911
+ errors.push('"data:fetch" permission declared but "allowedDomains" is empty. Add the domains your extension needs to fetch from.');
3912
+ }
3913
+ if (errors.length === 0) {
3914
+ return textContent("Manifest is valid.");
3915
+ }
3916
+ return errorContent(`Manifest validation failed:
3913
3917
  ${errors.map((e) => `- ${e}`).join("\n")}`);
3914
- });
3915
- server.registerTool("validate_permissions", {
3916
- description: "Static analysis: match declared manifest permissions against actual capability and event hook usage in source files. Detects unused permissions and missing permissions.",
3917
- inputSchema: {
3918
- manifestContent: z.string().describe("The full manifest.json content as a string"),
3919
- sourceFiles: z.record(z.string(), z.string()).describe("Map of filename \u2192 source content for all extension source files")
3920
- }
3921
- }, async ({ manifestContent, sourceFiles }) => {
3922
- let manifest;
3923
- try {
3924
- manifest = JSON.parse(manifestContent);
3925
- } catch {
3926
- return errorContent("Invalid JSON: could not parse manifest content.");
3927
- }
3928
- const declaredPermissions = new Set(manifest.permissions ?? []);
3929
- const usedPermissions = /* @__PURE__ */ new Set();
3930
- const allSource = Object.values(sourceFiles).join("\n");
3931
- for (const [capability, permission] of Object.entries(CAPABILITY_PERMISSION_MAP)) {
3932
- if (allSource.includes(capability)) {
3933
- usedPermissions.add(permission);
3918
+ });
3919
+ server2.registerTool("validate_permissions", {
3920
+ description: "Static analysis: match declared manifest permissions against actual capability and event hook usage in source files. Detects unused permissions and missing permissions.",
3921
+ inputSchema: {
3922
+ manifestContent: z.string().describe("The full manifest.json content as a string"),
3923
+ sourceFiles: z.record(z.string(), z.string()).describe("Map of filename \u2192 source content for all extension source files")
3934
3924
  }
3935
- }
3936
- const eventHookMap = {
3937
- useIdentityEvent: "events:identity",
3938
- useMessagingEvent: "events:messaging",
3939
- useActivityEvent: "events:activity"
3940
- };
3941
- for (const [hook, permission] of Object.entries(eventHookMap)) {
3942
- if (allSource.includes(hook)) {
3943
- usedPermissions.add(permission);
3925
+ }, async ({ manifestContent, sourceFiles }) => {
3926
+ let manifest;
3927
+ try {
3928
+ manifest = JSON.parse(manifestContent);
3929
+ } catch {
3930
+ return errorContent("Invalid JSON: could not parse manifest content.");
3944
3931
  }
3945
- }
3946
- const issues = [];
3947
- for (const perm of declaredPermissions) {
3948
- if (!usedPermissions.has(perm)) {
3949
- issues.push(`Unused permission "${perm}" \u2014 declared in manifest but no matching capability/hook usage found in source.`);
3932
+ const declaredPermissions = new Set(manifest.permissions ?? []);
3933
+ const usedPermissions = /* @__PURE__ */ new Set();
3934
+ const allSource = Object.values(sourceFiles).join("\n");
3935
+ for (const [capability, permission] of Object.entries(CAPABILITY_PERMISSION_MAP)) {
3936
+ if (allSource.includes(capability)) {
3937
+ usedPermissions.add(permission);
3938
+ }
3950
3939
  }
3951
- }
3952
- for (const perm of usedPermissions) {
3953
- if (!declaredPermissions.has(perm)) {
3954
- issues.push(`Missing permission "${perm}" \u2014 capability/hook used in source but not declared in manifest.`);
3940
+ const eventHookMap = {
3941
+ useIdentityEvent: "events:identity",
3942
+ useMessagingEvent: "events:messaging",
3943
+ useActivityEvent: "events:activity"
3944
+ };
3945
+ for (const [hook, permission] of Object.entries(eventHookMap)) {
3946
+ if (allSource.includes(hook)) {
3947
+ usedPermissions.add(permission);
3948
+ }
3955
3949
  }
3956
- }
3957
- if (issues.length === 0) {
3958
- return textContent("Permissions are consistent with source code usage.");
3959
- }
3960
- return textContent(`Permission analysis:
3950
+ const issues = [];
3951
+ for (const perm of declaredPermissions) {
3952
+ if (!usedPermissions.has(perm)) {
3953
+ issues.push(`Unused permission "${perm}" \u2014 declared in manifest but no matching capability/hook usage found in source.`);
3954
+ }
3955
+ }
3956
+ for (const perm of usedPermissions) {
3957
+ if (!declaredPermissions.has(perm)) {
3958
+ issues.push(`Missing permission "${perm}" \u2014 capability/hook used in source but not declared in manifest.`);
3959
+ }
3960
+ }
3961
+ if (issues.length === 0) {
3962
+ return textContent("Permissions are consistent with source code usage.");
3963
+ }
3964
+ return textContent(`Permission analysis:
3961
3965
  ${issues.map((i) => `- ${i}`).join("\n")}`);
3962
- });
3963
- server.registerTool("list_apps", {
3964
- description: "List available apps for development with IDs, names, and targets. Requires CLI auth."
3965
- }, async () => withAuth(() => callApi("/app-extension")));
3966
- server.registerTool("list_extensions", {
3967
- description: "List extensions under an app with status, version, and bundle URL. Requires CLI auth.",
3968
- inputSchema: { appId: z.string().describe("App ID") }
3969
- }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions`)));
3970
- server.registerTool("get_extension", {
3971
- description: "Get extension detail including manifest, permissions, and targets. Requires CLI auth.",
3972
- inputSchema: { appId: z.string().describe("App ID"), extensionId: z.string().describe("Extension ID") }
3973
- }, async ({ appId, extensionId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions/${extensionId}`)));
3974
- server.registerTool("list_instances", {
3975
- description: "List instances under an app. Requires CLI auth.",
3976
- inputSchema: { appId: z.string().describe("App ID") }
3977
- }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/instances`)));
3966
+ });
3967
+ server2.registerTool("list_apps", {
3968
+ description: "List available apps for development with IDs, names, and targets. Requires CLI auth."
3969
+ }, async () => withAuth(() => callApi("/app-extension")));
3970
+ server2.registerTool("list_extensions", {
3971
+ description: "List extensions under an app with status, version, and bundle URL. Requires CLI auth.",
3972
+ inputSchema: { appId: z.string().describe("App ID") }
3973
+ }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions`)));
3974
+ server2.registerTool("get_extension", {
3975
+ description: "Get extension detail including manifest, permissions, and targets. Requires CLI auth.",
3976
+ inputSchema: { appId: z.string().describe("App ID"), extensionId: z.string().describe("Extension ID") }
3977
+ }, async ({ appId, extensionId }) => withAuth(() => callApi(`/app-extension/${appId}/extensions/${extensionId}`)));
3978
+ server2.registerTool("list_instances", {
3979
+ description: "List instances under an app. Requires CLI auth.",
3980
+ inputSchema: { appId: z.string().describe("App ID") }
3981
+ }, async ({ appId }) => withAuth(() => callApi(`/app-extension/${appId}/instances`)));
3982
+ return server2;
3983
+ };
3984
+
3985
+ // src/index.ts
3986
+ var server = createMcpServer();
3978
3987
  var transport = new StdioServerTransport();
3979
3988
  await server.connect(transport);