1use crate::permission::PermissionLevel;
5use serde::Serialize;
6use serde_json::{Value, json};
7
8#[derive(Debug, Clone, Serialize)]
9pub struct ToolDefinition {
10 pub name: String,
11 pub description: String,
12 pub parameters: Value,
13 pub permission_level: PermissionLevel,
14 pub mutating: bool,
15 pub caveats: Vec<String>,
16}
17
18pub fn all_tools() -> Vec<ToolDefinition> {
20 vec![
21 tool(
22 "frontier_stats",
23 "Return frontier metadata and statistics: finding count, links, confidence distribution, gaps, categories, and review state.",
24 json!({"type": "object", "properties": {}}),
25 PermissionLevel::ReadOnly,
26 false,
27 vec![],
28 ),
29 tool(
30 "search_findings",
31 "Search findings by text content, entity name, entity type, or assertion type. Returns matching findings.",
32 json!({"type": "object", "properties": {
33 "query": {"type": "string"}, "entity": {"type": "string"},
34 "entity_type": {"type": "string"}, "assertion_type": {"type": "string"},
35 "limit": {"type": "integer"}
36 }}),
37 PermissionLevel::ReadOnly,
38 false,
39 vec![],
40 ),
41 tool(
42 "get_finding",
43 "Get a single finding by ID, including evidence, conditions, links, confidence, and provenance.",
44 json!({"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}),
45 PermissionLevel::ReadOnly,
46 false,
47 vec![],
48 ),
49 tool(
50 "get_finding_history",
51 "v0.17: Return the chronological event log for one finding (asserted, reviewed, caveated, noted, confidence-revised, superseded, retracted). Use this to walk the supersedes chain, audit corrections, or detect that a target has been refined since you last linked to it.",
52 json!({"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]}),
53 PermissionLevel::ReadOnly,
54 false,
55 vec![
56 "Event order reflects timestamps as recorded; sort client-side if you need a different ordering.",
57 ],
58 ),
59 tool(
60 "list_gaps",
61 "List findings flagged as candidate gap review leads.",
62 json!({"type": "object", "properties": {}}),
63 PermissionLevel::ReadOnly,
64 false,
65 vec![
66 "Candidate gap rankings are review leads, not guaranteed underexplored areas or experiment targets.",
67 ],
68 ),
69 tool(
70 "list_contradictions",
71 "List contradiction and dispute links between findings.",
72 json!({"type": "object", "properties": {}}),
73 PermissionLevel::ReadOnly,
74 false,
75 vec![
76 "Automated contradiction links are candidates for review, not definitive disagreements.",
77 ],
78 ),
79 tool(
80 "find_bridges",
81 "Find entities spanning multiple assertion categories, suggesting candidate cross-domain connections.",
82 json!({"type": "object", "properties": {
83 "min_categories": {"type": "integer"}, "limit": {"type": "integer"}
84 }}),
85 PermissionLevel::ReadOnly,
86 false,
87 vec!["Candidate bridges require review before being treated as domain knowledge."],
88 ),
89 tool(
90 "check_pubmed",
91 "Run a rough PubMed prior-art check for a hypothesis.",
92 json!({"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}),
93 PermissionLevel::ReadOnly,
94 false,
95 vec!["PubMed counts are rough prior-art signals, not proof of novelty."],
96 ),
97 tool(
98 "apply_observer",
99 "Rerank findings under an observer policy such as pharma, academic, regulatory, clinical, or exploration.",
100 json!({"type": "object", "properties": {
101 "policy": {"type": "string"}, "limit": {"type": "integer"}
102 }, "required": ["policy"]}),
103 PermissionLevel::ReadOnly,
104 false,
105 vec!["Observer policy output is a weighted view, not definitive disagreement."],
106 ),
107 tool(
108 "propagate_retraction",
109 "Simulate retraction cascade impact over declared dependency/support links.",
110 json!({"type": "object", "properties": {"finding_id": {"type": "string"}}, "required": ["finding_id"]}),
111 PermissionLevel::Dangerous,
112 false,
113 vec!["Retraction impact is simulated over declared links only."],
114 ),
115 tool(
116 "trace_evidence_chain",
117 "Trace evidence lineage for a finding, including support, dependency, contradiction, and chain strength.",
118 json!({"type": "object", "properties": {
119 "finding_id": {"type": "string"}, "depth": {"type": "integer"}
120 }, "required": ["finding_id"]}),
121 PermissionLevel::ReadOnly,
122 false,
123 vec!["Evidence-chain strength is heuristic and depends on declared links."],
124 ),
125 tool(
131 "list_events_since",
132 "List canonical events from the event log strictly after `cursor` (a `vev_…` id), ordered chronologically. Returns events plus a `next_cursor` for further pagination, or null when the tail is reached. Omit `cursor` to start from the genesis event.",
133 json!({"type": "object", "properties": {
134 "cursor": {"type": "string"},
135 "limit": {"type": "integer"}
136 }}),
137 PermissionLevel::ReadOnly,
138 false,
139 vec![
140 "Cursor must reference an event currently in the log; out-of-sync clients should restart from the beginning.",
141 ],
142 ),
143 tool(
149 "propose_review",
150 "Propose a `finding.review` decision on a finding (status: accepted/approved/contested/needs_revision/rejected). Requires the actor's Ed25519 signature over the canonical proposal preimage. Idempotent: identical logical proposals return the same `vpr_…`.",
151 json!({"type": "object", "properties": {
152 "actor_id": {"type": "string"},
153 "target_finding_id": {"type": "string"},
154 "status": {"type": "string"},
155 "reason": {"type": "string"},
156 "created_at": {"type": "string"},
157 "signature": {"type": "string"}
158 }, "required": ["actor_id", "target_finding_id", "status", "reason", "signature"]}),
159 PermissionLevel::Write,
160 true,
161 vec![
162 "actor_id must be registered in `frontier.actors` via `vela actor add` before writes verify.",
163 ],
164 ),
165 tool(
166 "propose_note",
167 "Propose attaching a `finding.note` annotation to a finding. Requires a registered actor and signature. Optional structured `provenance` (Phase β, v0.6): `{doi?, pmid?, title?, span?}` with at least one identifier. Stays `pending_review` until accepted.",
168 json!({"type": "object", "properties": {
169 "actor_id": {"type": "string"},
170 "target_finding_id": {"type": "string"},
171 "text": {"type": "string"},
172 "reason": {"type": "string"},
173 "created_at": {"type": "string"},
174 "signature": {"type": "string"},
175 "provenance": {
176 "type": "object",
177 "properties": {
178 "doi": {"type": "string"},
179 "pmid": {"type": "string"},
180 "title": {"type": "string"},
181 "span": {"type": "string"}
182 }
183 }
184 }, "required": ["actor_id", "target_finding_id", "text", "reason", "signature"]}),
185 PermissionLevel::Write,
186 true,
187 vec!["Notes do not change finding state; they accrete review context."],
188 ),
189 tool(
194 "propose_and_apply_note",
195 "Propose AND apply a `finding.note` annotation in one signed call. Requires the actor to have `tier=\"auto-notes\"` registered (`vela actor add --tier auto-notes`). Optional structured `provenance` (Phase β). Idempotent: a retry with identical content returns the same `applied_event_id`.",
196 json!({"type": "object", "properties": {
197 "actor_id": {"type": "string"},
198 "target_finding_id": {"type": "string"},
199 "text": {"type": "string"},
200 "reason": {"type": "string"},
201 "created_at": {"type": "string"},
202 "signature": {"type": "string"},
203 "provenance": {
204 "type": "object",
205 "properties": {
206 "doi": {"type": "string"},
207 "pmid": {"type": "string"},
208 "title": {"type": "string"},
209 "span": {"type": "string"}
210 }
211 }
212 }, "required": ["actor_id", "target_finding_id", "text", "reason", "signature"]}),
213 PermissionLevel::Write,
214 true,
215 vec![
216 "Requires actor.tier=auto-notes; calls from non-tiered actors are rejected.",
217 "Notes still do not change finding state — they accrete review context.",
218 ],
219 ),
220 tool(
221 "propose_revise_confidence",
222 "Propose a confidence revision (`finding.confidence_revise`) on a finding. `new_score` must be in [0.0, 1.0]. Requires a registered actor and signature.",
223 json!({"type": "object", "properties": {
224 "actor_id": {"type": "string"},
225 "target_finding_id": {"type": "string"},
226 "new_score": {"type": "number"},
227 "reason": {"type": "string"},
228 "created_at": {"type": "string"},
229 "signature": {"type": "string"}
230 }, "required": ["actor_id", "target_finding_id", "new_score", "reason", "signature"]}),
231 PermissionLevel::Write,
232 true,
233 vec![
234 "Confidence revisions update score and basis; they do not change scope or evidence.",
235 ],
236 ),
237 tool(
238 "propose_retract",
239 "Propose retracting a finding (`finding.retract`). Applying triggers per-dependent `finding.dependency_invalidated` events through the propagation graph. Requires a registered actor and signature.",
240 json!({"type": "object", "properties": {
241 "actor_id": {"type": "string"},
242 "target_finding_id": {"type": "string"},
243 "reason": {"type": "string"},
244 "created_at": {"type": "string"},
245 "signature": {"type": "string"}
246 }, "required": ["actor_id", "target_finding_id", "reason", "signature"]}),
247 PermissionLevel::Write,
248 true,
249 vec![
250 "Retraction propagates through declared dependency/support links; review impact before applying.",
251 ],
252 ),
253 tool(
254 "accept_proposal",
255 "Apply a pending proposal as the named reviewer. The reviewer must be registered. Signature is over `{action: \"accept\", proposal_id, reviewer_id, reason, timestamp}` canonicalized. Idempotent: re-applying returns the same `applied_event_id`.",
256 json!({"type": "object", "properties": {
257 "proposal_id": {"type": "string"},
258 "reviewer_id": {"type": "string"},
259 "reason": {"type": "string"},
260 "timestamp": {"type": "string"},
261 "signature": {"type": "string"}
262 }, "required": ["proposal_id", "reviewer_id", "reason", "signature"]}),
263 PermissionLevel::Write,
264 true,
265 vec![
266 "Accepting an applied proposal returns its existing event_id; no duplicate event is emitted.",
267 ],
268 ),
269 tool(
270 "reject_proposal",
271 "Reject a pending proposal as the named reviewer. The reviewer must be registered. Signature is over `{action: \"reject\", proposal_id, reviewer_id, reason, timestamp}` canonicalized.",
272 json!({"type": "object", "properties": {
273 "proposal_id": {"type": "string"},
274 "reviewer_id": {"type": "string"},
275 "reason": {"type": "string"},
276 "timestamp": {"type": "string"},
277 "signature": {"type": "string"}
278 }, "required": ["proposal_id", "reviewer_id", "reason", "signature"]}),
279 PermissionLevel::Write,
280 true,
281 vec![
282 "Rejection records the decision but emits no canonical event; rejected proposals stay on the proposal log.",
283 ],
284 ),
285 ]
286}
287
288pub fn get_tool(name: &str) -> Option<ToolDefinition> {
289 all_tools().into_iter().find(|tool| tool.name == name)
290}
291
292pub fn tool_caveats(name: &str) -> Vec<String> {
293 get_tool(name).map(|tool| tool.caveats).unwrap_or_default()
294}
295
296pub fn mcp_tools_json() -> Value {
297 Value::Array(
298 all_tools()
299 .into_iter()
300 .map(|tool| {
301 json!({
302 "name": tool.name,
303 "description": tool.description,
304 "inputSchema": tool.parameters,
305 "metadata": {
306 "permission_level": tool.permission_level,
307 "mutating": tool.mutating,
308 "caveats": tool.caveats,
309 }
310 })
311 })
312 .collect(),
313 )
314}
315
316fn tool(
317 name: &str,
318 description: &str,
319 parameters: Value,
320 permission_level: PermissionLevel,
321 mutating: bool,
322 caveats: Vec<&str>,
323) -> ToolDefinition {
324 ToolDefinition {
325 name: name.to_string(),
326 description: description.to_string(),
327 parameters,
328 permission_level,
329 mutating,
330 caveats: caveats.into_iter().map(str::to_string).collect(),
331 }
332}