Skip to content

Commit 8a47060

Browse files
committed
feat: marketplace-mcp server
1 parent b7164eb commit 8a47060

6 files changed

Lines changed: 288 additions & 32 deletions

File tree

automation/e2e-mcp/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
"start": "node dist/index.js"
1515
},
1616
"dependencies": {
17-
"@modelcontextprotocol/sdk": "^1.10.2",
18-
"zod": "^3.24.2"
17+
"@modelcontextprotocol/sdk": "^1.27.1",
18+
"zod": "^3.25.76"
1919
},
2020
"devDependencies": {
2121
"@types/node": "^22.0.0",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@mendix/marketplace-mcp",
3+
"version": "0.1.0",
4+
"description": "MCP server for the Mendix Marketplace Content API – browse and query Marketplace content from your Agent.",
5+
"bin": {
6+
"marketplace-mcp": "dist/index.js"
7+
},
8+
"type": "module",
9+
"main": "dist/index.js",
10+
"scripts": {
11+
"build": "tsc",
12+
"dev": "tsc --watch",
13+
"postinstall": "tsc",
14+
"start": "node dist/index.js"
15+
},
16+
"dependencies": {
17+
"@modelcontextprotocol/sdk": "^1.27.1",
18+
"zod": "^3.25.76"
19+
},
20+
"devDependencies": {
21+
"@types/node": "^22.0.0",
22+
"typescript": "^5.7.3"
23+
}
24+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env node
2+
/**
3+
* marketplace-mcp: MCP server for the Mendix Marketplace Content API
4+
*
5+
*
6+
* Transport: stdio
7+
*
8+
* Required environment variable:
9+
* MX_PAT – Personal Access Token with the mx:marketplace-content:read scope
10+
* Generate one at: https://sprintr.home.mendix.com/link/myprofile → API Keys
11+
*/
12+
13+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15+
import { z } from "zod";
16+
17+
// ---------------------------------------------------------------------------
18+
// Config / auth
19+
// ---------------------------------------------------------------------------
20+
21+
const BASE_URL = "https://marketplace-api.mendix.com/v1";
22+
23+
function getPat(): string {
24+
const pat = process.env.MX_PAT;
25+
if (!pat) {
26+
throw new Error(
27+
"MX_PAT environment variable is not set. Generate a PAT at https://sprintr.home.mendix.com/link/myprofile"
28+
);
29+
}
30+
return pat;
31+
}
32+
33+
function authHeader(): Record<string, string> {
34+
return { Authorization: `MxToken ${getPat()}` };
35+
}
36+
37+
// ---------------------------------------------------------------------------
38+
// HTTP helper
39+
// ---------------------------------------------------------------------------
40+
41+
type Params = Record<string, string | number | boolean | undefined>;
42+
43+
function buildQuery(params: Params): string {
44+
const entries = Object.entries(params).filter(
45+
(e): e is [string, string | number | boolean] => e[1] !== undefined && e[1] !== ""
46+
);
47+
if (!entries.length) return "";
48+
return "?" + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
49+
}
50+
51+
async function apiFetch<T = unknown>(path: string, params: Params = {}): Promise<T> {
52+
const url = `${BASE_URL}/${path}${buildQuery(params)}`;
53+
const res = await fetch(url, {
54+
headers: { Accept: "application/json", ...authHeader() }
55+
});
56+
if (!res.ok) {
57+
const body = await res.text().catch(() => "");
58+
throw new Error(`Marketplace API error ${res.status} ${res.statusText}: ${body}`);
59+
}
60+
return res.json();
61+
}
62+
63+
// ---------------------------------------------------------------------------
64+
// Types (OpenAPI schema subset)
65+
// ---------------------------------------------------------------------------
66+
67+
interface ContentVersion {
68+
name: string;
69+
versionId: string;
70+
versionNumber: string;
71+
minSupportedMendixVersion: string;
72+
publicationDate: string;
73+
releaseNotes?: string;
74+
}
75+
76+
interface SpecificContent {
77+
contentId: number;
78+
publisher: string;
79+
type: string;
80+
categories?: Array<{ name: string }>;
81+
supportCategory?: string;
82+
licenseUrl?: string;
83+
isPrivate: boolean;
84+
latestVersion?: ContentVersion;
85+
}
86+
87+
interface ContentVersionList {
88+
items?: ContentVersion[];
89+
}
90+
91+
// ---------------------------------------------------------------------------
92+
// Formatters
93+
// ---------------------------------------------------------------------------
94+
95+
function fmtContent(c: SpecificContent): string {
96+
const cats = c.categories?.map(x => x.name).join(", ") || "—";
97+
const latest = c.latestVersion;
98+
return [
99+
`contentId : ${c.contentId}`,
100+
`name : ${latest?.name ?? "—"}`,
101+
`publisher : ${c.publisher}`,
102+
`type : ${c.type}`,
103+
`categories : ${cats}`,
104+
`support : ${c.supportCategory ?? "—"}`,
105+
`private : ${c.isPrivate}`,
106+
`licenseUrl : ${c.licenseUrl ?? "—"}`,
107+
`latestVersion:`,
108+
` versionId : ${latest?.versionId ?? "—"}`,
109+
` number : ${latest?.versionNumber ?? "—"}`,
110+
` minMxVer : ${latest?.minSupportedMendixVersion ?? "—"}`,
111+
` published : ${latest?.publicationDate ?? "—"}`
112+
].join("\n");
113+
}
114+
115+
function fmtVersion(v: ContentVersion, index: number): string {
116+
return [
117+
`${index + 1}. ${v.versionNumber} (${v.publicationDate})`,
118+
` versionId : ${v.versionId}`,
119+
` minMxVer : ${v.minSupportedMendixVersion}`,
120+
...(v.releaseNotes ? [` notes : ${v.releaseNotes.split("\n").slice(0, 3).join(" | ")}`] : [])
121+
].join("\n");
122+
}
123+
124+
// ---------------------------------------------------------------------------
125+
// MCP Server
126+
// ---------------------------------------------------------------------------
127+
128+
const server = new McpServer({
129+
name: "marketplace-mcp",
130+
version: "0.1.0",
131+
description: "Query Mendix Marketplace content and versions via the Content API"
132+
});
133+
134+
// ── Tool 1: get_content ────────────────────────────────────────────────────
135+
136+
server.registerTool(
137+
"get_content",
138+
{
139+
description:
140+
"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.",
141+
inputSchema: {
142+
contentId: z.number().int().positive().describe("Numeric content ID from the Marketplace URL")
143+
}
144+
},
145+
async ({ contentId }) => {
146+
const data = await apiFetch<SpecificContent>(`content/${contentId}`);
147+
return {
148+
content: [{ type: "text", text: fmtContent(data) }]
149+
};
150+
}
151+
);
152+
153+
// ── Tool 2: get_content_versions ──────────────────────────────────────────
154+
155+
server.registerTool(
156+
"get_content_versions",
157+
{
158+
description:
159+
"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.",
160+
inputSchema: {
161+
contentId: z.number().int().positive().describe("Numeric content ID"),
162+
versionId: z.string().uuid().optional().describe("UUID of a specific published version"),
163+
supportedMendixVersion: z
164+
.string()
165+
.optional()
166+
.describe("Return the most recent version compatible with this Studio Pro version, e.g. '10.6.1'"),
167+
publishedSince: z
168+
.string()
169+
.optional()
170+
.describe("Only versions published on or after this date, format: yyyy-MM-dd"),
171+
limit: z.number().int().min(1).max(20).optional().describe("Max results (default 10, max 20)"),
172+
offset: z.number().int().min(0).optional().describe("Zero-based page offset (default 0)")
173+
}
174+
},
175+
async ({ contentId, versionId, supportedMendixVersion, publishedSince, limit = 10, offset = 0 }) => {
176+
const data = await apiFetch<ContentVersionList>(`content/${contentId}/versions`, {
177+
versionId,
178+
supportedMendixVersion,
179+
publishedSince,
180+
limit,
181+
offset
182+
});
183+
const items = data.items ?? [];
184+
185+
if (items.length === 0) {
186+
return {
187+
content: [
188+
{ type: "text", text: `No versions found for contentId ${contentId} with the given filters.` }
189+
]
190+
};
191+
}
192+
193+
const lines = items.map((v, i) => fmtVersion(v, i + offset));
194+
return {
195+
content: [
196+
{
197+
type: "text",
198+
text: `${items.length} version(s) for contentId ${contentId} (offset ${offset}):\n\n${lines.join("\n\n")}`
199+
}
200+
]
201+
};
202+
}
203+
);
204+
205+
// ---------------------------------------------------------------------------
206+
// Start server
207+
// ---------------------------------------------------------------------------
208+
209+
async function main(): Promise<void> {
210+
const transport = new StdioServerTransport();
211+
await server.connect(transport);
212+
}
213+
214+
main().catch(err => {
215+
console.error(err);
216+
process.exit(1);
217+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "Node16",
5+
"moduleResolution": "Node16",
6+
"outDir": "./dist",
7+
"rootDir": "./src",
8+
"strict": true,
9+
"esModuleInterop": true,
10+
"skipLibCheck": true,
11+
"declaration": true,
12+
"declarationMap": true,
13+
"sourceMap": true
14+
},
15+
"include": ["src/**/*"],
16+
"exclude": ["node_modules", "dist"]
17+
}

automation/utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@
5252
"peggy": "^1.2.0",
5353
"shelljs": "^0.8.5",
5454
"ts-node": "^10.9.1",
55-
"zod": "^3.25.67"
55+
"zod": "^3.25.76"
5656
}
5757
}

0 commit comments

Comments
 (0)