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 to return (default: all)", Some(1.0), None));
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 ToolDefinition {
259 name: "run_pipeline_job".into(),
260 description: "Run an existing manual GitLab CI job after verifying that it belongs to the supplied pipeline and is still manual.".into(),
261 category: ToolCategory::GitRepository,
262 input_schema: {
263 let mut s = ToolSchema::new();
264 s.add_property(
265 "pipelineId",
266 PropertySchema::string("Pipeline ID returned by get_pipeline"),
267 );
268 s.add_property(
269 "jobId",
270 PropertySchema::string("Manual job ID returned by get_pipeline"),
271 );
272 s.add_property(
273 "variables",
274 PropertySchema::object(
275 "Optional CI variables as an object whose values are strings",
276 ),
277 );
278 s.add_property(
279 "jobInputs",
280 PropertySchema::object(
281 "Optional typed GitLab job inputs as an object of JSON values",
282 ),
283 );
284 s.set_required("pipelineId", true);
285 s.set_required("jobId", true);
286 s
287 },
288 },
289
290 ToolDefinition {
292 name: "get_available_statuses".into(),
293 description: "Get available statuses for the issue tracker.".into(),
294 category: ToolCategory::IssueTracker,
295 input_schema: ToolSchema::new(),
296 },
297 ToolDefinition {
298 name: "get_users".into(),
299 description: "Get users from the issue tracker (Jira). Search by name, project, or ID.".into(),
300 category: ToolCategory::IssueTracker,
301 input_schema: {
302 let mut s = ToolSchema::new();
303 s.add_property("userId", PropertySchema::string("Get specific user by ID"));
304 s.add_property("projectKey", PropertySchema::string("Get assignable users for project"));
305 s.add_property("search", PropertySchema::string("Search by name or email"));
306 s.add_property("maxResults", PropertySchema::integer("Max results (default: 50)", Some(1.0), Some(1000.0)));
307 s
308 },
309 },
310 ToolDefinition {
311 name: "link_issues".into(),
312 description: "Link two issues together (blocks, relates_to, etc.).".into(),
313 category: ToolCategory::IssueTracker,
314 input_schema: {
315 let mut s = ToolSchema::new();
316 s.add_property("sourceIssueKey", PropertySchema::string("Source issue key"));
317 s.add_property("targetIssueKey", PropertySchema::string("Target issue key"));
318 s.add_property("linkType", PropertySchema::string(
319 "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.",
320 ));
321 s.set_required("sourceIssueKey", true);
322 s.set_required("targetIssueKey", true);
323 s.set_required("linkType", true);
324 s
325 },
326 },
327 ToolDefinition {
328 name: "unlink_issues".into(),
329 description: "Remove a link between two issues.".into(),
330 category: ToolCategory::IssueTracker,
331 input_schema: {
332 let mut s = ToolSchema::new();
333 s.add_property("sourceIssueKey", PropertySchema::string("Source issue key"));
334 s.add_property("targetIssueKey", PropertySchema::string("Target issue key"));
335 s.add_property("linkType", PropertySchema::string(
336 "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.",
337 ));
338 s.set_required("sourceIssueKey", true);
339 s.set_required("targetIssueKey", true);
340 s.set_required("linkType", true);
341 s
342 },
343 },
344 ToolDefinition {
345 name: "get_epics".into(),
346 description: "Get epics (high-level tasks) from the issue tracker.".into(),
347 category: ToolCategory::Epics,
348 input_schema: {
349 let mut s = ToolSchema::new();
350 s.add_property("search", PropertySchema::string("Search in epic title"));
351 s.add_property("limit", PropertySchema::integer("Max results (default: 50)", Some(1.0), Some(100.0)));
352 s.add_property("offset", PropertySchema::integer("Skip N results (default: 0)", Some(0.0), None));
353 s
354 },
355 },
356 ToolDefinition {
357 name: "create_epic".into(),
358 description: "Create a new epic.".into(),
359 category: ToolCategory::Epics,
360 input_schema: {
361 let mut s = ToolSchema::new();
362 s.add_property("title", PropertySchema::string("Epic title"));
363 s.add_property("description", PropertySchema::string("Epic description"));
364 s.set_required("title", true);
365 s
366 },
367 },
368 ToolDefinition {
369 name: "update_epic".into(),
370 description: "Update an existing epic.".into(),
371 category: ToolCategory::Epics,
372 input_schema: {
373 let mut s = ToolSchema::new();
374 s.add_property("epicKey", PropertySchema::string("Epic key (e.g., 'CU-abc', 'DEV-123')"));
375 s.add_property("title", PropertySchema::string("New title"));
376 s.add_property("description", PropertySchema::string("New description"));
377 s.add_property("state", PropertySchema::string("New epic state (generic open/closed). For ClickUp custom statuses use `status`."));
378 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`."));
379 s.add_property("goalId", PropertySchema::string("Goal ID (G1-G9) to associate with the epic"));
380 s.add_property("priority", PropertySchema::string("New priority (urgent/high/normal/low)"));
381 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to set"));
382 s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "Assignees to set"));
383 s.set_required("epicKey", true);
384 s
385 },
386 },
387 ToolDefinition {
389 name: "get_meeting_notes".into(),
390 description: "Get meeting notes and transcripts with optional filters (date range, participants, host).".into(),
391 category: ToolCategory::MeetingNotes,
392 input_schema: {
393 let mut s = ToolSchema::new();
394 s.add_property("from_date", PropertySchema::string("Filter from date (ISO 8601, e.g., '2025-01-01T00:00:00Z')"));
395 s.add_property("to_date", PropertySchema::string("Filter to date (ISO 8601)"));
396 s.add_property("participants", PropertySchema::array(PropertySchema::string("email"), "Filter by participant email addresses"));
397 s.add_property("host_email", PropertySchema::string("Filter by host email"));
398 s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 50)", Some(1.0), Some(50.0)));
399 s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
400 s
401 },
402 },
403 ToolDefinition {
404 name: "get_meeting_transcript".into(),
405 description: "Get the full transcript for a meeting. Returns speaker-attributed sentences with timestamps.".into(),
406 category: ToolCategory::MeetingNotes,
407 input_schema: {
408 let mut s = ToolSchema::new();
409 s.add_property("meeting_id", PropertySchema::string("Meeting ID from get_meeting_notes"));
410 s.set_required("meeting_id", true);
411 s.add_property("offset", PropertySchema::integer("Number of sentences to skip for pagination (default: 0)", Some(0.0), None));
418 s.add_property("limit", PropertySchema::integer("Maximum number of sentences (default: 50, max: 500)", Some(1.0), Some(500.0)));
419 s.add_property("speaker_filter", PropertySchema::string("Filter sentences by speaker name (case-insensitive substring match)"));
420 s.add_property("search_text", PropertySchema::string("Search sentence text (case-insensitive substring match)"));
421 s.add_property("format", PropertySchema::string_enum(&["flat", "grouped"], "Output format: 'flat' (default, per-sentence) or 'grouped' (same-speaker runs collapsed)"));
422 s
423 },
424 },
425 ToolDefinition {
426 name: "search_meeting_notes".into(),
427 description: "Search across meetings by keywords, topics, or action items, with optional filters (date range, participants, host).".into(),
428 category: ToolCategory::MeetingNotes,
429 input_schema: {
430 let mut s = ToolSchema::new();
431 s.add_property("query", PropertySchema::string("Search query"));
432 s.add_property("from_date", PropertySchema::string("Filter from date (ISO 8601)"));
433 s.add_property("to_date", PropertySchema::string("Filter to date (ISO 8601)"));
434 s.add_property("participants", PropertySchema::array(PropertySchema::string("email"), "Filter by participant email addresses"));
435 s.add_property("host_email", PropertySchema::string("Filter by host email"));
436 s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 50)", Some(1.0), Some(50.0)));
437 s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
438 s.set_required("query", true);
439 s
440 },
441 },
442 ToolDefinition {
444 name: "get_knowledge_base_spaces".into(),
445 description: "List available knowledge base spaces.".into(),
446 category: ToolCategory::KnowledgeBase,
447 input_schema: ToolSchema::new(),
448 },
449 ToolDefinition {
450 name: "list_knowledge_base_pages".into(),
451 description: "List pages in a knowledge base space with pagination.".into(),
452 category: ToolCategory::KnowledgeBase,
453 input_schema: {
454 let mut s = ToolSchema::new();
455 s.add_property("spaceKey", PropertySchema::string("Space key to list pages from"));
456 s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 25)", Some(1.0), Some(100.0)));
457 s.add_property("offset", PropertySchema::integer("Number of results to skip when offset pagination is supported", Some(0.0), None));
458 s.add_property("cursor", PropertySchema::string("Provider pagination cursor/token"));
459 s.add_property("search", PropertySchema::string("Optional free-text title/content filter"));
460 s.add_property("parentId", PropertySchema::string("Optional ancestor/parent page ID to scope the listing"));
461 s.set_required("spaceKey", true);
462 s
463 },
464 },
465 ToolDefinition {
466 name: "get_knowledge_base_page".into(),
467 description: "Get a knowledge base page with content, labels, and ancestors.".into(),
468 category: ToolCategory::KnowledgeBase,
469 input_schema: {
470 let mut s = ToolSchema::new();
471 s.add_property("pageId", PropertySchema::string("Knowledge base page ID"));
472 s.set_required("pageId", true);
473 s
474 },
475 },
476 ToolDefinition {
477 name: "create_knowledge_base_page".into(),
478 description: "Create a knowledge base page in a space.".into(),
479 category: ToolCategory::KnowledgeBase,
480 input_schema: {
481 let mut s = ToolSchema::new();
482 s.add_property("spaceKey", PropertySchema::string("Target space key"));
483 s.add_property("title", PropertySchema::string("Page title"));
484 s.add_property("content", PropertySchema::string("Page body content"));
485 s.add_property("contentType", PropertySchema::string_enum(&["markdown", "html", "storage"], "Content representation supplied by the caller"));
486 s.add_property("parentId", PropertySchema::string("Optional parent page ID"));
487 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to set on the page"));
488 s.set_required("spaceKey", true);
489 s.set_required("title", true);
490 s.set_required("content", true);
491 s
492 },
493 },
494 ToolDefinition {
495 name: "update_knowledge_base_page".into(),
496 description: "Update a knowledge base page title, content, metadata, or labels.".into(),
497 category: ToolCategory::KnowledgeBase,
498 input_schema: {
499 let mut s = ToolSchema::new();
500 s.add_property("pageId", PropertySchema::string("Knowledge base page ID"));
501 s.add_property("title", PropertySchema::string("New page title"));
502 s.add_property("content", PropertySchema::string("New page body content"));
503 s.add_property("contentType", PropertySchema::string_enum(&["markdown", "html", "storage"], "Content representation supplied by the caller"));
504 s.add_property("version", PropertySchema::integer("Expected current version for optimistic locking", Some(1.0), None));
505 s.add_property("parentId", PropertySchema::string("Optional new parent page ID"));
506 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to replace on the page"));
507 s.set_required("pageId", true);
508 s
509 },
510 },
511 ToolDefinition {
512 name: "search_knowledge_base".into(),
513 description: "Search knowledge base pages across spaces using free text or provider-native syntax such as CQL.".into(),
514 category: ToolCategory::KnowledgeBase,
515 input_schema: {
516 let mut s = ToolSchema::new();
517 s.add_property("query", PropertySchema::string("Free-text query or provider-native search expression"));
518 s.add_property("spaceKey", PropertySchema::string("Restrict search to a specific space key"));
519 s.add_property("cursor", PropertySchema::string("Provider pagination cursor/token"));
520 s.add_property("limit", PropertySchema::integer("Maximum number of matches to return", Some(1.0), Some(100.0)));
521 s.add_property("rawQuery", PropertySchema::boolean("Whether `query` should be treated as raw provider-native syntax"));
522 s.set_required("query", true);
523 s
524 },
525 },
526 ToolDefinition {
527 name: "update_merge_request".into(),
528 description: "Update a merge request / pull request (title, description, state, labels, draft).".into(),
529 category: ToolCategory::GitRepository,
530 input_schema: {
531 let mut s = ToolSchema::new();
532 s.add_property("key", PropertySchema::string("MR key (e.g. 'mr#1', 'pr#42')"));
533 s.add_property("title", PropertySchema::string("New title"));
534 s.add_property("description", PropertySchema::string("New description / body (supports markdown)"));
535 s.add_property("state", PropertySchema::string_enum(&["close", "reopen"], "Change MR state"));
536 s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "New labels (replaces existing)"));
537 s.set_required("key", true);
538 s
539 },
540 },
541 ToolDefinition {
545 name: "get_assets".into(),
546 description: "List file attachments for an issue or merge request.".into(),
547 category: ToolCategory::IssueTracker,
548 input_schema: {
549 let mut s = ToolSchema::new();
550 s.add_property("context_type", PropertySchema::string_enum(&["issue", "mr"], "Context type: 'issue' or 'mr' (merge request / pull request)"));
551 s.add_property("key", PropertySchema::string("Issue key (e.g. 'DEV-123', 'gitlab#42') or MR key (e.g. 'mr#42', 'pr#42')"));
552 s.set_required("context_type", true);
553 s.set_required("key", true);
554 s
555 },
556 },
557 ToolDefinition {
558 name: "upload_asset".into(),
559 description: "Upload a file attachment to an issue. Returns the download URL.".into(),
560 category: ToolCategory::IssueTracker,
561 input_schema: {
562 let mut s = ToolSchema::new();
563 s.add_property("context_type", PropertySchema::string_enum(&["issue"], "Context type (currently only 'issue' is supported for uploads)"));
564 s.add_property("key", PropertySchema::string("Issue key (e.g. 'DEV-123')"));
565 s.add_property("filename", PropertySchema::string("Original filename (e.g. 'screenshot.png')"));
566 s.add_property("fileData", PropertySchema::string("Base64-encoded file content"));
567 s.set_required("context_type", true);
568 s.set_required("key", true);
569 s.set_required("filename", true);
570 s.set_required("fileData", true);
571 s
572 },
573 },
574 ToolDefinition {
575 name: "download_asset".into(),
576 description: "Download a file attachment to local cache. Returns local file path when cache is available, base64-encoded content as fallback.".into(),
577 category: ToolCategory::IssueTracker,
578 input_schema: {
579 let mut s = ToolSchema::new();
580 s.add_property("context_type", PropertySchema::string_enum(&["issue", "mr"], "Context type: 'issue' or 'mr'"));
581 s.add_property("key", PropertySchema::string("Issue key or MR key"));
582 s.add_property("asset_id", PropertySchema::string("Asset identifier from get_assets response"));
583 s.set_required("context_type", true);
584 s.set_required("key", true);
585 s.set_required("asset_id", true);
586 s
587 },
588 },
589 ToolDefinition {
593 name: "get_messenger_chats".into(),
594 description: "List available messenger chats, channels, groups, or direct messages.".into(),
595 category: ToolCategory::Messenger,
596 input_schema: {
597 let mut s = ToolSchema::new();
598 s.add_property("search", PropertySchema::string("Optional chat name search"));
599 s.add_property("chat_type", PropertySchema::string_enum(&["direct", "group", "channel"], "Optional chat type filter"));
600 s.add_property("limit", PropertySchema::integer("Maximum number of chats to return", Some(1.0), Some(1000.0)));
601 s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
602 s.add_property("include_inactive", PropertySchema::boolean("Include archived or inactive chats"));
603 s
604 },
605 },
606 ToolDefinition {
607 name: "get_chat_messages".into(),
608 description: "Get message history for a chat or fetch replies for a specific thread.".into(),
609 category: ToolCategory::Messenger,
610 input_schema: {
611 let mut s = ToolSchema::new();
612 s.add_property("chat_id", PropertySchema::string("Messenger chat ID"));
613 s.add_property("limit", PropertySchema::integer("Maximum number of messages to return", Some(1.0), Some(1000.0)));
614 s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
615 s.add_property("thread_id", PropertySchema::string("Thread identifier to fetch replies for"));
616 s.add_property("since", PropertySchema::string("Only include messages after this provider timestamp"));
617 s.add_property("until", PropertySchema::string("Only include messages before this provider timestamp"));
618 s.set_required("chat_id", true);
619 s
620 },
621 },
622 ToolDefinition {
623 name: "search_chat_messages".into(),
624 description: "Search messages across accessible chats or within a specific chat.".into(),
625 category: ToolCategory::Messenger,
626 input_schema: {
627 let mut s = ToolSchema::new();
628 s.add_property("query", PropertySchema::string("Message search query"));
629 s.add_property("chat_id", PropertySchema::string("Optional chat ID to scope the search"));
630 s.add_property("limit", PropertySchema::integer("Maximum number of matches to return", Some(1.0), Some(1000.0)));
631 s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
632 s.add_property("since", PropertySchema::string("Only include messages after this provider timestamp"));
633 s.add_property("until", PropertySchema::string("Only include messages before this provider timestamp"));
634 s.set_required("query", true);
635 s
636 },
637 },
638 ToolDefinition {
639 name: "send_message".into(),
640 description: "Send a message to a chat or as a threaded reply.".into(),
641 category: ToolCategory::Messenger,
642 input_schema: {
643 let mut s = ToolSchema::new();
644 s.add_property("chat_id", PropertySchema::string("Messenger chat ID"));
645 s.add_property("text", PropertySchema::string("Message body"));
646 s.add_property("thread_id", PropertySchema::string("Thread identifier to post as a threaded reply"));
647 s.add_property("reply_to_id", PropertySchema::string("Direct parent message ID when supported"));
648 s.set_required("chat_id", true);
649 s.set_required("text", true);
650 s
651 },
652 },
653 ToolDefinition {
654 name: "delete_asset".into(),
655 description: "Delete a file attachment from an issue. Not all providers support this — check asset_capabilities first.".into(),
656 category: ToolCategory::IssueTracker,
657 input_schema: {
658 let mut s = ToolSchema::new();
659 s.add_property("key", PropertySchema::string("Issue key (e.g. 'PROJ-123')"));
660 s.add_property("asset_id", PropertySchema::string("Asset identifier to delete"));
661 s.set_required("key", true);
662 s.set_required("asset_id", true);
663 s
664 },
665 },
666 ToolDefinition {
668 name: "get_structures".into(),
669 description: "List all available Jira Structures. Returns structure ID, name, and description. Requires Jira with Structure plugin.".into(),
670 category: ToolCategory::JiraStructure,
671 input_schema: ToolSchema::new(),
672 },
673 ToolDefinition {
674 name: "get_structure_forest".into(),
675 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(),
676 category: ToolCategory::JiraStructure,
677 input_schema: {
678 let mut s = ToolSchema::new();
679 s.add_property("structureId", PropertySchema::integer("Structure ID. Use get_structures to find it.", None, None));
680 s.add_property("offset", PropertySchema::integer("Offset for pagination (default: 0)", Some(0.0), None));
681 s.add_property("limit", PropertySchema::integer("Max rows to return (default: 200)", Some(1.0), Some(10000.0)));
682 s.set_required("structureId", true);
683 s
684 },
685 },
686 ToolDefinition {
687 name: "add_structure_rows".into(),
688 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(),
689 category: ToolCategory::JiraStructure,
690 input_schema: {
691 let mut s = ToolSchema::new();
692 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
693 s.add_property("items", PropertySchema::array(
694 PropertySchema::string("Item: Jira issue key (e.g. 'PROJ-123') or JSON {\"itemId\":\"PROJ-123\",\"itemType\":\"issue\"}"),
695 "Items to add",
696 ));
697 s.add_property("under", PropertySchema::integer("Parent row ID — items become children of this row", None, None));
698 s.add_property("after", PropertySchema::integer("Sibling row ID — items placed after this row", None, None));
699 s.add_property("forestVersion", PropertySchema::integer("Forest version for optimistic locking (from get_structure_forest)", None, None));
700 s.set_required("structureId", true);
701 s.set_required("items", true);
702 s
703 },
704 },
705 ToolDefinition {
706 name: "move_structure_rows".into(),
707 description: "Move rows within a Jira Structure hierarchy. Specify new position with under (new parent) and/or after (sibling).".into(),
708 category: ToolCategory::JiraStructure,
709 input_schema: {
710 let mut s = ToolSchema::new();
711 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
712 s.add_property("rowIds", PropertySchema::array(
713 PropertySchema::integer("Row ID", None, None),
714 "Row IDs to move (from get_structure_forest)",
715 ));
716 s.add_property("under", PropertySchema::integer("New parent row ID", None, None));
717 s.add_property("after", PropertySchema::integer("Sibling row ID to place after", None, None));
718 s.add_property("forestVersion", PropertySchema::integer("Forest version for optimistic locking", None, None));
719 s.set_required("structureId", true);
720 s.set_required("rowIds", true);
721 s
722 },
723 },
724 ToolDefinition {
725 name: "remove_structure_row".into(),
726 description: "Remove a row from a Jira Structure. Only removes from the structure hierarchy — the underlying Jira issue is NOT deleted.".into(),
727 category: ToolCategory::JiraStructure,
728 input_schema: {
729 let mut s = ToolSchema::new();
730 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
731 s.add_property("rowId", PropertySchema::integer("Row ID to remove (from get_structure_forest)", None, None));
732 s.set_required("structureId", true);
733 s.set_required("rowId", true);
734 s
735 },
736 },
737 ToolDefinition {
738 name: "get_structure_values".into(),
739 description: "Read column values (including Expr formulas like SUM, PROGRESS, COUNT) for specific rows in a Jira Structure. Values are computed server-side.".into(),
740 category: ToolCategory::JiraStructure,
741 input_schema: {
742 let mut s = ToolSchema::new();
743 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
744 s.add_property("rows", PropertySchema::array(
745 PropertySchema::integer("Row ID", None, None),
746 "Row IDs to read values for",
747 ));
748 s.add_property("columns", PropertySchema::array(
749 PropertySchema::string("Column spec: field name (e.g. 'summary'), or JSON {\"field\":\"status\"} or {\"formula\":\"SUM(\\\"Story Points\\\")\"}"),
750 "Columns to read",
751 ));
752 s.set_required("structureId", true);
753 s.set_required("rows", true);
754 s.set_required("columns", true);
755 s
756 },
757 },
758 ToolDefinition {
759 name: "get_structure_views".into(),
760 description: "Get views for a Jira Structure. Without viewId: lists all views. With viewId: returns full view configuration (columns, grouping, sorting, filter).".into(),
761 category: ToolCategory::JiraStructure,
762 input_schema: {
763 let mut s = ToolSchema::new();
764 s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
765 s.add_property("viewId", PropertySchema::integer("View ID for full config (optional — omit to list all views)", None, None));
766 s.set_required("structureId", true);
767 s
768 },
769 },
770 ToolDefinition {
771 name: "save_structure_view".into(),
772 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(),
773 category: ToolCategory::JiraStructure,
774 input_schema: {
775 let mut s = ToolSchema::new();
776 s.add_property("id", PropertySchema::integer("View ID to update (omit to create new)", None, None));
777 s.add_property("structureId", PropertySchema::integer("Structure ID this view belongs to", None, None));
778 s.add_property("name", PropertySchema::string("View name"));
779 s.add_property("columns", PropertySchema::array(
780 PropertySchema::string("Column spec: JSON {\"field\":\"summary\"} or {\"formula\":\"SUM(\\\"Story Points\\\")\",\"width\":100}"),
781 "Column definitions",
782 ));
783 s.add_property("groupBy", PropertySchema::string("Field name to group by"));
784 s.add_property("sortBy", PropertySchema::string("Field name to sort by"));
785 s.add_property("filter", PropertySchema::string("JQL filter expression"));
786 s.set_required("structureId", true);
787 s.set_required("name", true);
788 s
789 },
790 },
791 ToolDefinition {
792 name: "create_structure".into(),
793 description: "Create a new Jira Structure. After creation, use add_structure_rows to populate and save_structure_view to configure columns.".into(),
794 category: ToolCategory::JiraStructure,
795 input_schema: {
796 let mut s = ToolSchema::new();
797 s.add_property("name", PropertySchema::string("Structure name"));
798 s.add_property("description", PropertySchema::string("Structure description"));
799 s.set_required("name", true);
800 s
801 },
802 },
803
804 ToolDefinition {
809 name: "list_project_versions".into(),
810 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(),
811 category: ToolCategory::IssueTracker,
812 input_schema: {
813 let mut s = ToolSchema::new();
814 s.add_property("project", PropertySchema::string("Jira project key (e.g., \"PROJ\"). Defaults to the configured project."));
815 s.add_property("released", PropertySchema::string_enum(&["true", "false", "all"], "Filter by release state: \"true\" → only released, \"false\" → only unreleased, \"all\" → both (default: \"all\")"));
816 s.add_property("archived", PropertySchema::string_enum(&["true", "false", "all"], "Filter by archived flag (default: \"false\" — hides archival noise)"));
817 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)));
818 s.add_property("includeIssueCount", PropertySchema::boolean("Fetch issue counts per version via Cloud `?expand=issuesstatus` (default: false). Adds latency on large projects."));
819 s
820 },
821 },
822 ToolDefinition {
823 name: "upsert_project_version".into(),
824 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(),
825 category: ToolCategory::IssueTracker,
826 input_schema: {
827 let mut s = ToolSchema::new();
828 s.add_property("project", PropertySchema::string("Jira project key (e.g., \"PROJ\"). Defaults to the configured project."));
829 s.add_property("name", PropertySchema::string("Version name — both the lookup key and, on create, the value (e.g., \"3.18.0\")."));
830 s.add_property("description", PropertySchema::string("Release notes / version description. Markdown-style text is preserved on Server/DC; Cloud accepts plain text."));
831 s.add_property("startDate", PropertySchema::string("Planned start date as ISO 8601 calendar date (`YYYY-MM-DD`)."));
832 s.add_property("releaseDate", PropertySchema::string("Planned or actual release date (`YYYY-MM-DD`)."));
833 s.add_property("released", PropertySchema::boolean("Mark released (true) / unreleased (false). Pair with `releaseDate` when closing a release."));
834 s.add_property("archived", PropertySchema::boolean("Archive (true) / unarchive (false) the version."));
835 s.set_required("name", true);
836 s
837 },
838 },
839 ToolDefinition {
843 name: "get_board_sprints".into(),
844 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(),
845 category: ToolCategory::IssueTracker,
846 input_schema: {
847 let mut s = ToolSchema::new();
848 s.add_property(
849 "boardId",
850 PropertySchema::integer(
851 "Numeric Jira board id. The Agile / Boards REST endpoint returns sprints scoped to one board — there is no global sprint list",
852 Some(0.0),
853 None,
854 ),
855 );
856 s.add_property(
857 "state",
858 PropertySchema::string_enum(
859 &["active", "future", "closed", "all"],
860 "Filter by sprint state. Default `all` returns every sprint on the board",
861 ),
862 );
863 s.set_required("boardId", true);
864 s
865 },
866 },
867 ToolDefinition {
868 name: "assign_to_sprint".into(),
869 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(),
870 category: ToolCategory::IssueTracker,
871 input_schema: {
872 let mut s = ToolSchema::new();
873 s.add_property(
874 "sprintId",
875 PropertySchema::integer(
876 "Numeric sprint id. Use `get_board_sprints` to discover ids on a board",
877 Some(0.0),
878 None,
879 ),
880 );
881 s.add_property(
882 "issueKeys",
883 PropertySchema::array(
884 PropertySchema::string("issue key (e.g., \"PROJ-1\")"),
885 "Issue keys to move onto the sprint. Must contain at least one key.",
886 ),
887 );
888 s.set_required("sprintId", true);
889 s.set_required("issueKeys", true);
890 s
891 },
892 },
893 ToolDefinition {
898 name: "get_custom_fields".into(),
899 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(),
900 category: ToolCategory::IssueTracker,
901 input_schema: {
902 let mut s = ToolSchema::new();
903 s.add_property(
904 "project",
905 PropertySchema::string(
906 "Optional project key. Reserved for providers that scope custom fields per project; ignored on Jira's global `/field` endpoint.",
907 ),
908 );
909 s.add_property(
910 "issueType",
911 PropertySchema::string(
912 "Optional issue type. Reserved for providers that scope custom fields per create-screen context.",
913 ),
914 );
915 s.add_property(
916 "search",
917 PropertySchema::string(
918 "Case-insensitive substring filter on the field name (e.g. `\"Epic\"` to find `Epic Link` and `Epic Name`).",
919 ),
920 );
921 s.add_property(
922 "limit",
923 PropertySchema::integer(
924 "Max fields to return after filtering (default 50). Sorted by name asc",
925 Some(1.0),
926 Some(200.0),
927 ),
928 );
929 s
930 },
931 },
932 ]
933}
934
935#[derive(Debug, Clone)]
941pub struct McpOnlyTool {
942 pub name: String,
943 pub description: String,
944 pub input_schema: ToolSchema,
945}
946
947pub fn mcp_only_tools() -> Vec<McpOnlyTool> {
949 vec![
950 McpOnlyTool {
951 name: "list_contexts".into(),
952 description: "List configured contexts and indicate the active context.".into(),
953 input_schema: ToolSchema::new(),
954 },
955 McpOnlyTool {
956 name: "use_context".into(),
957 description: "Switch active context at runtime.".into(),
958 input_schema: {
959 let mut s = ToolSchema::new();
960 s.add_property("name", PropertySchema::string("Context name to activate"));
961 s.set_required("name", true);
962 s
963 },
964 },
965 McpOnlyTool {
966 name: "get_current_context".into(),
967 description: "Get current active context name.".into(),
968 input_schema: ToolSchema::new(),
969 },
970 McpOnlyTool {
971 name: "secrets_list".into(),
972 description: "List secrets the active context's manifest declares. \
973 Returns metadata only — values are never included. \
974 Optional filter narrows by path substring, scope, status, or \
975 whether to include framework-internal paths."
976 .into(),
977 input_schema: {
978 let mut s = ToolSchema::new();
979 s.add_property(
980 "path_contains",
981 PropertySchema::string("Substring to match against the full ADR-020 path."),
982 );
983 s.add_property(
984 "scope",
985 PropertySchema::string(
986 "Exact match against the first path segment (e.g. team / personal).",
987 ),
988 );
989 s.add_property(
990 "status",
991 PropertySchema::string_enum(
992 &["registered", "expiring", "expired"],
993 "Lifecycle status filter computed from expires_at.",
994 ),
995 );
996 s.add_property(
997 "include_internal",
998 PropertySchema::boolean("Include framework-internal paths (default: false)."),
999 );
1000 s
1001 },
1002 },
1003 McpOnlyTool {
1004 name: "secrets_describe".into(),
1005 description: "Describe one secret by ADR-020 path. Returns the same \
1006 metadata fields as `secrets_list` plus description, retrieval URL, \
1007 rotation method, last rotated date, rotation cadence, and pattern \
1008 ID. The value is never returned."
1009 .into(),
1010 input_schema: {
1011 let mut s = ToolSchema::new();
1012 s.add_property(
1013 "path",
1014 PropertySchema::string("Full ADR-020 path to describe."),
1015 );
1016 s.set_required("path", true);
1017 s
1018 },
1019 },
1020 McpOnlyTool {
1021 name: "secrets_request_provision".into(),
1022 description: "Open the provisioning UI dialog for the given ADR-020 \
1023 path. The dialog hands the user-entered value directly to the \
1024 local daemon — the agent never sees it. Returns a `request_id` \
1025 that can be polled with `secrets_poll_status`. Mode defaults to \
1026 `provision`; pass `rotation` to surface the destructive-confirm \
1027 checkbox. Pending requests expire 5 minutes after issuance."
1028 .into(),
1029 input_schema: {
1030 let mut s = ToolSchema::new();
1031 s.add_property(
1032 "path",
1033 PropertySchema::string("Full ADR-020 path to provision."),
1034 );
1035 s.add_property(
1036 "mode",
1037 PropertySchema::string_enum(
1038 &["provision", "rotation"],
1039 "Dialog mode (default: provision).",
1040 ),
1041 );
1042 s.set_required("path", true);
1043 s
1044 },
1045 },
1046 McpOnlyTool {
1047 name: "secrets_poll_status".into(),
1048 description: "Poll a provisioning or rotation request issued by \
1049 `secrets_request_provision` / `secrets_request_rotation`. Returns \
1050 one of pending / ok / cancelled / expired / failed plus the \
1051 request's age in seconds and the path it was opened for."
1052 .into(),
1053 input_schema: {
1054 let mut s = ToolSchema::new();
1055 s.add_property(
1056 "request_id",
1057 PropertySchema::string(
1058 "Opaque id returned by request_provision / request_rotation.",
1059 ),
1060 );
1061 s.set_required("request_id", true);
1062 s
1063 },
1064 },
1065 McpOnlyTool {
1066 name: "secrets_request_rotation".into(),
1067 description: "Open the rotation UI dialog for the given ADR-020 \
1068 path. Same lifecycle as `secrets_request_provision` but the \
1069 dialog surfaces the destructive-confirm checkbox so the user \
1070 explicitly acknowledges that the existing value is being \
1071 overwritten. Reuses `secrets_poll_status` for status. Pending \
1072 requests expire 5 minutes after issuance."
1073 .into(),
1074 input_schema: {
1075 let mut s = ToolSchema::new();
1076 s.add_property(
1077 "path",
1078 PropertySchema::string("Full ADR-020 path to rotate."),
1079 );
1080 s.set_required("path", true);
1081 s
1082 },
1083 },
1084 McpOnlyTool {
1085 name: "secrets_request_use_approval".into(),
1086 description: "Open the use-approval dialog for an ADR-020 path \
1087 whose `approve_on_use` is set to `session` or `per-call`. \
1088 The agent supplies a short human-facing `reason` that the \
1089 dialog renders verbatim alongside the path; the user picks \
1090 `once`, `session`, or `denied`. Returns a `request_id` to \
1091 poll via `secrets_poll_status`. Pending requests expire 5 \
1092 minutes after issuance; `ttl_seconds` may shorten the \
1093 window but never extend it. The agent never sees the \
1094 secret — only whether the user approved its use."
1095 .into(),
1096 input_schema: {
1097 let mut s = ToolSchema::new();
1098 s.add_property(
1099 "path",
1100 PropertySchema::string("Full ADR-020 path the agent intends to resolve."),
1101 );
1102 s.add_property(
1103 "reason",
1104 PropertySchema::string(
1105 "Short human-facing reason rendered in the dialog \
1106 (e.g. 'pushing image to staging registry').",
1107 ),
1108 );
1109 s.add_property(
1110 "ttl_seconds",
1111 PropertySchema {
1112 schema_type: "integer".into(),
1113 description: Some(
1114 "Optional lifetime in seconds; capped at the \
1115 registry-wide TTL (5 minutes)."
1116 .into(),
1117 ),
1118 ..Default::default()
1119 },
1120 );
1121 s.set_required("path", true);
1122 s.set_required("reason", true);
1123 s
1124 },
1125 },
1126 McpOnlyTool {
1127 name: "secrets_propose_metadata".into(),
1128 description: "Suggest metadata edits for an existing ADR-020 \
1129 path. The dialog renders the manifest's current values as the \
1130 diff baseline (read straight from the index — agent strings \
1131 never replace trusted fields, mitigating prompt-injection). \
1132 The user picks which proposed fields to accept. Reuses \
1133 `secrets_poll_status` for status. Pending requests expire 5 \
1134 minutes after issuance."
1135 .into(),
1136 input_schema: {
1137 let mut s = ToolSchema::new();
1138 s.add_property("path", PropertySchema::string("Full ADR-020 path to edit."));
1139 s.add_property(
1140 "fields",
1141 PropertySchema {
1142 schema_type: "object".into(),
1143 description: Some(
1144 "Proposed field overrides (description, retrieval_url, \
1145 rotate_every_days, expires_at, pattern_id). Omitted \
1146 fields are not proposed for change."
1147 .into(),
1148 ),
1149 ..Default::default()
1150 },
1151 );
1152 s.set_required("path", true);
1153 s.set_required("fields", true);
1154 s
1155 },
1156 },
1157 McpOnlyTool {
1158 name: "secrets_propose_new_path".into(),
1159 description: "Suggest registering a new secret at the given path. \
1160 The dialog opens with the suggested path editable and the \
1161 proposed metadata visible in a diff column for review. The \
1162 user has the final say on the path and the metadata that \
1163 lands in the manifest. Reuses `secrets_poll_status` for status. \
1164 Pending requests expire 5 minutes after issuance."
1165 .into(),
1166 input_schema: {
1167 let mut s = ToolSchema::new();
1168 s.add_property(
1169 "suggested_path",
1170 PropertySchema::string("Suggested ADR-020 path; user may edit."),
1171 );
1172 s.add_property(
1173 "metadata",
1174 PropertySchema {
1175 schema_type: "object".into(),
1176 description: Some("Proposed metadata fields for the new entry.".into()),
1177 ..Default::default()
1178 },
1179 );
1180 s.set_required("suggested_path", true);
1181 s.set_required("metadata", true);
1182 s
1183 },
1184 },
1185 ]
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190 use super::*;
1191
1192 #[test]
1193 fn test_base_definitions_count() {
1194 let tools = base_tool_definitions();
1195 assert_eq!(tools.len(), 55);
1196 }
1197
1198 #[test]
1199 fn test_all_tools_have_names() {
1200 for tool in base_tool_definitions() {
1201 assert!(!tool.name.is_empty());
1202 assert!(!tool.description.is_empty());
1203 }
1204 }
1205
1206 #[test]
1207 fn test_tool_categories() {
1208 let tools = base_tool_definitions();
1209
1210 let issue_tracker_tools = [
1211 "get_issues",
1212 "get_issue",
1213 "get_issue_comments",
1214 "get_issue_relations",
1215 "create_issue",
1216 "update_issue",
1217 "add_issue_comment",
1218 "get_available_statuses",
1219 "get_users",
1220 "link_issues",
1221 "unlink_issues",
1222 "get_assets",
1223 "upload_asset",
1224 "download_asset",
1225 "delete_asset",
1226 "list_project_versions",
1227 "upsert_project_version",
1228 "get_board_sprints",
1229 "assign_to_sprint",
1230 "get_custom_fields",
1231 ];
1232 let git_repository_tools = [
1233 "get_merge_requests",
1234 "get_merge_request",
1235 "get_merge_request_discussions",
1236 "get_merge_request_diffs",
1237 "create_merge_request",
1238 "create_merge_request_comment",
1239 "update_merge_request",
1240 "get_pipeline",
1241 "get_job_logs",
1242 "run_pipeline_job",
1243 ];
1244 let epics_tools = ["get_epics", "create_epic", "update_epic"];
1245 let meeting_notes_tools = [
1246 "get_meeting_notes",
1247 "get_meeting_transcript",
1248 "search_meeting_notes",
1249 ];
1250 let knowledge_base_tools = [
1251 "get_knowledge_base_spaces",
1252 "list_knowledge_base_pages",
1253 "get_knowledge_base_page",
1254 "create_knowledge_base_page",
1255 "update_knowledge_base_page",
1256 "search_knowledge_base",
1257 ];
1258 let messenger_tools = [
1259 "get_messenger_chats",
1260 "get_chat_messages",
1261 "search_chat_messages",
1262 "send_message",
1263 ];
1264 let jira_structure_tools = [
1265 "get_structures",
1266 "get_structure_forest",
1267 "add_structure_rows",
1268 "move_structure_rows",
1269 "remove_structure_row",
1270 "get_structure_values",
1271 "get_structure_views",
1272 "save_structure_view",
1273 "create_structure",
1274 ];
1275
1276 for tool in &tools {
1277 if issue_tracker_tools.contains(&tool.name.as_str()) {
1278 assert_eq!(
1279 tool.category,
1280 ToolCategory::IssueTracker,
1281 "tool {} should be IssueTracker",
1282 tool.name
1283 );
1284 } else if git_repository_tools.contains(&tool.name.as_str()) {
1285 assert_eq!(
1286 tool.category,
1287 ToolCategory::GitRepository,
1288 "tool {} should be GitRepository",
1289 tool.name
1290 );
1291 } else if epics_tools.contains(&tool.name.as_str()) {
1292 assert_eq!(
1293 tool.category,
1294 ToolCategory::Epics,
1295 "tool {} should be Epics",
1296 tool.name
1297 );
1298 } else if meeting_notes_tools.contains(&tool.name.as_str()) {
1299 assert_eq!(
1300 tool.category,
1301 ToolCategory::MeetingNotes,
1302 "tool {} should be MeetingNotes",
1303 tool.name
1304 );
1305 } else if knowledge_base_tools.contains(&tool.name.as_str()) {
1306 assert_eq!(
1307 tool.category,
1308 ToolCategory::KnowledgeBase,
1309 "tool {} should be KnowledgeBase",
1310 tool.name
1311 );
1312 } else if messenger_tools.contains(&tool.name.as_str()) {
1313 assert_eq!(
1314 tool.category,
1315 ToolCategory::Messenger,
1316 "tool {} should be Messenger",
1317 tool.name
1318 );
1319 } else if jira_structure_tools.contains(&tool.name.as_str()) {
1320 assert_eq!(
1321 tool.category,
1322 ToolCategory::JiraStructure,
1323 "tool {} should be JiraStructure",
1324 tool.name
1325 );
1326 } else {
1327 panic!("tool {} has no expected category mapping", tool.name);
1328 }
1329 }
1330 }
1331
1332 #[test]
1333 fn test_required_params() {
1334 let tools = base_tool_definitions();
1335 let get_issue = tools.iter().find(|t| t.name == "get_issue").unwrap();
1336 assert!(get_issue.input_schema.required.contains(&"key".to_string()));
1337
1338 let create_mr = tools
1339 .iter()
1340 .find(|t| t.name == "create_merge_request")
1341 .unwrap();
1342 assert!(
1343 create_mr
1344 .input_schema
1345 .required
1346 .contains(&"title".to_string())
1347 );
1348 assert!(
1349 create_mr
1350 .input_schema
1351 .required
1352 .contains(&"source_branch".to_string())
1353 );
1354
1355 let run_job = tools
1356 .iter()
1357 .find(|tool| tool.name == "run_pipeline_job")
1358 .unwrap();
1359 assert_eq!(
1360 run_job.input_schema.required,
1361 vec!["pipelineId".to_string(), "jobId".to_string()]
1362 );
1363 assert_eq!(
1364 run_job.input_schema.properties["variables"].schema_type,
1365 "object"
1366 );
1367 assert_eq!(
1368 run_job.input_schema.properties["jobInputs"].schema_type,
1369 "object"
1370 );
1371 }
1372
1373 #[test]
1376 fn test_tool_definition_serializes_to_json() {
1377 let tools = base_tool_definitions();
1378 let tool = &tools[0]; let json = serde_json::to_string(tool).unwrap();
1380
1381 assert!(json.contains("\"name\":\"get_issues\""));
1382 assert!(json.contains("\"description\""));
1383 assert!(json.contains("\"category\""));
1384 assert!(json.contains("\"input_schema\""));
1385
1386 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1388 assert_eq!(value["name"], "get_issues");
1389 }
1390
1391 #[test]
1392 fn test_all_tool_definitions_serialize() {
1393 let tools = base_tool_definitions();
1394 for tool in &tools {
1395 let json = serde_json::to_string(tool);
1396 assert!(
1397 json.is_ok(),
1398 "tool '{}' failed to serialize: {:?}",
1399 tool.name,
1400 json.err()
1401 );
1402 }
1403 }
1404
1405 #[test]
1406 fn test_tool_definition_json_contains_properties() {
1407 let tools = base_tool_definitions();
1408 let get_issues = tools.iter().find(|t| t.name == "get_issues").unwrap();
1409 let json = serde_json::to_string_pretty(get_issues).unwrap();
1410
1411 assert!(json.contains("state"));
1413 assert!(json.contains("search"));
1414 assert!(json.contains("labels"));
1415 assert!(json.contains("assignee"));
1416 assert!(json.contains("limit"));
1417 assert!(json.contains("projectKey"));
1418 assert!(json.contains("nativeQuery"));
1419 }
1420
1421 #[test]
1422 fn test_tool_definition_required_fields_in_json() {
1423 let tools = base_tool_definitions();
1424 let add_comment = tools
1425 .iter()
1426 .find(|t| t.name == "add_issue_comment")
1427 .unwrap();
1428 let json_val: serde_json::Value = serde_json::to_value(add_comment).unwrap();
1429
1430 let required = json_val["input_schema"]["required"]
1431 .as_array()
1432 .expect("required should be an array");
1433 let required_strs: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
1434 assert!(required_strs.contains(&"key"));
1435 assert!(required_strs.contains(&"body"));
1436 }
1437
1438 #[test]
1439 fn test_tool_names_are_unique() {
1440 let tools = base_tool_definitions();
1441 let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1442 let original_len = names.len();
1443 names.sort();
1444 names.dedup();
1445 assert_eq!(names.len(), original_len, "tool names should all be unique");
1446 }
1447
1448 #[test]
1449 fn test_link_issues_required_params() {
1450 let tools = base_tool_definitions();
1451 let link = tools.iter().find(|t| t.name == "link_issues").unwrap();
1452 assert!(
1453 link.input_schema
1454 .required
1455 .contains(&"sourceIssueKey".to_string())
1456 );
1457 assert!(
1458 link.input_schema
1459 .required
1460 .contains(&"targetIssueKey".to_string())
1461 );
1462 assert!(link.input_schema.required.contains(&"linkType".to_string()));
1463 }
1464
1465 #[test]
1466 fn test_get_available_statuses_has_empty_schema() {
1467 let tools = base_tool_definitions();
1468 let statuses = tools
1469 .iter()
1470 .find(|t| t.name == "get_available_statuses")
1471 .unwrap();
1472 assert!(statuses.input_schema.required.is_empty());
1473 assert!(statuses.input_schema.properties.is_empty());
1474 }
1475
1476 #[test]
1481 fn test_get_meeting_transcript_exposes_pagination_and_filters() {
1482 let tools = base_tool_definitions();
1483 let t = tools
1484 .iter()
1485 .find(|t| t.name == "get_meeting_transcript")
1486 .expect("get_meeting_transcript tool must exist");
1487 assert_eq!(t.input_schema.required, vec!["meeting_id".to_string()]);
1489 for prop in [
1492 "meeting_id",
1493 "offset",
1494 "limit",
1495 "speaker_filter",
1496 "search_text",
1497 "format",
1498 ] {
1499 assert!(
1500 t.input_schema.properties.contains_key(prop),
1501 "schema must advertise `{prop}` so MCP agents can use it"
1502 );
1503 }
1504 }
1505
1506 #[test]
1507 fn test_epic_tools_exist() {
1508 let tools = base_tool_definitions();
1509 let epic_names: Vec<&str> = tools
1510 .iter()
1511 .filter(|t| t.category == ToolCategory::Epics)
1512 .map(|t| t.name.as_str())
1513 .collect();
1514 assert!(epic_names.contains(&"get_epics"));
1515 assert!(epic_names.contains(&"create_epic"));
1516 assert!(epic_names.contains(&"update_epic"));
1517 }
1518}