From 3a646a16901ebb1fe97e7d2753de479fd2d81dc0 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Mon, 20 Apr 2026 15:14:15 +0200 Subject: [PATCH 1/2] feat: marketplace-mcp server --- automation/e2e-mcp/package.json | 4 +- automation/marketplace-mcp/package.json | 24 +++ automation/marketplace-mcp/src/index.ts | 217 +++++++++++++++++++++++ automation/marketplace-mcp/tsconfig.json | 17 ++ automation/utils/package.json | 2 +- pnpm-lock.yaml | 56 +++--- 6 files changed, 288 insertions(+), 32 deletions(-) create mode 100644 automation/marketplace-mcp/package.json create mode 100644 automation/marketplace-mcp/src/index.ts create mode 100644 automation/marketplace-mcp/tsconfig.json diff --git a/automation/e2e-mcp/package.json b/automation/e2e-mcp/package.json index 11d4e8c0c3..05accbba04 100644 --- a/automation/e2e-mcp/package.json +++ b/automation/e2e-mcp/package.json @@ -14,8 +14,8 @@ "start": "node dist/index.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.10.2", - "zod": "^3.24.2" + "@modelcontextprotocol/sdk": "^1.27.1", + "zod": "^3.25.76" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/automation/marketplace-mcp/package.json b/automation/marketplace-mcp/package.json new file mode 100644 index 0000000000..50cc1b52d0 --- /dev/null +++ b/automation/marketplace-mcp/package.json @@ -0,0 +1,24 @@ +{ + "name": "@mendix/marketplace-mcp", + "version": "0.1.0", + "description": "MCP server for the Mendix Marketplace Content API – browse and query Marketplace content from your Agent.", + "bin": { + "marketplace-mcp": "dist/index.js" + }, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "postinstall": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.3" + } +} diff --git a/automation/marketplace-mcp/src/index.ts b/automation/marketplace-mcp/src/index.ts new file mode 100644 index 0000000000..9bfbbcdef9 --- /dev/null +++ b/automation/marketplace-mcp/src/index.ts @@ -0,0 +1,217 @@ +#!/usr/bin/env node +/** + * marketplace-mcp: MCP server for the Mendix Marketplace Content API + * + * + * Transport: stdio + * + * Required environment variable: + * MX_PAT – Personal Access Token with the mx:marketplace-content:read scope + * Generate one at: https://sprintr.home.mendix.com/link/myprofile → API Keys + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +// --------------------------------------------------------------------------- +// Config / auth +// --------------------------------------------------------------------------- + +const BASE_URL = "https://marketplace-api.mendix.com/v1"; + +function getPat(): string { + const pat = process.env.MX_PAT; + if (!pat) { + throw new Error( + "MX_PAT environment variable is not set. Generate a PAT at https://sprintr.home.mendix.com/link/myprofile" + ); + } + return pat; +} + +function authHeader(): Record { + return { Authorization: `MxToken ${getPat()}` }; +} + +// --------------------------------------------------------------------------- +// HTTP helper +// --------------------------------------------------------------------------- + +type Params = Record; + +function buildQuery(params: Params): string { + const entries = Object.entries(params).filter( + (e): e is [string, string | number | boolean] => e[1] !== undefined && e[1] !== "" + ); + if (!entries.length) return ""; + return "?" + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&"); +} + +async function apiFetch(path: string, params: Params = {}): Promise { + const url = `${BASE_URL}/${path}${buildQuery(params)}`; + const res = await fetch(url, { + headers: { Accept: "application/json", ...authHeader() } + }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`Marketplace API error ${res.status} ${res.statusText}: ${body}`); + } + return res.json(); +} + +// --------------------------------------------------------------------------- +// Types (OpenAPI schema subset) +// --------------------------------------------------------------------------- + +interface ContentVersion { + name: string; + versionId: string; + versionNumber: string; + minSupportedMendixVersion: string; + publicationDate: string; + releaseNotes?: string; +} + +interface SpecificContent { + contentId: number; + publisher: string; + type: string; + categories?: Array<{ name: string }>; + supportCategory?: string; + licenseUrl?: string; + isPrivate: boolean; + latestVersion?: ContentVersion; +} + +interface ContentVersionList { + items?: ContentVersion[]; +} + +// --------------------------------------------------------------------------- +// Formatters +// --------------------------------------------------------------------------- + +function fmtContent(c: SpecificContent): string { + const cats = c.categories?.map(x => x.name).join(", ") || "—"; + const latest = c.latestVersion; + return [ + `contentId : ${c.contentId}`, + `name : ${latest?.name ?? "—"}`, + `publisher : ${c.publisher}`, + `type : ${c.type}`, + `categories : ${cats}`, + `support : ${c.supportCategory ?? "—"}`, + `private : ${c.isPrivate}`, + `licenseUrl : ${c.licenseUrl ?? "—"}`, + `latestVersion:`, + ` versionId : ${latest?.versionId ?? "—"}`, + ` number : ${latest?.versionNumber ?? "—"}`, + ` minMxVer : ${latest?.minSupportedMendixVersion ?? "—"}`, + ` published : ${latest?.publicationDate ?? "—"}` + ].join("\n"); +} + +function fmtVersion(v: ContentVersion, index: number): string { + return [ + `${index + 1}. ${v.versionNumber} (${v.publicationDate})`, + ` versionId : ${v.versionId}`, + ` minMxVer : ${v.minSupportedMendixVersion}`, + ...(v.releaseNotes ? [` notes : ${v.releaseNotes.split("\n").slice(0, 3).join(" | ")}`] : []) + ].join("\n"); +} + +// --------------------------------------------------------------------------- +// MCP Server +// --------------------------------------------------------------------------- + +const server = new McpServer({ + name: "marketplace-mcp", + version: "0.1.0", + description: "Query Mendix Marketplace content and versions via the Content API" +}); + +// ── Tool 1: get_content ──────────────────────────────────────────────────── + +server.registerTool( + "get_content", + { + description: + "Get full details for a single Marketplace content item by its numeric content ID. Content ID can be found in corresponding package.json under `marketplace.appNumber` entry.", + inputSchema: { + contentId: z.number().int().positive().describe("Numeric content ID from the Marketplace URL") + } + }, + async ({ contentId }) => { + const data = await apiFetch(`content/${contentId}`); + return { + content: [{ type: "text", text: fmtContent(data) }] + }; + } +); + +// ── Tool 2: get_content_versions ────────────────────────────────────────── + +server.registerTool( + "get_content_versions", + { + description: + "List all published versions of a Marketplace content item. Optionally filter by a specific version UUID or find the version compatible with a given Studio Pro version.", + inputSchema: { + contentId: z.number().int().positive().describe("Numeric content ID"), + versionId: z.string().uuid().optional().describe("UUID of a specific published version"), + supportedMendixVersion: z + .string() + .optional() + .describe("Return the most recent version compatible with this Studio Pro version, e.g. '10.6.1'"), + publishedSince: z + .string() + .optional() + .describe("Only versions published on or after this date, format: yyyy-MM-dd"), + limit: z.number().int().min(1).max(20).optional().describe("Max results (default 10, max 20)"), + offset: z.number().int().min(0).optional().describe("Zero-based page offset (default 0)") + } + }, + async ({ contentId, versionId, supportedMendixVersion, publishedSince, limit = 10, offset = 0 }) => { + const data = await apiFetch(`content/${contentId}/versions`, { + versionId, + supportedMendixVersion, + publishedSince, + limit, + offset + }); + const items = data.items ?? []; + + if (items.length === 0) { + return { + content: [ + { type: "text", text: `No versions found for contentId ${contentId} with the given filters.` } + ] + }; + } + + const lines = items.map((v, i) => fmtVersion(v, i + offset)); + return { + content: [ + { + type: "text", + text: `${items.length} version(s) for contentId ${contentId} (offset ${offset}):\n\n${lines.join("\n\n")}` + } + ] + }; + } +); + +// --------------------------------------------------------------------------- +// Start server +// --------------------------------------------------------------------------- + +async function main(): Promise { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/automation/marketplace-mcp/tsconfig.json b/automation/marketplace-mcp/tsconfig.json new file mode 100644 index 0000000000..98dd8325f7 --- /dev/null +++ b/automation/marketplace-mcp/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/automation/utils/package.json b/automation/utils/package.json index 6754b37c84..5f0945a7e3 100644 --- a/automation/utils/package.json +++ b/automation/utils/package.json @@ -52,6 +52,6 @@ "peggy": "^1.2.0", "shelljs": "^0.8.5", "ts-node": "^10.9.1", - "zod": "^3.25.67" + "zod": "^3.25.76" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b02c4bf42..85ec8b57d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,10 +74,26 @@ importers: automation/e2e-mcp: dependencies: '@modelcontextprotocol/sdk': - specifier: ^1.10.2 - version: 1.27.1(zod@3.25.76) + specifier: ^1.27.1 + version: 1.29.0(zod@3.25.76) zod: - specifier: ^3.24.2 + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ~22.14.0 + version: 22.14.1 + typescript: + specifier: '>5.8.0' + version: 5.9.3 + + automation/marketplace-mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.29.0(zod@3.25.76) + zod: + specifier: ^3.25.76 version: 3.25.76 devDependencies: '@types/node': @@ -218,7 +234,7 @@ importers: specifier: 10.9.2 version: 10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3) zod: - specifier: ^3.25.67 + specifier: ^3.25.76 version: 3.25.76 packages/customWidgets/calendar-custom-web: @@ -4288,8 +4304,8 @@ packages: engines: {node: '>=20'} hasBin: true - '@modelcontextprotocol/sdk@1.27.1': - resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -7395,10 +7411,6 @@ packages: htmlparser2@4.1.0: resolution: {integrity: sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -10069,10 +10081,6 @@ packages: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -12678,7 +12686,7 @@ snapshots: - tslib - utf-8-validate - '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.10(hono@4.12.4) ajv: 8.17.1 @@ -14286,7 +14294,7 @@ snapshots: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 qs: 6.15.0 @@ -15735,7 +15743,7 @@ snapshots: etag: 1.8.1 finalhandler: 2.1.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) on-finished: 2.4.1 @@ -15747,7 +15755,7 @@ snapshots: router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: @@ -15842,7 +15850,7 @@ snapshots: escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -16316,14 +16324,6 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -19610,8 +19610,6 @@ snapshots: statuses@1.5.0: {} - statuses@2.0.1: {} - statuses@2.0.2: {} stop-iteration-iterator@1.1.0: From 37e20c830e4662776ccc3195b4c53454b14a28e4 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Mon, 20 Apr 2026 16:09:53 +0200 Subject: [PATCH 2/2] chore: add mcp.json for auto discovery --- .mcp.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000000..89e716f7f7 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "marketplace-mcp": { + "type": "stdio", + "command": "node", + "args": ["./automation/marketplace-mcp/dist/index.js"], + "env": { + "MX_PAT": "${MARKETPLACE_API_TOKEN}" + } + } + } +} +