-
Notifications
You must be signed in to change notification settings - Fork 747
feat(amazonq): Implement the MCP Abstractions and 3 Tests #7791
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
laura-codess
merged 6 commits into
aws:feature/ui-e2e-tests
from
laura-codess:implement_mcp
Aug 4, 2025
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c43fe76
fix(amazonq): gitignore fix (#7749)
laura-codess 3956219
adding some functions, not fully working yet
laura-codess 83a78dd
mcpconfigureserver
laura-codess 60fc14c
finish implementing mcp abstractions + 3 tests
laura-codess 0d4a2b6
fixing PR comments
laura-codess 8b8a080
fixing more PR comments
laura-codess File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
308 changes: 308 additions & 0 deletions
308
packages/amazonq/test/e2e_new/amazonq/helpers/mcpHelper.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,308 @@ | ||
| /*! | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| import { WebviewView, By, WebElement } from 'vscode-extension-tester' | ||
| import { waitForElement } from '../utils/generalUtils' | ||
|
|
||
| /** | ||
| * Clicks the tools to get to the MCP server overlay | ||
| * @param webviewView The WebviewView instance | ||
| * @returns Promise<boolean> True if tools button was found and clicked, false otherwise | ||
| */ | ||
| export async function clickToolsButton(webviewView: WebviewView): Promise<boolean> { | ||
| try { | ||
| const navWrapper = await waitForElement(webviewView, By.css('.mynah-nav-tabs-wrapper.mynah-ui-clickable-item')) | ||
| const buttonsWrapper = await navWrapper.findElement(By.css('.mynah-nav-tabs-bar-buttons-wrapper')) | ||
| const buttons = await buttonsWrapper.findElements( | ||
| By.css('.mynah-button.mynah-button-secondary.fill-state-always.mynah-ui-clickable-item') | ||
| ) | ||
| for (const button of buttons) { | ||
| const icon = await button.findElement(By.css('i.mynah-ui-icon.mynah-ui-icon-tools')) | ||
| if (icon) { | ||
| await button.click() | ||
| await webviewView.getDriver().actions().move({ x: 0, y: 0 }).perform() | ||
| return true | ||
| } | ||
| } | ||
| console.log('Tools button not found') | ||
| return false | ||
| } catch (e) { | ||
| console.error('Error clicking tools button:', e) | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Clicks the add button in the MCP server configuration panel | ||
| * @param webviewView The WebviewView instance | ||
| * @returns Promise<boolean> True if add button was found and clicked, false otherwise | ||
| */ | ||
| export async function clickMCPAddButton(webviewView: WebviewView): Promise<boolean> { | ||
| try { | ||
| const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper')) | ||
| const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header')) | ||
| const actionsContainer = await header.findElement(By.css('.mynah-sheet-header-actions-container')) | ||
| const addButton = await actionsContainer.findElement(By.css('button:has(i.mynah-ui-icon-plus)')) | ||
| await addButton.click() | ||
| return true | ||
| } catch (e) { | ||
| console.error('Error clicking the MCP add button:', e) | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Configures an MCP server with the provided settings | ||
| * @param webviewView The WebviewView instance | ||
| * @param config Configuration object with optional parameters | ||
| * @returns Promise<boolean> True if configuration was successful, false otherwise | ||
| * Note: I have the default settings in the defaultConfig | ||
| */ | ||
| interface MCPServerConfig { | ||
| scope?: 'global' | 'workspace' | ||
| name?: string | ||
| transport?: number | ||
| command?: string | ||
| args?: string[] | ||
| nameEnvironmentVariable?: string | ||
| valueEnvironmentVariable?: string | ||
| timeout?: number | ||
| } | ||
|
|
||
| const defaultConfig: MCPServerConfig = { | ||
| scope: 'global', | ||
| name: 'aws-documentation', | ||
| transport: 0, | ||
| command: 'uvx', | ||
| args: ['awslabs.aws-documentation-mcp-server@latest'], | ||
| nameEnvironmentVariable: 'hi', | ||
| valueEnvironmentVariable: 'hi', | ||
| timeout: 0, | ||
| } | ||
|
|
||
| const formItemsMap = { | ||
| SCOPE: 0, | ||
| NAME: 1, | ||
| TRANSPORT: 2, | ||
| COMMAND: 3, | ||
| ARGS: 4, | ||
| ENV_VARS: 6, | ||
| TIMEOUT: 9, | ||
| } as const | ||
|
|
||
| type McpFormItem = keyof typeof formItemsMap | ||
|
|
||
| async function selectScope(container: WebElement, scope: string) { | ||
| try { | ||
| const a = await container.findElements(By.css('.mynah-form-input-radio-label.mynah-ui-clickable-item')) | ||
| if (scope === 'global') { | ||
laura-codess marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const b = a[0] | ||
| await b.click() | ||
| } else { | ||
| const b = a[1] | ||
| await b.click() | ||
| } | ||
| } catch (e) { | ||
| console.error('Error selecting the scope:', e) | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| async function inputName(container: WebElement, name: string) { | ||
| try { | ||
| const input = container.findElement(By.css('.mynah-form-input')) | ||
| await input.sendKeys(name) | ||
| } catch (e) { | ||
| console.error('Error inputing the name:', e) | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| async function selectTransport(container: WebElement, transport: number) { | ||
| try { | ||
| const selectElement = await container.findElement(By.css('select')) | ||
| const options = await selectElement.findElements(By.css('option')) | ||
| const optionIndex = transport | ||
| await options[optionIndex].click() | ||
| } catch (e) { | ||
| console.error('Error selecting the transport:', e) | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| async function inputCommand(container: WebElement, command: string) { | ||
| try { | ||
| const input = container.findElement(By.css('.mynah-form-input')) | ||
| await input.sendKeys(command) | ||
| } catch (e) { | ||
| console.error('Error inputing the command:', e) | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| async function inputArgs(container: WebElement, args: string[]) { | ||
laura-codess marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try { | ||
| const input = container.findElement(By.css('.mynah-form-input')) | ||
| const addButton = container.findElement( | ||
| By.css( | ||
| '.mynah-button.mynah-button-secondary.fill-state-always.mynah-form-item-list-row-remove-button.mynah-ui-clickable-item' | ||
| ) | ||
| ) | ||
| for (let i = 0; i < args.length; i++) { | ||
| await input.sendKeys(args[i]) | ||
| await addButton.click() | ||
| } | ||
| } catch (e) { | ||
| console.error('Error inputing the arguments:', e) | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| async function inputEnvironmentVariables( | ||
| container: WebElement, | ||
| nameEnvironmentVariable?: string, | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| valueEnvironmentVariable?: string | ||
| ) { | ||
| try { | ||
| if (nameEnvironmentVariable && valueEnvironmentVariable) { | ||
| const a = await container.findElements(By.css('.mynah-form-input')) | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await a[0].sendKeys(nameEnvironmentVariable) | ||
| await a[1].sendKeys(valueEnvironmentVariable) | ||
| const addButton = await container.findElement(By.css('.mynah-form-item-list-add-button')) | ||
| await addButton.click() | ||
| } else { | ||
| console.log('No environmental variables for this configuration') | ||
| } | ||
| } catch (e) { | ||
| console.error('Error inputing the environment variables:', e) | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| async function inputTimeout(container: WebElement, timeout: number) { | ||
| try { | ||
| const input = container.findElement(By.css('.mynah-form-input')) | ||
| await input.clear() | ||
| await input.sendKeys(timeout) | ||
| } catch (e) { | ||
| console.error('Error inputing the timeout:', e) | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| async function processFormItems(mcpFormItem: McpFormItem, container: WebElement, config: MCPServerConfig) { | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| switch (mcpFormItem) { | ||
| case 'SCOPE': | ||
| await selectScope(container, config.scope!) | ||
| break | ||
| case 'NAME': | ||
| await inputName(container, config.name!) | ||
| break | ||
| case 'TRANSPORT': | ||
| await selectTransport(container, config.transport!) | ||
| break | ||
| case 'COMMAND': | ||
| await inputCommand(container, config.command!) | ||
| break | ||
| case 'ARGS': | ||
| await inputArgs(container, config.args!) | ||
| break | ||
| case 'ENV_VARS': | ||
| await inputEnvironmentVariables(container, config.nameEnvironmentVariable, config.valueEnvironmentVariable) | ||
| break | ||
| case 'TIMEOUT': | ||
| await inputTimeout(container, config.timeout!) | ||
| break | ||
| } | ||
| } | ||
|
|
||
| export async function configureMCPServer(webviewView: WebviewView, config: MCPServerConfig = {}): Promise<boolean> { | ||
| const mergedConfig = { ...defaultConfig, ...config } | ||
| try { | ||
| const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper')) | ||
| const sheetBody = await sheetWrapper.findElement(By.css('.mynah-sheet-body')) | ||
| const filtersWrapper = await sheetBody.findElement(By.css('.mynah-detailed-list-filters-wrapper')) | ||
| const formContainer = await filtersWrapper.findElement(By.css('.mynah-chat-item-form-items-container')) | ||
| const items = await formContainer.findElements(By.css('.mynah-form-input-wrapper')) | ||
|
|
||
| for (const [formItem, index] of Object.entries(formItemsMap)) { | ||
| if (index < items.length) { | ||
| await processFormItems(formItem as McpFormItem, items[index], mergedConfig) | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| return true | ||
| } catch (e) { | ||
| console.log('Error configuring the MCP Server') | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| export async function saveMCPServerConfiguration(webviewView: WebviewView): Promise<boolean> { | ||
| try { | ||
| const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper')) | ||
| const body = await sheetWrapper.findElement(By.css('.mynah-sheet-body')) | ||
| const filterActions = await body.findElement(By.css('.mynah-detailed-list-filter-actions-wrapper')) | ||
| const saveButton = await filterActions.findElement( | ||
| By.css('.mynah-button.fill-state-always.status-primary.mynah-ui-clickable-item') | ||
| ) | ||
| await saveButton.click() | ||
| return true | ||
| } catch (e) { | ||
| console.error('Error saving the MCP server configuration:', e) | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| export async function cancelMCPServerConfiguration(webviewView: WebviewView): Promise<boolean> { | ||
| try { | ||
| const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper')) | ||
| const body = await sheetWrapper.findElement(By.css('.mynah-sheet-body')) | ||
| const filterActions = await body.findElement(By.css('.mynah-detailed-list-filter-actions-wrapper')) | ||
| const saveButton = await filterActions.findElement( | ||
| By.css('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-ui-clickable-item') | ||
| ) | ||
| await saveButton.click() | ||
| return true | ||
| } catch (e) { | ||
| console.error('Error saving the MCP server configuration:', e) | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Clicks the refresh button in the MCP server configuration panel | ||
| * @param webviewView The WebviewView instance | ||
| * @returns Promise<boolean> True if refresh button was found and clicked, false otherwise | ||
| */ | ||
| export async function clickMCPRefreshButton(webviewView: WebviewView): Promise<boolean> { | ||
| try { | ||
| const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper')) | ||
| const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header')) | ||
| const actionsContainer = await header.findElement(By.css('.mynah-sheet-header-actions-container')) | ||
| const refreshButton = await actionsContainer.findElement(By.css('button:has(i.mynah-ui-icon-refresh)')) | ||
| await refreshButton.click() | ||
| return true | ||
| } catch (e) { | ||
| console.error('Error clicking the MCP refresh button:', e) | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Clicks the close/cancel button in the MCP server configuration panel | ||
| * @param webviewView The WebviewView instance | ||
| * @returns Promise<boolean> True if close button was found and clicked, false otherwise | ||
| */ | ||
| export async function clickMCPCloseButton(webviewView: WebviewView): Promise<boolean> { | ||
| try { | ||
| const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper')) | ||
| const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header')) | ||
| const cancelButton = await header.findElement(By.css('button:has(i.mynah-ui-icon-cancel)')) | ||
| await webviewView.getDriver().executeScript('arguments[0].click()', cancelButton) | ||
| return true | ||
| } catch (e) { | ||
| console.error('Error closing the MCP overlay:', e) | ||
| return false | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| /*! | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| import '../utils/setup' | ||
| import { WebviewView } from 'vscode-extension-tester' | ||
| import { testContext } from '../utils/testContext' | ||
| import { | ||
| clickMCPAddButton, | ||
| clickMCPCloseButton, | ||
| clickMCPRefreshButton, | ||
| clickToolsButton, | ||
| configureMCPServer, | ||
| saveMCPServerConfiguration, | ||
| } from '../helpers/mcpHelper' | ||
| import { closeAllTabs } from '../utils/cleanupUtils' | ||
|
|
||
| describe('Amazon Q MCP Functionality', function () { | ||
| // this timeout is the general timeout for the entire test suite | ||
| this.timeout(150000) | ||
| let webviewView: WebviewView | ||
|
|
||
| before(async function () { | ||
| webviewView = testContext.webviewView | ||
| }) | ||
|
|
||
| after(async function () { | ||
| await closeAllTabs(webviewView) | ||
| }) | ||
|
|
||
| it('Test Amazon Q MCP Servers and Built-in Tools Access', async () => { | ||
| await clickToolsButton(webviewView) | ||
laura-codess marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| await clickMCPCloseButton(webviewView) | ||
| }) | ||
|
|
||
| it('Add new MCP Server', async () => { | ||
| await clickToolsButton(webviewView) | ||
| await clickMCPAddButton(webviewView) | ||
| await configureMCPServer(webviewView) | ||
| await saveMCPServerConfiguration(webviewView) | ||
| await clickMCPCloseButton(webviewView) | ||
| }) | ||
|
|
||
| it('Refresh MCP Server', async () => { | ||
| await clickToolsButton(webviewView) | ||
| await clickMCPRefreshButton(webviewView) | ||
| await clickMCPCloseButton(webviewView) | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.