From 420b3d9b83314850227b07ff5fd87850ceeb6e70 Mon Sep 17 00:00:00 2001 From: James Ward Date: Wed, 26 Aug 2026 16:43:10 -0600 Subject: [PATCH] Install AWS Knowledge MCP instead of AWS MCP Currently `aws configure agent-toolkit` adds a local proxy-based MCP server definition (with user approval) to an agent's config. Since the user is already running the `aws` CLI they likely don't need the control plane part of the AWS MCP server, this can confuse the agent about what to use, it has a system dependency on `uv`, and is difficult to troubleshoot. Instead, the `aws` CLI should only add the HTTP-based AWS Knowledge MCP server. --- awscli/customizations/agenttoolkit/agents.py | 54 ++++---- .../customizations/agenttoolkit/configure.py | 9 +- .../agenttoolkit/test_agents.py | 44 +++--- .../agenttoolkit/test_configure.py | 17 ++- .../agenttoolkit/test_knowledge_mcp.py | 131 ++++++++++++++++++ 5 files changed, 203 insertions(+), 52 deletions(-) create mode 100644 tests/unit/customizations/agenttoolkit/test_knowledge_mcp.py diff --git a/awscli/customizations/agenttoolkit/agents.py b/awscli/customizations/agenttoolkit/agents.py index 7b2d13fc71f4..2481b7f29219 100644 --- a/awscli/customizations/agenttoolkit/agents.py +++ b/awscli/customizations/agenttoolkit/agents.py @@ -21,7 +21,8 @@ SKILL_FILENAME = 'SKILL.md' SKILL_METADATA_FILENAME = '.aws-skill-metadata' -AWS_MCP_SERVER_KEY = 'aws-mcp' +AWS_KNOWLEDGE_MCP_SERVER_KEY = 'aws-knowledge-mcp-server' +AWS_KNOWLEDGE_MCP_SERVER_URL = 'https://knowledge-mcp.global.api.aws' UNIVERSAL_ROW_ID = 'universal' @@ -40,17 +41,8 @@ class McpConfigureAction(enum.Enum): SKIPPED = 'skipped' -_AWS_MCP_PROXY_ARGS = [ - 'mcp-proxy-for-aws@latest', - 'https://aws-mcp.us-east-1.api.aws/mcp', - '--metadata', - 'INSTALL_SOURCE=aws-cli', -] - - DEFAULT_MCP_SERVER_CONFIG = { - 'command': 'uvx', - 'args': _AWS_MCP_PROXY_ARGS, + 'url': AWS_KNOWLEDGE_MCP_SERVER_URL, } @@ -98,20 +90,19 @@ class AgentConfig: agents use ``mcpServers``; OpenCode uses ``mcp``.""" mcp_extra_config: Optional[dict] = None - """Extra fields merged into the default MCP server entry (e.g. - Kiro requires ``timeout`` and ``transport``). Used only when + """Extra fields merged into the default ``{url}`` MCP server entry + (e.g. Claude Code requires ``type``). Used only when ``mcp_server_entry`` is not set.""" mcp_server_entry: Optional[dict] = None """Complete MCP server entry, replacing the default schema. Use - when an agent expects a different shape than ``{command, args}`` - (e.g. OpenCode expects ``{type, command: [...]}``).""" + when an agent expects a different remote-server shape (e.g. Gemini CLI + expects ``httpUrl`` and Windsurf documents ``serverUrl``).""" mcp_shell_command: Optional[list] = None - """Argv for a CLI invocation that registers the AWS MCP server - (e.g. ``codex mcp add ...``). Used for agents whose MCP config is - not JSON. The wizard skips MCP setup if the executable is not on - PATH.""" + """Argv for a CLI invocation that registers the AWS Knowledge MCP server + (e.g. ``codex mcp add ... --url ...``). Used for agents whose MCP config + is not JSON. The wizard skips MCP setup if the executable is not on PATH.""" def __post_init__(self): if self.mcp_extra_config is None: @@ -225,7 +216,7 @@ def configure_mcp_server(self): existing_config = self._read_mcp_config(config_path) servers = existing_config.setdefault(self.config.mcp_servers_key, {}) - if AWS_MCP_SERVER_KEY in servers: + if AWS_KNOWLEDGE_MCP_SERVER_KEY in servers: return McpConfigureAction.ALREADY_CONFIGURED, config_path if self.config.mcp_server_entry is not None: @@ -235,7 +226,7 @@ def configure_mcp_server(self): **DEFAULT_MCP_SERVER_CONFIG, **self.config.mcp_extra_config, } - servers[AWS_MCP_SERVER_KEY] = server_entry + servers[AWS_KNOWLEDGE_MCP_SERVER_KEY] = server_entry self._write_mcp_config(config_path, existing_config) return McpConfigureAction.CONFIGURED, config_path @@ -284,6 +275,7 @@ def __init__(self, agent, name, path): detection_path='~/.claude/', mcp_config_path='~/.claude.json', mcp_servers_key='mcpServers', + mcp_extra_config={'type': 'http'}, detection_path_env_override='CLAUDE_CONFIG_DIR', ), # https://docs.cline.bot/mcp/mcp-overview @@ -294,6 +286,10 @@ def __init__(self, agent, name, path): detection_path='~/.cline/', mcp_config_path='mcp.json', mcp_servers_key='mcpServers', + mcp_extra_config={ + 'type': 'streamableHttp', + 'disabled': False, + }, ), # https://developers.openai.com/codex/skills # https://github.com/openai/codex/blob/main/codex-rs/cli/src/mcp_cmd.rs @@ -309,10 +305,9 @@ def __init__(self, agent, name, path): 'codex', 'mcp', 'add', - AWS_MCP_SERVER_KEY, - '--', - 'uvx', - *_AWS_MCP_PROXY_ARGS, + AWS_KNOWLEDGE_MCP_SERVER_KEY, + '--url', + AWS_KNOWLEDGE_MCP_SERVER_URL, ], ), # https://docs.cursor.com/context/model-context-protocol @@ -333,6 +328,7 @@ def __init__(self, agent, name, path): skills_path_override='~/.agents/skills/', mcp_config_path='settings.json', mcp_servers_key='mcpServers', + mcp_server_entry={'httpUrl': AWS_KNOWLEDGE_MCP_SERVER_URL}, ), # https://kiro.dev/docs/mcp/configuration/ AgentConfig( @@ -341,7 +337,7 @@ def __init__(self, agent, name, path): detection_path='~/.kiro/', mcp_config_path='settings/mcp.json', mcp_servers_key='mcpServers', - mcp_extra_config={'timeout': 100000, 'transport': 'stdio'}, + mcp_extra_config={'disabled': False}, ), # https://openclaw-openclaw.mintlify.app/configuration # OpenClaw does not document MCP support — install skills only. @@ -360,8 +356,9 @@ def __init__(self, agent, name, path): mcp_config_path='opencode.json', mcp_servers_key='mcp', mcp_server_entry={ - 'type': 'local', - 'command': ['uvx', *_AWS_MCP_PROXY_ARGS], + 'type': 'remote', + 'url': AWS_KNOWLEDGE_MCP_SERVER_URL, + 'enabled': True, }, ), # https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/settings.md @@ -380,6 +377,7 @@ def __init__(self, agent, name, path): skills_path_override='~/.agents/skills/', mcp_config_path='~/.codeium/mcp_config.json', mcp_servers_key='mcpServers', + mcp_server_entry={'serverUrl': AWS_KNOWLEDGE_MCP_SERVER_URL}, ), ] diff --git a/awscli/customizations/agenttoolkit/configure.py b/awscli/customizations/agenttoolkit/configure.py index 834752183f07..535664e13c36 100644 --- a/awscli/customizations/agenttoolkit/configure.py +++ b/awscli/customizations/agenttoolkit/configure.py @@ -35,7 +35,8 @@ class ConfigureAgentToolkitCommand(BasicCommand): NAME = 'agent-toolkit' DESCRIPTION = ( - 'Set up AI coding agents with AWS skills and the AWS MCP Server.\n\n' + 'Set up AI coding agents with AWS skills and the AWS Knowledge MCP ' + 'server.\n\n' 'Supported agents are determined by the presence of configuration ' 'directories, such as ``~/.kiro``. Skills are installed globally in ' 'those configuration directories, not per project.' @@ -47,7 +48,7 @@ class ConfigureAgentToolkitCommand(BasicCommand): 'help_text': ( 'Skip all interactive prompts and accept defaults: ' 'select all detected agents, install default skills, ' - 'and configure the AWS MCP server.' + 'and configure the AWS Knowledge MCP server.' ), 'default': False, 'action': 'store_true', @@ -196,11 +197,11 @@ def _install_default_skills(self, selected_agents, client, yes=False): def _configure_mcp(self, agents, yes=False): if not yes and not yes_no_choice( - '\nConfigure AWS MCP server connection? [Y/n]: ' + '\nConfigure AWS Knowledge MCP server connection? [Y/n]: ' ): return - uni_print('\nAWS MCP server configured for:\n', self._stream) + uni_print('\nAWS Knowledge MCP server configured for:\n', self._stream) skipped = [] for agent in agents: action, detail = agent.configure_mcp_server() diff --git a/tests/unit/customizations/agenttoolkit/test_agents.py b/tests/unit/customizations/agenttoolkit/test_agents.py index 9d651db36331..3bfc33d3332d 100644 --- a/tests/unit/customizations/agenttoolkit/test_agents.py +++ b/tests/unit/customizations/agenttoolkit/test_agents.py @@ -177,8 +177,8 @@ def test_configure_mcp_new_file(tmp_path): action, path = agent.configure_mcp_server() assert action is McpConfigureAction.CONFIGURED data = json.loads(open(path).read()) - assert data['mcpServers']['aws-mcp']['command'] == 'uvx' - assert 'mcp-proxy-for-aws@latest' in data['mcpServers']['aws-mcp']['args'] + entry = data['mcpServers']['aws-knowledge-mcp-server'] + assert entry == {'url': 'https://knowledge-mcp.global.api.aws'} def test_configure_mcp_already_configured(tmp_path): @@ -186,12 +186,22 @@ def test_configure_mcp_already_configured(tmp_path): agent = make_config(tmp_path, mcp_config_path='mcp.json').detect() mcp_path = tmp_path / '.test-agent' / 'mcp.json' mcp_path.write_text( - json.dumps({'mcpServers': {'aws-mcp': {'command': 'custom'}}}) + json.dumps( + { + 'mcpServers': { + 'aws-knowledge-mcp-server': { + 'url': 'https://custom.example.com' + } + } + } + ) ) action, _ = agent.configure_mcp_server() assert action is McpConfigureAction.ALREADY_CONFIGURED data = json.loads(mcp_path.read_text()) - assert data['mcpServers']['aws-mcp']['command'] == 'custom' + assert data['mcpServers']['aws-knowledge-mcp-server'] == { + 'url': 'https://custom.example.com' + } def test_configure_mcp_preserves_other_servers(tmp_path): @@ -204,7 +214,7 @@ def test_configure_mcp_preserves_other_servers(tmp_path): agent.configure_mcp_server() data = json.loads(mcp_path.read_text()) assert data['mcpServers']['other']['command'] == 'foo' - assert 'aws-mcp' in data['mcpServers'] + assert 'aws-knowledge-mcp-server' in data['mcpServers'] def test_configure_mcp_extra_config_merged(tmp_path): @@ -212,21 +222,23 @@ def test_configure_mcp_extra_config_merged(tmp_path): agent = make_config( tmp_path, mcp_config_path='mcp.json', - mcp_extra_config={'timeout': 100000}, + mcp_extra_config={'disabled': False}, ).detect() agent.configure_mcp_server() mcp_path = tmp_path / '.test-agent' / 'mcp.json' data = json.loads(mcp_path.read_text()) - entry = data['mcpServers']['aws-mcp'] - assert entry['timeout'] == 100000 - assert entry['command'] == 'uvx' + entry = data['mcpServers']['aws-knowledge-mcp-server'] + assert entry == { + 'url': 'https://knowledge-mcp.global.api.aws', + 'disabled': False, + } def test_configure_mcp_server_entry_overrides_default(tmp_path): (tmp_path / '.test-agent').mkdir() custom_entry = { - 'type': 'local', - 'command': ['uvx', 'something@latest'], + 'type': 'remote', + 'url': 'https://custom.example.com', } agent = make_config( tmp_path, @@ -237,16 +249,16 @@ def test_configure_mcp_server_entry_overrides_default(tmp_path): agent.configure_mcp_server() mcp_path = tmp_path / '.test-agent' / 'mcp.json' data = json.loads(mcp_path.read_text()) - entry = data['mcp']['aws-mcp'] + entry = data['mcp']['aws-knowledge-mcp-server'] assert entry == custom_entry - assert 'args' not in entry + assert 'command' not in entry def test_configure_mcp_shell_command_runs_when_executable_present(tmp_path): (tmp_path / '.test-agent').mkdir() config = make_config( tmp_path, - mcp_shell_command=['some-cli', 'mcp', 'add', 'aws-mcp'], + mcp_shell_command=['some-cli', 'mcp', 'add', 'test-server'], ) agent = config.detect() with ( @@ -262,7 +274,7 @@ def test_configure_mcp_shell_command_runs_when_executable_present(tmp_path): assert action is McpConfigureAction.CONFIGURED assert detail == 'some-cli' run_mock.assert_called_once_with( - ['some-cli', 'mcp', 'add', 'aws-mcp'], check=True + ['some-cli', 'mcp', 'add', 'test-server'], check=True ) @@ -270,7 +282,7 @@ def test_configure_mcp_shell_command_skipped_when_executable_missing(tmp_path): (tmp_path / '.test-agent').mkdir() config = make_config( tmp_path, - mcp_shell_command=['missing-cli', 'mcp', 'add', 'aws-mcp'], + mcp_shell_command=['missing-cli', 'mcp', 'add', 'test-server'], ) agent = config.detect() with ( diff --git a/tests/unit/customizations/agenttoolkit/test_configure.py b/tests/unit/customizations/agenttoolkit/test_configure.py index de76a3d04e79..e62ef0e09c71 100644 --- a/tests/unit/customizations/agenttoolkit/test_configure.py +++ b/tests/unit/customizations/agenttoolkit/test_configure.py @@ -114,8 +114,9 @@ def test_mcp_configured_on_yes(tmp_path): mcp_path = tmp_path / '.agent-0' / 'mcp.json' assert mcp_path.exists() data = json.loads(mcp_path.read_text()) - assert 'aws-mcp' in data['mcpServers'] - assert data['mcpServers']['aws-mcp']['command'] == 'uvx' + assert data['mcpServers']['aws-knowledge-mcp-server'] == { + 'url': 'https://knowledge-mcp.global.api.aws' + } def test_mcp_skipped_on_no(tmp_path): @@ -130,12 +131,20 @@ def test_mcp_already_configured_not_overwritten(tmp_path): mcp_path = tmp_path / '.agent-0' / 'mcp.json' mcp_path.write_text( json.dumps( - {'mcpServers': {'aws-mcp': {'command': 'custom', 'args': []}}} + { + 'mcpServers': { + 'aws-knowledge-mcp-server': { + 'url': 'https://custom.example.com' + } + } + } ) ) _, stream = _run(configs, yes_no_return=True) data = json.loads(mcp_path.read_text()) - assert data['mcpServers']['aws-mcp']['command'] == 'custom' + assert data['mcpServers']['aws-knowledge-mcp-server'] == { + 'url': 'https://custom.example.com' + } assert 'already configured' in stream.getvalue() diff --git a/tests/unit/customizations/agenttoolkit/test_knowledge_mcp.py b/tests/unit/customizations/agenttoolkit/test_knowledge_mcp.py new file mode 100644 index 000000000000..9885b3ccc3d9 --- /dev/null +++ b/tests/unit/customizations/agenttoolkit/test_knowledge_mcp.py @@ -0,0 +1,131 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import dataclasses +import json +from io import StringIO +from unittest.mock import MagicMock, patch + +from awscli.customizations.agenttoolkit.agents import AGENT_CONFIGS +from awscli.customizations.agenttoolkit.configure import ( + ConfigureAgentToolkitCommand, +) +from tests.unit.customizations.agenttoolkit.utils import make_session + +KNOWLEDGE_SERVER_KEY = 'aws-knowledge-mcp-server' +KNOWLEDGE_SERVER_URL = 'https://knowledge-mcp.global.api.aws' + +EXPECTED_JSON_ENTRIES = { + 'claude-code': { + 'url': KNOWLEDGE_SERVER_URL, + 'type': 'http', + }, + 'cline': { + 'url': KNOWLEDGE_SERVER_URL, + 'type': 'streamableHttp', + 'disabled': False, + }, + 'cursor': { + 'url': KNOWLEDGE_SERVER_URL, + }, + 'gemini-cli': { + 'httpUrl': KNOWLEDGE_SERVER_URL, + }, + 'kiro': { + 'url': KNOWLEDGE_SERVER_URL, + 'disabled': False, + }, + 'opencode': { + 'type': 'remote', + 'url': KNOWLEDGE_SERVER_URL, + 'enabled': True, + }, + 'windsurf': { + 'serverUrl': KNOWLEDGE_SERVER_URL, + }, +} + + +def _get_agent_config(agent_id): + return next(config for config in AGENT_CONFIGS if config.id == agent_id) + + +def test_json_agents_use_their_remote_knowledge_server_schema(tmp_path): + for agent_id, expected_entry in EXPECTED_JSON_ENTRIES.items(): + base_dir = tmp_path / agent_id + base_dir.mkdir() + source_config = _get_agent_config(agent_id) + config = dataclasses.replace( + source_config, + detection_path=str(base_dir), + detection_path_env_override=None, + mcp_config_path='mcp.json', + ) + + agent = config.detect() + agent.configure_mcp_server() + + data = json.loads((base_dir / 'mcp.json').read_text()) + assert data[config.mcp_servers_key][KNOWLEDGE_SERVER_KEY] == ( + expected_entry + ) + + +def test_codex_registers_remote_knowledge_server_by_url(): + config = _get_agent_config('codex') + assert config.mcp_shell_command == [ + 'codex', + 'mcp', + 'add', + KNOWLEDGE_SERVER_KEY, + '--url', + KNOWLEDGE_SERVER_URL, + ] + + +def test_configure_prompt_names_aws_knowledge_mcp_server(tmp_path): + base_dir = tmp_path / '.test-agent' + base_dir.mkdir() + source_config = _get_agent_config('cursor') + config = dataclasses.replace( + source_config, + detection_path=str(base_dir), + mcp_config_path='mcp.json', + ) + client = MagicMock() + paginator = MagicMock() + paginator.paginate.return_value = [{'skills': []}] + client.get_paginator.return_value = paginator + command = ConfigureAgentToolkitCommand( + make_session(), + stream=StringIO(), + agent_configs=[config], + client=client, + ) + parsed_args = MagicMock() + parsed_args.yes = False + + with ( + patch( + 'awscli.customizations.agenttoolkit.configure.multiselect_choice', + side_effect=lambda message, items, **kwargs: items, + ), + patch( + 'awscli.customizations.agenttoolkit.configure.yes_no_choice', + return_value=False, + ) as yes_no_mock, + ): + command._run_main(parsed_args, None) + + yes_no_mock.assert_called_once_with( + '\nConfigure AWS Knowledge MCP server connection? [Y/n]: ' + )