Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,24 @@
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:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
strategy:
fail-fast: false
matrix:
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# MCP server logs
mcp-traffic.jsonl
error_logging.yaml
invocation_reasons.yaml

# Virtual environments
Expand Down Expand Up @@ -37,4 +40,3 @@ embedding-generation/intrinsic_chunks/*.yaml
embedding-generation/*.txt
embedding-generation/metadata.json
embedding-generation/usearch_index.bin

15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mcp-local/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions mcp-local/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
10 changes: 6 additions & 4 deletions mcp-local/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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},
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion mcp-local/tests/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 51 additions & 0 deletions mcp-local/tests/test_invocation_logger.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion mcp-local/utils/error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
),
}
51 changes: 34 additions & 17 deletions mcp-local/utils/invocation_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion mcp-local/utils/migrate_ease_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}

Expand Down
Loading