Skip to content

Commit 8dc480e

Browse files
authored
Merge pull request #30 from mia-platform/feat/marketplace-itd
feat: add marketplace tools to retrieve Item Type Definitions
2 parents 9f4b75d + 1afce0f commit 8dc480e

File tree

9 files changed

+800
-10
lines changed

9 files changed

+800
-10
lines changed

package-lock.json

Lines changed: 29 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"build": "rimraf mcp-server && tsc --build",
1414
"coverage": "c8 npm run test",
1515
"lint": "eslint --ext .ts src",
16+
"check:types": "tsc --noEmit",
1617
"local:test-client-credentials": "npm run build && npx @modelcontextprotocol/[email protected] -e MIA_PLATFORM_CLIENT_ID=\"${MIA_PLATFORM_CLIENT_ID}\" -e MIA_PLATFORM_CLIENT_SECRET=\"${MIA_PLATFORM_CLIENT_SECRET}\" -- node mcp-server start --stdio --server-host 127.0.0.1",
1718
"local:test": "npm run build && npx @modelcontextprotocol/[email protected] -- node mcp-server start --stdio --server-host 127.0.0.1",
1819
"start": "npm run build && node mcp-server start",
@@ -30,17 +31,19 @@
3031
},
3132
"homepage": "https://github.com/mia-platform/console-mcp-server#readme",
3233
"dependencies": {
33-
"@mia-platform/console-types": "^0.38.11",
34+
"@mia-platform/console-types": "^0.39.0",
3435
"@modelcontextprotocol/sdk": "^1.17.5",
3536
"commander": "^14.0.0",
3637
"fastify": "^5.5.0",
38+
"lodash-es": "^4.17.21",
3739
"undici": "^7.15.0",
3840
"dotenv": "^17.2.2",
3941
"zod": "^3.25.76"
4042
},
4143
"devDependencies": {
4244
"@eslint/js": "^9.24.0",
4345
"@stylistic/eslint-plugin": "^5.3.1",
46+
"@types/lodash-es": "^4.17.12",
4447
"@types/node": "^22.18.0",
4548
"c8": "^10.1.3",
4649
"eslint": "^9.34.0",

src/apis/client.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ suite('create service from marketplace plugin', () => {
124124
'simple-service': inputResources,
125125
},
126126
},
127+
itemTypeDefinitionRef: { namespace: 'mia-platform', name: 'plugin' },
127128
}
128129

129130
const output = await servicePayloadFromMarketplaceItem(marketplaceItem, 'simple-service', 'some-description')
@@ -193,6 +194,7 @@ suite('create service from marketplace plugin', () => {
193194
'simple-service': inputResources,
194195
},
195196
},
197+
itemTypeDefinitionRef: { namespace: 'mia-platform', name: 'plugin' },
196198
}
197199

198200
const output = await servicePayloadFromMarketplaceItem(marketplaceItem, 'simple-service', 'some-description')
@@ -330,6 +332,7 @@ suite('create service from marketplace plugin', () => {
330332
'simple-service': inputResources,
331333
},
332334
},
335+
itemTypeDefinitionRef: { namespace: 'mia-platform', name: 'plugin' },
333336
}
334337

335338
const output = await servicePayloadFromMarketplaceItem(marketplaceItem, 'simple-service', 'some-description')
@@ -435,6 +438,7 @@ suite('create service from marketplace template', () => {
435438
'simple-template': inputResources,
436439
},
437440
},
441+
itemTypeDefinitionRef: { namespace: 'mia-platform', name: 'template' },
438442
}
439443

440444
const output = await servicePayloadFromMarketplaceItem(
@@ -527,6 +531,7 @@ suite('create service from marketplace template', () => {
527531
'simple-template': inputResources,
528532
},
529533
},
534+
itemTypeDefinitionRef: { namespace: 'mia-platform', name: 'template' },
530535
}
531536

