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.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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
805pub struct McpOnlyTool {
806 pub name: String,
807 pub description: String,
808 pub input_schema: ToolSchema,
809}
810
811pub 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 #[test]
1004 fn test_tool_definition_serializes_to_json() {
1005 let tools = base_tool_definitions();
1006 let tool = &tools[0]; 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 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 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}