Skip to main content

devboy_executor/
tools.rs

1//! Base tool definitions for all provider tools.
2//!
3//! These are the "generic" schemas before enrichment.
4//! Provider enrichers modify them based on capabilities and metadata.
5
6use devboy_core::{PropertySchema, ToolCategory, ToolSchema};
7
8/// A tool definition with name, description, category, and input schema.
9#[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
17/// Get all base tool definitions (before enrichment).
18pub fn base_tool_definitions() -> Vec<ToolDefinition> {
19    vec![
20        // Issue tools
21        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                // Issue #197 — component names. The Jira enricher
90                // populates an enum at metadata-assembly time so schema
91                // consumers see the valid values; here we just declare
92                // the property exists (Codex review on PR #205).
93                s.add_property("components", PropertySchema::array(
94                    PropertySchema::string("component name"),
95                    "Jira component names to associate with the issue. Ignored by providers that don't have Components (GitHub/GitLab/ClickUp).",
96                ));
97                s.set_required("title", true);
98                s
99            },
100        },
101        ToolDefinition {
102            name: "update_issue".into(),
103            description: "Update an existing issue. Only provided fields will be changed.".into(),
104            category: ToolCategory::IssueTracker,
105            input_schema: {
106                let mut s = ToolSchema::new();
107                s.add_property("key", PropertySchema::string("Issue key"));
108                s.add_property("title", PropertySchema::string("New title"));
109                s.add_property("description", PropertySchema::string("New description"));
110                s.add_property("state", PropertySchema::string_enum(&["open", "closed"], "New state"));
111                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "New labels (replaces existing)"));
112                s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "New assignees"));
113                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."));
114                s.add_property("markdown", PropertySchema::boolean("Whether the description is markdown (default: true). When true, ClickUp renders formatted text."));
115                // Issue #197 — `None` leaves components untouched, empty
116                // array clears them, array with names replaces them.
117                s.add_property("components", PropertySchema::array(
118                    PropertySchema::string("component name"),
119                    "Replace components with these Jira component names. Omit the field to leave existing components untouched; pass an empty array to clear.",
120                ));
121                s.set_required("key", true);
122                s
123            },
124        },
125        ToolDefinition {
126            name: "add_issue_comment".into(),
127            description: "Add a comment to an issue with optional file attachments (ClickUp only).".into(),
128            category: ToolCategory::IssueTracker,
129            input_schema: {
130                let mut s = ToolSchema::new();
131                s.add_property("key", PropertySchema::string("Issue key"));
132                s.add_property("body", PropertySchema::string("Comment text"));
133                s.add_property("attachments", PropertySchema::array(
134                    PropertySchema::string("Attachment object with fileData (base64) and filename"),
135                    "File attachments (ClickUp only, max 10MB per file). Each: {fileData: base64, filename: string}",
136                ));
137                s.set_required("key", true);
138                s.set_required("body", true);
139                s
140            },
141        },
142
143        // MR/PR tools
144        ToolDefinition {
145            name: "get_merge_requests".into(),
146            description: "Get merge requests / pull requests from configured provider.".into(),
147            category: ToolCategory::GitRepository,
148            input_schema: {
149                let mut s = ToolSchema::new();
150                s.add_property("state", PropertySchema::string_enum(&["open", "closed", "merged", "all"], "Filter by state (default: open)"));
151                s.add_property("author", PropertySchema::string("Filter by author username"));
152                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Filter by label names"));
153                s.add_property("source_branch", PropertySchema::string("Filter by source branch"));
154                s.add_property("target_branch", PropertySchema::string("Filter by target branch"));
155                s.add_property("limit", PropertySchema::integer("Maximum results (default: 20)", Some(1.0), Some(100.0)));
156                s
157            },
158        },
159        ToolDefinition {
160            name: "get_merge_request".into(),
161            description: "Get a single merge request by key (e.g., 'pr#123', 'mr#456').".into(),
162            category: ToolCategory::GitRepository,
163            input_schema: {
164                let mut s = ToolSchema::new();
165                s.add_property("key", PropertySchema::string("MR/PR key"));
166                s.set_required("key", true);
167                s
168            },
169        },
170        ToolDefinition {
171            name: "get_merge_request_discussions".into(),
172            description: "Get discussions/review comments for a merge request with code positions.".into(),
173            category: ToolCategory::GitRepository,
174            input_schema: {
175                let mut s = ToolSchema::new();
176                s.add_property("key", PropertySchema::string("MR/PR key"));
177                s.add_property("limit", PropertySchema::integer("Max discussions (default: 20)", Some(1.0), Some(100.0)));
178                s.add_property("offset", PropertySchema::integer("Skip N discussions (default: 0)", Some(0.0), None));
179                s.set_required("key", true);
180                s
181            },
182        },
183        ToolDefinition {
184            name: "get_merge_request_diffs".into(),
185            description: "Get file diffs for a merge request.".into(),
186            category: ToolCategory::GitRepository,
187            input_schema: {
188                let mut s = ToolSchema::new();
189                s.add_property("key", PropertySchema::string("MR/PR key"));
190                s.set_required("key", true);
191                s
192            },
193        },
194        ToolDefinition {
195            name: "create_merge_request".into(),
196            description: "Create a new merge request (GitLab) or pull request (GitHub).".into(),
197            category: ToolCategory::GitRepository,
198            input_schema: {
199                let mut s = ToolSchema::new();
200                s.add_property("title", PropertySchema::string("MR/PR title"));
201                s.add_property("description", PropertySchema::string("MR/PR description"));
202                s.add_property("source_branch", PropertySchema::string("Source branch"));
203                s.add_property("target_branch", PropertySchema::string("Target branch"));
204                s.add_property("draft", PropertySchema::boolean("Create as draft (default: false)"));
205                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels"));
206                s.add_property("reviewers", PropertySchema::array(PropertySchema::string("reviewer"), "Reviewers"));
207                s.set_required("title", true);
208                s.set_required("source_branch", true);
209                s.set_required("target_branch", true);
210                s
211            },
212        },
213        ToolDefinition {
214            name: "create_merge_request_comment".into(),
215            description: "Add a comment to a merge request. Can be general or inline code review.".into(),
216            category: ToolCategory::GitRepository,
217            input_schema: {
218                let mut s = ToolSchema::new();
219                s.add_property("key", PropertySchema::string("MR/PR key"));
220                s.add_property("body", PropertySchema::string("Comment text"));
221                s.add_property("file_path", PropertySchema::string("File path for inline comment"));
222                s.add_property("line", PropertySchema::integer("Line number for inline comment", None, None));
223                s.add_property("line_type", PropertySchema::string_enum(&["old", "new"], "Line type (default: new)"));
224                s.add_property("commit_sha", PropertySchema::string("Commit SHA for inline comment"));
225                s.add_property("discussion_id", PropertySchema::string("Reply to existing discussion"));
226                s.set_required("key", true);
227                s.set_required("body", true);
228                s
229            },
230        },
231
232        // Pipeline tools
233        ToolDefinition {
234            name: "get_pipeline".into(),
235            description: "Get CI/CD pipeline status for branch or MR/PR with job details.".into(),
236            category: ToolCategory::GitRepository,
237            input_schema: {
238                let mut s = ToolSchema::new();
239                s.add_property("branch", PropertySchema::string("Branch name (default: main)"));
240                s.add_property("mrKey", PropertySchema::string("MR/PR key (priority over branch)"));
241                s.add_property("includeFailedLogs", PropertySchema::boolean("Include error extraction for failed jobs (default: true)"));
242                s
243            },
244        },
245        ToolDefinition {
246            name: "get_job_logs".into(),
247            description: "Get CI/CD job logs. Modes: smart (auto errors), search (pattern), paginated, full.".into(),
248            category: ToolCategory::GitRepository,
249            input_schema: {
250                let mut s = ToolSchema::new();
251                s.add_property("jobId", PropertySchema::string("Job ID from get_pipeline"));
252                s.add_property("pattern", PropertySchema::string("Regex/keyword search pattern"));
253                s.add_property("context", PropertySchema::integer("Context lines around match (default: 5)", None, None));
254                s.add_property("maxMatches", PropertySchema::integer("Max search results (default: 20)", None, None));
255                s.add_property("offset", PropertySchema::integer("Start line for paginated mode", None, None));
256                s.add_property("limit", PropertySchema::integer("Lines to return (default: 200, max: 1000)", Some(1.0), Some(1000.0)));
257                s.add_property("full", PropertySchema::boolean("Return entire log"));
258                s.set_required("jobId", true);
259                s
260            },
261        },
262
263        // Status / user / link / epic tools
264        ToolDefinition {
265            name: "get_available_statuses".into(),
266            description: "Get available statuses for the issue tracker.".into(),
267            category: ToolCategory::IssueTracker,
268            input_schema: ToolSchema::new(),
269        },
270        ToolDefinition {
271            name: "get_users".into(),
272            description: "Get users from the issue tracker (Jira). Search by name, project, or ID.".into(),
273            category: ToolCategory::IssueTracker,
274            input_schema: {
275                let mut s = ToolSchema::new();
276                s.add_property("userId", PropertySchema::string("Get specific user by ID"));
277                s.add_property("projectKey", PropertySchema::string("Get assignable users for project"));
278                s.add_property("search", PropertySchema::string("Search by name or email"));
279                s.add_property("maxResults", PropertySchema::integer("Max results (default: 50)", Some(1.0), Some(1000.0)));
280                s
281            },
282        },
283        ToolDefinition {
284            name: "link_issues".into(),
285            description: "Link two issues together (blocks, relates_to, etc.).".into(),
286            category: ToolCategory::IssueTracker,
287            input_schema: {
288                let mut s = ToolSchema::new();
289                s.add_property("sourceIssueKey", PropertySchema::string("Source issue key"));
290                s.add_property("targetIssueKey", PropertySchema::string("Target issue key"));
291                s.add_property("linkType", PropertySchema::string("Link type (e.g., blocks, relates_to)"));
292                s.set_required("sourceIssueKey", true);
293                s.set_required("targetIssueKey", true);
294                s.set_required("linkType", true);
295                s
296            },
297        },
298        ToolDefinition {
299            name: "unlink_issues".into(),
300            description: "Remove a link between two issues.".into(),
301            category: ToolCategory::IssueTracker,
302            input_schema: {
303                let mut s = ToolSchema::new();
304                s.add_property("sourceIssueKey", PropertySchema::string("Source issue key"));
305                s.add_property("targetIssueKey", PropertySchema::string("Target issue key"));
306                s.add_property("linkType", PropertySchema::string("Link type to remove (e.g., blocks, relates_to, subtask)"));
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"));
347                s.add_property("goalId", PropertySchema::string("Goal ID (G1-G9) to associate with the epic"));
348                s.add_property("priority", PropertySchema::string("New priority (urgent/high/normal/low)"));
349                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to set"));
350                s.add_property("assignees", PropertySchema::array(PropertySchema::string("assignee"), "Assignees to set"));
351                s.set_required("epicKey", true);
352                s
353            },
354        },
355        // Meeting notes tools
356        ToolDefinition {
357            name: "get_meeting_notes".into(),
358            description: "Get meeting notes and transcripts with optional filters (date range, participants, host).".into(),
359            category: ToolCategory::MeetingNotes,
360            input_schema: {
361                let mut s = ToolSchema::new();
362                s.add_property("from_date", PropertySchema::string("Filter from date (ISO 8601, e.g., '2025-01-01T00:00:00Z')"));
363                s.add_property("to_date", PropertySchema::string("Filter to date (ISO 8601)"));
364                s.add_property("participants", PropertySchema::array(PropertySchema::string("email"), "Filter by participant email addresses"));
365                s.add_property("host_email", PropertySchema::string("Filter by host email"));
366                s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 50)", Some(1.0), Some(50.0)));
367                s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
368                s
369            },
370        },
371        ToolDefinition {
372            name: "get_meeting_transcript".into(),
373            description: "Get the full transcript for a meeting. Returns speaker-attributed sentences with timestamps.".into(),
374            category: ToolCategory::MeetingNotes,
375            input_schema: {
376                let mut s = ToolSchema::new();
377                s.add_property("meeting_id", PropertySchema::string("Meeting ID from get_meeting_notes"));
378                s.set_required("meeting_id", true);
379                s
380            },
381        },
382        ToolDefinition {
383            name: "search_meeting_notes".into(),
384            description: "Search across meetings by keywords, topics, or action items, with optional filters (date range, participants, host).".into(),
385            category: ToolCategory::MeetingNotes,
386            input_schema: {
387                let mut s = ToolSchema::new();
388                s.add_property("query", PropertySchema::string("Search query"));
389                s.add_property("from_date", PropertySchema::string("Filter from date (ISO 8601)"));
390                s.add_property("to_date", PropertySchema::string("Filter to date (ISO 8601)"));
391                s.add_property("participants", PropertySchema::array(PropertySchema::string("email"), "Filter by participant email addresses"));
392                s.add_property("host_email", PropertySchema::string("Filter by host email"));
393                s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 50)", Some(1.0), Some(50.0)));
394                s.add_property("offset", PropertySchema::integer("Number of results to skip (default: 0)", Some(0.0), None));
395                s.set_required("query", true);
396                s
397            },
398        },
399        // Knowledge base tools
400        ToolDefinition {
401            name: "get_knowledge_base_spaces".into(),
402            description: "List available knowledge base spaces.".into(),
403            category: ToolCategory::KnowledgeBase,
404            input_schema: ToolSchema::new(),
405        },
406        ToolDefinition {
407            name: "list_knowledge_base_pages".into(),
408            description: "List pages in a knowledge base space with pagination.".into(),
409            category: ToolCategory::KnowledgeBase,
410            input_schema: {
411                let mut s = ToolSchema::new();
412                s.add_property("spaceKey", PropertySchema::string("Space key to list pages from"));
413                s.add_property("limit", PropertySchema::integer("Maximum number of results (default: 25)", Some(1.0), Some(100.0)));
414                s.add_property("offset", PropertySchema::integer("Number of results to skip when offset pagination is supported", Some(0.0), None));
415                s.add_property("cursor", PropertySchema::string("Provider pagination cursor/token"));
416                s.add_property("search", PropertySchema::string("Optional free-text title/content filter"));
417                s.add_property("parentId", PropertySchema::string("Optional ancestor/parent page ID to scope the listing"));
418                s.set_required("spaceKey", true);
419                s
420            },
421        },
422        ToolDefinition {
423            name: "get_knowledge_base_page".into(),
424            description: "Get a knowledge base page with content, labels, and ancestors.".into(),
425            category: ToolCategory::KnowledgeBase,
426            input_schema: {
427                let mut s = ToolSchema::new();
428                s.add_property("pageId", PropertySchema::string("Knowledge base page ID"));
429                s.set_required("pageId", true);
430                s
431            },
432        },
433        ToolDefinition {
434            name: "create_knowledge_base_page".into(),
435            description: "Create a knowledge base page in a space.".into(),
436            category: ToolCategory::KnowledgeBase,
437            input_schema: {
438                let mut s = ToolSchema::new();
439                s.add_property("spaceKey", PropertySchema::string("Target space key"));
440                s.add_property("title", PropertySchema::string("Page title"));
441                s.add_property("content", PropertySchema::string("Page body content"));
442                s.add_property("contentType", PropertySchema::string_enum(&["markdown", "html", "storage"], "Content representation supplied by the caller"));
443                s.add_property("parentId", PropertySchema::string("Optional parent page ID"));
444                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to set on the page"));
445                s.set_required("spaceKey", true);
446                s.set_required("title", true);
447                s.set_required("content", true);
448                s
449            },
450        },
451        ToolDefinition {
452            name: "update_knowledge_base_page".into(),
453            description: "Update a knowledge base page title, content, metadata, or labels.".into(),
454            category: ToolCategory::KnowledgeBase,
455            input_schema: {
456                let mut s = ToolSchema::new();
457                s.add_property("pageId", PropertySchema::string("Knowledge base page ID"));
458                s.add_property("title", PropertySchema::string("New page title"));
459                s.add_property("content", PropertySchema::string("New page body content"));
460                s.add_property("contentType", PropertySchema::string_enum(&["markdown", "html", "storage"], "Content representation supplied by the caller"));
461                s.add_property("version", PropertySchema::integer("Expected current version for optimistic locking", Some(1.0), None));
462                s.add_property("parentId", PropertySchema::string("Optional new parent page ID"));
463                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "Labels to replace on the page"));
464                s.set_required("pageId", true);
465                s
466            },
467        },
468        ToolDefinition {
469            name: "search_knowledge_base".into(),
470            description: "Search knowledge base pages across spaces using free text or provider-native syntax such as CQL.".into(),
471            category: ToolCategory::KnowledgeBase,
472            input_schema: {
473                let mut s = ToolSchema::new();
474                s.add_property("query", PropertySchema::string("Free-text query or provider-native search expression"));
475                s.add_property("spaceKey", PropertySchema::string("Restrict search to a specific space key"));
476                s.add_property("cursor", PropertySchema::string("Provider pagination cursor/token"));
477                s.add_property("limit", PropertySchema::integer("Maximum number of matches to return", Some(1.0), Some(100.0)));
478                s.add_property("rawQuery", PropertySchema::boolean("Whether `query` should be treated as raw provider-native syntax"));
479                s.set_required("query", true);
480                s
481            },
482        },
483        ToolDefinition {
484            name: "update_merge_request".into(),
485            description: "Update a merge request / pull request (title, description, state, labels, draft).".into(),
486            category: ToolCategory::GitRepository,
487            input_schema: {
488                let mut s = ToolSchema::new();
489                s.add_property("key", PropertySchema::string("MR key (e.g. 'mr#1', 'pr#42')"));
490                s.add_property("title", PropertySchema::string("New title"));
491                s.add_property("description", PropertySchema::string("New description / body (supports markdown)"));
492                s.add_property("state", PropertySchema::string_enum(&["close", "reopen"], "Change MR state"));
493                s.add_property("labels", PropertySchema::array(PropertySchema::string("label"), "New labels (replaces existing)"));
494                s.set_required("key", true);
495                s
496            },
497        },
498        // =====================================================================
499        // Asset tools
500        // =====================================================================
501        ToolDefinition {
502            name: "get_assets".into(),
503            description: "List file attachments for an issue or merge request.".into(),
504            category: ToolCategory::IssueTracker,
505            input_schema: {
506                let mut s = ToolSchema::new();
507                s.add_property("context_type", PropertySchema::string_enum(&["issue", "mr"], "Context type: 'issue' or 'mr' (merge request / pull request)"));
508                s.add_property("key", PropertySchema::string("Issue key (e.g. 'DEV-123', 'gitlab#42') or MR key (e.g. 'mr#42', 'pr#42')"));
509                s.set_required("context_type", true);
510                s.set_required("key", true);
511                s
512            },
513        },
514        ToolDefinition {
515            name: "upload_asset".into(),
516            description: "Upload a file attachment to an issue. Returns the download URL.".into(),
517            category: ToolCategory::IssueTracker,
518            input_schema: {
519                let mut s = ToolSchema::new();
520                s.add_property("context_type", PropertySchema::string_enum(&["issue"], "Context type (currently only 'issue' is supported for uploads)"));
521                s.add_property("key", PropertySchema::string("Issue key (e.g. 'DEV-123')"));
522                s.add_property("filename", PropertySchema::string("Original filename (e.g. 'screenshot.png')"));
523                s.add_property("fileData", PropertySchema::string("Base64-encoded file content"));
524                s.set_required("context_type", true);
525                s.set_required("key", true);
526                s.set_required("filename", true);
527                s.set_required("fileData", true);
528                s
529            },
530        },
531        ToolDefinition {
532            name: "download_asset".into(),
533            description: "Download a file attachment to local cache. Returns local file path when cache is available, base64-encoded content as fallback.".into(),
534            category: ToolCategory::IssueTracker,
535            input_schema: {
536                let mut s = ToolSchema::new();
537                s.add_property("context_type", PropertySchema::string_enum(&["issue", "mr"], "Context type: 'issue' or 'mr'"));
538                s.add_property("key", PropertySchema::string("Issue key or MR key"));
539                s.add_property("asset_id", PropertySchema::string("Asset identifier from get_assets response"));
540                s.set_required("context_type", true);
541                s.set_required("key", true);
542                s.set_required("asset_id", true);
543                s
544            },
545        },
546        // =====================================================================
547        // Messenger tools
548        // =====================================================================
549        ToolDefinition {
550            name: "get_messenger_chats".into(),
551            description: "List available messenger chats, channels, groups, or direct messages.".into(),
552            category: ToolCategory::Messenger,
553            input_schema: {
554                let mut s = ToolSchema::new();
555                s.add_property("search", PropertySchema::string("Optional chat name search"));
556                s.add_property("chat_type", PropertySchema::string_enum(&["direct", "group", "channel"], "Optional chat type filter"));
557                s.add_property("limit", PropertySchema::integer("Maximum number of chats to return", Some(1.0), Some(1000.0)));
558                s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
559                s.add_property("include_inactive", PropertySchema::boolean("Include archived or inactive chats"));
560                s
561            },
562        },
563        ToolDefinition {
564            name: "get_chat_messages".into(),
565            description: "Get message history for a chat or fetch replies for a specific thread.".into(),
566            category: ToolCategory::Messenger,
567            input_schema: {
568                let mut s = ToolSchema::new();
569                s.add_property("chat_id", PropertySchema::string("Messenger chat ID"));
570                s.add_property("limit", PropertySchema::integer("Maximum number of messages to return", Some(1.0), Some(1000.0)));
571                s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
572                s.add_property("thread_id", PropertySchema::string("Thread identifier to fetch replies for"));
573                s.add_property("since", PropertySchema::string("Only include messages after this provider timestamp"));
574                s.add_property("until", PropertySchema::string("Only include messages before this provider timestamp"));
575                s.set_required("chat_id", true);
576                s
577            },
578        },
579        ToolDefinition {
580            name: "search_chat_messages".into(),
581            description: "Search messages across accessible chats or within a specific chat.".into(),
582            category: ToolCategory::Messenger,
583            input_schema: {
584                let mut s = ToolSchema::new();
585                s.add_property("query", PropertySchema::string("Message search query"));
586                s.add_property("chat_id", PropertySchema::string("Optional chat ID to scope the search"));
587                s.add_property("limit", PropertySchema::integer("Maximum number of matches to return", Some(1.0), Some(1000.0)));
588                s.add_property("cursor", PropertySchema::string("Provider pagination cursor"));
589                s.add_property("since", PropertySchema::string("Only include messages after this provider timestamp"));
590                s.add_property("until", PropertySchema::string("Only include messages before this provider timestamp"));
591                s.set_required("query", true);
592                s
593            },
594        },
595        ToolDefinition {
596            name: "send_message".into(),
597            description: "Send a message to a chat or as a threaded reply.".into(),
598            category: ToolCategory::Messenger,
599            input_schema: {
600                let mut s = ToolSchema::new();
601                s.add_property("chat_id", PropertySchema::string("Messenger chat ID"));
602                s.add_property("text", PropertySchema::string("Message body"));
603                s.add_property("thread_id", PropertySchema::string("Thread identifier to post as a threaded reply"));
604                s.add_property("reply_to_id", PropertySchema::string("Direct parent message ID when supported"));
605                s.set_required("chat_id", true);
606                s.set_required("text", true);
607                s
608            },
609        },
610        ToolDefinition {
611            name: "delete_asset".into(),
612            description: "Delete a file attachment from an issue. Not all providers support this — check asset_capabilities first.".into(),
613            category: ToolCategory::IssueTracker,
614            input_schema: {
615                let mut s = ToolSchema::new();
616                s.add_property("key", PropertySchema::string("Issue key (e.g. 'PROJ-123')"));
617                s.add_property("asset_id", PropertySchema::string("Asset identifier to delete"));
618                s.set_required("key", true);
619                s.set_required("asset_id", true);
620                s
621            },
622        },
623        // Jira Structure plugin tools
624        ToolDefinition {
625            name: "get_structures".into(),
626            description: "List all available Jira Structures. Returns structure ID, name, and description. Requires Jira with Structure plugin.".into(),
627            category: ToolCategory::JiraStructure,
628            input_schema: ToolSchema::new(),
629        },
630        ToolDefinition {
631            name: "get_structure_forest".into(),
632            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(),
633            category: ToolCategory::JiraStructure,
634            input_schema: {
635                let mut s = ToolSchema::new();
636                s.add_property("structureId", PropertySchema::integer("Structure ID. Use get_structures to find it.", None, None));
637                s.add_property("offset", PropertySchema::integer("Offset for pagination (default: 0)", Some(0.0), None));
638                s.add_property("limit", PropertySchema::integer("Max rows to return (default: 200)", Some(1.0), Some(10000.0)));
639                s.set_required("structureId", true);
640                s
641            },
642        },
643        ToolDefinition {
644            name: "add_structure_rows".into(),
645            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(),
646            category: ToolCategory::JiraStructure,
647            input_schema: {
648                let mut s = ToolSchema::new();
649                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
650                s.add_property("items", PropertySchema::array(
651                    PropertySchema::string("Item: Jira issue key (e.g. 'PROJ-123') or JSON {\"itemId\":\"PROJ-123\",\"itemType\":\"issue\"}"),
652                    "Items to add",
653                ));
654                s.add_property("under", PropertySchema::integer("Parent row ID — items become children of this row", None, None));
655                s.add_property("after", PropertySchema::integer("Sibling row ID — items placed after this row", None, None));
656                s.add_property("forestVersion", PropertySchema::integer("Forest version for optimistic locking (from get_structure_forest)", None, None));
657                s.set_required("structureId", true);
658                s.set_required("items", true);
659                s
660            },
661        },
662        ToolDefinition {
663            name: "move_structure_rows".into(),
664            description: "Move rows within a Jira Structure hierarchy. Specify new position with under (new parent) and/or after (sibling).".into(),
665            category: ToolCategory::JiraStructure,
666            input_schema: {
667                let mut s = ToolSchema::new();
668                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
669                s.add_property("rowIds", PropertySchema::array(
670                    PropertySchema::integer("Row ID", None, None),
671                    "Row IDs to move (from get_structure_forest)",
672                ));
673                s.add_property("under", PropertySchema::integer("New parent row ID", None, None));
674                s.add_property("after", PropertySchema::integer("Sibling row ID to place after", None, None));
675                s.add_property("forestVersion", PropertySchema::integer("Forest version for optimistic locking", None, None));
676                s.set_required("structureId", true);
677                s.set_required("rowIds", true);
678                s
679            },
680        },
681        ToolDefinition {
682            name: "remove_structure_row".into(),
683            description: "Remove a row from a Jira Structure. Only removes from the structure hierarchy — the underlying Jira issue is NOT deleted.".into(),
684            category: ToolCategory::JiraStructure,
685            input_schema: {
686                let mut s = ToolSchema::new();
687                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
688                s.add_property("rowId", PropertySchema::integer("Row ID to remove (from get_structure_forest)", None, None));
689                s.set_required("structureId", true);
690                s.set_required("rowId", true);
691                s
692            },
693        },
694        ToolDefinition {
695            name: "get_structure_values".into(),
696            description: "Read column values (including Expr formulas like SUM, PROGRESS, COUNT) for specific rows in a Jira Structure. Values are computed server-side.".into(),
697            category: ToolCategory::JiraStructure,
698            input_schema: {
699                let mut s = ToolSchema::new();
700                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
701                s.add_property("rows", PropertySchema::array(
702                    PropertySchema::integer("Row ID", None, None),
703                    "Row IDs to read values for",
704                ));
705                s.add_property("columns", PropertySchema::array(
706                    PropertySchema::string("Column spec: field name (e.g. 'summary'), or JSON {\"field\":\"status\"} or {\"formula\":\"SUM(\\\"Story Points\\\")\"}"),
707                    "Columns to read",
708                ));
709                s.set_required("structureId", true);
710                s.set_required("rows", true);
711                s.set_required("columns", true);
712                s
713            },
714        },
715        ToolDefinition {
716            name: "get_structure_views".into(),
717            description: "Get views for a Jira Structure. Without viewId: lists all views. With viewId: returns full view configuration (columns, grouping, sorting, filter).".into(),
718            category: ToolCategory::JiraStructure,
719            input_schema: {
720                let mut s = ToolSchema::new();
721                s.add_property("structureId", PropertySchema::integer("Structure ID", None, None));
722                s.add_property("viewId", PropertySchema::integer("View ID for full config (optional — omit to list all views)", None, None));
723                s.set_required("structureId", true);
724                s
725            },
726        },
727        ToolDefinition {
728            name: "save_structure_view".into(),
729            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(),
730            category: ToolCategory::JiraStructure,
731            input_schema: {
732                let mut s = ToolSchema::new();
733                s.add_property("id", PropertySchema::integer("View ID to update (omit to create new)", None, None));
734                s.add_property("structureId", PropertySchema::integer("Structure ID this view belongs to", None, None));
735                s.add_property("name", PropertySchema::string("View name"));
736                s.add_property("columns", PropertySchema::array(
737                    PropertySchema::string("Column spec: JSON {\"field\":\"summary\"} or {\"formula\":\"SUM(\\\"Story Points\\\")\",\"width\":100}"),
738                    "Column definitions",
739                ));
740                s.add_property("groupBy", PropertySchema::string("Field name to group by"));
741                s.add_property("sortBy", PropertySchema::string("Field name to sort by"));
742                s.add_property("filter", PropertySchema::string("JQL filter expression"));
743                s.set_required("structureId", true);
744                s.set_required("name", true);
745                s
746            },
747        },
748        ToolDefinition {
749            name: "create_structure".into(),
750            description: "Create a new Jira Structure. After creation, use add_structure_rows to populate and save_structure_view to configure columns.".into(),
751            category: ToolCategory::JiraStructure,
752            input_schema: {
753                let mut s = ToolSchema::new();
754                s.add_property("name", PropertySchema::string("Structure name"));
755                s.add_property("description", PropertySchema::string("Structure description"));
756                s.set_required("name", true);
757                s
758            },
759        },
760
761        // Project versions / fixVersion targets (issue #238).
762        // Two-tool surface — list returns a rich payload so a per-id GET
763        // is unnecessary, upsert is name-keyed so the LLM never deals
764        // with numeric ids. See `docs/research/paper-3-context-enrichment.md`.
765        ToolDefinition {
766            name: "list_project_versions".into(),
767            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(),
768            category: ToolCategory::IssueTracker,
769            input_schema: {
770                let mut s = ToolSchema::new();
771                s.add_property("project", PropertySchema::string("Jira project key (e.g., \"PROJ\"). Defaults to the configured project."));
772                s.add_property("released", PropertySchema::string_enum(&["true", "false", "all"], "Filter by release state: \"true\" → only released, \"false\" → only unreleased, \"all\" → both (default: \"all\")."));
773                s.add_property("archived", PropertySchema::string_enum(&["true", "false", "all"], "Filter by archived flag (default: \"false\" — hides archival noise)."));
774                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)));
775                s.add_property("includeIssueCount", PropertySchema::boolean("Fetch issue counts per version via Cloud `?expand=issuesstatus` (default: false). Adds latency on large projects."));
776                s
777            },
778        },
779        ToolDefinition {
780            name: "upsert_project_version".into(),
781            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(),
782            category: ToolCategory::IssueTracker,
783            input_schema: {
784                let mut s = ToolSchema::new();
785                s.add_property("project", PropertySchema::string("Jira project key (e.g., \"PROJ\"). Defaults to the configured project."));
786                s.add_property("name", PropertySchema::string("Version name — both the lookup key and, on create, the value (e.g., \"3.18.0\")."));
787                s.add_property("description", PropertySchema::string("Release notes / version description. Markdown-style text is preserved on Server/DC; Cloud accepts plain text."));
788                s.add_property("startDate", PropertySchema::string("Planned start date as ISO 8601 calendar date (`YYYY-MM-DD`)."));
789                s.add_property("releaseDate", PropertySchema::string("Planned or actual release date (`YYYY-MM-DD`)."));
790                s.add_property("released", PropertySchema::boolean("Mark released (true) / unreleased (false). Pair with `releaseDate` when closing a release."));
791                s.add_property("archived", PropertySchema::boolean("Archive (true) / unarchive (false) the version."));
792                s.set_required("name", true);
793                s
794            },
795        },
796    ]
797}
798
799/// Always-available MCP tools that don't belong to a provider category.
800///
801/// These are surfaced by `devboy-mcp` on every `tools/list` regardless of
802/// which providers are configured. They live here (not in the MCP crate)
803/// so the published reference doc can render them from a single source.
804#[derive(Debug, Clone)]
805pub struct McpOnlyTool {
806    pub name: String,
807    pub description: String,
808    pub input_schema: ToolSchema,
809}
810
811/// MCP-only tools (context management). Always advertised by the server.
812pub fn mcp_only_tools() -> Vec<McpOnlyTool> {
813    vec![
814        McpOnlyTool {
815            name: "list_contexts".into(),
816            description: "List configured contexts and indicate the active context.".into(),
817            input_schema: ToolSchema::new(),
818        },
819        McpOnlyTool {
820            name: "use_context".into(),
821            description: "Switch active context at runtime.".into(),
822            input_schema: {
823                let mut s = ToolSchema::new();
824                s.add_property("name", PropertySchema::string("Context name to activate"));
825                s.set_required("name", true);
826                s
827            },
828        },
829        McpOnlyTool {
830            name: "get_current_context".into(),
831            description: "Get current active context name.".into(),
832            input_schema: ToolSchema::new(),
833        },
834    ]
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840
841    #[test]
842    fn test_base_definitions_count() {
843        let tools = base_tool_definitions();
844        assert_eq!(tools.len(), 51);
845    }
846
847    #[test]
848    fn test_all_tools_have_names() {
849        for tool in base_tool_definitions() {
850            assert!(!tool.name.is_empty());
851            assert!(!tool.description.is_empty());
852        }
853    }
854
855    #[test]
856    fn test_tool_categories() {
857        let tools = base_tool_definitions();
858
859        let issue_tracker_tools = [
860            "get_issues",
861            "get_issue",
862            "get_issue_comments",
863            "get_issue_relations",
864            "create_issue",
865            "update_issue",
866            "add_issue_comment",
867            "get_available_statuses",
868            "get_users",
869            "link_issues",
870            "unlink_issues",
871            "get_assets",
872            "upload_asset",
873            "download_asset",
874            "delete_asset",
875            "list_project_versions",
876            "upsert_project_version",
877        ];
878        let git_repository_tools = [
879            "get_merge_requests",
880            "get_merge_request",
881            "get_merge_request_discussions",
882            "get_merge_request_diffs",
883            "create_merge_request",
884            "create_merge_request_comment",
885            "update_merge_request",
886            "get_pipeline",
887            "get_job_logs",
888        ];
889        let epics_tools = ["get_epics", "create_epic", "update_epic"];
890        let meeting_notes_tools = [
891            "get_meeting_notes",
892            "get_meeting_transcript",
893            "search_meeting_notes",
894        ];
895        let knowledge_base_tools = [
896            "get_knowledge_base_spaces",
897            "list_knowledge_base_pages",
898            "get_knowledge_base_page",
899            "create_knowledge_base_page",
900            "update_knowledge_base_page",
901            "search_knowledge_base",
902        ];
903        let messenger_tools = [
904            "get_messenger_chats",
905            "get_chat_messages",
906            "search_chat_messages",
907            "send_message",
908        ];
909        let jira_structure_tools = [
910            "get_structures",
911            "get_structure_forest",
912            "add_structure_rows",
913            "move_structure_rows",
914            "remove_structure_row",
915            "get_structure_values",
916            "get_structure_views",
917            "save_structure_view",
918            "create_structure",
919        ];
920
921        for tool in &tools {
922            if issue_tracker_tools.contains(&tool.name.as_str()) {
923                assert_eq!(
924                    tool.category,
925                    ToolCategory::IssueTracker,
926                    "tool {} should be IssueTracker",
927                    tool.name
928                );
929            } else if git_repository_tools.contains(&tool.name.as_str()) {
930                assert_eq!(
931                    tool.category,
932                    ToolCategory::GitRepository,
933                    "tool {} should be GitRepository",
934                    tool.name
935                );
936            } else if epics_tools.contains(&tool.name.as_str()) {
937                assert_eq!(
938                    tool.category,
939                    ToolCategory::Epics,
940                    "tool {} should be Epics",
941                    tool.name
942                );
943            } else if meeting_notes_tools.contains(&tool.name.as_str()) {
944                assert_eq!(
945                    tool.category,
946                    ToolCategory::MeetingNotes,
947                    "tool {} should be MeetingNotes",
948                    tool.name
949                );
950            } else if knowledge_base_tools.contains(&tool.name.as_str()) {
951                assert_eq!(
952                    tool.category,
953                    ToolCategory::KnowledgeBase,
954                    "tool {} should be KnowledgeBase",
955                    tool.name
956                );
957            } else if messenger_tools.contains(&tool.name.as_str()) {
958                assert_eq!(
959                    tool.category,
960                    ToolCategory::Messenger,
961                    "tool {} should be Messenger",
962                    tool.name
963                );
964            } else if jira_structure_tools.contains(&tool.name.as_str()) {
965                assert_eq!(
966                    tool.category,
967                    ToolCategory::JiraStructure,
968                    "tool {} should be JiraStructure",
969                    tool.name
970                );
971            } else {
972                panic!("tool {} has no expected category mapping", tool.name);
973            }
974        }
975    }
976
977    #[test]
978    fn test_required_params() {
979        let tools = base_tool_definitions();
980        let get_issue = tools.iter().find(|t| t.name == "get_issue").unwrap();
981        assert!(get_issue.input_schema.required.contains(&"key".to_string()));
982
983        let create_mr = tools
984            .iter()
985            .find(|t| t.name == "create_merge_request")
986            .unwrap();
987        assert!(
988            create_mr
989                .input_schema
990                .required
991                .contains(&"title".to_string())
992        );
993        assert!(
994            create_mr
995                .input_schema
996                .required
997                .contains(&"source_branch".to_string())
998        );
999    }
1000
1001    // --- ToolDefinition serialization ---
1002
1003    #[test]
1004    fn test_tool_definition_serializes_to_json() {
1005        let tools = base_tool_definitions();
1006        let tool = &tools[0]; // get_issues
1007        let json = serde_json::to_string(tool).unwrap();
1008
1009        assert!(json.contains("\"name\":\"get_issues\""));
1010        assert!(json.contains("\"description\""));
1011        assert!(json.contains("\"category\""));
1012        assert!(json.contains("\"input_schema\""));
1013
1014        // Should be valid JSON
1015        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1016        assert_eq!(value["name"], "get_issues");
1017    }
1018
1019    #[test]
1020    fn test_all_tool_definitions_serialize() {
1021        let tools = base_tool_definitions();
1022        for tool in &tools {
1023            let json = serde_json::to_string(tool);
1024            assert!(
1025                json.is_ok(),
1026                "tool '{}' failed to serialize: {:?}",
1027                tool.name,
1028                json.err()
1029            );
1030        }
1031    }
1032
1033    #[test]
1034    fn test_tool_definition_json_contains_properties() {
1035        let tools = base_tool_definitions();
1036        let get_issues = tools.iter().find(|t| t.name == "get_issues").unwrap();
1037        let json = serde_json::to_string_pretty(get_issues).unwrap();
1038
1039        // get_issues should have state, search, labels, assignee, limit, offset, sort_by, sort_order, projectKey, nativeQuery
1040        assert!(json.contains("state"));
1041        assert!(json.contains("search"));
1042        assert!(json.contains("labels"));
1043        assert!(json.contains("assignee"));
1044        assert!(json.contains("limit"));
1045        assert!(json.contains("projectKey"));
1046        assert!(json.contains("nativeQuery"));
1047    }
1048
1049    #[test]
1050    fn test_tool_definition_required_fields_in_json() {
1051        let tools = base_tool_definitions();
1052        let add_comment = tools
1053            .iter()
1054            .find(|t| t.name == "add_issue_comment")
1055            .unwrap();
1056        let json_val: serde_json::Value = serde_json::to_value(add_comment).unwrap();
1057
1058        let required = json_val["input_schema"]["required"]
1059            .as_array()
1060            .expect("required should be an array");
1061        let required_strs: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
1062        assert!(required_strs.contains(&"key"));
1063        assert!(required_strs.contains(&"body"));
1064    }
1065
1066    #[test]
1067    fn test_tool_names_are_unique() {
1068        let tools = base_tool_definitions();
1069        let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1070        let original_len = names.len();
1071        names.sort();
1072        names.dedup();
1073        assert_eq!(names.len(), original_len, "tool names should all be unique");
1074    }
1075
1076    #[test]
1077    fn test_link_issues_required_params() {
1078        let tools = base_tool_definitions();
1079        let link = tools.iter().find(|t| t.name == "link_issues").unwrap();
1080        assert!(
1081            link.input_schema
1082                .required
1083                .contains(&"sourceIssueKey".to_string())
1084        );
1085        assert!(
1086            link.input_schema
1087                .required
1088                .contains(&"targetIssueKey".to_string())
1089        );
1090        assert!(link.input_schema.required.contains(&"linkType".to_string()));
1091    }
1092
1093    #[test]
1094    fn test_get_available_statuses_has_empty_schema() {
1095        let tools = base_tool_definitions();
1096        let statuses = tools
1097            .iter()
1098            .find(|t| t.name == "get_available_statuses")
1099            .unwrap();
1100        assert!(statuses.input_schema.required.is_empty());
1101        assert!(statuses.input_schema.properties.is_empty());
1102    }
1103
1104    #[test]
1105    fn test_epic_tools_exist() {
1106        let tools = base_tool_definitions();
1107        let epic_names: Vec<&str> = tools
1108            .iter()
1109            .filter(|t| t.category == ToolCategory::Epics)
1110            .map(|t| t.name.as_str())
1111            .collect();
1112        assert!(epic_names.contains(&"get_epics"));
1113        assert!(epic_names.contains(&"create_epic"));
1114        assert!(epic_names.contains(&"update_epic"));
1115    }
1116}