532537
const output = await servicePayloadFromMarketplaceItem(

src/apis/client.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import path from 'node:path'
1818
import { UndiciHeaders } from 'undici/types/dispatcher'
1919
import {
2020
CatalogItemRelease,
21+
CatalogItemTypeDefinition,
2122
CatalogVersionedItem,
2223
ConfigMaps,
2324
ConfigServiceSecrets,
@@ -149,6 +150,8 @@ export interface IAPIClient {
149150
listMarketplaceItems(tenantID?: string, type?: string, search?: string): Promise<Record<string, unknown>[]>
150151
marketplaceItemVersions(tenantID: string, itemID: string): Promise<CatalogItemRelease[]>
151152
marketplaceItemInfo(tenantID: string, itemID: string, version?: string): Promise<CatalogVersionedItem>
153+
listMarketplaceItemTypeDefinitions(namespace?: string, name?: string, displayName?: string): Promise<CatalogItemTypeDefinition[]>
154+
marketplaceItemTypeDefinitionInfo (tenantID: string, name: string): Promise<CatalogItemTypeDefinition>
152155
// #endregion
153156
}
154157

@@ -394,6 +397,14 @@ export class APIClient implements IAPIClient {
394397
return this.#marketplaceClient.marketplaceItemInfo(tenantID, itemID, version)
395398
}
396399

400+
async listMarketplaceItemTypeDefinitions (namespace?: string, name?: string, displayName?: string): Promise<CatalogItemTypeDefinition[]> {
401+
return this.#marketplaceClient.listMarketplaceItemTypeDefinitions(namespace, name, displayName)
402+
}
403+
404+
async marketplaceItemTypeDefinitionInfo (tenantID: string, name: string): Promise<CatalogItemTypeDefinition> {
405+
return this.#marketplaceClient.marketplaceItemTypeDefinitionInfo(tenantID, name)
406+
}
407+
397408
async #resourceToCreateFromMarketplaceItem (
398409
project: IProject,
399410
marketplaceItem: CatalogVersionedItem,
@@ -839,6 +850,8 @@ export interface APIClientMockFunctions {
839850
listMarketplaceItemsMockFn?: (tenantID?: string, type?: string, search?: string) => Promise<Record<string, unknown>[]>
840851
marketplaceItemVersionsMockFn?: (tenantID: string, itemID: string) => Promise<CatalogItemRelease[]>
841852
marketplaceItemInfoMockFn?: (tenantID: string, itemID: string, version?: string) => Promise<CatalogVersionedItem>
853+
listMarketplaceItemTypeDefinitionsMockFn?: (namespace?: string, name?: string, displayName?: string) => Promise<CatalogItemTypeDefinition[]>
854+
marketplaceItemTypeDefinitionInfoMockFn?: (tenantID: string, name: string) => Promise<CatalogItemTypeDefinition>
842855
// #endregion
843856
}
844857

@@ -1080,6 +1093,22 @@ export class APIClientMock implements IAPIClient {
10801093
return this.mocks.marketplaceItemInfoMockFn(tenantID, itemID, version)
10811094
}
10821095

1096+
async listMarketplaceItemTypeDefinitions (namespace?: string, name?: string, displayName?: string): Promise<CatalogItemTypeDefinition[]> {
1097+
if (!this.mocks.listMarketplaceItemTypeDefinitionsMockFn) {
1098+
throw new Error('listMarketplaceItemTypeDefinitionsMockFn not mocked')
1099+
}
1100+
1101+
return this.mocks.listMarketplaceItemTypeDefinitionsMockFn(namespace, name, displayName)
1102+
}
1103+
1104+
async marketplaceItemTypeDefinitionInfo (tenantID: string, name: string): Promise<CatalogItemTypeDefinition> {
1105+
if (!this.mocks.marketplaceItemTypeDefinitionInfoMockFn) {
1106+
throw new Error('marketplaceItemTypeDefinitionInfoMockFn not mocked')
1107+
}
1108+
1109+
return this.mocks.marketplaceItemTypeDefinitionInfoMockFn(tenantID, name)
1110+
}
1111+
10831112
// #endregion
10841113
}
10851114

src/apis/marketplaceClient.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
import { beforeEach, suite, test, TestContext } from 'node:test'
1717
import { MockAgent, setGlobalDispatcher } from 'undici'
1818

19+
import { CatalogItemTypeDefinition } from '@mia-platform/console-types'
1920
import { HTTPClient } from './http-client'
2021
import { internalEndpoint, MarketplaceClient, MarketplaceClientInternal } from './marketplaceClient'
2122

2223
const tenantID = 'tenantID'
2324
const itemID = 'itemid'
25+
const itdName = 'itdName'
2426
const search = 'item1'
2527
const type = 'example'
2628
const version = '1.0.1'
@@ -34,6 +36,48 @@ const publicItems = [
3436
{ itemId: 'item-id-2', name: 'item2', tenantId: 'public' },
3537
]
3638

39+
const itemTypeDefinitions: CatalogItemTypeDefinition[] = [
40+
{
41+
apiVersion: 'software-catalog.mia-platform.eu/v1',
42+
kind: 'item-type-definition',
43+
metadata: {
44+
namespace: { scope: 'tenant', id: 'mia-platform' },
45+
displayName: 'Plugin ITD',
46+
name: 'plugin',
47+
visibility: { scope: 'console' },
48+
},
49+
spec: {
50+
isVersioningSupported: true,
51+
type: 'plugin',
52+
scope: 'tenant',
53+
validation: {
54+
mechanism: 'json-schema',
55+
schema: { type: 'object' },
56+
},
57+
},
58+
__v: 1,
59+
},
60+
{
61+
apiVersion: 'software-catalog.mia-platform.eu/v1',
62+
kind: 'item-type-definition',
63+
metadata: {
64+
namespace: { scope: 'tenant', id: 'my-company' },
65+
displayName: 'Custom Workload ITD',
66+
name: 'custom-workload',
67+
visibility: { scope: 'tenant', ids: [ 'my-company' ] },
68+
},
69+
spec: {
70+
type: 'custom-workload',
71+
scope: 'tenant',
72+
validation: {
73+
mechanism: 'json-schema',
74+
schema: { type: 'object' },
75+
},
76+
},
77+
__v: 1,
78+
},
79+
]
80+
3781
suite('Marketplace Internal Client', () => {
3882
const client = MarketplaceClientInternal('', '')
3983
let agent: MockAgent
@@ -167,6 +211,62 @@ suite('Marketplace Internal Client', () => {
167211
}).reply(500, { error: 'Internal Server Error' })
168212
await t.assert.rejects(async () => await client.marketplaceItemInfo(tenantID, itemID, version), { name: 'Error' })
169213
})
214+
215+
test('list marketplace Item Type Definitions', async (t: TestContext) => {
216+
agent.get(internalEndpoint).intercept({
217+
path: '/api/marketplace/item-type-definitions',
218+
method: 'GET',
219+
query: {
220+
perPage: 200,
221+
per_page: 200,
222+
page: 1,
223+
},
224+
}).reply(200, itemTypeDefinitions)
225+
t.assert.deepEqual(await client.listMarketplaceItemTypeDefinitions(), itemTypeDefinitions)
226+
227+
agent.get(internalEndpoint).intercept({
228+
path: '/api/marketplace/item-type-definitions',
229+
method: 'GET',
230+
query: {
231+
perPage: 200,
232+
per_page: 200,
233+
page: 1,
234+
namespace: 'mia-platform,my-company',
235+
name: 'plugin,custom-workload',
236+
displayName: 'Plugin,Custom Workload',
237+
},
238+
}).reply(200, itemTypeDefinitions)
239+
t.assert.deepEqual(await client.listMarketplaceItemTypeDefinitions('mia-platform,my-company', 'plugin,custom-workload', 'Plugin,Custom Workload'), itemTypeDefinitions)
240+
})
241+
242+
test('list marketplace Item Type Definitions must throw if the API call fails', async (t: TestContext) => {
243+
agent.get(internalEndpoint).intercept({
244+
path: '/api/marketplace/item-type-definitions',
245+
method: 'GET',
246+
query: {
247+
perPage: 200,
248+
per_page: 200,
249+
page: 1,
250+
},
251+
}).reply(500, { error: 'Internal Server Error' })
252+
await t.assert.rejects(async () => await client.listMarketplaceItemTypeDefinitions(), { name: 'Error' })
253+
})
254+
255+
test('marketplace Item Type Definition info', async (t: TestContext) => {
256+
agent.get(internalEndpoint).intercept({
257+
path: `/api/tenants/${tenantID}/marketplace/item-type-definitions/${itdName}`,
258+
method: 'GET',
259+
}).reply(200, itemTypeDefinitions.at(0))
260+
t.assert.deepEqual(await client.marketplaceItemTypeDefinitionInfo(tenantID, itdName), itemTypeDefinitions.at(0))
261+
})
262+
263+
test('marketplace Item Type Definitions info must throw if the API call fails', async (t: TestContext) => {
264+
agent.get(internalEndpoint).intercept({
265+
path: `/api/tenants/${tenantID}/marketplace/item-type-definitions/${itdName}`,
266+
method: 'GET',
267+
}).reply(500, { error: 'Internal Server Error' })
268+
await t.assert.rejects(async () => await client.marketplaceItemTypeDefinitionInfo(tenantID, itdName), { name: 'Error' })
269+
})
170270
})
171271

