Skip to content

Commit 079bca9

Browse files
author
demetrio.marino
committed
feat: support for MCP prompts, created "deploy" prompt
1 parent 08ad5b4 commit 079bca9

File tree

5 files changed

+246
-3
lines changed

5 files changed

+246
-3
lines changed

src/prompts/deploy.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright Mia srl
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
import assert from 'node:assert'
17+
import { beforeEach, suite, test } from 'node:test'
18+
19+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
20+
import { GetPromptResultSchema, ListPromptsResultSchema } from '@modelcontextprotocol/sdk/types.js'
21+
22+
import { addDeployPrompt } from './deploy'
23+
import { TestMCPServer } from '../server/utils.test'
24+
25+
suite('setup deploy prompt', () => {
26+
let client: Client
27+
beforeEach(async () => {
28+
client = await TestMCPServer((server) => {
29+
addDeployPrompt(server)
30+
})
31+
})
32+
33+
test('the prompt is included in the list of prompts', async () => {
34+
const result = await client.request(
35+
{
36+
method: 'prompts/list',
37+
},
38+
ListPromptsResultSchema,
39+
)
40+
41+
assert.equal(result.prompts.length, 1)
42+
})
43+
44+
test('the prompt is retrieved with the requested parameters', async () => {
45+
const result = await client.request(
46+
{
47+
method: 'prompts/get',
48+
params: {
49+
name: 'deploy',
50+
arguments: {
51+
tenant: 'my-tenant',
52+
project: 'my-project',
53+
environment: 'my-environment',
54+
},
55+
},
56+
},
57+
GetPromptResultSchema,
58+
)
59+
60+
assert.equal(result.messages.length, 3)
61+
const [ firstUserMessage, firstAssistantMessage, secondUserMessage ] = result.messages
62+
assert.equal(firstUserMessage.role, 'user')
63+
assert.equal(firstAssistantMessage.role, 'assistant')
64+
assert.equal(secondUserMessage.role, 'user')
65+
66+
const firstUserMessageText = firstUserMessage.content.text
67+
if (typeof firstUserMessageText !== 'string') {
68+
assert.fail('message text is not a string')
69+
}
70+
71+
assert.ok(firstUserMessageText.includes('my-project'))
72+
assert.ok(firstUserMessageText.includes('my-tenant'))
73+
assert.ok(firstUserMessageText.includes('my-environment'))
74+
assert.ok(firstUserMessageText.includes('Please fetch which revision'))
75+
76+
const firstAssistantMessageText = firstAssistantMessage.content.text
77+
if (typeof firstAssistantMessageText !== 'string') {
78+
assert.fail('message text is not a string')
79+
}
80+
81+
assert.ok(firstAssistantMessageText.includes('my-project'))
82+
assert.ok(firstAssistantMessageText.includes('my-tenant'))
83+
assert.ok(firstAssistantMessageText.includes('my-environment'))
84+
assert.ok(firstAssistantMessageText.includes('to be fetched'))
85+
})
86+
87+
test('the prompt is retrieved including the revisionName', async () => {
88+
const result = await client.request(
89+
{
90+
method: 'prompts/get',
91+
params: {
92+
name: 'deploy',
93+
arguments: {
94+
tenant: 'my-tenant',
95+
project: 'my-project',
96+
environment: 'my-environment',
97+
revisionName: 'my-revision',
98+
},
99+
},
100+
},
101+
GetPromptResultSchema,
102+
)
103+
104+
assert.equal(result.messages.length, 3)
105+
const [ firstUserMessage, firstAssistantMessage, secondUserMessage ] = result.messages
106+
assert.equal(firstUserMessage.role, 'user')
107+
assert.equal(firstAssistantMessage.role, 'assistant')
108+
assert.equal(secondUserMessage.role, 'user')
109+
110+
const firstUserMessageText = firstUserMessage.content.text
111+
if (typeof firstUserMessageText !== 'string') {
112+
assert.fail('message text is not a string')
113+
}
114+
115+
assert.ok(firstUserMessageText.includes('my-revision'))
116+
117+
const firstAssistantMessageText = firstAssistantMessage.content.text
118+
if (typeof firstAssistantMessageText !== 'string') {
119+
assert.fail('message text is not a string')
120+
}
121+
122+
assert.ok(firstAssistantMessageText.includes('my-revision'))
123+
})
124+
})

