Skip to content

Commit b020242

Browse files
authored
feat(amazonq): Implement the MCP Abstractions and 3 Tests (#7791)
## Change I implemented the abstractions for the MCP Server Test Suite, the functions include: clickToolsButton() clickMCPAddButton() configureMCPServer + various nested functions saveMCPServerConfiguration cancelMCPServerConfiguration clickMCPRefreshButton clickMCPCloseButton Along with the abstractions, I implemented 3 tests: [Test Amazon Q MCP Servers and Built-in Tools Access](https://quip-amazon.com/prX5ATmJnTQH/Test-Amazon-Q-MCP-Servers-and-Built-in-Tools-Access#temp:C:PMb1a32b1fc4eda47c49fcf1e311) [Add New MCP Server](https://quip-amazon.com/zixVAly68agD/Add-a-new-MCP-server#temp:C:BZRc2917aff34e4481da2d27633e) [Refresh MCP Server](https://quip-amazon.com/FfIOAYVC0Nm2/Refresh-MCP-Server#temp:C:UVL3c9fcca6b4bf447687a131a1e) I verified that the test works with all other webview based test suites (as of now the inline completion test is still being fixed) <img width="487" height="630" alt="Screenshot 2025-07-31 at 3 29 48 PM" src="https://github.com/user-attachments/assets/e0e50be6-ed85-4f21-9c7d-46371ee27cd2" /> --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 4080c31 commit b020242

File tree

2 files changed

+335
-0
lines changed

2 files changed

+335
-0
lines changed
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { WebviewView, By, WebElement } from 'vscode-extension-tester'
6+
import { waitForElement } from '../utils/generalUtils'
7+
8+
/**
9+
* Clicks the tools to get to the MCP server overlay
10+
* @param webviewView The WebviewView instance
11+
* @returns Promise<boolean> True if tools button was found and clicked, false otherwise
12+
*/
13+
export async function clickToolsButton(webviewView: WebviewView): Promise<void> {
14+
try {
15+
const navWrapper = await waitForElement(webviewView, By.css('.mynah-nav-tabs-wrapper.mynah-ui-clickable-item'))
16+
const buttonsWrapper = await navWrapper.findElement(By.css('.mynah-nav-tabs-bar-buttons-wrapper'))
17+
const buttons = await buttonsWrapper.findElements(
18+
By.css('.mynah-button.mynah-button-secondary.fill-state-always.mynah-ui-clickable-item')
19+
)
20+
for (const button of buttons) {
21+
const icon = await button.findElement(By.css('i.mynah-ui-icon.mynah-ui-icon-tools'))
22+
if (icon) {
23+
await button.click()
24+
await webviewView.getDriver().actions().move({ x: 0, y: 0 }).perform()
25+
}
26+
}
27+
console.log('Tools button not found')
28+
} catch (e) {
29+
console.error('Error clicking tools button:', e)
30+
}
31+
}
32+
33+
/**
34+
* Clicks the add button in the MCP server configuration panel
35+
* @param webviewView The WebviewView instance
36+
* @returns Promise<boolean> True if add button was found and clicked, false otherwise
37+
*/
38+
export async function clickMCPAddButton(webviewView: WebviewView): Promise<void> {
39+
try {
40+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
41+
const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header'))
42+
const actionsContainer = await header.findElement(By.css('.mynah-sheet-header-actions-container'))
43+
const addButton = await actionsContainer.findElement(By.css('button:has(i.mynah-ui-icon-plus)'))
44+
await addButton.click()
45+
} catch (e) {
46+
console.error('Error clicking the MCP add button:', e)
47+
}
48+
}
49+
50+
/**
51+
* Configures an MCP server with the provided settings
52+
* @param webviewView The WebviewView instance
53+
* @param config Configuration object with optional parameters
54+
* @returns Promise<boolean> True if configuration was successful, false otherwise
55+
* Note: I have the default settings in the defaultConfig
56+
*/
57+
interface MCPServerConfig {
58+
scope?: 'global' | 'workspace'
59+
name?: string
60+
transport?: number
61+
command?: string
62+
args?: string[]
63+
environmentVariable?: { name: string; value: string }
64+
timeout?: number
65+
}
66+
67+
const defaultConfig: MCPServerConfig = {
68+
scope: 'global',
69+
name: 'aws-documentation',
70+
transport: 0,
71+
command: 'uvx',
72+
args: ['awslabs.aws-documentation-mcp-server@latest'],
73+
timeout: 0,
74+
}
75+
76+
// Each name maps to an index in the '.mynah-form-input-wrapper' array
77+
const formItemsMap = {
78+
SCOPE: 0,
79+
NAME: 1,
80+
TRANSPORT: 2,
81+
COMMAND: 3,
82+
ARGS: 4,
83+
ENV_VARS: 6,
84+
TIMEOUT: 9,
85+
} as const
86+
87+
type McpFormItem = keyof typeof formItemsMap
88+
89+
async function selectScope(container: WebElement, scope: string) {
90+
try {
91+
const radioLabels = await container.findElements(
92+
By.css('.mynah-form-input-radio-label.mynah-ui-clickable-item')
93+
)
94+
if (scope === 'global') {
95+
const globalOption = radioLabels[0]
96+
await globalOption.click()
97+
} else {
98+
const workspaceOption = radioLabels[1]
99+
await workspaceOption.click()
100+
}
101+
} catch (e) {
102+
console.error('Error selecting the scope:', e)
103+
throw e
104+
}
105+
}
106+
107+
async function inputName(container: WebElement, name: string) {
108+
try {
109+
const input = await container.findElement(By.css('.mynah-form-input'))
110+
await input.sendKeys(name)
111+
} catch (e) {
112+
console.error('Error inputing the name:', e)
113+
throw e
114+
}
115+
}
116+
117+
async function selectTransport(container: WebElement, transport: number) {
118+
try {
119+
const selectElement = await container.findElement(By.css('select'))
120+
const options = await selectElement.findElements(By.css('option'))
121+
const optionIndex = transport
122+
await options[optionIndex].click()
123+
} catch (e) {
124+
console.error('Error selecting the transport:', e)
125+
throw e
126+
}
127+
}
128+
129+
async function inputCommand(container: WebElement, command: string) {
130+
try {
131+
const input = await container.findElement(By.css('.mynah-form-input'))
132+
await input.sendKeys(command)
133+
} catch (e) {
134+
console.error('Error inputing the command:', e)
135+
throw e
136+
}
137+
}
138+
139+
async function inputArgs(container: WebElement, args: string[]) {
140+
try {
141+
const input = await container.findElement(By.css('.mynah-form-input'))
142+
const addButton = await container.findElement(By.css('.mynah-form-item-list-add-button'))
143+
for (let i = 0; i < args.length; i++) {
144+
await input.sendKeys(args[i])
145+
await addButton.click()
146+
}
147+
} catch (e) {
148+
console.error('Error inputing the arguments:', e)
149+
throw e
150+
}
151+
}
152+
153+
async function inputEnvironmentVariables(container: WebElement, environmentVariable?: { name: string; value: string }) {
154+
try {
155+
if (environmentVariable) {
156+
const envInputs = await container.findElements(By.css('.mynah-form-input'))
157+
await envInputs[0].sendKeys(environmentVariable.name)
158+
await envInputs[1].sendKeys(environmentVariable.value)
159+
const addButton = await container.findElement(By.css('.mynah-form-item-list-add-button'))
160+
await addButton.click()
161+
} else {
162+
console.log('No environmental variables for this configuration')
163+
}
164+
} catch (e) {
165+
console.error('Error inputing the environment variables:', e)
166+
throw e
167+
}
168+
}
169+
170+
async function inputTimeout(container: WebElement, timeout: number) {
171+
try {
172+
const input = await container.findElement(By.css('.mynah-form-input'))
173+
await input.clear()
174+
await input.sendKeys(timeout.toString())
175+
} catch (e) {
176+
console.error('Error inputing the timeout:', e)
177+
throw e
178+
}
179+
}
180+
181+
async function processFormItem(mcpFormItem: McpFormItem, container: WebElement, config: MCPServerConfig) {
182+
switch (mcpFormItem) {
183+
case 'SCOPE':
184+
await selectScope(container, config.scope!)
185+
break
186+
case 'NAME':
187+
await inputName(container, config.name!)
188+
break
189+
case 'TRANSPORT':
190+
await selectTransport(container, config.transport!)
191+
break
192+
case 'COMMAND':
193+
await inputCommand(container, config.command!)
194+
break
195+
case 'ARGS':
196+
await inputArgs(container, config.args!)
197+
break
198+
case 'ENV_VARS':
199+
await inputEnvironmentVariables(container, config.environmentVariable)
200+
break
201+
case 'TIMEOUT':
202+
await inputTimeout(container, config.timeout!)
203+
break
204+
}
205+
}
206+
207+
export async function configureMCPServer(webviewView: WebviewView, config: MCPServerConfig = {}): Promise<void> {
208+
const mergedConfig = { ...defaultConfig, ...config }
209+
try {
210+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
211+
const sheetBody = await sheetWrapper.findElement(By.css('.mynah-sheet-body'))
212+
const filtersWrapper = await sheetBody.findElement(By.css('.mynah-detailed-list-filters-wrapper'))
213+
const formContainer = await filtersWrapper.findElement(By.css('.mynah-chat-item-form-items-container'))
214+
const items = await formContainer.findElements(By.css('.mynah-form-input-wrapper'))
215+
216+
for (const formItem of Object.keys(formItemsMap) as McpFormItem[]) {
217+
const index = formItemsMap[formItem]
218+
if (index < items.length) {
219+
await processFormItem(formItem, items[index], mergedConfig)
220+
}
221+
}
222+
} catch (e) {
223+
console.log('Error configuring the MCP Server')
224+
}
225+
}
226+
227+
export async function saveMCPServerConfiguration(webviewView: WebviewView): Promise<void> {
228+
try {
229+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
230+
const body = await sheetWrapper.findElement(By.css('.mynah-sheet-body'))
231+
const filterActions = await body.findElement(By.css('.mynah-detailed-list-filter-actions-wrapper'))
232+
const saveButton = await filterActions.findElement(
233+
By.css('.mynah-button.fill-state-always.status-primary.mynah-ui-clickable-item')
234+
)
235+
await saveButton.click()
236+
} catch (e) {
237+
console.error('Error saving the MCP server configuration:', e)
238+
}
239+
}
240+
241+
export async function cancelMCPServerConfiguration(webviewView: WebviewView): Promise<void> {
242+
try {
243+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
244+
const body = await sheetWrapper.findElement(By.css('.mynah-sheet-body'))
245+
const filterActions = await body.findElement(By.css('.mynah-detailed-list-filter-actions-wrapper'))
246+
const saveButton = await filterActions.findElement(
247+
By.css('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-ui-clickable-item')
248+
)
249+
await saveButton.click()
250+
} catch (e) {
251+
console.error('Error saving the MCP server configuration:', e)
252+
}
253+
}
254+
255+
/**
256+
* Clicks the refresh button in the MCP server configuration panel
257+
* @param webviewView The WebviewView instance
258+
* @returns Promise<boolean> True if refresh button was found and clicked, false otherwise
259+
*/
260+
export async function clickMCPRefreshButton(webviewView: WebviewView): Promise<void> {
261+
try {
262+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
263+
const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header'))
264+
const actionsContainer = await header.findElement(By.css('.mynah-sheet-header-actions-container'))
265+
const refreshButton = await actionsContainer.findElement(By.css('button:has(i.mynah-ui-icon-refresh)'))
266+
await refreshButton.click()
267+
} catch (e) {
268+
console.error('Error clicking the MCP refresh button:', e)
269+
}
270+
}
271+
272+
/**
273+
* Clicks the close/cancel button in the MCP server configuration panel
274+
* @param webviewView The WebviewView instance
275+
* @returns Promise<boolean> True if close button was found and clicked, false otherwise
276+
*/
277+
export async function clickMCPCloseButton(webviewView: WebviewView): Promise<void> {
278+
try {
279+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
280+
const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header'))
281+
const cancelButton = await header.findElement(By.css('button:has(i.mynah-ui-icon-cancel)'))
282+
await webviewView.getDriver().executeScript('arguments[0].click()', cancelButton)
283+
} catch (e) {
284+
console.error('Error closing the MCP overlay:', e)
285+
}
286+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import '../utils/setup'
6+
import { WebviewView } from 'vscode-extension-tester'
7+
import { testContext } from '../utils/testContext'
8+
import {
9+
clickMCPAddButton,
10+
clickMCPCloseButton,
11+
clickMCPRefreshButton,
12+
clickToolsButton,
13+
configureMCPServer,
14+
saveMCPServerConfiguration,
15+
} from '../helpers/mcpHelper'
16+
import { closeAllTabs } from '../utils/cleanupUtils'
17+
18+
describe('Amazon Q MCP Functionality', function () {
19+
// this timeout is the general timeout for the entire test suite
20+
this.timeout(150000)
21+
let webviewView: WebviewView
22+
23+
before(async function () {
24+
webviewView = testContext.webviewView
25+
})
26+
27+
after(async function () {
28+
await closeAllTabs(webviewView)
29+
})
30+
31+
it('Test Amazon Q MCP Servers and Built-in Tools Access', async () => {
32+
await clickToolsButton(webviewView)
33+
await clickMCPCloseButton(webviewView)
34+
})
35+
36+
it('Add new MCP Server', async () => {
37+
await clickToolsButton(webviewView)
38+
await clickMCPAddButton(webviewView)
39+
await configureMCPServer(webviewView)
40+
await saveMCPServerConfiguration(webviewView)
41+
await clickMCPCloseButton(webviewView)
42+
})
43+
44+
it('Refresh MCP Server', async () => {
45+
await clickToolsButton(webviewView)
46+
await clickMCPRefreshButton(webviewView)
47+
await clickMCPCloseButton(webviewView)
48+
})
49+
})

0 commit comments

Comments
 (0)