172272
suite('Marketplace Client', () => {
@@ -279,4 +379,60 @@ suite('Marketplace Client', () => {
279379
}).reply(500, { error: 'Internal Server Error' })
280380
await t.assert.rejects(async () => await client.marketplaceItemInfo(tenantID, itemID, version), { name: 'Error' })
281381
})
382+
383+
test('list marketplace Item Type Definitions', async (t: TestContext) => {
384+
agent.get(mockedEndpoint).intercept({
385+
path: '/api/marketplace/item-type-definitions',
386+
method: 'GET',
387+
query: {
388+
perPage: 200,
389+
per_page: 200,
390+
page: 1,
391+
},
392+
}).reply(200, itemTypeDefinitions)
393+
t.assert.deepEqual(await client.listMarketplaceItemTypeDefinitions(), itemTypeDefinitions)
394+
395+
agent.get(mockedEndpoint).intercept({
396+
path: '/api/marketplace/item-type-definitions',
397+
method: 'GET',
398+
query: {
399+
perPage: 200,
400+
per_page: 200,
401+
page: 1,
402+
namespace: 'mia-platform,my-company',
403+
name: 'plugin,custom-workload',
404+
displayName: 'Plugin,Custom Workload',
405+
},
406+
}).reply(200, itemTypeDefinitions)
407+
t.assert.deepEqual(await client.listMarketplaceItemTypeDefinitions('mia-platform,my-company', 'plugin,custom-workload', 'Plugin,Custom Workload'), itemTypeDefinitions)
408+
})
409+
410+
test('list marketplace Item Type Definitions must throw if the API call fails', async (t: TestContext) => {
411+
agent.get(mockedEndpoint).intercept({
412+
path: '/api/marketplace/item-type-definitions',
413+
method: 'GET',
414+
query: {
415+
perPage: 200,
416+
per_page: 200,
417+
page: 1,
418+
},
419+
}).reply(500, { error: 'Internal Server Error' })
420+
await t.assert.rejects(async () => await client.listMarketplaceItemTypeDefinitions(), { name: 'Error' })
421+
})
422+
423+
test('marketplace Item Type Definition info', async (t: TestContext) => {
424+
agent.get(mockedEndpoint).intercept({
425+
path: `/api/tenants/${tenantID}/marketplace/item-type-definitions/${itdName}`,
426+
method: 'GET',
427+
}).reply(200, itemTypeDefinitions.at(0))
428+
t.assert.deepEqual(await client.marketplaceItemTypeDefinitionInfo(tenantID, itdName), itemTypeDefinitions.at(0))
429+
})
430+
431+
test('marketplace Item Type Definitions info must throw if the API call fails', async (t: TestContext) => {
432+
agent.get(mockedEndpoint).intercept({
433+
path: `/api/tenants/${tenantID}/marketplace/item-type-definitions/${itdName}`,
434+
method: 'GET',
435+
}).reply(500, { error: 'Internal Server Error' })
436+
await t.assert.rejects(async () => await client.marketplaceItemTypeDefinitionInfo(tenantID, itdName), { name: 'Error' })
437+
})
282438
})

0 commit comments

Comments
 (0)