src/prompts/deploy.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright Mia srl
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'
17+
18+
import z from 'zod'
19+
20+
export const addDeployPrompt = (server: McpServer) => {
21+
server.registerPrompt(
22+
'deploy',
23+
{
24+
title: 'Deploy Mia-Platform project',
25+
description: 'Base prompt to deploy a Mia-Platform project',
26+
argsSchema: {
27+
tenant: z.string().describe('The name or the identifier tenant where the project to deploy is located.'),
28+
project: z.string().describe('The name or the identifier of the project to deploy.'),
29+
environment: z.string().describe('In which environment the project has to be deployed.'),
30+
revisionName: z.string().optional().describe('The name of the revision to deploy. If not provided, the main revision will be used.'),
31+
},
32+
},
33+
async ({ revisionName, environment, project, tenant }) => {
34+
const revisionInstruction = revisionName
35+
? `The revision to deploy is called ${revisionName}`
36+
: 'Please fetch which revision is the main one to execute the deployment of that configuration'
37+
38+
const revisionConfirm = revisionName || 'to be fetched'
39+
40+
return {
41+
messages: [
42+
{
43+
role: 'user',
44+
content: {
45+
type: 'text',
46+
text: `Please execute the deployment of project ${project} in tenant ${tenant} to environment ${environment}. ${revisionInstruction}.\n\nAfter the deployment, provide:\n1. The pipeline execution URL.\n2. The deployment status, including whether pods are being created correctly or if there are any errors.`,
47+
},
48+
},
49+
{
50+
role: 'assistant',
51+
content: {
52+
type: 'text',
53+
text: `I understand you want me to deploy this project. \
54+
First I will review the list of available tenants and the list of available projects to be sure that I find the project you want to be developed. \
55+
If I don't find the tenant or the project, or the environment, I will try to use tools to get \
56+
the list of available tenants, projects, environments and the revision (if missing or incorrect) to be sure that the parameters you provided are correct. \
57+
Then, I will validate the configuration, start the deployment, and then return:
58+
- The pipeline URL
59+
- The deployment status (pods creation and error checks).
60+
61+
Before proceeding, please confirm these parameters:
62+
- Project: ${project}
63+
- Tenant: ${tenant}
64+
- Environment: ${environment}
65+
- Revision: ${revisionConfirm}\
66+
`,
67+
},
68+
},
69+
{
70+
role: 'user',
71+
content: {
72+
type: 'text',
73+
text: `Yes, proceed.`,
74+
},
75+
},
76+
],
77+
}
78+
},
79+
)
80+
}

src/prompts/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright Mia srl
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
import { addDeployPrompt } from './deploy'
17+
18+
export { addDeployPrompt }

src/server/server.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { suite, test } from 'node:test'
1717

1818
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1919
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
20-
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
20+
import { ListPromptsResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
2121

2222
import { getMcpServer } from './server'
2323
import { toolsDescriptions } from '../tools/descriptions'
@@ -46,6 +46,15 @@ suite('initialize server', () => {
4646
)
4747
t.assert.equal(toolsResult.tools.length, Object.keys(toolsDescriptions).length, 'should return all the registred tools')
4848

49+
const promptsResult = await testClient.request(
50+
{
51+
method: 'prompts/list',
52+
},
53+
ListPromptsResultSchema,
54+
)
55+
56+
t.assert.equal(promptsResult.prompts.length, 1, 'should return all the registred prompts')
57+
4958
await clientTransport.close()
5059
await serverTransport.close()
5160
await testClient.close()

src/server/server.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
import { Implementation } from '@modelcontextprotocol/sdk/types.js'
1617
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
18+
import { ServerOptions } from '@modelcontextprotocol/sdk/server/index.js'
1719
import { UndiciHeaders } from 'undici/types/dispatcher'
1820

1921
import { addConfigurationCapabilities } from '../tools/configuration'
2022
import { addDeployCapabilities } from '../tools/deploy'
23+
import { addDeployPrompt } from '../prompts'
2124
import { addGovernanceCapabilities } from '../tools/governance'
2225
import { addMarketplaceCapabilities } from '../tools/marketplace'
2326
import { addRuntimeCapabilities } from '../tools/runtime'
@@ -31,26 +34,35 @@ export function getMcpServer (
3134
clientSecret: string,
3235
additionalHeaders: UndiciHeaders = {},
3336
): McpServer {
34-
const server = new McpServer({
37+
const implementation: Implementation = {
3538
name,
3639
description,
3740
version,
41+
}
42+
43+
const options: ServerOptions = {
3844
capabilities: {
3945
logging: {},
4046
resources: {},
4147
prompts: {},
4248
tools: {},
4349
},
44-
})
50+
}
51+
52+
const server = new McpServer(implementation, options)
4553

4654
const apiClient = new APIClient(host, clientID, clientSecret, additionalHeaders)
4755

56+
// Tools
4857
addMarketplaceCapabilities(server, apiClient)
4958
addGovernanceCapabilities(server, apiClient)
5059
addServicesCapabilities(server, apiClient)
5160
addConfigurationCapabilities(server, apiClient)
5261
addDeployCapabilities(server, apiClient)
5362
addRuntimeCapabilities(server, apiClient)
5463

64+
// Prompts
65+
addDeployPrompt(server)
66+
5567
return server
5668
}

0 commit comments

Comments
 (0)