ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
ra8_mcp.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""A Model Context Protocol (MCP) server for this firmware repo.
5
6This server gives an MCP-aware assistant live, structured context about the
7ra8-firmware tree -- the firmware app catalogue, build / test / quality
8workflows, the hardware-in-the-loop (HIL) rig, code search, and the project's
9authoritative docs -- so the assistant can reason about what the repo can
10actually *do* instead of guessing from loose markdown files.
11
12Design goals
13------------
14 * Zero third-party dependencies. It speaks the MCP stdio transport
15 (newline-delimited JSON-RPC 2.0) using only the Python standard library,
16 so it runs anywhere ``python3`` exists with nothing to ``pip install``.
17 That matches the repo's hand-written, minimal-dependency ethos.
18 * Read-only by default. Every tool that only inspects the tree (catalogue,
19 search, docs) runs freely. Every tool that touches real hardware (flash,
20 HIL) is gated behind an explicit ``confirm`` flag and otherwise returns a
21 dry-run preview of the exact command it would run.
22 * Honest output. Build / test / gate tools shell out to the repo's real
23 Just recipes and helper scripts, capture the combined output, and
24 return the tail verbatim with the real exit status.
25
26Transport
27---------
28MCP stdio framing is one JSON-RPC message per line on stdin / stdout. All
29human-readable logging goes to stderr so it never corrupts the protocol
30stream. Run ``python3 tools/mcp/src/ra8_mcp.py --selftest`` to exercise the
31dispatcher in-process without a client (used by ``just tools::mcp``).
32
33The methods implemented are the subset a tools+resources server needs:
34``initialize``, ``ping``, ``tools/list``, ``tools/call``, ``resources/list``,
35``resources/read``, plus empty ``prompts/list`` /
36``resources/templates/list`` replies for clients that probe them.
37"""
38
39from __future__ import annotations
40
41import argparse
42import json
43import sys
44from pathlib import Path
45
46sys.path.insert(0, str(Path(__file__).resolve().parent))
47
48from mcp_protocol import (
49 ERR_INVALID_REQUEST,
50 ERR_PARSE,
51 SERVER_NAME,
52 SERVER_VERSION,
53 _error,
54 dispatch,
55)
56from mcp_util import REPO_ROOT, log
57
58
59# ---------------------------------------------------------------------------
60# stdio serve loop and self-test
61# ---------------------------------------------------------------------------
62def run_selftest() -> int:
63 """Load and run the sibling test module only for ``--selftest``."""
64 tests_dir = Path(__file__).resolve().parents[1] / "tests"
65 sys.path.insert(0, str(tests_dir))
66 from mcp_selftest import selftest # noqa: PLC0415 -- selftest path added above
67
68 return selftest()
69
70
71def serve() -> int:
72 """Run the newline-delimited JSON-RPC loop on stdin/stdout until EOF."""
73 log(f"serving {SERVER_NAME} {SERVER_VERSION} on stdio (root {REPO_ROOT})")
74 out = sys.stdout
75 for raw in sys.stdin:
76 line = raw.strip()
77 if not line:
78 continue
79 try:
80 request = json.loads(line)
81 except json.JSONDecodeError as exc:
82 out.write(json.dumps(_error(None, ERR_PARSE, f"parse error: {exc}")) + "\n")
83 out.flush()
84 continue
85 if not isinstance(request, dict):
86 out.write(
87 json.dumps(_error(None, ERR_INVALID_REQUEST, "request must be an object")) + "\n"
88 )
89 out.flush()
90 continue
91 response = dispatch(request)
92 if response is not None:
93 out.write(json.dumps(response) + "\n")
94 out.flush()
95 log("stdin closed, exiting")
96 return 0
97
98
99def main(argv: list[str]) -> int:
100 """Run the server, or the in-process self-test with `--selftest`.
101
102 With no flags this blocks in `serve()` reading JSON-RPC from stdin until
103 EOF, which is the normal mode: an MCP client spawns this as a subprocess and
104 speaks the stdio transport to it. Running it from a terminal looks like a
105 hang -- it is waiting for a request.
106
107 `--selftest` exercises the dispatcher without a client or a board, so it is
108 safe to run anywhere and is what CI invokes.
109
110 Args:
111 argv: Argument list WITHOUT the program name (callers pass
112 `sys.argv[1:]`).
113
114 Returns:
115 Process exit status: 0 on clean shutdown or a passing self-test, 1 on a
116 failing one.
117 """
118 parser = argparse.ArgumentParser(description="MCP server for ra8-firmware.")
119 parser.add_argument(
120 "--selftest", action="store_true", help="run the in-process dispatcher self-test and exit"
121 )
122 options = parser.parse_args(argv)
123 if options.selftest:
124 return run_selftest()
125 return serve()
126
127
128if __name__ == "__main__":
129 sys.exit(main(sys.argv[1:]))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298