Skip to content

Commit 414bd9d

Browse files
committed
feat: add marketplace tools to retrieve Item Type Definitions
1 parent ff7dd48 commit 414bd9d

File tree

11 files changed

+490
-9
lines changed

11 files changed

+490
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ following command:
7878
npm ci
7979
```
8080

81-
Once has finished you will have all the dependencies installed on the project, then you have to prepare an environent
81+
Once has finished you will have all the dependencies installed on the project, then you have to prepare an environment
8282
file by copying the default.env file and edit it accordingly.
8383

8484
```sh

package-lock.json

Lines changed: 4 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: 2 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,7 +31,7 @@
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",

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,
@@ -140,6 +141,8 @@ export interface IAPIClient {
140141
listMarketplaceItems(tenantID?: string, type?: string, search?: string): Promise<Record<string, unknown>[]>
141142
marketplaceItemVersions(tenantID: string, itemID: string): Promise<CatalogItemRelease[]>
142143
marketplaceItemInfo(tenantID: string, itemID: string, version?: string): Promise<CatalogVersionedItem>
144+
listMarketplaceItemTypeDefinitions(namespace?: string, name?: string, displayName?: string): Promise<CatalogItemTypeDefinition[]>
145+
marketplaceItemTypeDefinitionInfo (tenantID: string, name: string): Promise<CatalogItemTypeDefinition>
143146
// #endregion
144147
}
145148

@@ -367,6 +370,14 @@ export class APIClient implements IAPIClient {
367370
return this.#marketplaceClient.marketplaceItemInfo(tenantID, itemID, version)
368371
}
369372

373+
async listMarketplaceItemTypeDefinitions (namespace?: string, name?: string, displayName?: string): Promise<CatalogItemTypeDefinition[]> {
374+
return this.#marketplaceClient.listMarketplaceItemTypeDefinitions(namespace, name, displayName)
375+
}
376+
377+
async marketplaceItemTypeDefinitionInfo (tenantID: string, name: string): Promise<CatalogItemTypeDefinition> {
378+
return this.#marketplaceClient.marketplaceItemTypeDefinitionInfo(tenantID, name)
379+
}
380+
370381
async #resourceToCreateFromMarketplaceItem (
371382
project: IProject,
372383
marketplaceItem: CatalogVersionedItem,
@@ -543,6 +554,8 @@ export interface APIClientMockFunctions {
543554
listMarketplaceItemsMockFn?: (tenantID?: string, type?: string, search?: string) => Promise<Record<string, unknown>[]>
544555
marketplaceItemVersionsMockFn?: (tenantID: string, itemID: string) => Promise<CatalogItemRelease[]>
545556
marketplaceItemInfoMockFn?: (tenantID: string, itemID: string, version?: string) => Promise<CatalogVersionedItem>
557+
listMarketplaceItemTypeDefinitionsMockFn?: (namespace?: string, name?: string, displayName?: string) => Promise<CatalogItemTypeDefinition[]>
558+
marketplaceItemTypeDefinitionInfoMockFn?: (tenantID: string, name: string) => Promise<CatalogItemTypeDefinition>
546559
// #endregion
547560
}
548561

@@ -770,6 +783,22 @@ export class APIClientMock implements IAPIClient {
770783
return this.mocks.marketplaceItemInfoMockFn(tenantID, itemID, version)
771784
}
772785

786+
async listMarketplaceItemTypeDefinitions (namespace?: string, name?: string, displayName?: string): Promise<CatalogItemTypeDefinition[]> {
787+
if (!this.mocks.listMarketplaceItemTypeDefinitionsMockFn) {
788+
throw new Error('listMarketplaceItemTypeDefinitionsMockFn not mocked')
789+
}
790+
791+
return this.mocks.listMarketplaceItemTypeDefinitionsMockFn(namespace, name, displayName)
792+
}
793+
794+
async marketplaceItemTypeDefinitionInfo (tenantID: string, name: string): Promise<CatalogItemTypeDefinition> {
795+
if (!this.mocks.marketplaceItemTypeDefinitionInfoMockFn) {
796+
throw new Error('marketplaceItemTypeDefinitionInfoMockFn not mocked')
797+
}
798+
799+
return this.mocks.marketplaceItemTypeDefinitionInfoMockFn(tenantID, name)
800+
}
801+
773802
// #endregion
774803
}
775804

src/apis/http-client/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export class HTTPClient {
8989
maxPage = 10,
9090
): Promise<T[]> {
9191
const url = new URL(path, this.baseURL)
92+
// TODO: should it be `perPage`?
9293
params.set('per_page', '200')
9394

9495
const results: T[] = []

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
})

src/apis/marketplaceClient.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import { UndiciHeaders } from 'undici/types/dispatcher'
1717

1818
import { HTTPClient } from './http-client'
19-
import { CatalogItemRelease, CatalogVersionedItem } from '@mia-platform/console-types'
19+
import { CatalogItemRelease, CatalogItemTypeDefinition, CatalogVersionedItem } from '@mia-platform/console-types'
2020

2121
export const internalEndpoint = process.env.MARKETPLACE_INTERNAL_ENDPOINT || 'http://internal.local:3000'
2222

@@ -73,6 +73,21 @@ export class MarketplaceClient {
7373
return this.#client.get<CatalogVersionedItem>(this.#itemInfoPath(tenantID, itemID, itemVersion))
7474
}
7575

76+
async listMarketplaceItemTypeDefinitions (namespace?: string, name?: string, displayName?: string): Promise<CatalogItemTypeDefinition[]> {
77+
const params = new URLSearchParams({
78+
perPage: '200',
79+
...namespace && { namespace },
80+
...name && { name },
81+
...displayName && { displayName },
82+
})
83+
84+
return this.#client.getPaginated<CatalogItemTypeDefinition>(this.#itemTypeDefinitionsPath(), params)
85+
}
86+
87+
async marketplaceItemTypeDefinitionInfo (tenantID: string, name: string): Promise<CatalogItemTypeDefinition> {
88+
return this.#client.get<CatalogItemTypeDefinition>(this.#itemTypeDefinitionInfoPath(tenantID, name))
89+
}
90+
7691
#marketplacePath (): string {
7792
return '/api/marketplace/'
7893
}
@@ -84,4 +99,12 @@ export class MarketplaceClient {
8499
#itemInfoPath (tenantID: string, itemID: string, version: string): string {
85100
return `/api/tenants/${tenantID}/marketplace/items/${itemID}/versions/${version}`
86101
}
102+
103+
#itemTypeDefinitionsPath (): string {
104+
return '/api/marketplace/item-type-definitions'
105+
}
106+
107+
#itemTypeDefinitionInfoPath (tenantID: string, name: string): string {
108+
return `/api/tenants/${tenantID}/marketplace/item-type-definitions/${name}`
109+
}
87110
}

0 commit comments

Comments
 (0)