1use devboy_core::{PropertySchema, ToolCategory, ToolSchema};
7
8#[derive(Debug, Clone, serde::Serialize)]
10pub struct ToolDefinition {
11 pub name: String,
12 pub description: String,
13 pub category: ToolCategory,
14 pub input_schema: ToolSchema,
15}
16
17pub fn base_tool_definitions() -> Vec<ToolDefinition> {
19 vec![
20 ToolDefinition {
22 name: "get_issues".into(),
23 description: "Get issues from configured provider. Returns a list with filters.".into(),
24 category: ToolCategory::IssueTracker,
25 input_schema: {
26 let mut s = ToolSchema::new();
27 s.add_property("state", PropertySchema::string_enum(&["open", "closed", "all"], "Filter by issue state (default: open)"));
28 s.add_property("search", PropertySchema::string("Search query for title and description"));
29 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Filter by label names"));
30 s.add_property("assignee", PropertySchema::string("Filter by assignee username"));
31 s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 20)", Some(1.0), Some(100.0)));
32 s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
33 s.add_property("sort_by", PropertySchema::string_enum(&["created_at", "updated_at"], "Sort by field (default: updated_at)"));
34 s.add_property("sort_order", PropertySchema::string_enum(&["asc", "desc"], "Sort order (default: desc)"));
35 s.add_property("projectKey", PropertySchema::string("Project key to filter issues (e.g., \"PROJ\"). Overrides default project. Removed by providers that don't support it."));
36 s.add_property("nativeQuery", PropertySchema::string("Native query passed directly to provider (e.g., Jira JQL). Replaces auto-generated filters. If the query omits a project clause, the default project is auto-injected."));
37 s
38 },
39 },
40 ToolDefinition {
41 name: "get_issue".into(),
42 description: "Get a single issue by key with optional comments and relations.".into(),
43 category: ToolCategory::IssueTracker,
44 input_schema: {
45 let mut s = ToolSchema::new();
46 s.add_property("key", PropertySchema::string("Issue key (e.g., 'gh#123', 'gitlab#456', 'CU-abc', 'DEV-42', 'jira#PROJ-123')"));
47 s.add_property("includeComments", PropertySchema::boolean("Include issue comments (default: true)"));
48 s.add_property("includeRelations", PropertySchema::boolean("Include issue relations — parent, subtasks, dependencies (default: true)"));
49 s.set_required("key", true);
50 s
51 },
52 },
53 ToolDefinition {
54 name: "get_issue_comments".into(),
55 description: "Get comments for an issue.".into(),
56 category: ToolCategory::IssueTracker,
57 input_schema: {
58 let mut s = ToolSchema::new();
59 s.add_property("key", PropertySchema::string("Issue key"));
60 s.set_required("key", true);
61 s
62 },
63 },
64 ToolDefinition {
65 name: "get_issue_relations".into(),
66 description: "Get relations for an issue (parent, subtasks, linked issues).".into(),
67 category: ToolCategory::IssueTracker,
68 input_schema: {
69 let mut s = ToolSchema::new();
70 s.add_property("key", PropertySchema::string("Issue key"));
71 s.set_required("key", true);
72 s
73 },
74 },
75 ToolDefinition {
76 name: "create_issue".into(),
77 description: "Create a new issue in the configured provider.".into(),
78 category: ToolCategory::IssueTracker,
79 input_schema: {
80 let mut s = ToolSchema::new();
81 s.add_property("title", PropertySchema::string("Issue title"));
82 s.add_property("description", PropertySchema::string("Issue description/body"));
83 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to add"));
84 s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "Assignee usernames"));
85 s.add_property("parentId", PropertySchema::string("Parent issue key to create a subtask (e.g., 'CU-abc123' or 'DEV-42'). Only supported by ClickUp."));
86 s.add_property("markdown", PropertySchema::boolean("Whether the description is markdown (default: true). When true, ClickUp renders formatted text."));
87 s.add_property("projectId", PropertySchema::string("Jira project key (not numeric ID) for issue creation (e.g., \"PROJ\"). Optional — overrides the default project."));
88 s.add_property("issueType", PropertySchema::string("Issue type (e.g., \"Task\", \"Bug\", \"Story\"). Default: \"Task\". Removed by providers that don't support it."));
89 s.set_required("title", true);
96 s
97 },
98 },
99 ToolDefinition {
100 name: "update_issue".into(),
101 description: "Update an existing issue. Only provided fields will be changed.".into(),
102 category: ToolCategory::IssueTracker,
103 input_schema: {
104 let mut s = ToolSchema::new();
105 s.add_property("key", PropertySchema::string("Issue key"));
106 s.add_property("title", PropertySchema::string("New title"));
107 s.add_property("description", PropertySchema::string("New description"));
108 s.add_property("state", PropertySchema::string_enum(&["open", "closed"], "New state (generic open/closed). For ClickUp custom statuses (\"in progress\", \"review\", \"to do\", …) use `status` instead — `get_available_statuses` lists valid names"));
109 s.add_property("status", PropertySchema::string("Provider-specific status name. ClickUp: any custom status from `get_available_statuses` (e.g. \"in progress\", \"review\"). Other providers: ignored. Takes precedence over `state` when both are set."));
110 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "New labels (replaces existing)"));
111 s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "New assignees"));
112 s.add_property("parentId", PropertySchema::string("Parent issue key to move task as subtask (e.g., 'CU-abc123' or 'DEV-42'). Only supported by ClickUp."));
113 s.add_property("markdown", PropertySchema::boolean("Whether the description is markdown (default: true). When true, ClickUp renders formatted text."));
114 s.set_required("key", true);
118 s
119 },
120 },
121 ToolDefinition {
122 name: "add_issue_comment".into(),
123 description: "Add a comment to an issue with optional file attachments (ClickUp only).".into(),
124 category: ToolCategory::IssueTracker,
125 input_schema: {
126 let mut s = ToolSchema::new();
127 s.add_property("key", PropertySchema::string("Issue key"));
128 s.add_property("body", PropertySchema::string("Comment text"));
129 s.add_property("attachments", PropertySchema::array(
130 PropertySchema::string("Attachment object with fileData (base64) and filename"),
131 "File attachments (ClickUp only, max 10MB per file). Each: {fileData: base64, filename: string}",
132 ));
133 s.set_required("key", true);
134 s.set_required("body", true);
135 s
136 },
137 },
138
139 ToolDefinition {
141 name: "get_merge_requests".into(),
142 description: "Get merge requests / pull requests from configured provider.".into(),
143 category: ToolCategory::GitRepository,
144 input_schema: {
145 let mut s = ToolSchema::new();
146 s.add_property("state", PropertySchema::string_enum(&["open", "closed", "merged", "all"], "Filter by state (default: open)"));
147 s.add_property("author", PropertySchema::string("Filter by author username"));
148 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Filter by label names"));
149 s.add_property("source_branch", PropertySchema::string("Filter by source branch"));
150 s.add_property("target_branch", PropertySchema::string("Filter by target branch"));
151 s.add_property("limit", PropertySchema::integer("Maximum results (default: 20)", Some(1.0), Some(100.0)));
152 s
153 },
154 },
155 ToolDefinition {
156 name: "get_merge_request".into(),
157 description: "Get a single merge request by key (e.g., 'pr#123', 'mr#456').".into(),
158 category: ToolCategory::GitRepository,
159 input_schema: {
160 let mut s = ToolSchema::new();
161 s.add_property("key", PropertySchema::string("MR/PR key"));
162 s.set_required("key", true);
163 s
164 },
165 },
166 ToolDefinition {
167 name: "get_merge_request_discussions".into(),
168 description: "Get discussions/review comments for a merge request with code positions.".into(),
169 category: ToolCategory::GitRepository,
170 input_schema: {
171 let mut s = ToolSchema::new();
172 s.add_property("key", PropertySchema::string("MR/PR key"));
173 s.add_property("limit", PropertySchema::integer("Max discussions (default: 20)", Some(1.0), Some(100.0)));
174 s.add_property("offset", PropertySchema::integer("Skip N discussions (default: 0)", Some(0.0), None));
175 s.set_required("key", true);
176 s
177 },
178 },
179 ToolDefinition {
180 name: "get_merge_request_diffs".into(),
181 description: "Get file diffs for a merge request.".into(),
182 category: ToolCategory::GitRepository,
183 input_schema: {
184 let mut s = ToolSchema::new();
185 s.add_property("key", PropertySchema::string("MR/PR key"));
186 s.set_required("key", true);
187 s
188 },
189 },
190 ToolDefinition {
191 name: "create_merge_request".into(),
192 description: "Create a new merge request (GitLab) or pull request (GitHub).".into(),
193 category: ToolCategory::GitRepository,
194 input_schema: {
195 let mut s = ToolSchema::new();
196 s.add_property("title", PropertySchema::string("MR/PR title"));
197 s.add_property("description", PropertySchema::string("MR/PR description"));
198 s.add_property("source_branch", PropertySchema::string("Source branch"));
199 s.add_property("target_branch", PropertySchema::string("Target branch"));
200 s.add_property("draft", PropertySchema::boolean("Create as draft (default: false)"));
201 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels"));
202 s.add_property("reviewers", PropertySchema::array(PropertySchema::string("reviewer"), "Reviewers"));
203 s.set_required("title", true);
204 s.set_required("source_branch", true);
205 s.set_required("target_branch", true);
206 s
207 },
208 },
209 ToolDefinition {
210 name: "create_merge_request_comment".into(),
211 description: "Add a comment to a merge request. Can be general or inline code review.".into(),
212 category: ToolCategory::GitRepository,
213 input_schema: {
214 let mut s = ToolSchema::new();
215 s.add_property("key", PropertySchema::string("MR/PR key"));
216 s.add_property("body", PropertySchema::string("Comment text"));
217 s.add_property("file_path", PropertySchema::string("File path for inline comment"));
218 s.add_property("line", PropertySchema::integer("Line number for inline comment", None, None));
219 s.add_property("line_type", PropertySchema::string_enum(&["old", "new"], "Line type (default: new)"));
220 s.add_property("commit_sha", PropertySchema::string("Commit SHA for inline comment"));
221 s.add_property("discussion_id", PropertySchema::string("Reply to existing discussion"));
222 s.set_required("key", true);
223 s.set_required("body", true);
224 s
225 },
226 },
227
228 ToolDefinition {
230 name: "get_pipeline".into(),
231 description: "Get CI/CD pipeline status for branch or MR/PR with job details.".into(),
232 category: ToolCategory::GitRepository,
233 input_schema: {
234 let mut s = ToolSchema::new();
235 s.add_property("branch", PropertySchema::string("Branch name (default: main)"));
236 s.add_property("mrKey", PropertySchema::string("MR/PR key (priority over branch)"));
237 s.add_property("includeFailedLogs", PropertySchema::boolean("Include error extraction for failed jobs (default: true)"));
238 s
239 },
240 },
241 ToolDefinition {
242 name: "get_job_logs".into(),
243 description: "Get CI/CD job logs. Modes: smart (auto errors), search (pattern), paginated, full.".into(),
244 category: ToolCategory::GitRepository,
245 input_schema: {
246 let mut s = ToolSchema::new();
247 s.add_property("jobId", PropertySchema::string("Job ID from get_pipeline"));
248 s.add_property("pattern", PropertySchema::string("Regex/keyword search pattern"));
249 s.add_property("context", PropertySchema::integer("Context lines around match (default: 5)", None, None));
250 s.add_property("maxMatches", PropertySchema::integer("Max search results (default: 20)", None, None));
251 s.add_property("offset", PropertySchema::integer("Start line for paginated mode", None, None));
252 s.add_property("limit", PropertySchema::integer("Lines to return (default: 200, max: 1000)", Some(1.0), Some(1000.0)));
253 s.add_property("full", PropertySchema::boolean("Return entire log"));
254 s.set_required("jobId", true);
255 s
256 },
257 },
258
259 ToolDefinition {
261 name: "get_available_statuses".into(),
262 description: "Get available statuses for the issue tracker.".into(),
263 category: ToolCategory::IssueTracker,
264 input_schema: ToolSchema::new(),
265 },
266 ToolDefinition {
267 name: "get_users".into(),
268 description: "Get users from the issue tracker (Jira). Search by name, project, or ID.".into(),
269 category: ToolCategory::IssueTracker,
270 input_schema: {
271 let mut s = ToolSchema::new();
272 s.add_property("userId", PropertySchema::string("Get specific user by ID"));
273 s.add_property("projectKey", PropertySchema::string("Get assignable users for project"));
274 s.add_property("search", PropertySchema::string("Search by name or email"));
275 s.add_property("maxResults", PropertySchema::integer("Max results (default: 50)", Some(1.0), Some(1000.0)));
276 s
277 },
278 },
279 ToolDefinition {
280 name: "link_issues".into(),
281 description: "Link two issues together (blocks, relates_to, etc.).".into(),
282 category: ToolCategory::IssueTracker,
283 input_schema: {
284 let mut s = ToolSchema::new();
285 s.add_property("sourceIssueKey", PropertySchema::string("Source issue key"));
286 s.add_property("targetIssueKey", PropertySchema::string("Target issue key"));
287 s.add_property("linkType", PropertySchema::string(
288 "Issue link type. Accepts canonical Jira names (`Blocks`, `Relates`, `Causes`, `Implements`, `Created By`, `Duplicate`, `Cloners`) and snake_case aliases (`blocks`, `blocked_by`, `relates_to`, `causes`, `caused_by`, `implements`, `implemented_by`, `created_by`, `creates`, `duplicates`, `duplicated_by`, `clones`, `cloned_by`). The `*_by` variants flip direction. Custom link types configured on the instance also work — pass the exact name. GitHub/GitLab providers ignore this field.",
289 ));
290 s.set_required("sourceIssueKey", true);
291 s.set_required("targetIssueKey", true);
292 s.set_required("linkType", true);
293 s
294 },
295 },
296 ToolDefinition {
297 name: "unlink_issues".into(),
298 description: "Remove a link between two issues.".into(),
299 category: ToolCategory::IssueTracker,
300 input_schema: {
301 let mut s = ToolSchema::new();
302 s.add_property("sourceIssueKey", PropertySchema::string("Source issue key"));
303 s.add_property("targetIssueKey", PropertySchema::string("Target issue key"));
304 s.add_property("linkType", PropertySchema::string(
305 "Issue link type to remove. Accepts the same canonical names and snake_case aliases as `link_issues` (`Blocks`, `Causes`, `Implements`, `Created By`, `Duplicate`, `Cloners`, plus `*_by` direction flips and `subtask`). Custom link types pass through as-is.",
306 ));
307 s.set_required("sourceIssueKey", true);
308 s.set_required("targetIssueKey", true);
309 s.set_required("linkType", true);
310 s
311 },
312 },
313 ToolDefinition {
314 name: "get_epics".into(),
315 description: "Get epics (high-level tasks) from the issue tracker.".into(),
316 category: ToolCategory::Epics,
317 input_schema: {
318 let mut s = ToolSchema::new();
319 s.add_property("search", PropertySchema::string("Search in epic title"));
320 s.add_property("limit", PropertySchema::integer("Max results (default: 50)", Some(1.0), Some(100.0)));
321 s.add_property("offset", PropertySchema::integer("Skip N results (default: 0)", Some(0.0), None));
322 s
323 },
324 },
325 ToolDefinition {
326 name: "create_epic".into(),
327 description: "Create a new epic.".into(),
328 category: ToolCategory::Epics,
329 input_schema: {
330 let mut s = ToolSchema::new();
331 s.add_property("title", PropertySchema::string("Epic title"));
332 s.add_property("description", PropertySchema::string("Epic description"));
333 s.set_required("title", true);
334 s
335 },
336 },
337 ToolDefinition {
338 name: "update_epic".into(),
339 description: "Update an existing epic.".into(),
340 category: ToolCategory::Epics,
341 input_schema: {
342 let mut s = ToolSchema::new();
343 s.add_property("epicKey", PropertySchema::string("Epic key (e.g., 'CU-abc', 'DEV-123')"));
344 s.add_property("title", PropertySchema::string("New title"));
345 s.add_property("description", PropertySchema::string("New description"));
346 s.add_property("state", PropertySchema::string("New epic state (generic open/closed). For ClickUp custom statuses use `status`."));
347 s.add_property("status", PropertySchema::string("Provider-specific status name. Same as `update_issue.status` — for ClickUp, any custom status from `get_available_statuses`. Takes precedence over `state`."));
348 s.add_property("goalId", PropertySchema::string("Goal ID (G1-G9) to associate with the epic"));
349 s.add_property("priority", PropertySchema::string("New priority (urgent/high/normal/low)"));
350 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to set"));
351 s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "Assignees to set"));
352 s.set_required("epicKey", true);
353 s
354 },
355 },
356 ToolDefinition {
358 name: "get_meeting_notes".into(),
359 description: "Get meeting notes and transcripts with optional filters (date range, participants, host).".into(),
360 category: ToolCategory::MeetingNotes,
361 input_schema: {
362 let mut s = ToolSchema::new();
363 s.add_property("from_date", PropertySchema::string("Filter from date (ISO 8601, e.g., '2025-01-01T00:00:00Z')"));
364 s.add_property("to_date", PropertySchema::string("Filter to date (ISO 8601)"));
365 s.add_property("participants", PropertySchema::array(PropertySchema::string("email"), "Filter by participant email addresses"));
366 s.add_property("host_email", PropertySchema::string("Filter by host email"));
367 s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 50)", Some(1.0), Some(50.0)));
368 s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
369 s
370 },
371 },
372 ToolDefinition {
373 name: "get_meeting_transcript".into(),
374 description: "Get the full transcript for a meeting. Returns speaker-attributed sentences with timestamps.".into(),
375 category: ToolCategory::MeetingNotes,
376 input_schema: {
377 let mut s = ToolSchema::new();
378 s.add_property("meeting_id", PropertySchema::string("Meeting ID from get_meeting_notes"));
379 s.set_required("meeting_id", true);
380 s.add_property("offset", PropertySchema::integer("Number of sentences to skip for pagination (default: 0)", Some(0.0), None));
387 s.add_property("limit", PropertySchema::integer("Maximum number of sentences (default: 50, max: 500)", Some(1.0), Some(500.0)));
388 s.add_property("speaker_filter", PropertySchema::string("Filter sentences by speaker name (case-insensitive substring match)"));
389 s.add_property("search_text", PropertySchema::string("Search sentence text (case-insensitive substring match)"));
390 s.add_property("format", PropertySchema::string_enum(&["flat", "grouped"], "Output format: 'flat' (default, per-sentence) or 'grouped' (same-speaker runs collapsed)"));
391 s
392 },
393 },
394 ToolDefinition {
395 name: "search_meeting_notes".into(),
396 description: "Search across meetings by keywords, topics, or action items, with optional filters (date range, participants, host).".into(),
397 category: ToolCategory::MeetingNotes,
398 input_schema: {
399 let mut s = ToolSchema::new();
400 s.add_property("query", PropertySchema::string("Search query"));
401 s.add_property("from_date", PropertySchema::string("Filter from date (ISO 8601)"));
402 s.add_property("to_date", PropertySchema::string("Filter to date (ISO 8601)"));
403 s.add_property("participants", PropertySchema::array(PropertySchema::string("email"), "Filter by participant email addresses"));
404 s.add_property("host_email", PropertySchema::string("Filter by host email"));
405 s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 50)", Some(1.0), Some(50.0)));
406 s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
407 s.set_required("query", true);
408 s
409 },
410 },
411 ToolDefinition {
413 name: "get_knowledge_base_spaces".into(),
414 description: "List available knowledge base spaces.".into(),
415 category: ToolCategory::KnowledgeBase,
416 input_schema: ToolSchema::new(),
417 },
418 ToolDefinition {
419 name: "list_knowledge_base_pages".into(),
420 description: "List pages in a knowledge base space with pagination.".into(),
421 category: ToolCategory::KnowledgeBase,
422 input_schema: {
423 let mut s = ToolSchema::new();
424 s.add_property("spaceKey", PropertySchema::string("Space key to list pages from"));
425 s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 25)", Some(1.0), Some(100.0)));
426 s.add_property("offset", PropertySchema::integer("Number of results to skip when offset pagination is supported", Some(0.0), None));
427 s.add_property("cursor", PropertySchema::string("Provider pagination cursor/token"));
428 s.add_property("search", PropertySchema::string("Optional free-text title/content filter"));
429 s.add_property("parentId", PropertySchema::string("Optional ancestor/parent page ID to scope the listing"));
430 s.set_required("spaceKey", true);
431 s
432 },
433 },
434 ToolDefinition {
435 name: "get_knowledge_base_page".into(),
436 description: "Get a knowledge base page with content, labels, and ancestors.".into(),
437 category: ToolCategory::KnowledgeBase,
438 input_schema: {
439 let mut s = ToolSchema::new();
440 s.add_property("pageId", PropertySchema::string("Knowledge base page ID"));
441 s.set_required("pageId", true);
442 s
443 },
444 },
445 ToolDefinition {
446 name: "create_knowledge_base_page".into(),
447 description: "Create a knowledge base page in a space.".into(),
448 category: ToolCategory::KnowledgeBase,
449 input_schema: {
450 let mut s = ToolSchema::new();
451 s.add_property("spaceKey", PropertySchema::string("Target space key"));
452 s.add_property("title", PropertySchema::string("Page title"));
453 s.add_property("content", PropertySchema::string("Page body content"));
454 s.add_property("contentType", PropertySchema::string_enum(&["markdown", "html", "storage"], "Content representation supplied by the caller"));
455 s.add_property("parentId", PropertySchema::string("Optional parent page ID"));
456 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to set on the page"));
457 s.set_required("spaceKey", true);
458 s.set_required("title", true);
459 s.set_required("content", true);
460 s
461 },
462 },
463 ToolDefinition {
464 name: "update_knowledge_base_page".into(),
465 description: "Update a knowledge base page title, content, metadata, or labels.".into(),
466 category: ToolCategory::KnowledgeBase,
467 input_schema: {
468 let mut s = ToolSchema::new();
469 s.add_property("pageId", PropertySchema::string("Knowledge base page ID"));
470 s.add_property("title", PropertySchema::string("New page title"));
471 s.add_property("content", PropertySchema::string("New page body content"));
472 s.add_property("contentType", PropertySchema::string_enum(&["markdown", "html", "storage"], "Content representation supplied by the caller"));
473 s.add_property("version", PropertySchema::integer("Expected current version for optimistic locking", Some(1.0), None));
474 s.add_property("parentId", PropertySchema::string("Optional new parent page ID"));
475 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to replace on the page"));
476 s.set_required("pageId", true);
477 s
478 },
479 },
480 ToolDefinition {
481 name: "search_knowledge_base".into(),
482 description: "Search knowledge base pages across spaces using free text or provider-native syntax such as CQL.".into(),
483 category: ToolCategory::KnowledgeBase,
484 input_schema: {
485 let mut s = ToolSchema::new();
486 s.add_property("query", PropertySchema::string("Free-text query or provider-native search expression"));
487 s.add_property("spaceKey", PropertySchema::string("Restrict search to a specific space key"));
488 s.add_property("cursor", PropertySchema::string("Provider pagination cursor/token"));
489 s.add_property("limit", PropertySchema::integer("Maximum number of matches to return", Some(1.0), Some(100.0)));
490 s.add_property("rawQuery", PropertySchema::boolean("Whether `query` should be treated as raw provider-native syntax"));
491 s.set_required("query", true);
492 s
493 },
494 },
495 ToolDefinition {
496 name: "update_merge_request".into(),
497 description: "Update a merge request / pull request (title, description, state, labels, draft).".into(),
498 category: ToolCategory::GitRepository,
499 input_schema: {
500 let mut s = ToolSchema::new();
501 s.add_property("key", PropertySchema::string("MR key (e.g. 'mr#1', 'pr#42')"));
502 s.add_property("title", PropertySchema::string("New title"));
503 s.add_property("description", PropertySchema::string("New description / body (supports markdown)"));
504 s.add_property("state", PropertySchema::string_enum(&["close", "reopen"], "Change MR state"));
505 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "New labels (replaces existing)"));
506 s.set_required("key", true);
507 s
508 },
509 },
510 ToolDefinition {
514 name: "get_assets".into(),
515 description: "List file attachments for an issue or merge request.".into(),
516 category: ToolCategory::IssueTracker,
517 input_schema: {
518 let mut s = ToolSchema::new();
519 s.add_property("context_type", PropertySchema::string_enum(&["issue", "mr"], "Context type: 'issue' or 'mr' (merge request / pull request)"));
520 s.add_property("key", PropertySchema::string("Issue key (e.g. 'DEV-123', 'gitlab#42') or MR key (e.g. 'mr#42', 'pr#42')"));
521 s.set_required("context_type", true);
522 s.set_required("key", true);
523 s
524 },
525 },
526 ToolDefinition {
527 name: "upload_asset".into(),
528 description: "Upload a file attachment to an issue. Returns the download URL.".into(),
529 category: ToolCategory::IssueTracker,
530 input_schema: {
531 let mut s = ToolSchema::new();
532 s.add_property("context_type", PropertySchema::string_enum(&["issue"], "Context type (currently only 'issue' is supported for uploads)"));
533 s.add_property("key", PropertySchema::string("Issue key (e.g. 'DEV-123')"));
534 s.add_property("filename", PropertySchema::string("Original filename (e.g. 'screenshot.png')"));
535 s.add_property("fileData", PropertySchema::string("Base64-encoded file content"));
536 s.set_required("context_type", true);
537 s.set_required("key", true);
538 s.set_required("filename", true);
539 s.set_required("fileData", true);
540 s
541 },
542 },
543 ToolDefinition {
544 name: "download_asset".into(),
545 description: "Download a file attachment to local cache. Returns local file path when cache is available, base64-encoded content as fallback.".into(),
546 category: ToolCategory::IssueTracker,
547 input_schema: {
548 let mut s = ToolSchema::new();
549 s.add_property("context_type", PropertySchema::string_enum(&["issue", "mr"], "Context type: 'issue' or 'mr'"));
550 s.add_property("key", PropertySchema::string("Issue key or MR key"));
551 s.add_property("asset_id", PropertySchema::string("Asset identifier from get_assets response"));
552 s.set_required("context_type", true);
553 s.set_required("key", true);
554 s.set_required("asset_id", true);
555 s
556 },
557 },
558 ToolDefinition {
562 name: "get_messenger_chats".into(),
563 description: "List available messenger chats, channels, groups, or direct messages.".into(),
564 category: ToolCategory::Messenger,
565 input_schema: {
566 let mut s = ToolSchema::new();
567 s.add_property("search", PropertySchema::string("Optional chat name search"));
568 s.add_property("chat_type", PropertySchema::string_enum(&["direct", "group", "channel"], "Optional chat type filter"));
569 s.add_property("limit", PropertySchema::integer("Maximum number of chats to return", Some(1.0), Some(1000.0)));
570 s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
571 s.add_property("include_inactive", PropertySchema::boolean("Include archived or inactive chats"));
572 s
573 },
574 },
575 ToolDefinition {
576 name: "get_chat_messages".into(),
577 description: "Get message history for a chat or fetch replies for a specific thread.".into(),
578 category: ToolCategory::Messenger,
579 input_schema: {
580 let mut s = ToolSchema::new();
581 s.add_property("chat_id", PropertySchema::string("Messenger chat ID"));
582 s.add_property("limit", PropertySchema::integer("Maximum number of messages to return", Some(1.0), Some(1000.0)));
583 s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
584 s.add_property("thread_id", PropertySchema::string("Thread identifier to fetch replies for"));
585 s.add_property("since", PropertySchema::string("Only include messages after this provider timestamp"));
586 s.add_property("until", PropertySchema::string("Only include messages before this provider timestamp"));
587 s.set_required("chat_id", true);
588 s
589 },
590 },
591 ToolDefinition {
592 name: "search_chat_messages".into(),
593 description: "Search messages across accessible chats or within a specific chat.".into(),
594 category: ToolCategory::Messenger,
595 input_schema: {
596 let mut s = ToolSchema::new();
597 s.add_property("query", PropertySchema::string("Message search query"));
598 s.add_property("chat_id", PropertySchema::string("Optional chat ID to scope the search"));
599 s.add_property("limit", PropertySchema::integer("Maximum number of matches to return", Some(1.0), Some(1000.0)));
600 s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
601 s.add_property("since", PropertySchema::string("Only include messages after this provider timestamp"));
602 s.add_property("until", PropertySchema::string("Only include messages before this provider timestamp"));
603 s.set_required("query", true);
604 s
605 },
606 },
607 ToolDefinition {
608 name: "send_message".into(),
609 description: "Send a message to a chat or as a threaded reply.".into(),
610 category: ToolCategory::Messenger,
611 input_schema: {
612 let mut s = ToolSchema::new();
613 s.add_property("chat_id", PropertySchema::string("Messenger chat ID"));
614 s.add_property("text", PropertySchema::string("Message body"));
615 s.add_property("thread_id", PropertySchema::string("Thread identifier to post as a threaded reply"));
616 s.add_property("reply_to_id", PropertySchema::string("Direct parent message ID when supported"));
617 s.set_required("chat_id", true);
618 s.set_required("text", true);
619 s
620 },
621 },
622 ToolDefinition {
623 name: "delete_asset".into(),
624 description: "Delete a file attachment from an issue. Not all providers support this — check asset_capabilities first.".into(),
625 category: ToolCategory::IssueTracker,
626 input_schema: {
627 let mut s = ToolSchema::new();
628 s.add_property("key", PropertySchema::string("Issue key (e.g. 'PROJ-123')"));
629 s.add_property("asset_id", PropertySchema::string("Asset identifier to delete"));
630 s.set_required("key", true);
631 s.set_required("asset_id", true);
632 s
633 },
634 },
635 ToolDefinition {
637 name: "get_structures".into(),
638 description: "List all available Jira Structures. Returns structure ID, name, and description. Requires Jira with Structure plugin.".into(),
639 category: ToolCategory::JiraStructure,
640 input_schema: ToolSchema::new(),
641 },
642 ToolDefinition {
643 name: "get_structure_forest".into(),
644 description: "Get the hierarchy tree of a Jira Structure. Returns nested tree with rowId, itemId (Jira issue key), itemType, and children. Supports pagination for large structures.".into(),
645 category: ToolCategory::JiraStructure,
646 input_schema: {
647 let mut s = ToolSchema::new();
648 s.add_property("structureId", PropertySchema::integer("Structure ID. Use get_structures to find it.", None, None));
649 s.add_property("offset", PropertySchema::integer("Offset for pagination (default: 0)", Some(0.0), None));
650 s.add_property("limit", PropertySchema::integer("Max rows to return (default: 200)", Some(1.0), Some(10000.0)));
651 s.set_required("structureId", true);
652 s
653 },
654 },
655 ToolDefinition {
656 name: "add_structure_rows".into(),
657 description: "Add items (Jira issues or folders) to a Structure. Specify position with under (parent row) and/or after (sibling row). Use forestVersion for optimistic concurrency.".into(),
658 category: ToolCategory::JiraStructure,
659 input_schema: {
660 let mut s = ToolSchema::new();
661 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
662 s.add_property("items", PropertySchema::array(
663 PropertySchema::string("Item: Jira issue key (e.g. 'PROJ-123') or JSON {\"itemId\":\"PROJ-123\",\"itemType\":\"issue\"}"),
664 "Items to add",
665 ));
666 s.add_property("under", PropertySchema::integer("Parent row ID — items become children of this row", None, None));
667 s.add_property("after", PropertySchema::integer("Sibling row ID — items placed after this row", None, None));
668 s.add_property("forestVersion", PropertySchema::integer("Forest version for optimistic locking (from get_structure_forest)", None, None));
669 s.set_required("structureId", true);
670 s.set_required("items", true);
671 s
672 },
673 },
674 ToolDefinition {
675 name: "move_structure_rows".into(),
676 description: "Move rows within a Jira Structure hierarchy. Specify new position with under (new parent) and/or after (sibling).".into(),
677 category: ToolCategory::JiraStructure,
678 input_schema: {
679 let mut s = ToolSchema::new();
680 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
681 s.add_property("rowIds", PropertySchema::array(
682 PropertySchema::integer("Row ID", None, None),
683 "Row IDs to move (from get_structure_forest)",
684 ));
685 s.add_property("under", PropertySchema::integer("New parent row ID", None, None));
686 s.add_property("after", PropertySchema::integer("Sibling row ID to place after", None, None));
687 s.add_property("forestVersion", PropertySchema::integer("Forest version for optimistic locking", None, None));
688 s.set_required("structureId", true);
689 s.set_required("rowIds", true);
690 s
691 },
692 },
693 ToolDefinition {
694 name: "remove_structure_row".into(),
695 description: "Remove a row from a Jira Structure. Only removes from the structure hierarchy — the underlying Jira issue is NOT deleted.".into(),
696 category: ToolCategory::JiraStructure,
697 input_schema: {
698 let mut s = ToolSchema::new();
699 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
700 s.add_property("rowId", PropertySchema::integer("Row ID to remove (from get_structure_forest)", None, None));
701 s.set_required("structureId", true);
702 s.set_required("rowId", true);
703 s
704 },
705 },
706 ToolDefinition {
707 name: "get_structure_values".into(),
708 description: "Read column values (including Expr formulas like SUM, PROGRESS, COUNT) for specific rows in a Jira Structure. Values are computed server-side.".into(),
709 category: ToolCategory::JiraStructure,
710 input_schema: {
711 let mut s = ToolSchema::new();
712 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
713 s.add_property("rows", PropertySchema::array(
714 PropertySchema::integer("Row ID", None, None),
715 "Row IDs to read values for",
716 ));
717 s.add_property("columns", PropertySchema::array(
718 PropertySchema::string("Column spec: field name (e.g. 'summary'), or JSON {\"field\":\"status\"} or {\"formula\":\"SUM(\\\"Story Points\\\")\"}"),
719 "Columns to read",
720 ));
721 s.set_required("structureId", true);
722 s.set_required("rows", true);
723 s.set_required("columns", true);
724 s
725 },
726 },
727 ToolDefinition {
728 name: "get_structure_views".into(),
729 description: "Get views for a Jira Structure. Without viewId: lists all views. With viewId: returns full view configuration (columns, grouping, sorting, filter).".into(),
730 category: ToolCategory::JiraStructure,
731 input_schema: {
732 let mut s = ToolSchema::new();
733 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
734 s.add_property("viewId", PropertySchema::integer("View ID for full config (optional — omit to list all views)", None, None));
735 s.set_required("structureId", true);
736 s
737 },
738 },
739 ToolDefinition {
740 name: "save_structure_view".into(),
741 description: "Create or update a Jira Structure view. Views define column layout (fields and formulas), grouping, sorting, and filters. Omit id to create new.".into(),
742 category: ToolCategory::JiraStructure,
743 input_schema: {
744 let mut s = ToolSchema::new();
745 s.add_property("id", PropertySchema::integer("View ID to update (omit to create new)", None, None));
746 s.add_property("structureId", PropertySchema::integer("Structure ID this view belongs to", None, None));
747 s.add_property("name", PropertySchema::string("View name"));
748 s.add_property("columns", PropertySchema::array(
749 PropertySchema::string("Column spec: JSON {\"field\":\"summary\"} or {\"formula\":\"SUM(\\\"Story Points\\\")\",\"width\":100}"),
750 "Column definitions",
751 ));
752 s.add_property("groupBy", PropertySchema::string("Field name to group by"));
753 s.add_property("sortBy", PropertySchema::string("Field name to sort by"));
754 s.add_property("filter", PropertySchema::string("JQL filter expression"));
755 s.set_required("structureId", true);
756 s.set_required("name", true);
757 s
758 },
759 },
760 ToolDefinition {
761 name: "create_structure".into(),
762 description: "Create a new Jira Structure. After creation, use add_structure_rows to populate and save_structure_view to configure columns.".into(),
763 category: ToolCategory::JiraStructure,
764 input_schema: {
765 let mut s = ToolSchema::new();
766 s.add_property("name", PropertySchema::string("Structure name"));
767 s.add_property("description", PropertySchema::string("Structure description"));
768 s.set_required("name", true);
769 s
770 },
771 },
772
773 ToolDefinition {
778 name: "list_project_versions".into(),
779 description: "List Jira project versions / fixVersion targets (releases). Returns rich per-version payload (description, dates, released/archived flags, optional issue counts). Default filter hides archived versions and limits to 20 most recent (unreleased first, then released by releaseDate desc). For issue-level details on a release, follow up with `get_issues` and a JQL `nativeQuery` such as `fixVersion = \"<name>\"` — there is no per-id get tool by design.".into(),
780 category: ToolCategory::IssueTracker,
781 input_schema: {
782 let mut s = ToolSchema::new();
783 s.add_property("project", PropertySchema::string("Jira project key (e.g., \"PROJ\"). Defaults to the configured project."));
784 s.add_property("released", PropertySchema::string_enum(&["true", "false", "all"], "Filter by release state: \"true\" → only released, \"false\" → only unreleased, \"all\" → both (default: \"all\")"));
785 s.add_property("archived", PropertySchema::string_enum(&["true", "false", "all"], "Filter by archived flag (default: \"false\" — hides archival noise)"));
786 s.add_property("limit", PropertySchema::integer("Max versions to return (default: 20). Sorted by releaseDate desc; oldest archival entries trimmed first", Some(1.0), Some(200.0)));
787 s.add_property("includeIssueCount", PropertySchema::boolean("Fetch issue counts per version via Cloud `?expand=issuesstatus` (default: false). Adds latency on large projects."));
788 s
789 },
790 },
791 ToolDefinition {
792 name: "upsert_project_version".into(),
793 description: "Create or partially update a Jira project version, keyed by `(project, name)`. If a version with this name exists, fields you supply are updated and unspecified fields are preserved. If not, a new version is created. Useful for writing release notes (`description`) or closing a release (`released: true`, `releaseDate`).".into(),
794 category: ToolCategory::IssueTracker,
795 input_schema: {
796 let mut s = ToolSchema::new();
797 s.add_property("project", PropertySchema::string("Jira project key (e.g., \"PROJ\"). Defaults to the configured project."));
798 s.add_property("name", PropertySchema::string("Version name — both the lookup key and, on create, the value (e.g., \"3.18.0\")."));
799 s.add_property("description", PropertySchema::string("Release notes / version description. Markdown-style text is preserved on Server/DC; Cloud accepts plain text."));
800 s.add_property("startDate", PropertySchema::string("Planned start date as ISO 8601 calendar date (`YYYY-MM-DD`)."));
801 s.add_property("releaseDate", PropertySchema::string("Planned or actual release date (`YYYY-MM-DD`)."));
802 s.add_property("released", PropertySchema::boolean("Mark released (true) / unreleased (false). Pair with `releaseDate` when closing a release."));
803 s.add_property("archived", PropertySchema::boolean("Archive (true) / unarchive (false) the version."));
804 s.set_required("name", true);
805 s
806 },
807 },
808 ToolDefinition {
812 name: "get_board_sprints".into(),
813 description: "List sprints visible on a Jira agile board. Use to discover the numeric `sprintId` accepted by `create_issue` / `update_issue` and `assign_to_sprint`. Returns name, state (active/future/closed), planned start/end, and goal — enough for the agent to pick the right sprint without a follow-up call.".into(),
814 category: ToolCategory::IssueTracker,
815 input_schema: {
816 let mut s = ToolSchema::new();
817 s.add_property(
818 "boardId",
819 PropertySchema::integer(
820 "Numeric Jira board id. The Agile / Boards REST endpoint returns sprints scoped to one board — there is no global sprint list",
821 Some(0.0),
822 None,
823 ),
824 );
825 s.add_property(
826 "state",
827 PropertySchema::string_enum(
828 &["active", "future", "closed", "all"],
829 "Filter by sprint state. Default `all` returns every sprint on the board",
830 ),
831 );
832 s.set_required("boardId", true);
833 s
834 },
835 },
836 ToolDefinition {
837 name: "assign_to_sprint".into(),
838 description: "Move one or more issues onto a Jira sprint. Pair with `get_board_sprints` to look up the numeric `sprintId`. Issues already on a sprint are silently moved.".into(),
839 category: ToolCategory::IssueTracker,
840 input_schema: {
841 let mut s = ToolSchema::new();
842 s.add_property(
843 "sprintId",
844 PropertySchema::integer(
845 "Numeric sprint id. Use `get_board_sprints` to discover ids on a board",
846 Some(0.0),
847 None,
848 ),
849 );
850 s.add_property(
851 "issueKeys",
852 PropertySchema::array(
853 PropertySchema::string("issue key (e.g., \"PROJ-1\")"),
854 "Issue keys to move onto the sprint. Must contain at least one key.",
855 ),
856 );
857 s.set_required("sprintId", true);
858 s.set_required("issueKeys", true);
859 s
860 },
861 },
862 ToolDefinition {
867 name: "get_custom_fields".into(),
868 description: "List custom fields available on the issue tracker, with their id, name, and field type. Use to discover the `customfield_*` id of a Jira instance — names like `Epic Link`, `Sprint`, `Epic Name` map to different ids on every deployment. Pair with `customFields: { \"<id>\": <value> }` on `create_issue` / `update_issue` for fields not yet exposed as first-class params.".into(),
869 category: ToolCategory::IssueTracker,
870 input_schema: {
871 let mut s = ToolSchema::new();
872 s.add_property(
873 "project",
874 PropertySchema::string(
875 "Optional project key. Reserved for providers that scope custom fields per project; ignored on Jira's global `/field` endpoint.",
876 ),
877 );
878 s.add_property(
879 "issueType",
880 PropertySchema::string(
881 "Optional issue type. Reserved for providers that scope custom fields per create-screen context.",
882 ),
883 );
884 s.add_property(
885 "search",
886 PropertySchema::string(
887 "Case-insensitive substring filter on the field name (e.g. `\"Epic\"` to find `Epic Link` and `Epic Name`).",
888 ),
889 );
890 s.add_property(
891 "limit",
892 PropertySchema::integer(
893 "Max fields to return after filtering (default 50). Sorted by name asc",
894 Some(1.0),
895 Some(200.0),
896 ),
897 );
898 s
899 },
900 },
901 ]
902}
903
904#[derive(Debug, Clone)]
910pub struct McpOnlyTool {
911 pub name: String,
912 pub description: String,
913 pub input_schema: ToolSchema,
914}
915
916pub fn mcp_only_tools() -> Vec<McpOnlyTool> {
918 vec![
919 McpOnlyTool {
920 name: "list_contexts".into(),
921 description: "List configured contexts and indicate the active context.".into(),
922 input_schema: ToolSchema::new(),
923 },
924 McpOnlyTool {
925 name: "use_context".into(),
926 description: "Switch active context at runtime.".into(),
927 input_schema: {
928 let mut s = ToolSchema::new();
929 s.add_property("name", PropertySchema::string("Context name to activate"));
930 s.set_required("name", true);
931 s
932 },
933 },
934 McpOnlyTool {
935 name: "get_current_context".into(),
936 description: "Get current active context name.".into(),
937 input_schema: ToolSchema::new(),
938 },
939 McpOnlyTool {
940 name: "secrets_list".into(),
941 description: "List secrets the active context's manifest declares. \
942 Returns metadata only — values are never included. \
943 Optional filter narrows by path substring, scope, status, or \
944 whether to include framework-internal paths."
945 .into(),
946 input_schema: {
947 let mut s = ToolSchema::new();
948 s.add_property(
949 "path_contains",
950 PropertySchema::string("Substring to match against the full ADR-020 path."),
951 );
952 s.add_property(
953 "scope",
954 PropertySchema::string(
955 "Exact match against the first path segment (e.g. team / personal).",
956 ),
957 );
958 s.add_property(
959 "status",
960 PropertySchema::string_enum(
961 &["registered", "expiring", "expired"],
962 "Lifecycle status filter computed from expires_at.",
963 ),
964 );
965 s.add_property(
966 "include_internal",
967 PropertySchema::boolean("Include framework-internal paths (default: false)."),
968 );
969 s
970 },
971 },
972 McpOnlyTool {
973 name: "secrets_describe".into(),
974 description: "Describe one secret by ADR-020 path. Returns the same \
975 metadata fields as `secrets_list` plus description, retrieval URL, \
976 rotation method, last rotated date, rotation cadence, and pattern \
977 ID. The value is never returned."
978 .into(),
979 input_schema: {
980 let mut s = ToolSchema::new();
981 s.add_property(
982 "path",
983 PropertySchema::string("Full ADR-020 path to describe."),
984 );
985 s.set_required("path", true);
986 s
987 },
988 },
989 McpOnlyTool {
990 name: "secrets_request_provision".into(),
991 description: "Open the provisioning UI dialog for the given ADR-020 \
992 path. The dialog hands the user-entered value directly to the \
993 local daemon — the agent never sees it. Returns a `request_id` \
994 that can be polled with `secrets_poll_status`. Mode defaults to \
995 `provision`; pass `rotation` to surface the destructive-confirm \
996 checkbox. Pending requests expire 5 minutes after issuance."
997 .into(),
998 input_schema: {
999 let mut s = ToolSchema::new();
1000 s.add_property(
1001 "path",
1002 PropertySchema::string("Full ADR-020 path to provision."),
1003 );
1004 s.add_property(
1005 "mode",
1006 PropertySchema::string_enum(
1007 &["provision", "rotation"],
1008 "Dialog mode (default: provision).",
1009 ),
1010 );
1011 s.set_required("path", true);
1012 s
1013 },
1014 },
1015 McpOnlyTool {
1016 name: "secrets_poll_status".into(),
1017 description: "Poll a provisioning or rotation request issued by \
1018 `secrets_request_provision` / `secrets_request_rotation`. Returns \
1019 one of pending / ok / cancelled / expired / failed plus the \
1020 request's age in seconds and the path it was opened for."
1021 .into(),
1022 input_schema: {
1023 let mut s = ToolSchema::new();
1024 s.add_property(
1025 "request_id",
1026 PropertySchema::string(
1027 "Opaque id returned by request_provision / request_rotation.",
1028 ),
1029 );
1030 s.set_required("request_id", true);
1031 s
1032 },
1033 },
1034 McpOnlyTool {
1035 name: "secrets_request_rotation".into(),
1036 description: "Open the rotation UI dialog for the given ADR-020 \
1037 path. Same lifecycle as `secrets_request_provision` but the \
1038 dialog surfaces the destructive-confirm checkbox so the user \
1039 explicitly acknowledges that the existing value is being \
1040 overwritten. Reuses `secrets_poll_status` for status. Pending \
1041 requests expire 5 minutes after issuance."
1042 .into(),
1043 input_schema: {
1044 let mut s = ToolSchema::new();
1045 s.add_property(
1046 "path",
1047 PropertySchema::string("Full ADR-020 path to rotate."),
1048 );
1049 s.set_required("path", true);
1050 s
1051 },
1052 },
1053 McpOnlyTool {
1054 name: "secrets_request_use_approval".into(),
1055 description: "Open the use-approval dialog for an ADR-020 path \
1056 whose `approve_on_use` is set to `session` or `per-call`. \
1057 The agent supplies a short human-facing `reason` that the \
1058 dialog renders verbatim alongside the path; the user picks \
1059 `once`, `session`, or `denied`. Returns a `request_id` to \
1060 poll via `secrets_poll_status`. Pending requests expire 5 \
1061 minutes after issuance; `ttl_seconds` may shorten the \
1062 window but never extend it. The agent never sees the \
1063 secret — only whether the user approved its use."
1064 .into(),
1065 input_schema: {
1066 let mut s = ToolSchema::new();
1067 s.add_property(
1068 "path",
1069 PropertySchema::string("Full ADR-020 path the agent intends to resolve."),
1070 );
1071 s.add_property(
1072 "reason",
1073 PropertySchema::string(
1074 "Short human-facing reason rendered in the dialog \
1075 (e.g. 'pushing image to staging registry').",
1076 ),
1077 );
1078 s.add_property(
1079 "ttl_seconds",
1080 PropertySchema {
1081 schema_type: "integer".into(),
1082 description: Some(
1083 "Optional lifetime in seconds; capped at the \
1084 registry-wide TTL (5 minutes)."
1085 .into(),
1086 ),
1087 ..Default::default()
1088 },
1089 );
1090 s.set_required("path", true);
1091 s.set_required("reason", true);
1092 s
1093 },
1094 },
1095 McpOnlyTool {
1096 name: "secrets_propose_metadata".into(),
1097 description: "Suggest metadata edits for an existing ADR-020 \
1098 path. The dialog renders the manifest's current values as the \
1099 diff baseline (read straight from the index — agent strings \
1100 never replace trusted fields, mitigating prompt-injection). \
1101 The user picks which proposed fields to accept. Reuses \
1102 `secrets_poll_status` for status. Pending requests expire 5 \
1103 minutes after issuance."
1104 .into(),
1105 input_schema: {
1106 let mut s = ToolSchema::new();
1107 s.add_property("path", PropertySchema::string("Full ADR-020 path to edit."));
1108 s.add_property(
1109 "fields",
1110 PropertySchema {
1111 schema_type: "object".into(),
1112 description: Some(
1113 "Proposed field overrides (description, retrieval_url, \
1114 rotate_every_days, expires_at, pattern_id). Omitted \
1115 fields are not proposed for change."
1116 .into(),
1117 ),
1118 ..Default::default()
1119 },
1120 );
1121 s.set_required("path", true);
1122 s.set_required("fields", true);
1123 s
1124 },
1125 },
1126 McpOnlyTool {
1127 name: "secrets_propose_new_path".into(),
1128 description: "Suggest registering a new secret at the given path. \
1129 The dialog opens with the suggested path editable and the \
1130 proposed metadata visible in a diff column for review. The \
1131 user has the final say on the path and the metadata that \
1132 lands in the manifest. Reuses `secrets_poll_status` for status. \
1133 Pending requests expire 5 minutes after issuance."
1134 .into(),
1135 input_schema: {
1136 let mut s = ToolSchema::new();
1137 s.add_property(
1138 "suggested_path",
1139 PropertySchema::string("Suggested ADR-020 path; user may edit."),
1140 );
1141 s.add_property(
1142 "metadata",
1143 PropertySchema {
1144 schema_type: "object".into(),
1145 description: Some("Proposed metadata fields for the new entry.".into()),
1146 ..Default::default()
1147 },
1148 );
1149 s.set_required("suggested_path", true);
1150 s.set_required("metadata", true);
1151 s
1152 },
1153 },
1154 ]
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use super::*;
1160
1161 #[test]
1162 fn test_base_definitions_count() {
1163 let tools = base_tool_definitions();
1164 assert_eq!(tools.len(), 54);
1165 }
1166
1167 #[test]
1168 fn test_all_tools_have_names() {
1169 for tool in base_tool_definitions() {
1170 assert!(!tool.name.is_empty());
1171 assert!(!tool.description.is_empty());
1172 }
1173 }
1174
1175 #[test]
1176 fn test_tool_categories() {
1177 let tools = base_tool_definitions();
1178
1179 let issue_tracker_tools = [
1180 "get_issues",
1181 "get_issue",
1182 "get_issue_comments",
1183 "get_issue_relations",
1184 "create_issue",
1185 "update_issue",
1186 "add_issue_comment",
1187 "get_available_statuses",
1188 "get_users",
1189 "link_issues",
1190 "unlink_issues",
1191 "get_assets",
1192 "upload_asset",
1193 "download_asset",
1194 "delete_asset",
1195 "list_project_versions",
1196 "upsert_project_version",
1197 "get_board_sprints",
1198 "assign_to_sprint",
1199 "get_custom_fields",
1200 ];
1201 let git_repository_tools = [
1202 "get_merge_requests",
1203 "get_merge_request",
1204 "get_merge_request_discussions",
1205 "get_merge_request_diffs",
1206 "create_merge_request",
1207 "create_merge_request_comment",
1208 "update_merge_request",
1209 "get_pipeline",
1210 "get_job_logs",
1211 ];
1212 let epics_tools = ["get_epics", "create_epic", "update_epic"];
1213 let meeting_notes_tools = [
1214 "get_meeting_notes",
1215 "get_meeting_transcript",
1216 "search_meeting_notes",
1217 ];
1218 let knowledge_base_tools = [
1219 "get_knowledge_base_spaces",
1220 "list_knowledge_base_pages",
1221 "get_knowledge_base_page",
1222 "create_knowledge_base_page",
1223 "update_knowledge_base_page",
1224 "search_knowledge_base",
1225 ];
1226 let messenger_tools = [
1227 "get_messenger_chats",
1228 "get_chat_messages",
1229 "search_chat_messages",
1230 "send_message",
1231 ];
1232 let jira_structure_tools = [
1233 "get_structures",
1234 "get_structure_forest",
1235 "add_structure_rows",
1236 "move_structure_rows",
1237 "remove_structure_row",
1238 "get_structure_values",
1239 "get_structure_views",
1240 "save_structure_view",
1241 "create_structure",
1242 ];
1243
1244 for tool in &tools {
1245 if issue_tracker_tools.contains(&tool.name.as_str()) {
1246 assert_eq!(
1247 tool.category,
1248 ToolCategory::IssueTracker,
1249 "tool {} should be IssueTracker",
1250 tool.name
1251 );
1252 } else if git_repository_tools.contains(&tool.name.as_str()) {
1253 assert_eq!(
1254 tool.category,
1255 ToolCategory::GitRepository,
1256 "tool {} should be GitRepository",
1257 tool.name
1258 );
1259 } else if epics_tools.contains(&tool.name.as_str()) {
1260 assert_eq!(
1261 tool.category,
1262 ToolCategory::Epics,
1263 "tool {} should be Epics",
1264 tool.name
1265 );
1266 } else if meeting_notes_tools.contains(&tool.name.as_str()) {
1267 assert_eq!(
1268 tool.category,
1269 ToolCategory::MeetingNotes,
1270 "tool {} should be MeetingNotes",
1271 tool.name
1272 );
1273 } else if knowledge_base_tools.contains(&tool.name.as_str()) {
1274 assert_eq!(
1275 tool.category,
1276 ToolCategory::KnowledgeBase,
1277 "tool {} should be KnowledgeBase",
1278 tool.name
1279 );
1280 } else if messenger_tools.contains(&tool.name.as_str()) {
1281 assert_eq!(
1282 tool.category,
1283 ToolCategory::Messenger,
1284 "tool {} should be Messenger",
1285 tool.name
1286 );
1287 } else if jira_structure_tools.contains(&tool.name.as_str()) {
1288 assert_eq!(
1289 tool.category,
1290 ToolCategory::JiraStructure,
1291 "tool {} should be JiraStructure",
1292 tool.name
1293 );
1294 } else {
1295 panic!("tool {} has no expected category mapping", tool.name);
1296 }
1297 }
1298 }
1299
1300 #[test]
1301 fn test_required_params() {
1302 let tools = base_tool_definitions();
1303 let get_issue = tools.iter().find(|t| t.name == "get_issue").unwrap();
1304 assert!(get_issue.input_schema.required.contains(&"key".to_string()));
1305
1306 let create_mr = tools
1307 .iter()
1308 .find(|t| t.name == "create_merge_request")
1309 .unwrap();
1310 assert!(
1311 create_mr
1312 .input_schema
1313 .required
1314 .contains(&"title".to_string())
1315 );
1316 assert!(
1317 create_mr
1318 .input_schema
1319 .required
1320 .contains(&"source_branch".to_string())
1321 );
1322 }
1323
1324 #[test]
1327 fn test_tool_definition_serializes_to_json() {
1328 let tools = base_tool_definitions();
1329 let tool = &tools[0]; let json = serde_json::to_string(tool).unwrap();
1331
1332 assert!(json.contains("\"name\":\"get_issues\""));
1333 assert!(json.contains("\"description\""));
1334 assert!(json.contains("\"category\""));
1335 assert!(json.contains("\"input_schema\""));
1336
1337 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1339 assert_eq!(value["name"], "get_issues");
1340 }
1341
1342 #[test]
1343 fn test_all_tool_definitions_serialize() {
1344 let tools = base_tool_definitions();
1345 for tool in &tools {
1346 let json = serde_json::to_string(tool);
1347 assert!(
1348 json.is_ok(),
1349 "tool '{}' failed to serialize: {:?}",
1350 tool.name,
1351 json.err()
1352 );
1353 }
1354 }
1355
1356 #[test]
1357 fn test_tool_definition_json_contains_properties() {
1358 let tools = base_tool_definitions();
1359 let get_issues = tools.iter().find(|t| t.name == "get_issues").unwrap();
1360 let json = serde_json::to_string_pretty(get_issues).unwrap();
1361
1362 assert!(json.contains("state"));
1364 assert!(json.contains("search"));
1365 assert!(json.contains("labels"));
1366 assert!(json.contains("assignee"));
1367 assert!(json.contains("limit"));
1368 assert!(json.contains("projectKey"));
1369 assert!(json.contains("nativeQuery"));
1370 }
1371
1372 #[test]
1373 fn test_tool_definition_required_fields_in_json() {
1374 let tools = base_tool_definitions();
1375 let add_comment = tools
1376 .iter()
1377 .find(|t| t.name == "add_issue_comment")
1378 .unwrap();
1379 let json_val: serde_json::Value = serde_json::to_value(add_comment).unwrap();
1380
1381 let required = json_val["input_schema"]["required"]
1382 .as_array()
1383 .expect("required should be an array");
1384 let required_strs: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
1385 assert!(required_strs.contains(&"key"));
1386 assert!(required_strs.contains(&"body"));
1387 }
1388
1389 #[test]
1390 fn test_tool_names_are_unique() {
1391 let tools = base_tool_definitions();
1392 let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1393 let original_len = names.len();
1394 names.sort();
1395 names.dedup();
1396 assert_eq!(names.len(), original_len, "tool names should all be unique");
1397 }
1398
1399 #[test]
1400 fn test_link_issues_required_params() {
1401 let tools = base_tool_definitions();
1402 let link = tools.iter().find(|t| t.name == "link_issues").unwrap();
1403 assert!(
1404 link.input_schema
1405 .required
1406 .contains(&"sourceIssueKey".to_string())
1407 );
1408 assert!(
1409 link.input_schema
1410 .required
1411 .contains(&"targetIssueKey".to_string())
1412 );
1413 assert!(link.input_schema.required.contains(&"linkType".to_string()));
1414 }
1415
1416 #[test]
1417 fn test_get_available_statuses_has_empty_schema() {
1418 let tools = base_tool_definitions();
1419 let statuses = tools
1420 .iter()
1421 .find(|t| t.name == "get_available_statuses")
1422 .unwrap();
1423 assert!(statuses.input_schema.required.is_empty());
1424 assert!(statuses.input_schema.properties.is_empty());
1425 }
1426
1427 #[test]
1432 fn test_get_meeting_transcript_exposes_pagination_and_filters() {
1433 let tools = base_tool_definitions();
1434 let t = tools
1435 .iter()
1436 .find(|t| t.name == "get_meeting_transcript")
1437 .expect("get_meeting_transcript tool must exist");
1438 assert_eq!(t.input_schema.required, vec!["meeting_id".to_string()]);
1440 for prop in [
1443 "meeting_id",
1444 "offset",
1445 "limit",
1446 "speaker_filter",
1447 "search_text",
1448 "format",
1449 ] {
1450 assert!(
1451 t.input_schema.properties.contains_key(prop),
1452 "schema must advertise `{prop}` so MCP agents can use it"
1453 );
1454 }
1455 }
1456
1457 #[test]
1458 fn test_epic_tools_exist() {
1459 let tools = base_tool_definitions();
1460 let epic_names: Vec<&str> = tools
1461 .iter()
1462 .filter(|t| t.category == ToolCategory::Epics)
1463 .map(|t| t.name.as_str())
1464 .collect();
1465 assert!(epic_names.contains(&"get_epics"));
1466 assert!(epic_names.contains(&"create_epic"));
1467 assert!(epic_names.contains(&"update_epic"));
1468 }
1469}