diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6c9d9003..e9317dbd 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -5,6 +5,23 @@ on: workflow_dispatch: jobs: + unit-tests: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Install unit test dependencies + run: python -m pip install pytest + + - name: Run unit tests + run: python -m pytest -q mcp-local/tests/test_invocation_logger.py + integration-tests: strategy: fail-fast: false diff --git a/.gitignore b/.gitignore index 5025e4b5..3ab765b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# MCP server logs +mcp-traffic.jsonl +error_logging.yaml invocation_reasons.yaml # Virtual environments @@ -37,4 +40,3 @@ embedding-generation/intrinsic_chunks/*.yaml embedding-generation/*.txt embedding-generation/metadata.json embedding-generation/usearch_index.bin - diff --git a/README.md b/README.md index 43c69518..72a44f9e 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,21 @@ args = [ After updating the configuration, restart your MCP client to load the Arm MCP server. +## Logging + +Depending on usage, the server may write two log files under `/workspace`. With the +configuration examples above, these files appear in the project directory on +your computer: + +- `mcp-traffic.jsonl` records when tools are used, the inputs provided, and the + reason for each tool call. It also records results from knowledge base + searches. +- `error_logging.yaml` records details about errors encountered by the server. + This information can help with troubleshooting. + +These logs may contain information from your project and tool requests. Review +their contents before sharing them. + ## Repository Structure - **`mcp-local/`**: The MCP server implementation diff --git a/mcp-local/requirements.txt b/mcp-local/requirements.txt index 0a6675bd..c7b596a0 100644 --- a/mcp-local/requirements.txt +++ b/mcp-local/requirements.txt @@ -1,4 +1,4 @@ -torch==2.10.0 +torch==2.13.0 usearch==2.26.0 # USearch leaves NumKong unconstrained, so pin it explicitly for reproducible wheel installs. numkong==7.7.0 diff --git a/mcp-local/server.json b/mcp-local/server.json index c9bd4310..92f5eec4 100644 --- a/mcp-local/server.json +++ b/mcp-local/server.json @@ -3,7 +3,7 @@ "name": "io.github.arm/arm-mcp", "title": "Arm MCP Server", "description": "Official Arm MCP server for code migration, optimization, and Arm architecture guidance", - "version": "2.8.0", + "version": "2.10.0", "repository": { "url": "https://github.com/arm/mcp", "source": "github" @@ -12,7 +12,7 @@ "packages": [ { "registryType": "oci", - "identifier": "docker.io/armlimited/arm-mcp:2.8.0", + "identifier": "docker.io/armlimited/arm-mcp:2.10.0", "runtimeHint": "docker", "transport": { "type": "stdio" diff --git a/mcp-local/server.py b/mcp-local/server.py index b08e6d58..bccd1767 100644 --- a/mcp-local/server.py +++ b/mcp-local/server.py @@ -28,7 +28,7 @@ from utils.migrate_ease_utils import run_migrate_ease_scan from utils.skopeo_tool import skopeo_help, skopeo_inspect from utils.llvm_mca_tool import mca_help, llvm_mca_analyze -from utils.invocation_logger import log_invocation_reason +from utils.invocation_logger import log_invocation_reason, log_tool_result from utils.error_handling import format_tool_error # Initialize the MCP server @@ -51,8 +51,8 @@ description="If a user asks to migrate a codebase to Arm, strongly consider using this tool as a part of your strategy. Searches an Arm knowledge base of learning resources, Arm intrinsics, and software version compatibility using semantic similarity. Given a natural language query, returns a list of matching resources with URLs, titles, and content snippets, ranked by relevance. Useful for finding documentation, tutorials, or version compatibility for Arm migrations. Returned URLs may include tracking query parameters such as utm_source=arm-mcp and URL fragments. When sharing or citing returned URLs, preserve each URL exactly as returned, including query parameters and fragments; do not remove, normalize, shorten, or rewrite them. Includes 'invocation_reason' parameter so the model can briefly explain why it is calling this tool to provide additional context." ) def knowledge_base_search(query: str, invocation_reason: Optional[str] = None) -> List[Dict[str, Any]]: - # Log invocation reason if provided - log_invocation_reason( + # Log the call and retain its ID for the paired search result. + entry_id = log_invocation_reason( tool="knowledge_base_search", reason=invocation_reason, args={"query": query}, @@ -67,7 +67,9 @@ def knowledge_base_search(query: str, invocation_reason: Optional[str] = None) - List of dictionaries with metadata including url and text snippets. """ try: - return arm_kb_search.search(query, SEARCH_RESOURCES) + results = arm_kb_search.search(query, SEARCH_RESOURCES) + log_tool_result(entry_id, "knowledge_base_search", results) + return results except Exception as e: return format_tool_error( tool="knowledge_base_search", diff --git a/mcp-local/tests/constants.py b/mcp-local/tests/constants.py index 8c300749..1479d299 100644 --- a/mcp-local/tests/constants.py +++ b/mcp-local/tests/constants.py @@ -131,7 +131,7 @@ "output_file": "/tmp/migrate_ease_java_20260126-215207.json", "output_format": "json", "workspace_listing": [ - "invocation_reasons.yaml" + "mcp-traffic.jsonl" ], "excluded_items": [], "excluded_count": 0, diff --git a/mcp-local/tests/test_invocation_logger.py b/mcp-local/tests/test_invocation_logger.py new file mode 100644 index 00000000..ce01c9ab --- /dev/null +++ b/mcp-local/tests/test_invocation_logger.py @@ -0,0 +1,51 @@ +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from utils import invocation_logger + + +def test_logs_paired_call_and_result(tmp_path, monkeypatch): + traffic_path = tmp_path / "mcp-traffic.jsonl" + monkeypatch.setattr(invocation_logger, "WORKSPACE_DIR", str(tmp_path)) + + entry_id = invocation_logger.log_invocation_reason( + tool="knowledge_base_search", + reason="Need current Arm documentation", + args={"query": "SME overview"}, + ) + result = [{"title": "SME guide", "score": 0.9}] + invocation_logger.log_tool_result(entry_id, "knowledge_base_search", result) + + entries = [json.loads(line) for line in traffic_path.read_text().splitlines()] + assert entries == [ + { + "id": entry_id, + "timestamp": entries[0]["timestamp"], + "tool": "knowledge_base_search", + "args": {"query": "SME overview"}, + "invocation_reason": "Need current Arm documentation", + }, + { + "id": entry_id, + "type": "result", + "tool": "knowledge_base_search", + "result": result, + }, + ] + + +def test_logs_call_without_invocation_reason(tmp_path, monkeypatch): + traffic_path = tmp_path / "mcp-traffic.jsonl" + monkeypatch.setattr(invocation_logger, "WORKSPACE_DIR", str(tmp_path)) + + entry_id = invocation_logger.log_invocation_reason( + tool="knowledge_base_search", + reason=None, + args={"query": "SVE2"}, + ) + + entry = json.loads(traffic_path.read_text()) + assert entry["id"] == entry_id + assert entry["invocation_reason"] is None diff --git a/mcp-local/utils/error_handling.py b/mcp-local/utils/error_handling.py index 6fb3e958..3c18b863 100644 --- a/mcp-local/utils/error_handling.py +++ b/mcp-local/utils/error_handling.py @@ -70,6 +70,6 @@ def format_tool_error(tool: str, exc: Exception, args: Optional[Dict[str, Any]] "traceback": traceback.format_exc(), "args": args or {}, "guidance": ( - "Use this information to suggest fixes to the user. Also add that if the user is experiencing problems that are due to the MCP server itself, they can email mcpserver@arm.com. There is a file called invocation_reasons.yaml located in the /workspace container filesystem mount that records why the model decided to invoke a particular tool, and a file called error_logging.yaml that records recent errors. The user can also send these files to mcpserver@arm.com for added helpful troubleshooting context." + "Use this information to suggest fixes to the user. Also add that if the user is experiencing problems that are due to the MCP server itself, they can email mcpserver@arm.com. There is a file called mcp-traffic.jsonl located in the /workspace container filesystem mount that records tool calls and selected tool results, and a file called error_logging.yaml that records recent errors. The user can also send these files to mcpserver@arm.com for added helpful troubleshooting context." ), } diff --git a/mcp-local/utils/invocation_logger.py b/mcp-local/utils/invocation_logger.py index 64d2e6c3..34d4371c 100644 --- a/mcp-local/utils/invocation_logger.py +++ b/mcp-local/utils/invocation_logger.py @@ -12,49 +12,66 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import os import uuid from datetime import datetime, timezone from typing import Optional, Dict, Any -import yaml - from .config import WORKSPACE_DIR -LOG_FILE_NAME = "invocation_reasons.yaml" +LOG_FILE_NAME = "mcp-traffic.jsonl" def _now_iso() -> str: return datetime.now(timezone.utc).isoformat() -def log_invocation_reason(tool: str, reason: Optional[str], args: Optional[Dict[str, Any]] = None) -> None: +def log_invocation_reason( + tool: str, + reason: Optional[str], + args: Optional[Dict[str, Any]] = None, +) -> str: """ - Append a YAML document with the tool invocation reason and metadata to /workspace/invocation_reasons.yaml. + Append a JSONL call entry to the workspace traffic log. - Each call writes a separate YAML document with fields: id, timestamp, tool, args, reason. + Returns the entry ID so the caller can pair the tool result with this invocation. Errors are swallowed to avoid impacting tool execution. """ - if not reason: - return + entry_id = str(uuid.uuid4()) + timestamp = _now_iso() - entry = { - "id": str(uuid.uuid4()), - "timestamp": _now_iso(), + traffic_entry = { + "id": entry_id, + "timestamp": timestamp, "tool": tool, "args": args or {}, - "reason": str(reason), + "invocation_reason": reason, } - log_path = os.path.join(WORKSPACE_DIR, LOG_FILE_NAME) - try: - # Ensure workspace directory exists (it should in runtime environments) os.makedirs(WORKSPACE_DIR, exist_ok=True) with open(log_path, "a", encoding="utf-8") as f: - yaml.safe_dump(entry, f, explicit_start=True, sort_keys=False, allow_unicode=True) + f.write(json.dumps(traffic_entry) + "\n") except Exception: - # Do not break tool execution if logging fails pass + return entry_id + + +def log_tool_result(entry_id: str, tool: str, result: Any) -> None: + """Append a JSONL result entry paired with a tool invocation.""" + log_path = os.path.join(WORKSPACE_DIR, LOG_FILE_NAME) + result_entry = { + "id": entry_id, + "type": "result", + "tool": tool, + "result": result, + } + try: + os.makedirs(WORKSPACE_DIR, exist_ok=True) + with open(log_path, "a", encoding="utf-8") as f: + f.write(json.dumps(result_entry, default=str) + "\n") + except Exception: + pass diff --git a/mcp-local/utils/migrate_ease_utils.py b/mcp-local/utils/migrate_ease_utils.py index ee1ca97e..a6635d73 100644 --- a/mcp-local/utils/migrate_ease_utils.py +++ b/mcp-local/utils/migrate_ease_utils.py @@ -35,7 +35,7 @@ '.git', '.svn', '.hg', # IDE and editor directories '.vscode', '.idea', '.eclipse', - # Other common build/cache directories + 'mcp-traffic.jsonl', 'error_logging.yaml', 'target', 'out', '.cache', }