From 0fe8c205f40b20531f3999e10b055e070172b859 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Wed, 28 Jan 2026 15:01:51 -0700 Subject: [PATCH 01/15] feat: add mcp integration tests --- mcp-local/tests/constants.py | 237 +++++++++++++++++++++++++++++++ mcp-local/tests/requirements.txt | 2 + mcp-local/tests/sum_test.s | 5 + mcp-local/tests/test_mcp.py | 158 +++++++++++++++++++++ 4 files changed, 402 insertions(+) create mode 100644 mcp-local/tests/constants.py create mode 100644 mcp-local/tests/requirements.txt create mode 100644 mcp-local/tests/sum_test.s create mode 100644 mcp-local/tests/test_mcp.py diff --git a/mcp-local/tests/constants.py b/mcp-local/tests/constants.py new file mode 100644 index 0000000..1ded281 --- /dev/null +++ b/mcp-local/tests/constants.py @@ -0,0 +1,237 @@ +# Copyright © 2025, Arm Limited and Contributors. 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +REPO_ROOT = "" + +INIT_REQUEST = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "pytest", "version": "0.1"}, + }, + } + +CHECK_IMAGE_REQUEST = { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "check_image", + "arguments": { + "image": "ubuntu:24.04", + "invocation_reason": ( + "Checking ARM architecture compatibility for ubuntu:24.04 " + "container image as requested by the user" + ), + }, + }, + } + +EXPECTED_CHECK_IMAGE_RESPONSE = { + "status": "success", + "message": "Image ubuntu:24.04 supports all required architectures", + "architectures": [ + "amd64", + "unknown", + "arm", + "unknown", + "arm64", + "unknown", + "ppc64le", + "unknown", + "riscv64", + "unknown", + "s390x", + "unknown", + ], + } + +CHECK_SKOPEO_REQUEST = { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "skopeo", + "arguments": { + "image": "armswdev/arm-mcp", + "invocation_reason": ( + "Checking the architecture support of the armswdev/arm-mcp container image to verify ARM compatibility as requested by the user." + ), + }, + }, + } + +EXPECTED_CHECK_SKOPEO_RESPONSE = { + "status": "ok", + "code": 0, + "stdout": "{\n \"Name\": \"docker.io/armswdev/arm-mcp\",\n \"Digest\": \"sha256:dab8aea984074c48c011fe2a7171c2f37ac7403738e0641b0ff99eee384621ea\",\n \"RepoTags\": [\n \"latest\"\n ],\n \"Created\": \"2025-11-07T00:10:00.848837841Z\",\n \"DockerVersion\": \"\",\n \"Labels\": {\n \"org.opencontainers.image.ref.name\": \"ubuntu\",\n \"org.opencontainers.image.version\": \"24.04\"\n },\n \"Architecture\": \"arm64\",\n \"Os\": \"linux\",\n \"Layers\": [\n \"sha256:b8a35db46e38ce87d4e743e1265ff436ed36e01d23246b24a1cbbeaae18ec432\",\n \"sha256:aee89bc0d0e3194f35090f6d190868784dee4057f910a6af3a7c68cba5cd2c46\",\n \"sha256:569ccd11958806cb03ec8fa1575bea7b0b45c290c1045b9b0ecf6fa354da53d7\",\n \"sha256:4f13b03e4ce3d1ab958df1f4f3e0edcb3a276fd0c5bc08406ebc978811a172f6\",\n \"sha256:2c0e15ad1ba57372508f93cf701e2bdc90600144ce2c8632264f4419976c55bf\",\n \"sha256:0eb217d3f4f4dcd7419cfcb70a77645d271ed768e88ef136ab49abd56bf478e3\",\n \"sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1\"\n ],\n \"LayersData\": [\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:b8a35db46e38ce87d4e743e1265ff436ed36e01d23246b24a1cbbeaae18ec432\",\n \"Size\": 28861712,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:aee89bc0d0e3194f35090f6d190868784dee4057f910a6af3a7c68cba5cd2c46\",\n \"Size\": 142025708,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:569ccd11958806cb03ec8fa1575bea7b0b45c290c1045b9b0ecf6fa354da53d7\",\n \"Size\": 107240731,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:4f13b03e4ce3d1ab958df1f4f3e0edcb3a276fd0c5bc08406ebc978811a172f6\",\n \"Size\": 1180,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:2c0e15ad1ba57372508f93cf701e2bdc90600144ce2c8632264f4419976c55bf\",\n \"Size\": 7105736,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:0eb217d3f4f4dcd7419cfcb70a77645d271ed768e88ef136ab49abd56bf478e3\",\n \"Size\": 392970938,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1\",\n \"Size\": 32,\n \"Annotations\": null\n }\n ],\n \"Env\": [\n \"PATH=/app/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n \"DEBIAN_FRONTEND=noninteractive\",\n \"PYTHONUNBUFFERED=1\",\n \"PIP_NO_CACHE_DIR=1\",\n \"WORKSPACE_DIR=/workspace\",\n \"VIRTUAL_ENV=/app/.venv\"\n ]\n}\n", + "stderr": "", + "cmd": [ + "skopeo", + "inspect", + "docker://armswdev/arm-mcp" + ] +} + +CHECK_NGINX_REQUEST = { + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "knowledge_base_search", + "arguments": { + "query": "nginx performance tweaks", + }, + }, + } + +EXPECTED_CHECK_NGINX_RESPONSE = [ +"https://learn.arm.com/learning-paths/servers-and-cloud-computing/nginx_tune/tune_static_file_server", + "https://learn.arm.com/learning-paths/servers-and-cloud-computing/nginx_tune/test_optimizations", + "https://learn.arm.com/learning-paths/servers-and-cloud-computing/nginx_tune/kernel_comp_lib", + "https://learn.arm.com/learning-paths/servers-and-cloud-computing/nginx_tune/before_and_after", +"https://learn.arm.com/learning-paths/servers-and-cloud-computing/nginx_tune/tune_revprox_and_apigw" + ] + +CHECK_MIGRATE_EASE_TOOL_REQUEST = { + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "migrate_ease_scan", + "arguments": { + "scanner": "java", + }, + }, + } +EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE = { + "status": "success", + "returncode": 0, + "command": "migrate-ease-java --march armv8-a --output /tmp/migrate_ease_java_20260126-215207.json /tmp/migrate_ease_filtered_s45ojwm1", + "ran_from": "/app", + "target": "/workspace (filtered)", + "stdout": "[Java] Loading of check_points.yaml took 0.002821 seconds.\n[Java] Initialization of checkpoints took 0.000328 seconds.\nNo issue found.\n", + "stderr": "", + "output_file": "/tmp/migrate_ease_java_20260126-215207.json", + "output_format": "json", + "workspace_listing": [ + "invocation_reasons.yaml" + ], + "excluded_items": [], + "excluded_count": 0, + "parsed_results": { + "branch": None, + "commit": None, + "errors": [], + "file_summary": { + "jar": { + "count": 0, + "fileName": "Jar", + "loc": 0 + }, + "java": { + "count": 0, + "fileName": "java", + "loc": 0 + }, + "pom": { + "count": 0, + "fileName": "POM", + "loc": 0 + } + }, + "git_repo": None, + "issue_summary": { + "Error": { + "count": 0, + "des": "Exception encountered by the code scanning tool during the scanning process, not an issue with the code logic itself. User can ignore it." + }, + "JarIssue": { + "count": 0, + "des": "JAR package does not support target arch. Need to rebuild or upgrade." + }, + "JavaSourceIssue": { + "count": 0, + "des": "Java source file contains native call that may need modify/rebuild for target arch." + }, + "OtherIssue": { + "count": 0, + "des": "Issues exceeding the limit will be categorized as OtherIssue. when the issue count limit option is enabled" + }, + "PomIssue": { + "count": 0, + "des": "Pom imports java artifact that does not support target arch." + } + }, + "issue_type_config": None, + "issues": [], + "language_type": "java", + "march": "armv8-a", + "output": None, + "progress": True, + "quiet": False, + "remarks": [], + "root_directory": "/tmp/migrate_ease_filtered_s45ojwm1", + "source_dirs": [], + "source_files": [], + "target_os": "OpenAnolis", + "total_issue_count": 0 + }, + "output_file_deleted": True +} + +CHECK_SYSREPORT_TOOL_REQUEST = { + "jsonrpc": "2.0", + "id": 6, + "method": "tools/call", + "params": { + "name": "sysreport_instructions", + "arguments": { + "invocation_reason": "Providing instructions for using the sysreport tool as requested by the user.", + }, + }, + } +EXPECTED_CHECK_SYSREPORT_TOOL_RESPONSE = { + "instructions": "\n# SysReport Installation and Usage\n\n## Installation\n```bash\ngit clone https://github.com/ArmDeveloperEcosystem/sysreport.git\ncd sysreport\n```\n\n## Usage\n```bash\npython3 sysreport.py\n```\n\n## What SysReport Does\n- Gathers comprehensive system information including architecture, CPU, memory, and hardware details\n- Useful for diagnosing system issues or understanding system capabilities\n- Provides detailed hardware and software configuration data\n\n## Note\nRun these commands directly on your host system (not in a container) to get accurate system information.\n", + "repository": "https://github.com/ArmDeveloperEcosystem/sysreport.git", + "usage_command": "python3 sysreport.py", + "note": "This tool must be run on the host system to provide accurate system information." +} + +CHECK_MCA_TOOL_REQUEST = { + "jsonrpc": "2.0", + "id": 7, + "method": "tools/call", + "params": { + "name": "mca", + "arguments": { + "input_path": "/workspace/tests/sum_test.s", + "invocation_reason": "User requested to run the MCA tool on the ARM assembly file sum_test.s to analyze its performance characteristics, using the correct workspace path" + }, + }, + } + +EXPECTED_CHECK_MCA_TOOL_RESPONSE = { + "status": "ok", + "code": 0, + "stdout": "Iterations: 100\nInstructions: 500\nTotal Cycles: 501\nTotal uOps: 500\n\nDispatch Width: 3\nuOps Per Cycle: 1.00\nIPC: 1.00\nBlock RThroughput: 1.7\n\n\nInstruction Info:\n[1]: #uOps\n[2]: Latency\n[3]: RThroughput\n[4]: MayLoad\n[5]: MayStore\n[6]: HasSideEffects (U)\n\n[1] [2] [3] [4] [5] [6] Instructions:\n 1 1 0.33 add\tx1, x1, x2\n 1 1 0.33 add\tx1, x1, x3\n 1 1 0.33 add\tx1, x1, x4\n 1 1 0.33 add\tx1, x1, x5\n 1 1 0.33 add\tx1, x1, x6\n\n\nResources:\n[0] - CortexA510UnitALU0\n[1.0] - CortexA510UnitALU12\n[1.1] - CortexA510UnitALU12\n[2] - CortexA510UnitB\n[3] - CortexA510UnitDiv\n[4] - CortexA510UnitLd1\n[5] - CortexA510UnitLdSt\n[6] - CortexA510UnitMAC\n[7] - CortexA510UnitPAC\n[8] - CortexA510UnitVALU0\n[9] - CortexA510UnitVALU1\n[10.0] - CortexA510UnitVMAC\n[10.1] - CortexA510UnitVMAC\n[11] - CortexA510UnitVMC\n\n\nResource pressure per iteration:\n[0] [1.0] [1.1] [2] [3] [4] [5] [6] [7] [8] [9] [10.0] [10.1] [11] \n - 2.50 2.50 - - - - - - - - - - - \n\nResource pressure by instruction:\n[0] [1.0] [1.1] [2] [3] [4] [5] [6] [7] [8] [9] [10.0] [10.1] [11] Instructions:\n - 0.50 0.50 - - - - - - - - - - - add\tx1, x1, x2\n - 0.50 0.50 - - - - - - - - - - - add\tx1, x1, x3\n - 0.50 0.50 - - - - - - - - - - - add\tx1, x1, x4\n - 0.50 0.50 - - - - - - - - - - - add\tx1, x1, x5\n - 0.50 0.50 - - - - - - - - - - - add\tx1, x1, x6\n", + "stderr": "", + "cmd": [ + "llvm-mca", + "/workspace/tests/sum_test.s" + ] + } diff --git a/mcp-local/tests/requirements.txt b/mcp-local/tests/requirements.txt new file mode 100644 index 0000000..2e2dc05 --- /dev/null +++ b/mcp-local/tests/requirements.txt @@ -0,0 +1,2 @@ +testcontainers +pytest \ No newline at end of file diff --git a/mcp-local/tests/sum_test.s b/mcp-local/tests/sum_test.s new file mode 100644 index 0000000..bc81d73 --- /dev/null +++ b/mcp-local/tests/sum_test.s @@ -0,0 +1,5 @@ +add x1, x1, x2 +add x1, x1, x3 +add x1, x1, x4 +add x1, x1, x5 +add x1, x1, x6 \ No newline at end of file diff --git a/mcp-local/tests/test_mcp.py b/mcp-local/tests/test_mcp.py new file mode 100644 index 0000000..0c5644d --- /dev/null +++ b/mcp-local/tests/test_mcp.py @@ -0,0 +1,158 @@ +# Copyright © 2025, Arm Limited and Contributors. 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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 json +import constants +import os +import time +from pathlib import Path + +import pytest +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for_logs + +def _encode_mcp_message(payload: dict) -> bytes: + # FastMCP stdio expects raw JSON per message (newline-delimited). + return (json.dumps(payload) + "\n").encode("utf-8") + + +def _read_docker_frame(sock, timeout: float) -> bytes: + deadline = time.time() + timeout + header = b"" + while len(header) < 8: + if time.time() > deadline: + raise TimeoutError("Timed out waiting for docker frame header.") + chunk = sock.recv(8 - len(header)) + if not chunk: + time.sleep(0.01) + continue + header += chunk + + if header[1:4] != b"\x00\x00\x00": + return header + + size = int.from_bytes(header[4:8], "big") + payload = b"" + while len(payload) < size: + if time.time() > deadline: + raise TimeoutError("Timed out waiting for docker frame payload.") + chunk = sock.recv(size - len(payload)) + if not chunk: + time.sleep(0.01) + continue + payload += chunk + return payload + + +def _read_mcp_message(sock, timeout: float = 10.0) -> dict: + deadline = time.time() + timeout + buffer = b"" + while True: + if time.time() > deadline: + raise TimeoutError("Timed out waiting for MCP response line.") + try: + frame = _read_docker_frame(sock, timeout) + except TimeoutError: + raise + buffer += frame + while b"\n" in buffer: + line, buffer = buffer.split(b"\n", 1) + if not line: + continue + try: + return json.loads(line.decode("utf-8")) + except json.JSONDecodeError: + idx = line.find(b"{") + if idx != -1: + try: + return json.loads(line[idx:].decode("utf-8")) + except json.JSONDecodeError: + continue + +def test_mcp_stdio_transport_responds(): + image = os.getenv("MCP_IMAGE", "arm-mcp:latest") + constants.REPO_ROOT = Path(__file__).resolve().parents[1] + print("\n***repo root: ", constants.REPO_ROOT ) + with ( + DockerContainer(image) + .with_volume_mapping(str(constants.REPO_ROOT ), "/workspace") + .with_kwargs(stdin_open=True, tty=False) + ) as container: + wait_for_logs(container, "Starting MCP server", timeout=60) + socket_wrapper = container.get_wrapped_container().attach_socket( + params={"stdin": 1, "stdout": 1, "stderr": 1, "stream": 1} + ) + raw_socket = socket_wrapper._sock + raw_socket.settimeout(10) + + raw_socket.sendall(_encode_mcp_message(constants.INIT_REQUEST)) + response = _read_mcp_message(raw_socket, timeout=20) + + #Check Container Init Test + assert response.get("id") == 1, "Test Failed: MCP initialize response id mismatch." + assert "result" in response, "Test Failed: MCP initialize response missing result field." + assert "serverInfo" in response["result"], "Test Failed: MCP initialize response missing serverInfo field." + raw_socket.sendall( + _encode_mcp_message({"jsonrpc": "2.0", "method": "initialized", "params": {}}) + ) + + def _read_response(expected_id: int, timeout: float = 10.0) -> dict: + deadline = time.time() + timeout + while time.time() < deadline: + message = _read_mcp_message(raw_socket, timeout=timeout) + if message.get("id") == expected_id: + return message + raise TimeoutError(f"Timed out waiting for MCP response id={expected_id}.") + + print("\n***Test Passed: arm-mcp container initilized and ran successfully") + + #Check Image Tool Test + raw_socket.sendall(_encode_mcp_message(constants.CHECK_IMAGE_REQUEST)) + check_image_response = _read_response(2, timeout=60) + assert check_image_response.get("result")["structuredContent"] == constants.EXPECTED_CHECK_IMAGE_RESPONSE, "Test Failed: MCP check_image tool failed: content mismatch. Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_IMAGE_RESPONSE,indent=2), json.dumps(check_image_response.get("result")["structuredContent"],indent=2)) + print("\n***Test Passed: MCP check_image tool succeeded") + + #Check Skopeo Tool Test + raw_socket.sendall(_encode_mcp_message(constants.CHECK_SKOPEO_REQUEST)) + check_skopeo_response = _read_response(3, timeout=60) + assert check_skopeo_response.get("result")["structuredContent"] == constants.EXPECTED_CHECK_SKOPEO_RESPONSE, "Test Failed: MCP check_skopeo tool failed: content mismatch. Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_SKOPEO_RESPONSE,indent=2), json.dumps(check_skopeo_response.get("result")["structuredContent"],indent=2)) + print("\n***Test Passed: MCP check_skopeo tool succeeded") + + #Check NGINX Query Test + raw_socket.sendall(_encode_mcp_message(constants.CHECK_NGINX_REQUEST)) + check_nginx_response = _read_response(4, timeout=60) + assert any(item in str(check_nginx_response.get("result")["structuredContent"]) for item in constants.EXPECTED_CHECK_NGINX_RESPONSE), "Test Failed: MCP check_nginx tool failed: content mismatch., Expected one of: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_NGINX_RESPONSE,indent=2), json.dumps(check_nginx_response.get("result")["structuredContent"],indent=2)) + print("\n***Test Passed: MCP check_nginx tool succeeded") + + #Check Migrate Ease Tool Test + raw_socket.sendall(_encode_mcp_message(constants.CHECK_MIGRATE_EASE_TOOL_REQUEST)) + check_migrate_ease_tool_response = _read_response(5, timeout=60) + #assert only the status field to avoid mismatches due to dynamic fields + assert check_migrate_ease_tool_response.get("result")["structuredContent"]["status"] == constants.EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE["status"], "Test Failed: MCP check_migrate_ease_tool tool failed: status mismatch. Expected: {}, Received: {}".format(constants.EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE["status"], check_migrate_ease_tool_response.get("result")["structuredContent"]["status"]) + print("\n***Test Passed: MCP check_migrate_ease_tool tool succeeded") + + #Check Sysreport Tool Test + raw_socket.sendall(_encode_mcp_message(constants.CHECK_SYSREPORT_TOOL_REQUEST)) + check_sysreport_response = _read_response(6, timeout=60) + assert check_sysreport_response.get("result")["structuredContent"] == constants.EXPECTED_CHECK_SYSREPORT_TOOL_RESPONSE, "Test Failed: MCP sysreport_instructions tool failed: content mismatch. Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_SYSREPORT_TOOL_RESPONSE,indent=2), json.dumps(check_sysreport_response.get("result")["structuredContent"],indent=2)) + print("\n***Test Passed: MCP sysreport_instructions tool succeeded") + + #Check MCA Tool Test + raw_socket.sendall(_encode_mcp_message(constants.CHECK_MCA_TOOL_REQUEST)) + check_mca_response = _read_response(7, timeout=60) + assert check_mca_response.get("result")["structuredContent"] == constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE, "Test Failed: MCP mca tool failed: content mismatch.Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE,indent=2), json.dumps(check_mca_response.get("result")["structuredContent"],indent=2)) + print("\n***Test Passed: MCP mca tool succeeded") + +if __name__ == "__main__": + pytest.main([__file__]) From 9945c7ed5c4594b9e614f5e1571c8a57118b8103 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Wed, 28 Jan 2026 15:07:48 -0700 Subject: [PATCH 02/15] fix: change copyright year --- mcp-local/tests/constants.py | 4 +--- mcp-local/tests/sum_test.s | 14 ++++++++++++++ mcp-local/tests/test_mcp.py | 8 ++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/mcp-local/tests/constants.py b/mcp-local/tests/constants.py index 1ded281..ddf0ea1 100644 --- a/mcp-local/tests/constants.py +++ b/mcp-local/tests/constants.py @@ -1,4 +1,4 @@ -# Copyright © 2025, Arm Limited and Contributors. All rights reserved. +# Copyright © 2026, Arm Limited and Contributors. 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. @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -REPO_ROOT = "" - INIT_REQUEST = { "jsonrpc": "2.0", "id": 1, diff --git a/mcp-local/tests/sum_test.s b/mcp-local/tests/sum_test.s index bc81d73..abf14c9 100644 --- a/mcp-local/tests/sum_test.s +++ b/mcp-local/tests/sum_test.s @@ -1,3 +1,17 @@ +# Copyright © 2026, Arm Limited and Contributors. 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + add x1, x1, x2 add x1, x1, x3 add x1, x1, x4 diff --git a/mcp-local/tests/test_mcp.py b/mcp-local/tests/test_mcp.py index 0c5644d..07e8f09 100644 --- a/mcp-local/tests/test_mcp.py +++ b/mcp-local/tests/test_mcp.py @@ -1,4 +1,4 @@ -# Copyright © 2025, Arm Limited and Contributors. All rights reserved. +# Copyright © 2026, Arm Limited and Contributors. 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. @@ -82,11 +82,11 @@ def _read_mcp_message(sock, timeout: float = 10.0) -> dict: def test_mcp_stdio_transport_responds(): image = os.getenv("MCP_IMAGE", "arm-mcp:latest") - constants.REPO_ROOT = Path(__file__).resolve().parents[1] - print("\n***repo root: ", constants.REPO_ROOT ) + repo_root = Path(__file__).resolve().parents[1] + print("\n***repo root: ", repo_root) with ( DockerContainer(image) - .with_volume_mapping(str(constants.REPO_ROOT ), "/workspace") + .with_volume_mapping(str(repo_root), "/workspace") .with_kwargs(stdin_open=True, tty=False) ) as container: wait_for_logs(container, "Starting MCP server", timeout=60) From e4ad8087dd8cbeb9dccfaf1e97eefefa751ccf60 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Thu, 29 Jan 2026 08:57:47 -0700 Subject: [PATCH 03/15] fix: move mcp docker image to constants.py --- mcp-local/tests/constants.py | 2 ++ mcp-local/tests/test_mcp.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mcp-local/tests/constants.py b/mcp-local/tests/constants.py index ddf0ea1..3ca082b 100644 --- a/mcp-local/tests/constants.py +++ b/mcp-local/tests/constants.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +MCP_DOCKER_IMAGE = "arm-mcp:latest" + INIT_REQUEST = { "jsonrpc": "2.0", "id": 1, diff --git a/mcp-local/tests/test_mcp.py b/mcp-local/tests/test_mcp.py index 07e8f09..1985eed 100644 --- a/mcp-local/tests/test_mcp.py +++ b/mcp-local/tests/test_mcp.py @@ -81,7 +81,7 @@ def _read_mcp_message(sock, timeout: float = 10.0) -> dict: continue def test_mcp_stdio_transport_responds(): - image = os.getenv("MCP_IMAGE", "arm-mcp:latest") + image = os.getenv("MCP_IMAGE", constants.MCP_DOCKER_IMAGE) repo_root = Path(__file__).resolve().parents[1] print("\n***repo root: ", repo_root) with ( From 1019729e24971dea796c830960c3d27a02bd4ae0 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Fri, 30 Jan 2026 13:13:22 -0700 Subject: [PATCH 04/15] feat: add github action for integration testing --- .github/workflows/integration-tests.yml | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/integration-tests.yml diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..e404d26 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,31 @@ +name: Integration Tests + +on: + push: + pull_request: + +jobs: + integration-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - name: Install test dependencies + run: | + python -m pip install --upgrade pip + pip install -r mcp-local/tests/requirements.txt + + - name: Build MCP Docker image + run: docker build -t arm-mcp:latest -f mcp-local/Dockerfile mcp-local + + - name: Run integration tests + env: + MCP_IMAGE: arm-mcp:latest + run: pytest -v mcp-local/tests/test_mcp.py From 60c35d98a76dd3867b7c45aee7487c5c288615de Mon Sep 17 00:00:00 2001 From: NeethuESim Date: Fri, 30 Jan 2026 13:19:45 -0700 Subject: [PATCH 05/15] doc: update README with integration testing steps Added integration testing section with prerequisites and steps. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 59887d6..4991fc8 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,19 @@ After updating the configuration, restart your MCP client to load the Arm MCP se - `Dockerfile`: Multi-stage Docker build - **`embedding-generation/`**: Scripts for regenerating the knowledge base from source documents +## Integration Testing + +### Pre-requisites + +- Build the mcp server docker image +- Install the required test packages using - `pip install -r tests/requirements.txt` + +### Testing Steps + +- Run the test script - `python -m pytest -s tests/test_mcp.py` +- Check if following 2 docker containers have started - **mcp server** & **testcontainer** +- All tests should pass without any errors. Warnings can be ignored. + ## Troubleshooting ### Accessing the Container Shell From 8f71baa538a6c61afde75a7354efb67c6da75905 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Fri, 30 Jan 2026 13:24:13 -0700 Subject: [PATCH 06/15] fix: update docker build command --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e404d26..9ac651b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -23,7 +23,7 @@ jobs: pip install -r mcp-local/tests/requirements.txt - name: Build MCP Docker image - run: docker build -t arm-mcp:latest -f mcp-local/Dockerfile mcp-local + run: docker buildx build -f mcp-local/Dockerfile -t arm-mcp mcp-local - name: Run integration tests env: From b5df9b164f8cc3616b0bc1a5777ad33eea233c48 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Mon, 2 Feb 2026 12:43:45 -0700 Subject: [PATCH 07/15] fix: change to ubuntu-arm image --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 9ac651b..6baddbe 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -6,7 +6,7 @@ on: jobs: integration-tests: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm steps: - name: Checkout uses: actions/checkout@v4 From 919321d4872419f5904c887ef6cb1f38398f33dc Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Wed, 4 Feb 2026 11:16:45 -0700 Subject: [PATCH 08/15] fix: address PR comments --- README.md | 2 +- mcp-local/tests/constants.py | 11 ++++++++--- mcp-local/tests/test_mcp.py | 18 +++++++++++++++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4991fc8..f05a601 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ After updating the configuration, restart your MCP client to load the Arm MCP se ### Pre-requisites - Build the mcp server docker image -- Install the required test packages using - `pip install -r tests/requirements.txt` +- Install the required test packages using - `pip install -r tests/requirements.txt` within the `mcp_local` directory. ### Testing Steps diff --git a/mcp-local/tests/constants.py b/mcp-local/tests/constants.py index 3ca082b..23e408a 100644 --- a/mcp-local/tests/constants.py +++ b/mcp-local/tests/constants.py @@ -74,11 +74,11 @@ }, }, } - +# Fields Architecture, Os and Status are asserted in test to avoid mismatches due to dynamic fields EXPECTED_CHECK_SKOPEO_RESPONSE = { "status": "ok", "code": 0, - "stdout": "{\n \"Name\": \"docker.io/armswdev/arm-mcp\",\n \"Digest\": \"sha256:dab8aea984074c48c011fe2a7171c2f37ac7403738e0641b0ff99eee384621ea\",\n \"RepoTags\": [\n \"latest\"\n ],\n \"Created\": \"2025-11-07T00:10:00.848837841Z\",\n \"DockerVersion\": \"\",\n \"Labels\": {\n \"org.opencontainers.image.ref.name\": \"ubuntu\",\n \"org.opencontainers.image.version\": \"24.04\"\n },\n \"Architecture\": \"arm64\",\n \"Os\": \"linux\",\n \"Layers\": [\n \"sha256:b8a35db46e38ce87d4e743e1265ff436ed36e01d23246b24a1cbbeaae18ec432\",\n \"sha256:aee89bc0d0e3194f35090f6d190868784dee4057f910a6af3a7c68cba5cd2c46\",\n \"sha256:569ccd11958806cb03ec8fa1575bea7b0b45c290c1045b9b0ecf6fa354da53d7\",\n \"sha256:4f13b03e4ce3d1ab958df1f4f3e0edcb3a276fd0c5bc08406ebc978811a172f6\",\n \"sha256:2c0e15ad1ba57372508f93cf701e2bdc90600144ce2c8632264f4419976c55bf\",\n \"sha256:0eb217d3f4f4dcd7419cfcb70a77645d271ed768e88ef136ab49abd56bf478e3\",\n \"sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1\"\n ],\n \"LayersData\": [\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:b8a35db46e38ce87d4e743e1265ff436ed36e01d23246b24a1cbbeaae18ec432\",\n \"Size\": 28861712,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:aee89bc0d0e3194f35090f6d190868784dee4057f910a6af3a7c68cba5cd2c46\",\n \"Size\": 142025708,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:569ccd11958806cb03ec8fa1575bea7b0b45c290c1045b9b0ecf6fa354da53d7\",\n \"Size\": 107240731,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:4f13b03e4ce3d1ab958df1f4f3e0edcb3a276fd0c5bc08406ebc978811a172f6\",\n \"Size\": 1180,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:2c0e15ad1ba57372508f93cf701e2bdc90600144ce2c8632264f4419976c55bf\",\n \"Size\": 7105736,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:0eb217d3f4f4dcd7419cfcb70a77645d271ed768e88ef136ab49abd56bf478e3\",\n \"Size\": 392970938,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1\",\n \"Size\": 32,\n \"Annotations\": null\n }\n ],\n \"Env\": [\n \"PATH=/app/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n \"DEBIAN_FRONTEND=noninteractive\",\n \"PYTHONUNBUFFERED=1\",\n \"PIP_NO_CACHE_DIR=1\",\n \"WORKSPACE_DIR=/workspace\",\n \"VIRTUAL_ENV=/app/.venv\"\n ]\n}\n", + "stdout": "{\n \"Name\": \"docker.io/armswdev/arm-mcp\",\n \"Digest\": \"\",\n \"RepoTags\": [\n \"latest\"\n ],\n \"Created\": \"\",\n \"DockerVersion\": \"\",\n \"Labels\": {\n \"org.opencontainers.image.ref.name\": \"ubuntu\",\n \"org.opencontainers.image.version\": \"24.04\"\n },\n \"Architecture\": \"arm64\",\n \"Os\": \"linux\",\n \"Layers\": [\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\",\n \"\"\n ],\n \"LayersData\": [\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"\",\n \"Size\": 28861712,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"\",\n \"Size\": 142025708,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"\",\n \"Size\": 107240731,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"\",\n \"Size\": 1180,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"\",\n \"Size\": 7105736,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"\",\n \"Size\": 392970938,\n \"Annotations\": null\n },\n {\n \"MIMEType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"Digest\": \"\",\n \"Size\": 32,\n \"Annotations\": null\n }\n ],\n \"Env\": [\n \"PATH=/app/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n \"DEBIAN_FRONTEND=noninteractive\",\n \"PYTHONUNBUFFERED=1\",\n \"PIP_NO_CACHE_DIR=1\",\n \"WORKSPACE_DIR=/workspace\",\n \"VIRTUAL_ENV=/app/.venv\"\n ]\n}\n", "stderr": "", "cmd": [ "skopeo", @@ -118,6 +118,9 @@ }, }, } +'''TODO: Need to use a user-controlled repo with static example for testing to check more detailed response params. +For now, only status field is asserted in test to avoid mismatches due to dynamic fields. +Sample response below for reference - EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE = { "status": "success", "returncode": 0, @@ -192,7 +195,9 @@ "total_issue_count": 0 }, "output_file_deleted": True -} +}''' + +EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE_STATUS = "success" CHECK_SYSREPORT_TOOL_REQUEST = { "jsonrpc": "2.0", diff --git a/mcp-local/tests/test_mcp.py b/mcp-local/tests/test_mcp.py index 1985eed..060abc1 100644 --- a/mcp-local/tests/test_mcp.py +++ b/mcp-local/tests/test_mcp.py @@ -39,6 +39,12 @@ def _read_docker_frame(sock, timeout: float) -> bytes: continue header += chunk + # Docker frame format can be either in multiplexed (each frame prefixed with an 8-byte header) or raw mode. + # byte 0: stream type (0x01 = stdout, 0x02 = stderr) + # bytes 1-3: Reserved, always \x00\x00\x00 + # bytes 4-7: Payload size (big-endian uint32) + # This checks on header if frame is multiplexed or in raw mode. If bytes 1-3 are not zeros, the data is likely raw/unframed output, + # so the function returns it directly instead of trying to parse frame headers and extract payloads if header[1:4] != b"\x00\x00\x00": return header @@ -126,20 +132,26 @@ def _read_response(expected_id: int, timeout: float = 10.0) -> dict: #Check Skopeo Tool Test raw_socket.sendall(_encode_mcp_message(constants.CHECK_SKOPEO_REQUEST)) check_skopeo_response = _read_response(3, timeout=60) - assert check_skopeo_response.get("result")["structuredContent"] == constants.EXPECTED_CHECK_SKOPEO_RESPONSE, "Test Failed: MCP check_skopeo tool failed: content mismatch. Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_SKOPEO_RESPONSE,indent=2), json.dumps(check_skopeo_response.get("result")["structuredContent"],indent=2)) + actual_architecture = json.loads(check_skopeo_response.get("result")["structuredContent"]["stdout"]).get("Architecture") + actual_os = json.loads(check_skopeo_response.get("result")["structuredContent"]["stdout"]).get("Os") + actual_status = check_skopeo_response.get("result")["structuredContent"].get("status") + assert actual_architecture == json.loads(constants.EXPECTED_CHECK_SKOPEO_RESPONSE["stdout"]).get("Architecture"), "Test Failed: MCP check_skopeo tool failed: Architecture mismatch. Expected: {}, Received: {}".format(constants.EXPECTED_CHECK_SKOPEO_RESPONSE["Architecture"], actual_architecture) + assert actual_os == json.loads(constants.EXPECTED_CHECK_SKOPEO_RESPONSE["stdout"]).get("Os"), "Test Failed: MCP check_skopeo tool failed: Os mismatch. Expected: {}, Received: {}".format(constants.EXPECTED_CHECK_SKOPEO_RESPONSE["Os"], actual_os) + assert actual_status == constants.EXPECTED_CHECK_SKOPEO_RESPONSE["status"], "Test Failed: MCP check_skopeo tool failed: Status mismatch. Expected: {}, Received: {}".format(constants.EXPECTED_CHECK_SKOPEO_RESPONSE["status"], actual_status) print("\n***Test Passed: MCP check_skopeo tool succeeded") #Check NGINX Query Test raw_socket.sendall(_encode_mcp_message(constants.CHECK_NGINX_REQUEST)) check_nginx_response = _read_response(4, timeout=60) - assert any(item in str(check_nginx_response.get("result")["structuredContent"]) for item in constants.EXPECTED_CHECK_NGINX_RESPONSE), "Test Failed: MCP check_nginx tool failed: content mismatch., Expected one of: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_NGINX_RESPONSE,indent=2), json.dumps(check_nginx_response.get("result")["structuredContent"],indent=2)) + urls = json.dumps(check_nginx_response["result"]["structuredContent"]) + assert any(expected in urls for expected in constants.EXPECTED_CHECK_NGINX_RESPONSE), "Test Failed: MCP check_nginx tool failed: content mismatch., Expected one of: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_NGINX_RESPONSE,indent=2), json.dumps(check_nginx_response.get("result")["structuredContent"],indent=2)) print("\n***Test Passed: MCP check_nginx tool succeeded") #Check Migrate Ease Tool Test raw_socket.sendall(_encode_mcp_message(constants.CHECK_MIGRATE_EASE_TOOL_REQUEST)) check_migrate_ease_tool_response = _read_response(5, timeout=60) #assert only the status field to avoid mismatches due to dynamic fields - assert check_migrate_ease_tool_response.get("result")["structuredContent"]["status"] == constants.EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE["status"], "Test Failed: MCP check_migrate_ease_tool tool failed: status mismatch. Expected: {}, Received: {}".format(constants.EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE["status"], check_migrate_ease_tool_response.get("result")["structuredContent"]["status"]) + assert check_migrate_ease_tool_response.get("result")["structuredContent"]["status"] == constants.EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE_STATUS, "Test Failed: MCP check_migrate_ease_tool tool failed: status mismatch. Expected: {}, Received: {}".format(constants.EXPECTED_CHECK_MIGRATE_EASE_TOOL_RESPONSE_STATUS, check_migrate_ease_tool_response.get("result")["structuredContent"]["status"]) print("\n***Test Passed: MCP check_migrate_ease_tool tool succeeded") #Check Sysreport Tool Test From eb4be5fd8f71d45ef8abf88cc4fb8e25ab8acebb Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Wed, 4 Feb 2026 14:28:29 -0700 Subject: [PATCH 09/15] fix: github action issues --- .gitignore | 27 ++++++++++++++++++++++++ embedding-generation/generate-chunks.py | 28 ++++++++++++++++++------- 2 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01a8744 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Virtual environments +venv/ +.venv/ +env/ +.env/ +embedding-generation/venv/ +embedding-generation/yaml_data +embedding-generation/info + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.eggs/ +*.egg + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/embedding-generation/generate-chunks.py b/embedding-generation/generate-chunks.py index 7f777fe..703e213 100644 --- a/embedding-generation/generate-chunks.py +++ b/embedding-generation/generate-chunks.py @@ -424,17 +424,29 @@ def createLearningPathChunks(): learn_url = "https://learn.arm.com/" response = http_session.get(learn_url, timeout=60) soup = BeautifulSoup(response.text, 'html.parser') - for card in soup.find_all(class_='main-topic-card'): - if 'tool-install' == card.get('id'): - ig_rel_path = card.get('link') - processLearningPath(ig_rel_path,"Install Guide") + + # Process Install Guides separately (directly from /install-guides page) + processLearningPath("/install-guides", "Install Guide") + + # Find category links - main-topic-card elements are now wrapped in tags + # Look for tags that contain main-topic-card divs + for a_tag in soup.find_all('a', href=True): + card = a_tag.find(class_='main-topic-card') + if card: + cat_rel_path = a_tag.get('href') + if cat_rel_path is None or cat_rel_path.startswith('http'): + continue + # Skip non-learning-path links (like /tag/ml/ or install guides button) + if not cat_rel_path.startswith('/learning-paths/'): + continue - else: - cat_rel_path = card.get('link') - cat_response = http_session.get(learn_url+cat_rel_path, timeout=60) + cat_response = http_session.get(learn_url.rstrip('/') + cat_rel_path, timeout=60) cat_soup = BeautifulSoup(cat_response.text, 'html.parser') for lp_card in cat_soup.find_all(class_="path-card"): - lp_url = learn_url + lp_card.get('link') + lp_link = lp_card.get('link') + if lp_link is None: + continue + lp_url = learn_url.rstrip('/') + lp_link # Chunking step processLearningPath(lp_url, "Learning Path") From e0fb8ffc724e3668027aeeaebdcef52cdd419b69 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Thu, 5 Feb 2026 12:23:39 -0700 Subject: [PATCH 10/15] fix: docker build issue --- .github/workflows/integration-tests.yml | 2 +- mcp-local/Dockerfile | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6baddbe..7e2cd3f 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -23,7 +23,7 @@ jobs: pip install -r mcp-local/tests/requirements.txt - name: Build MCP Docker image - run: docker buildx build -f mcp-local/Dockerfile -t arm-mcp mcp-local + run: docker buildx build -f mcp-local/Dockerfile -t arm-mcp . - name: Run integration tests env: diff --git a/mcp-local/Dockerfile b/mcp-local/Dockerfile index b48e6df..b2c4169 100644 --- a/mcp-local/Dockerfile +++ b/mcp-local/Dockerfile @@ -46,6 +46,9 @@ RUN rm -f /usr/local/bin/aperf \ # Copy embedding data and scripts from the embedding base image COPY --from=joestech324/mcp-embedding-base:latest /embedding-data /tmp/embedding-data +# Override with local generate-chunks.py (fixes for updated learn.arm.com HTML structure) +COPY embedding-generation/generate-chunks.py /tmp/embedding-data/generate-chunks.py + # Install dependencies for vector database generation WORKDIR /tmp/embedding-data RUN pip3 install --no-cache-dir --break-system-packages -r requirements.txt @@ -61,7 +64,7 @@ ENV VIRTUAL_ENV=/app/.venv \ RUN python3 -m venv "$VIRTUAL_ENV" && pip install --upgrade pip -COPY requirements.txt . +COPY mcp-local/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy generated vector database files @@ -69,8 +72,8 @@ RUN mkdir -p ./data RUN cp /tmp/embedding-data/metadata.json ./data/ && \ cp /tmp/embedding-data/usearch_index.bin ./data/ -COPY utils/ ./utils/ -COPY server.py . +COPY mcp-local/utils/ ./utils/ +COPY mcp-local/server.py . FROM ubuntu:24.04 AS runtime From 48a25e5ce60b6adfa21adb0776e8b68f801874f7 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Thu, 5 Feb 2026 12:51:16 -0700 Subject: [PATCH 11/15] fix: mca integration test for gh actions --- mcp-local/tests/test_mcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp-local/tests/test_mcp.py b/mcp-local/tests/test_mcp.py index 060abc1..f5df782 100644 --- a/mcp-local/tests/test_mcp.py +++ b/mcp-local/tests/test_mcp.py @@ -163,7 +163,7 @@ def _read_response(expected_id: int, timeout: float = 10.0) -> dict: #Check MCA Tool Test raw_socket.sendall(_encode_mcp_message(constants.CHECK_MCA_TOOL_REQUEST)) check_mca_response = _read_response(7, timeout=60) - assert check_mca_response.get("result")["structuredContent"] == constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE, "Test Failed: MCP mca tool failed: content mismatch.Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE,indent=2), json.dumps(check_mca_response.get("result")["structuredContent"],indent=2)) + assert check_mca_response.get("result")["structuredContent"]["status"] == constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE["status"], "Test Failed: MCP mca tool failed: content mismatch.Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE,indent=2), json.dumps(check_mca_response.get("result")["structuredContent"],indent=2)) print("\n***Test Passed: MCP mca tool succeeded") if __name__ == "__main__": From 457d0c75436bc3ef43ba597c6b08b46da47ba5ca Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Thu, 5 Feb 2026 13:52:26 -0700 Subject: [PATCH 12/15] fix: update assertion for mca tool test case --- mcp-local/tests/constants.py | 7 ++++++- mcp-local/tests/test_mcp.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mcp-local/tests/constants.py b/mcp-local/tests/constants.py index 23e408a..0e3b6bc 100644 --- a/mcp-local/tests/constants.py +++ b/mcp-local/tests/constants.py @@ -230,6 +230,9 @@ }, } +'''TODO: Need to use a user-controlled repo with static example for testing to check more detailed response params. +For now, only status field is asserted in test to avoid mismatches due to dynamic fields. +Sample response below for reference - EXPECTED_CHECK_MCA_TOOL_RESPONSE = { "status": "ok", "code": 0, @@ -239,4 +242,6 @@ "llvm-mca", "/workspace/tests/sum_test.s" ] - } + }''' + +EXPECTED_CHECK_MCA_TOOL_RESPONSE_STATUS = "ok" diff --git a/mcp-local/tests/test_mcp.py b/mcp-local/tests/test_mcp.py index f5df782..0aa3cae 100644 --- a/mcp-local/tests/test_mcp.py +++ b/mcp-local/tests/test_mcp.py @@ -163,7 +163,7 @@ def _read_response(expected_id: int, timeout: float = 10.0) -> dict: #Check MCA Tool Test raw_socket.sendall(_encode_mcp_message(constants.CHECK_MCA_TOOL_REQUEST)) check_mca_response = _read_response(7, timeout=60) - assert check_mca_response.get("result")["structuredContent"]["status"] == constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE["status"], "Test Failed: MCP mca tool failed: content mismatch.Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE,indent=2), json.dumps(check_mca_response.get("result")["structuredContent"],indent=2)) + assert check_mca_response.get("result")["structuredContent"]["status"] == constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE_STATUS, "Test Failed: MCP mca tool failed: status mismatch.Expected: {}, Received: {}".format(json.dumps(constants.EXPECTED_CHECK_MCA_TOOL_RESPONSE_STATUS,indent=2), json.dumps(check_mca_response.get("result")["structuredContent"]["status"],indent=2)) print("\n***Test Passed: MCP mca tool succeeded") if __name__ == "__main__": From ae5237791368a26eaf31463452378a008cfaddd3 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Thu, 5 Feb 2026 15:55:40 -0700 Subject: [PATCH 13/15] fix: docker build issue --- README.md | 4 ++-- mcp-local/Dockerfile | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f05a601..7453837 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ If you would prefer to use a pre-built, multi-arch image, the official image can From the root of this repository: ```bash -docker buildx build --platform linux/arm64,linux/amd64 -f mcp-local/Dockerfile -t arm-mcp mcp-local +docker buildx build --platform linux/arm64,linux/amd64 -f mcp-local/Dockerfile -t arm-mcp . ``` For a single-platform build (faster): ```bash -docker buildx build -f mcp-local/Dockerfile -t arm-mcp mcp-local +docker buildx build -f mcp-local/Dockerfile -t arm-mcp . ``` ### 2. Configure Your MCP Client diff --git a/mcp-local/Dockerfile b/mcp-local/Dockerfile index b2c4169..21a65ba 100644 --- a/mcp-local/Dockerfile +++ b/mcp-local/Dockerfile @@ -43,14 +43,16 @@ RUN rm -f /usr/local/bin/aperf \ /opt/arm-migration-tools/processwatch \ /opt/arm-migration-tools/papi -# Copy embedding data and scripts from the embedding base image -COPY --from=joestech324/mcp-embedding-base:latest /embedding-data /tmp/embedding-data +COPY embedding-generation/generate-chunks.py /tmp/embedding-generation/generate-chunks.py +COPY embedding-generation/local_vectorstore_creation.py /tmp/embedding-generation/local_vectorstore_creation.py +COPY embedding-generation/requirements.txt /tmp/embedding-generation/requirements.txt +COPY embedding-generation/urls-to-chunk.csv /tmp/embedding-generation/urls-to-chunk.csv -# Override with local generate-chunks.py (fixes for updated learn.arm.com HTML structure) -COPY embedding-generation/generate-chunks.py /tmp/embedding-data/generate-chunks.py +# Copy embedding data and scripts from the embedding base image +COPY --from=joestech324/mcp-embedding-base:latest /embedding-data/intrinsic_chunks /tmp/embedding-generation/intrinsic_chunks # Install dependencies for vector database generation -WORKDIR /tmp/embedding-data +WORKDIR /tmp/embedding-generation RUN pip3 install --no-cache-dir --break-system-packages -r requirements.txt # Generate vector database @@ -69,8 +71,8 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy generated vector database files RUN mkdir -p ./data -RUN cp /tmp/embedding-data/metadata.json ./data/ && \ - cp /tmp/embedding-data/usearch_index.bin ./data/ +RUN cp /tmp/embedding-generation/metadata.json ./data/ && \ + cp /tmp/embedding-generation/usearch_index.bin ./data/ COPY mcp-local/utils/ ./utils/ COPY mcp-local/server.py . From 653010cf41cabba2139436ffedbcabee016fd8ad Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Thu, 5 Feb 2026 16:33:17 -0700 Subject: [PATCH 14/15] test: multi platofrm build --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7e2cd3f..ab20bd3 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -23,7 +23,7 @@ jobs: pip install -r mcp-local/tests/requirements.txt - name: Build MCP Docker image - run: docker buildx build -f mcp-local/Dockerfile -t arm-mcp . + run: docker buildx build --platform linux/arm64,linux/amd64 -f mcp-local/Dockerfile -t arm-mcp . - name: Run integration tests env: From eeeb6eb5da39de62634101a6f0a231c5d7ccb0d4 Mon Sep 17 00:00:00 2001 From: Neethu Elizabeth Simon Date: Thu, 5 Feb 2026 16:48:41 -0700 Subject: [PATCH 15/15] fix: revert build changes --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ab20bd3..637db26 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -23,7 +23,7 @@ jobs: pip install -r mcp-local/tests/requirements.txt - name: Build MCP Docker image - run: docker buildx build --platform linux/arm64,linux/amd64 -f mcp-local/Dockerfile -t arm-mcp . + run: docker buildx build -f mcp-local/Dockerfile -t arm-mcp . - name: Run integration tests env: