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