@mashroom/mashroom-mcp-server 3.0.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) nonblocking.at gmbh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+
2
+ # Mashroom MCP Server
3
+
4
+ Plugin for [Mashroom Server](https://www.mashroom-server.com), a **Microfrontend Integration Platform**.
5
+
6
+ A minimal MCP plugin that allows agents to register new Portal Apps (Microfrontends) and to place them on pages.
7
+
8
+ ## Usage
9
+
10
+ If *node_modules/@mashroom* is configured as a plugin path, add **@mashroom/mashroom-mcp-server** as *dependency*.
11
+
12
+ The MCP server will be available under *http://localhost:5050/mcp*.
13
+
14
+ > [!WARNING]
15
+ > The MCP server currently has no security whatsoever, so don't install in on production servers.
16
+ > Also, the route /mcp must be accessible without authentication.
17
+
18
+ Check out the documentation of your coding agent how to register an MCP server:
19
+
20
+ * Claude Code: https://code.claude.com/docs/en/mcp-quickstart
21
+ * Codex: https://learn.chatgpt.com/docs/extend/mcp?surface=cli
22
+ * VSC: https://code.visualstudio.com/docs/agent-customization/mcp-servers
23
+ * JetBrains Junie: https://junie.jetbrains.com/docs/junie-plugin-mcp-settings.html
24
+
25
+ Example conversations:
26
+
27
+ ![Example1](./examples/example1.png)
28
+
29
+ ![Example2](./examples/example2.png)
30
+
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ var _express = _interopRequireDefault(require("express"));
4
+ var _mcpApi = _interopRequireDefault(require("./mcp-api"));
5
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
6
+ const start = async () => {
7
+ const app = (0, _express.default)();
8
+
9
+ // Disable CORS
10
+ app.use((req, res, next) => {
11
+ res.header('Access-Control-Allow-Origin', '*');
12
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, MCP-Protocol-Version');
13
+ next();
14
+ });
15
+
16
+ // Dummy services
17
+ app.use((req, res, next) => {
18
+ const pluginContext = {
19
+ loggerFactory: () => console,
20
+ services: {
21
+ core: {},
22
+ portal: {
23
+ service: {
24
+ getSites: async () => {
25
+ return [];
26
+ },
27
+ getSite: async siteId => {
28
+ return null;
29
+ }
30
+ }
31
+ }
32
+ }
33
+ };
34
+ req.pluginContext = pluginContext;
35
+ next();
36
+ });
37
+ app.use('/mcp', _mcpApi.default);
38
+ app.listen(5099, () => {
39
+ console.log('MCP Server started on http://localhost:5099/mcp');
40
+ });
41
+ app.once('error', error => {
42
+ console.error('Failed to start server!', error);
43
+ });
44
+ };
45
+ start();
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _mcpApi = _interopRequireDefault(require("./mcp-api"));
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
+ const bootstrap = async () => {
10
+ return _mcpApi.default;
11
+ };
12
+ var _default = exports.default = bootstrap;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
8
+ var _express = require("express");
9
+ var _bodyParser = _interopRequireDefault(require("body-parser"));
10
+ var _mcpServer = _interopRequireDefault(require("./mcp-server"));
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ const router = (0, _express.Router)();
13
+ router.use(_bodyParser.default.json());
14
+ router.post('/', async (req, res) => {
15
+ const logger = req.pluginContext.loggerFactory('mashroom.mcp');
16
+ logger.debug('Received MCP request:', req.body);
17
+ try {
18
+ const server = (0, _mcpServer.default)(req);
19
+ const transport = new _streamableHttp.StreamableHTTPServerTransport({
20
+ sessionIdGenerator: undefined
21
+ });
22
+ await server.connect(transport);
23
+ await transport.handleRequest(req, res, req.body);
24
+ res.on('close', () => {
25
+ logger.debug('Close MCP request');
26
+ transport.close();
27
+ server.close();
28
+ });
29
+ } catch (error) {
30
+ logger.error('Error handling MCP request:', error);
31
+ if (!res.headersSent) {
32
+ res.status(500).json({
33
+ jsonrpc: '2.0',
34
+ error: {
35
+ code: -32603,
36
+ message: 'Internal server error'
37
+ },
38
+ id: null
39
+ });
40
+ }
41
+ }
42
+ });
43
+ router.get('/', async (req, res) => {
44
+ res.writeHead(405).end(JSON.stringify({
45
+ jsonrpc: '2.0',
46
+ error: {
47
+ code: -32000,
48
+ message: 'Method not allowed.'
49
+ },
50
+ id: null
51
+ }));
52
+ });
53
+ router.delete('/', async (req, res) => {
54
+ res.writeHead(405).end(JSON.stringify({
55
+ jsonrpc: '2.0',
56
+ error: {
57
+ code: -32000,
58
+ message: 'Method not allowed.'
59
+ },
60
+ id: null
61
+ }));
62
+ });
63
+ var _default = exports.default = router;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
8
+ var z = _interopRequireWildcard(require("zod/v4"));
9
+ var _listPortalSites = _interopRequireDefault(require("./tools/sites/list-portal-sites"));
10
+ var _listPortalPages = _interopRequireDefault(require("./tools/pages/list-portal-pages"));
11
+ var _serverInfo = _interopRequireDefault(require("./tools/server-info"));
12
+ var _portalPageDetails = _interopRequireDefault(require("./tools/pages/portal-page-details"));
13
+ var _listRegisteredPortalApps = _interopRequireDefault(require("./tools/apps/list-registered-portal-apps"));
14
+ var _registerRemotePortalApp = _interopRequireDefault(require("./tools/apps/register-remote-portal-app"));
15
+ var _unregisterRemotePortalApp = _interopRequireDefault(require("./tools/apps/unregister-remote-portal-app"));
16
+ var _addPortalAppToPage = _interopRequireDefault(require("./tools/apps/add-portal-app-to-page"));
17
+ var _removePortalAppFromPage = _interopRequireDefault(require("./tools/apps/remove-portal-app-from-page"));
18
+ var _movePortalAppOnPage = _interopRequireDefault(require("./tools/apps/move-portal-app-on-page"));
19
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
20
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
21
+ var _default = req => {
22
+ const server = new _mcp.McpServer({
23
+ name: 'mashroom-mcp-server',
24
+ version: '1.0.0',
25
+ websiteUrl: 'https://www.mashroom-server.com'
26
+ }, {
27
+ capabilities: {
28
+ logging: {}
29
+ }
30
+ });
31
+ server.registerTool('server-info', {
32
+ title: 'Mashroom Server Info Tool',
33
+ description: 'Mashroom Server info such as version and default language',
34
+ inputSchema: {}
35
+ }, (0, _serverInfo.default)(req.pluginContext));
36
+ server.registerTool('list-portal-sites', {
37
+ title: 'List Portal Sites Tool',
38
+ description: 'List all sites configured in Mashroom Portal',
39
+ inputSchema: {}
40
+ }, (0, _listPortalSites.default)(req));
41
+ server.registerTool('list-portal-pages', {
42
+ title: 'List Portal Pages Tool',
43
+ description: 'List all configured pages in Mashroom Portal',
44
+ inputSchema: {
45
+ siteId: z.string().optional().describe('Filter pages by Site ID (optional)')
46
+ }
47
+ }, (0, _listPortalPages.default)(req));
48
+ server.registerTool('portal-page-details', {
49
+ title: 'Portal Page Details Tool',
50
+ description: 'Details for a given Page',
51
+ inputSchema: {
52
+ pageId: z.string().describe('The Page ID')
53
+ }
54
+ }, (0, _portalPageDetails.default)(req));
55
+ server.registerTool('list-registered-portal-apps', {
56
+ title: 'List Registered Portal Apps Tool',
57
+ description: 'List all Apps currently registered in Mashroom Portal',
58
+ inputSchema: {}
59
+ }, (0, _listRegisteredPortalApps.default)(req));
60
+ server.registerTool('register-remote-portal-app', {
61
+ title: 'Register Remote Portal App Tool',
62
+ description: 'Register a remote App running on some server with Mashroom Portal',
63
+ inputSchema: {
64
+ url: z.string().describe('The full URL of the remote App'),
65
+ waitFor: z.number().optional().describe('Number of seconds to wait for a successful registration (optional, default: 20)')
66
+ }
67
+ }, (0, _registerRemotePortalApp.default)(req));
68
+ server.registerTool('unregister-remote-portal-app', {
69
+ title: 'Unregister Remote Portal App Tool',
70
+ description: 'Unregister a remote App registered with Mashroom Portal',
71
+ inputSchema: {
72
+ url: z.string().describe('The full URL of the remote App')
73
+ }
74
+ }, (0, _unregisterRemotePortalApp.default)(req));
75
+ server.registerTool('add-portal-app-to-page', {
76
+ title: 'Add Portal App to Page Tool',
77
+ description: 'Add an already registered App to a specific Page and area',
78
+ inputSchema: {
79
+ appName: z.string().describe('The App name'),
80
+ pageId: z.string().describe('The Page ID'),
81
+ areaId: z.string().describe('The Area ID'),
82
+ position: z.number().optional().describe('The position within the Area (optional, default: 0)'),
83
+ overrideAppConfig: z.object({}).optional().describe('Override the App config (optional)')
84
+ }
85
+ }, (0, _addPortalAppToPage.default)(req));
86
+ server.registerTool('remove-portal-app-from-page', {
87
+ title: 'Remove Portal App from Page Tool',
88
+ description: 'Remove an existing App instance from a Page',
89
+ inputSchema: {
90
+ appName: z.string().describe('The App name'),
91
+ appInstanceId: z.string().describe('The App Instance ID'),
92
+ pageId: z.string().describe('The Page ID')
93
+ }
94
+ }, (0, _removePortalAppFromPage.default)(req));
95
+ server.registerTool('move-portal-app-on-page', {
96
+ title: 'Move Portal App on Page Tool',
97
+ description: 'Move an existing App instance to a different area or position on a Page',
98
+ inputSchema: {
99
+ appName: z.string().describe('The App name'),
100
+ appInstanceId: z.string().describe('The App Instance ID'),
101
+ pageId: z.string().describe('The Page ID'),
102
+ newAreaId: z.string().describe('The new Area ID'),
103
+ newPosition: z.number().optional().describe('The new position within the Area (optional, default: 0)')
104
+ }
105
+ }, (0, _movePortalAppOnPage.default)(req));
106
+ return server;
107
+ };
108
+ exports.default = _default;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _utils = require("../utils");
8
+ var _default = req => async ({
9
+ appName,
10
+ pageId,
11
+ areaId,
12
+ position = 0,
13
+ overrideAppConfig = {}
14
+ }) => {
15
+ const {
16
+ pluginContext
17
+ } = req;
18
+ const {
19
+ services,
20
+ loggerFactory
21
+ } = pluginContext;
22
+ const logger = loggerFactory('mashroom.mcp');
23
+ const portalService = services.portal.service;
24
+ const securityService = services.security.service;
25
+ logger.info('Executing add-portal-app-to-page');
26
+ const page = await portalService.getPage(pageId);
27
+ if (!page) {
28
+ return {
29
+ content: [{
30
+ type: 'text',
31
+ text: `Error: No page with ID "${pageId}" found.`
32
+ }]
33
+ };
34
+ }
35
+ const {
36
+ site,
37
+ pageRef
38
+ } = await (0, _utils.determineSiteAndPageRef)(pageId, pluginContext);
39
+ const serverUrl = (0, _utils.determineServerUrl)(req);
40
+ const pageUrl = `${serverUrl}${site?.path}${pageRef?.friendlyUrl}`;
41
+ const portalApp = portalService.getPortalApps().find(a => a.name === appName);
42
+ if (!portalApp) {
43
+ return {
44
+ content: [{
45
+ type: 'text',
46
+ text: `Error: No Portal App with name "${appName}" found.`
47
+ }]
48
+ };
49
+ }
50
+ const portalAppInstance = {
51
+ pluginName: appName,
52
+ instanceId: crypto.randomUUID()
53
+ };
54
+ const actualPos = (0, _utils.insertAppInstance)(page, portalAppInstance, areaId, position);
55
+ await portalService.updatePage(page);
56
+ const appConfig = {
57
+ ...(portalApp.defaultAppConfig ?? {}),
58
+ ...(overrideAppConfig ?? {})
59
+ };
60
+ const appInstance = {
61
+ ...portalAppInstance,
62
+ appConfig
63
+ };
64
+ await portalService.insertPortalAppInstance(appInstance);
65
+ if (portalApp.defaultRestrictViewToRoles && Array.isArray(portalApp.defaultRestrictViewToRoles) && portalApp.defaultRestrictViewToRoles.length > 0) {
66
+ await securityService.updateResourcePermission(req, {
67
+ type: 'Portal-App',
68
+ key: (0, _utils.getPortalAppResourceKey)(appName, appInstance.instanceId),
69
+ permissions: [{
70
+ permissions: ['View'],
71
+ roles: portalApp.defaultRestrictViewToRoles || []
72
+ }]
73
+ });
74
+ }
75
+ return {
76
+ content: [{
77
+ type: 'text',
78
+ text: `Success: Portal App "${appName}" added to page "${pageId}" at position ${actualPos} in area "${areaId}. App Instance ID: ${appInstance.instanceId}. Full page URL: ${pageUrl}`
79
+ }]
80
+ };
81
+ };
82
+ exports.default = _default;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _utils = require("../utils");
8
+ var _default = req => async () => {
9
+ const {
10
+ pluginContext
11
+ } = req;
12
+ const {
13
+ services,
14
+ loggerFactory
15
+ } = pluginContext;
16
+ const logger = loggerFactory('mashroom.mcp');
17
+ const pluginService = services.core.pluginService;
18
+ logger.info('Executing list-registered-portal-apps');
19
+ const apps = pluginService.getPlugins().filter(p => p.type === 'portal-app2' || p.type === 'portal-app');
20
+ const pluginPackages = pluginService.getPotentialPluginPackages();
21
+ const lines = apps.map((a, idx) => {
22
+ const {
23
+ config,
24
+ type,
25
+ pluginDefinition
26
+ } = a;
27
+ const {
28
+ defaultConfig
29
+ } = a.pluginDefinition;
30
+ const fullConfig = {
31
+ ...defaultConfig,
32
+ ...config
33
+ };
34
+ const version = type === 'portal-app2' ? 2 : 1;
35
+ const title = version === 2 ? fullConfig.title : pluginDefinition.title;
36
+ const category = version === 2 ? fullConfig.category : pluginDefinition.category;
37
+ const description = fullConfig.description || pluginDefinition.description;
38
+ let source = a.pluginPackage.pluginPackageUrl.toString();
39
+ if (pluginPackages.find(pp => pp.url.toString() === source)?.scannerName === 'Mashroom Remote Package Scanner Kubernetes') {
40
+ source += ' (Kubernetes)';
41
+ }
42
+ return `${idx + 1}. Name: ${a.name}, Title: ${(0, _utils.serializeI18NString)(title, pluginContext)}, Description: ${(0, _utils.serializeI18NString)(description, pluginContext)}, Category: ${category}, Source: ${source}, Status: ${a.status}, config: ${JSON.stringify(fullConfig)}`;
43
+ });
44
+ return {
45
+ content: [{
46
+ type: 'text',
47
+ text: `
48
+ Registered Portal Apps (${lines.length}):
49
+
50
+ ${lines.join('\n')}
51
+ `
52
+ }]
53
+ };
54
+ };
55
+ exports.default = _default;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _utils = require("../utils");
8
+ var _default = req => async ({
9
+ appName,
10
+ appInstanceId,
11
+ pageId,
12
+ newAreaId,
13
+ newPosition = 0
14
+ }) => {
15
+ const {
16
+ pluginContext
17
+ } = req;
18
+ const {
19
+ services,
20
+ loggerFactory
21
+ } = pluginContext;
22
+ const logger = loggerFactory('mashroom.mcp');
23
+ const portalService = services.portal.service;
24
+ logger.info('Executing move-portal-app-on-page');
25
+ const page = await portalService.getPage(pageId);
26
+ if (!page) {
27
+ return {
28
+ content: [{
29
+ type: 'text',
30
+ text: `Error: No page with ID "${pageId}" found.`
31
+ }]
32
+ };
33
+ }
34
+ const areaId = Object.keys(page.portalApps ?? {}).find(areaId => page.portalApps?.[areaId]?.some(app => app.instanceId === appInstanceId));
35
+ let portalAppInstancePosition = -1;
36
+ if (areaId) {
37
+ portalAppInstancePosition = page.portalApps[areaId].findIndex(app => app.instanceId === appInstanceId);
38
+ }
39
+ if (portalAppInstancePosition === -1) {
40
+ return {
41
+ content: [{
42
+ type: 'text',
43
+ text: `Error: No Instance of Portal App ${appName} IO "${appInstanceId}" found.`
44
+ }]
45
+ };
46
+ }
47
+ const portalAppInstance = page.portalApps[areaId].splice(portalAppInstancePosition, 1)[0];
48
+ const actualPos = (0, _utils.insertAppInstance)(page, portalAppInstance, newAreaId, newPosition);
49
+ await portalService.updatePage(page);
50
+ return {
51
+ content: [{
52
+ type: 'text',
53
+ text: `Success: Portal App "${appName}" with Instance ID ${appInstanceId} moved to position ${actualPos} in area "${newAreaId}"`
54
+ }]
55
+ };
56
+ };
57
+ exports.default = _default;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _utils = require("../utils");
8
+ const WAIT_FOR_REGISTRATION_SUCCESS_SEC_DEFAULT = 20;
9
+ var _default = req => async ({
10
+ url,
11
+ waitForSec = WAIT_FOR_REGISTRATION_SUCCESS_SEC_DEFAULT
12
+ }) => {
13
+ const {
14
+ pluginContext
15
+ } = req;
16
+ const {
17
+ services,
18
+ loggerFactory
19
+ } = pluginContext;
20
+ const logger = loggerFactory('mashroom.mcp');
21
+ const remoteScannerService = services.remotePackageScanner?.service;
22
+ logger.info('Executing register-remote-portal-app');
23
+ if (!remoteScannerService) {
24
+ return {
25
+ content: [{
26
+ type: 'text',
27
+ text: 'Error: This tool only works if the plugin @mashroom/mashroom-remote-package-scanner is installed.'
28
+ }]
29
+ };
30
+ }
31
+ let targetURL;
32
+ try {
33
+ targetURL = new URL(url);
34
+ } catch (e) {
35
+ return {
36
+ content: [{
37
+ type: 'text',
38
+ text: `Error: Invalid url ${url}. Error: ${e.message}.`
39
+ }]
40
+ };
41
+ }
42
+ if (!targetURL.protocol.startsWith('http')) {
43
+ return {
44
+ content: [{
45
+ type: 'text',
46
+ text: 'Error: Invalid protocol, only http and https supported!'
47
+ }]
48
+ };
49
+ }
50
+
51
+ // Check if the package is already registered
52
+ const alreadyRegisteredPackage = (0, _utils.findPluginPackage)(targetURL, pluginContext);
53
+ if (alreadyRegisteredPackage) {
54
+ let statusText = alreadyRegisteredPackage.foundPlugins?.length ? `the following Apps have been found: ${alreadyRegisteredPackage.foundPlugins.join(', ')}` : 'no Apps have been found';
55
+ if (alreadyRegisteredPackage.updateErrors?.length) {
56
+ statusText += ` and the following errors occurred: ${alreadyRegisteredPackage.updateErrors.join(', ')}`;
57
+ }
58
+ return {
59
+ content: [{
60
+ type: 'text',
61
+ text: `Error: The URL ${url} is already registered and ${statusText}.`
62
+ }]
63
+ };
64
+ }
65
+ try {
66
+ const start = Date.now();
67
+ await remoteScannerService.addOrUpdatePackageUrl(req, new URL(url));
68
+ let newlyRegisteredPluginPackage;
69
+ while (!newlyRegisteredPluginPackage?.foundPlugins?.length && Date.now() - start < waitForSec * 1000) {
70
+ newlyRegisteredPluginPackage = (0, _utils.findPluginPackage)(targetURL, pluginContext);
71
+ await new Promise(resolve => setTimeout(resolve, 1000));
72
+ }
73
+ if (newlyRegisteredPluginPackage?.foundPlugins?.length) {
74
+ return {
75
+ content: [{
76
+ type: 'text',
77
+ text: `Success. The following new Apps have been registered: ${newlyRegisteredPluginPackage.foundPlugins.join(', ')}`
78
+ }]
79
+ };
80
+ } else {
81
+ return {
82
+ content: [{
83
+ type: 'text',
84
+ text: `Error: No new Apps registered on URL ${url} after ${waitForSec} seconds!`
85
+ }]
86
+ };
87
+ }
88
+ } catch (e) {
89
+ return {
90
+ content: [{
91
+ type: 'text',
92
+ text: `Error: Registering the URL failed with: ${e.message}`
93
+ }]
94
+ };
95
+ }
96
+ };
97
+ exports.default = _default;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _default = req => async ({
8
+ appName,
9
+ appInstanceId,
10
+ pageId
11
+ }) => {
12
+ const {
13
+ pluginContext
14
+ } = req;
15
+ const {
16
+ services,
17
+ loggerFactory
18
+ } = pluginContext;
19
+ const logger = loggerFactory('mashroom.mcp');
20
+ const portalService = services.portal.service;
21
+ logger.info('Executing remove-portal-app-from-page');
22
+ const page = await portalService.getPage(pageId);
23
+ if (!page) {
24
+ return {
25
+ content: [{
26
+ type: 'text',
27
+ text: `Error: No page with ID "${pageId}" found.`
28
+ }]
29
+ };
30
+ }
31
+ const areaId = Object.keys(page.portalApps ?? {}).find(areaId => page.portalApps?.[areaId]?.some(app => app.instanceId === appInstanceId));
32
+ let portalAppInstancePosition = -1;
33
+ if (areaId) {
34
+ portalAppInstancePosition = page.portalApps[areaId].findIndex(app => app.instanceId === appInstanceId);
35
+ }
36
+ if (portalAppInstancePosition === -1) {
37
+ return {
38
+ content: [{
39
+ type: 'text',
40
+ text: `Error: No Instance of Portal App ${appName} IO "${appInstanceId}" found.`
41
+ }]
42
+ };
43
+ }
44
+ page.portalApps[areaId].splice(portalAppInstancePosition, 1);
45
+ await portalService.updatePage(page);
46
+ await portalService.deletePortalAppInstance(req, appName, appInstanceId);
47
+ return {
48
+ content: [{
49
+ type: 'text',
50
+ text: `Success: Portal App Instance "${appInstanceId}" removed from page "${pageId}"`
51
+ }]
52
+ };
53
+ };
54
+ exports.default = _default;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _utils = require("../utils");
8
+ var _default = req => async ({
9
+ url
10
+ }) => {
11
+ const {
12
+ pluginContext
13
+ } = req;
14
+ const {
15
+ services,
16
+ loggerFactory
17
+ } = pluginContext;
18
+ const logger = loggerFactory('mashroom.mcp');
19
+ const remoteScannerService = services.remotePackageScanner?.service;
20
+ logger.info('Executing unregister-remote-portal-app');
21
+ if (!remoteScannerService) {
22
+ return {
23
+ content: [{
24
+ type: 'text',
25
+ text: 'Error: This tool only works if the plugin @mashroom/mashroom-remote-package-scanner is installed.'
26
+ }]
27
+ };
28
+ }
29
+ let targetURL;
30
+ try {
31
+ targetURL = new URL(url);
32
+ } catch (e) {
33
+ return {
34
+ content: [{
35
+ type: 'text',
36
+ text: `Error: Invalid url ${url}. Error: ${e.message}.`
37
+ }]
38
+ };
39
+ }
40
+ if (!targetURL.protocol.startsWith('http')) {
41
+ return {
42
+ content: [{
43
+ type: 'text',
44
+ text: 'Error: Invalid protocol, only http and https supported!'
45
+ }]
46
+ };
47
+ }
48
+
49
+ // Check if the package is actually registered
50
+ const registeredPackage = (0, _utils.findPluginPackage)(targetURL, pluginContext);
51
+ if (!registeredPackage) {
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: `Error: The URL ${url} is not registered at the moment!`
56
+ }]
57
+ };
58
+ }
59
+ remoteScannerService.removePackageUrl(req, targetURL);
60
+ return {
61
+ content: [{
62
+ type: 'text',
63
+ text: `Success. The following Apps have been unregistered: ${registeredPackage.foundPlugins?.join(', ')}`
64
+ }]
65
+ };
66
+ };
67
+ exports.default = _default;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _utils = require("../utils");
8
+ var _default = req => async ({
9
+ siteId
10
+ }) => {
11
+ const {
12
+ pluginContext
13
+ } = req;
14
+ const {
15
+ services,
16
+ loggerFactory
17
+ } = pluginContext;
18
+ const logger = loggerFactory('mashroom.mcp');
19
+ const portalService = services.portal.service;
20
+ logger.info('Executing list-portal-pages with siteId:', siteId);
21
+ const serverUrl = (0, _utils.determineServerUrl)(req);
22
+ const sites = (await portalService.getSites()).filter(s => !siteId || s.siteId === siteId);
23
+ let lines = [];
24
+ for (const site of sites) {
25
+ const {
26
+ path: sitePath,
27
+ pages
28
+ } = site;
29
+ const allPages = [];
30
+ const addPages = async (page, parentPageId) => {
31
+ const fullUrl = `${serverUrl}${sitePath}${page.friendlyUrl}`;
32
+ allPages.push({
33
+ ...page,
34
+ parentPageId,
35
+ fullUrl
36
+ });
37
+ if (page.subPages) {
38
+ page.subPages.forEach(p => addPages(p, page.pageId));
39
+ }
40
+ };
41
+ pages.forEach(p => addPages(p, null));
42
+ const pageLines = allPages.map((p, idx) => `${idx + 1}. Page ID: ${p.pageId}, Parent Page ID: ${p.parentPageId ?? '(none)'}, Friendly URL: ${p.friendlyUrl}, Full URL: ${p.fullUrl}, Title: ${(0, _utils.serializeI18NString)(p.title, pluginContext)}, Number sub pages: ${p.subPages?.length ?? 0}, Hidden: ${p.hidden ? 'yes' : 'no'}`);
43
+ lines.push(`Site "${site.siteId}" pages (${allPages.length}):\n`);
44
+ lines.push(...pageLines);
45
+ lines.push('\n');
46
+ }
47
+ return {
48
+ content: [{
49
+ type: 'text',
50
+ text: lines.join('\n')
51
+ }]
52
+ };
53
+ };
54
+ exports.default = _default;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _promises = require("fs/promises");
8
+ var _utils = require("../utils");
9
+ var _default = req => async ({
10
+ pageId
11
+ }) => {
12
+ const {
13
+ pluginContext
14
+ } = req;
15
+ const {
16
+ services,
17
+ loggerFactory
18
+ } = pluginContext;
19
+ const logger = loggerFactory('mashroom.mcp');
20
+ const portalService = services.portal.service;
21
+ logger.info('Executing portal-page-details with pageId:', pageId);
22
+ const page = await portalService.getPage(pageId);
23
+ if (!page) {
24
+ return {
25
+ content: [{
26
+ type: 'text',
27
+ text: `No page with ID "${pageId}" found.`
28
+ }]
29
+ };
30
+ }
31
+ const {
32
+ site,
33
+ pageRef
34
+ } = await (0, _utils.determineSiteAndPageRef)(pageId, pluginContext);
35
+ const serverUrl = (0, _utils.determineServerUrl)(req);
36
+ const pageUrl = `${serverUrl}${site?.path}${pageRef?.friendlyUrl}`;
37
+ const layoutName = page.layout ?? site?.defaultLayout;
38
+ const layoutAreaIds = [];
39
+ if (layoutName) {
40
+ const layout = portalService.getLayouts().find(p => p.name === layoutName);
41
+ if (layout) {
42
+ try {
43
+ const layoutTemplate = await (0, _promises.readFile)(layout.layoutPath, {
44
+ encoding: 'utf-8'
45
+ });
46
+ Array.from(layoutTemplate.matchAll(/ id=["'](.+)["']/g)).forEach(match => layoutAreaIds.push(match[1]));
47
+ } catch (e) {
48
+ logger.warn(`Error reading layout file: ${layout.layoutPath}`, e);
49
+ }
50
+ }
51
+ }
52
+ const appLines = Object.keys(page.portalApps ?? {}).flatMap((areaCode, idx) => page.portalApps[areaCode].map(app => `${idx + 1}. Name: ${app.pluginName}, Instance ID: ${app.instanceId}, Area ID: ${areaCode}`));
53
+ return {
54
+ content: [{
55
+ type: 'text',
56
+ text: `
57
+ Page "${pageId}" Details:
58
+
59
+ Title: ${(0, _utils.serializeI18NString)(pageRef?.title, pluginContext)}
60
+ Description: ${page.description}
61
+ FriendlyUrl: ${pageRef?.friendlyUrl ?? '(none)'}
62
+ Full URL: ${pageUrl}
63
+ Layout: ${layoutName ?? '(none)'}
64
+ Layout Area IDs: ${layoutAreaIds.join(', ')}
65
+ Apps on the Page:
66
+ ${appLines.join('\n')}
67
+ `
68
+ }]
69
+ };
70
+ };
71
+ exports.default = _default;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _default = pluginContext => async () => {
8
+ const {
9
+ serverInfo,
10
+ serverConfig,
11
+ services,
12
+ loggerFactory
13
+ } = pluginContext;
14
+ const logger = loggerFactory('mashroom.mcp');
15
+ const i18nService = services.i18n.service;
16
+ const defaultLanguage = i18nService.defaultLanguage;
17
+ const availableLanguages = i18nService.availableLanguages;
18
+ logger.info('Executing server-info');
19
+ return {
20
+ content: [{
21
+ type: 'text',
22
+ text: `
23
+ Mashroom Server is a Microfrontend Integration Platform.
24
+
25
+ Mashroom Portal is the component that can be used to build web pages consisting of multiple Microfrontends.
26
+
27
+ The Microfrontends can be developed in any framework and can run remotely on a different server.
28
+
29
+ Mashroom Portal is organized in Sites which can have multiple Pages which build a tree structure.
30
+
31
+ Every Page has a configurable Layout with multiple areas where Microfrontends can be placed.
32
+
33
+ Details about this Mashroom Server:
34
+
35
+ Server Name: ${serverConfig.name}
36
+ Server Version: ${serverInfo.version}
37
+ Default language: ${defaultLanguage}
38
+ Available languages: ${availableLanguages.join(', ')}
39
+ `
40
+ }]
41
+ };
42
+ };
43
+ exports.default = _default;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _utils = require("../utils");
8
+ var _default = req => async () => {
9
+ const {
10
+ pluginContext
11
+ } = req;
12
+ const {
13
+ services,
14
+ loggerFactory
15
+ } = pluginContext;
16
+ const logger = loggerFactory('mashroom.mcp');
17
+ const portalService = services.portal.service;
18
+ logger.info('Executing list-portal-sites');
19
+ const sites = await portalService.getSites();
20
+ const serverUrl = (0, _utils.determineServerUrl)(req);
21
+ const lines = sites.map((s, idx) => {
22
+ const fullUrl = `${serverUrl}${s.path}`;
23
+ return `${idx + 1}. Site ID: ${s.siteId}, Path: ${s.path}, Full URL: ${fullUrl}, Title: ${(0, _utils.serializeI18NString)(s.title, pluginContext)}, Number pages: ${s.pages.length}, Default Theme: ${s.defaultTheme ?? '(none)'}, Default Layout: ${s.defaultLayout ?? '(none)'}`;
24
+ });
25
+ return {
26
+ content: [{
27
+ type: 'text',
28
+ text: `
29
+ Sites (${sites.length}):
30
+
31
+ ${lines.join('\n')}
32
+ `
33
+ }]
34
+ };
35
+ };
36
+ exports.default = _default;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.serializeI18NString = exports.insertAppInstance = exports.getPortalAppResourceKey = exports.findPluginPackage = exports.determineSiteAndPageRef = exports.determineServerUrl = void 0;
7
+ const findPluginPackage = (url, pluginContext) => {
8
+ const pluginService = pluginContext.services.core.pluginService;
9
+ return pluginService.getPotentialPluginPackages().find(pp => pp.url.toString() === url.toString());
10
+ };
11
+ exports.findPluginPackage = findPluginPackage;
12
+ const determinePortalBasePath = req => {
13
+ const portalPlugin = req.pluginContext.services.core.pluginService.getPlugins().find(p => p.name === 'Mashroom Portal WebApp');
14
+ if (portalPlugin) {
15
+ return portalPlugin.config?.path ?? '/portal';
16
+ }
17
+ return '/portal';
18
+ };
19
+ const determineServerUrl = req => {
20
+ const portalBasePath = determinePortalBasePath(req);
21
+ const forwardedHost = req.headers['x-forwarded-host'];
22
+ if (forwardedHost) {
23
+ const hostname = forwardedHost.split(',')[0];
24
+ const proto = req.headers['x-forwarded-proto']?.split(',')[0] ?? 'http';
25
+ const port = req.headers['x-forwarded-port']?.split(',')[0];
26
+ return `${proto}://${hostname}${port ? `:${port}` : ''}${portalBasePath}`;
27
+ }
28
+ return `http://${req.host}${portalBasePath}`;
29
+ };
30
+ exports.determineServerUrl = determineServerUrl;
31
+ const determineSiteAndPageRef = async (pageId, pluginContext) => {
32
+ const portalService = pluginContext.services.portal.service;
33
+ const sites = await portalService.getSites();
34
+ let pageRef;
35
+ let site;
36
+ for (const s of sites) {
37
+ pageRef = await portalService.findPageRefByPageId(s, pageId);
38
+ if (pageRef) {
39
+ site = s;
40
+ break;
41
+ }
42
+ }
43
+ return {
44
+ site,
45
+ pageRef
46
+ };
47
+ };
48
+ exports.determineSiteAndPageRef = determineSiteAndPageRef;
49
+ const insertAppInstance = (page, portalAppInstance, areaId, position) => {
50
+ page.portalApps = page.portalApps || {};
51
+ const areaAppInstances = page.portalApps[areaId] = page.portalApps[areaId] || [];
52
+ let actualPos = areaAppInstances.length;
53
+ if (typeof position === 'number' && position >= 0 && position < areaAppInstances.length) {
54
+ actualPos = position;
55
+ }
56
+ areaAppInstances.splice(actualPos, 0, portalAppInstance);
57
+ return actualPos;
58
+ };
59
+ exports.insertAppInstance = insertAppInstance;
60
+ const getPortalAppResourceKey = (pluginName, instanceId) => {
61
+ return `${pluginName}_${instanceId || 'global'}`;
62
+ };
63
+ exports.getPortalAppResourceKey = getPortalAppResourceKey;
64
+ const serializeI18NString = (i18nString, pluginContext) => {
65
+ if (!i18nString) {
66
+ return '';
67
+ }
68
+ if (typeof i18nString === 'string') {
69
+ return i18nString;
70
+ }
71
+ const i18nService = pluginContext.services.i18n.service;
72
+ const defaultLanguage = i18nService.defaultLanguage;
73
+ const defaultMessageKey = i18nString[defaultLanguage] ? defaultLanguage : Object.keys(i18nString)[0];
74
+ const defaultMessage = i18nString[defaultMessageKey];
75
+ const translations = Object.keys(i18nString).filter(lang => lang !== defaultLanguage).map(lang => `${lang}: ${i18nString[lang]}`).join(', ');
76
+ return `${defaultMessage} (Translations: ${translations})`;
77
+ };
78
+ exports.serializeI18NString = serializeI18NString;
Binary file
Binary file
package/mashroom.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "../../../node_modules/@mashroom/mashroom-json-schemas/schemas/mashroom-plugins.json",
3
+ "devModeBuildScript": "build",
4
+ "plugins": [
5
+ {
6
+ "name": "Mashroom MCP Server",
7
+ "type": "api",
8
+ "bootstrap": "./dist/mashroom-bootstrap.js",
9
+ "requires": [
10
+ "Mashroom Security Services",
11
+ "Mashroom Portal Services",
12
+ "Mashroom Internationalization Services"
13
+ ],
14
+ "defaultConfig": {
15
+ "path": "/mcp"
16
+ }
17
+ }
18
+ ]
19
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@mashroom/mashroom-mcp-server",
3
+ "description": "Mashroom MCP Server",
4
+ "homepage": "https://www.mashroom-server.com",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/nonblocking/mashroom.git"
8
+ },
9
+ "license": "MIT",
10
+ "version": "3.0.0-alpha.0",
11
+ "files": [
12
+ "dist/**",
13
+ "examples/**",
14
+ "mashroom.json"
15
+ ],
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.29.0",
18
+ "zod": "^4.4.3"
19
+ },
20
+ "devDependencies": {
21
+ "@mashroom/mashroom": "3.0.0-alpha.0",
22
+ "@mashroom/mashroom-i18n": "3.0.0-alpha.0",
23
+ "@mashroom/mashroom-portal": "3.0.0-alpha.0",
24
+ "@mashroom/mashroom-remote-package-scanner": "3.0.0-alpha.0"
25
+ },
26
+ "jest": {
27
+ "testEnvironment": "node",
28
+ "roots": [
29
+ "<rootDir>/test"
30
+ ],
31
+ "testRegex": "(\\.(test|spec))\\.ts",
32
+ "transform": {
33
+ "^.+\\.ts$": "ts-jest"
34
+ },
35
+ "reporters": [
36
+ "default",
37
+ "jest-junit"
38
+ ]
39
+ },
40
+ "jest-junit": {
41
+ "outputDirectory": "./test-reports"
42
+ },
43
+ "scripts": {
44
+ "lint": "eslint --fix",
45
+ "type-check": "tsc --noEmit",
46
+ "test": "jest",
47
+ "build": "babel src -d dist --extensions \".ts\"",
48
+ "dev": "nodemon --watch src --ext ts --exec \"npm run build && node dist/dev-server.js\"",
49
+ "inspector": "npx @modelcontextprotocol/inspector"
50
+ }
51
+ }