ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
mcp_content.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""The two read-only surfaces: resources (documents) and prompts (canned tasks).
4
5Neither runs anything. Resources hand back repository documents by URI, and
6prompts hand back a pre-written instruction with the caller's arguments
7substituted -- so both are grouped apart from :mod:`mcp_tools`, whose entries
8build firmware, run tests and touch hardware.
9"""
10
11from __future__ import annotations
12
13from collections.abc import Callable
14from typing import Any
15
16from mcp_tools import tool_list_apps
17from mcp_util import REPO_ROOT, read_text
18
19
20# ---------------------------------------------------------------------------
21# Resource registry -- read-only project context exposed by stable URIs
22# ---------------------------------------------------------------------------
23def _resource_doc(rel_path: str) -> Callable[[], str]:
24 def reader() -> str:
25 path = REPO_ROOT / rel_path
26 if not path.is_file():
27 return f"{rel_path} not found in this tree."
28 return read_text(path)
29
30 return reader
31
32
33def _resource_catalogue() -> str:
34 return tool_list_apps({})
35
36
37RESOURCES: list[dict[str, Any]] = [
38 {
39 "uri": "ra8d2://doc/claude-md", # AI-OK: MCP resource id serving CLAUDE.md, not attribution
40 "name": "CLAUDE.md",
41 "description": "Project rules for AI assistants (the most-violated rules).",
42 "mimeType": "text/markdown",
43 "reader": _resource_doc("CLAUDE.md"),
44 },
45 {
46 "uri": "ra8d2://doc/style-guide",
47 "name": "docs/STYLE_GUIDE.md",
48 "description": "Authoritative C23 + Doxygen style guide.",
49 "mimeType": "text/markdown",
50 "reader": _resource_doc("docs/STYLE_GUIDE.md"),
51 },
52 {
53 "uri": "ra8d2://doc/ring-and-world",
54 "name": "docs/RING_AND_WORLD.md",
55 "description": "Architectural-ring + TrustZone-world tagging system.",
56 "mimeType": "text/markdown",
57 "reader": _resource_doc("docs/RING_AND_WORLD.md"),
58 },
59 {
60 "uri": "ra8d2://doc/contributing",
61 "name": "CONTRIBUTING.md",
62 "description": "Contributor workflow and gate reference.",
63 "mimeType": "text/markdown",
64 "reader": _resource_doc("CONTRIBUTING.md"),
65 },
66 {
67 "uri": "ra8d2://reference/chapter-map",
68 "name": "HUM chapter map",
69 "description": "Hardware User's Manual chapter-to-page ranges for citations.",
70 "mimeType": "text/markdown",
71 "reader": _resource_doc("docs/reference/CHAPTER_MAP.md"),
72 },
73 {
74 "uri": "ra8d2://doc/hil",
75 "name": "docs/HIL_SUITE.md",
76 "description": "Hardware-in-the-loop suite + how each app is verified in CI.",
77 "mimeType": "text/markdown",
78 "reader": _resource_doc("docs/HIL_SUITE.md"),
79 },
80 {
81 "uri": "ra8d2://doc/ai-attribution",
82 "name": "docs/AI_ATTRIBUTION_POLICY.md",
83 "description": "The zero-AI-attribution policy enforced across the tree.",
84 "mimeType": "text/markdown",
85 "reader": _resource_doc("docs/AI_ATTRIBUTION_POLICY.md"),
86 },
87 {
88 "uri": "ra8d2://apps/catalogue",
89 "name": "Firmware app catalogue",
90 "description": "Live list of every discovered firmware app.",
91 "mimeType": "text/plain",
92 "reader": _resource_catalogue,
93 },
94]
95
96RESOURCE_INDEX: dict[str, dict[str, Any]] = {r["uri"]: r for r in RESOURCES}
97
98# ---------------------------------------------------------------------------
99# Prompt registry -- reusable review flows the assistant can invoke by name.
100# Each ``template`` is filled with the call's arguments by handle_prompts_get.
101# ---------------------------------------------------------------------------
102PROMPTS: list[dict[str, Any]] = [
103 {
104 "name": "audit_register_access",
105 "description": "Audit a direct MMIO register access for a valid HUM citation + style.",
106 "arguments": [
107 {"name": "code", "description": "the register read/write line(s)", "required": True}
108 ],
109 "template": (
110 "Audit this RA8D2 register access against the project rules: every direct "
111 "register read/write MUST be immediately preceded by a HUM citation comment "
112 '`/* HUM Ch X.Y "section" p NNNN */`, must go through an inline accessor '
113 "(never a macro address), and must be pure 7-bit ASCII. Report each violation "
114 "with a concrete fix.\n\n```c\n{code}\n```"
115 ),
116 },
117 {
118 "name": "mcdc_vectors",
119 "description": "Write minimal MC/DC test vectors for a compound boolean decision.",
120 "arguments": [
121 {"name": "decision", "description": "the C boolean decision", "required": True}
122 ],
123 "template": (
124 "Write the minimal (N+1) MC/DC test vectors for this decision, demonstrating "
125 "that each condition independently affects the outcome, formatted as the "
126 "project's `@par MC/DC:` Doxygen block.\n\n```c\n{decision}\n```"
127 ),
128 },
129]
130
131PROMPT_INDEX: dict[str, dict[str, Any]] = {p["name"]: p for p in PROMPTS}
132
133
134def handle_prompts_list() -> dict[str, Any]:
135 """Answer MCP `prompts/list` with the catalogue's public fields.
136
137 Deliberately projects each entry rather than returning it whole: `template`
138 is server-side detail, and shipping it would leak the prompt text to every
139 client that merely enumerates.
140
141 Returns:
142 `{"prompts": [...]}` with name, description and arguments per entry, in
143 PROMPTS order.
144 """
145 listed = [
146 {"name": p["name"], "description": p["description"], "arguments": p["arguments"]}
147 for p in PROMPTS
148 ]
149 return {"prompts": listed}
150
151
152def handle_prompts_get(params: dict[str, Any]) -> dict[str, Any]:
153 """Answer MCP `prompts/get` by substituting arguments into a template.
154
155 Every declared argument is substituted, and a missing one becomes "" rather
156 than an error -- so a partially-filled prompt renders with a gap instead of
157 failing the call. Only declared arguments are passed to `format()`, so an
158 undeclared extra in `params` is ignored, and a `{placeholder}` in the
159 template with no matching declared argument raises.
160
161 Args:
162 params: JSON-RPC params; "name" selects the prompt, "arguments" is an
163 optional name -> value mapping whose values are coerced to str.
164
165 Returns:
166 `{"description": ..., "messages": [...]}` with one user-role text
167 message.
168
169 Raises:
170 ValueError: No prompt by that name, or the template referenced a
171 placeholder that is not a declared argument. `dispatch` converts
172 this into a JSON-RPC error response.
173 """
174 name = str(params.get("name", ""))
175 prompt = PROMPT_INDEX.get(name)
176 if prompt is None:
177 msg = f"unknown prompt: {name}"
178 raise ValueError(msg)
179 arguments = params.get("arguments") or {}
180 try:
181 text = prompt["template"].format(
182 **{a["name"]: str(arguments.get(a["name"], "")) for a in prompt["arguments"]}
183 )
184 except (KeyError, IndexError) as exc:
185 msg = f"bad prompt arguments: {exc}"
186 raise ValueError(msg) from exc
187 return {
188 "description": prompt["description"],
189 "messages": [{"role": "user", "content": {"type": "text", "text": text}}],
190 }