asanamcp 0.3.1

MCP server for Asana API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
//! Parameter types for MCP tool inputs.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Level of detail to include in responses.
///
/// Controls how many fields are returned for each resource. Use `minimal` for
/// discovery/listing operations where you just need to identify resources,
/// and `default` for full details.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DetailLevel {
    /// Just gid, name, resource_type - smallest response for discovery
    Minimal,
    /// Curated useful fields per resource type (default behavior)
    #[default]
    Default,
}

/// Parameters for listing workspaces (no parameters needed).
#[derive(Debug, Deserialize, JsonSchema)]
pub struct WorkspacesParams {}

/// The type of resource to fetch.
///
/// Note: The `gid` parameter meaning varies by resource type:
/// - `project`, `portfolio`, `task`, `workspace`, `project_template`, `section`, `tag`:
///   GID of that specific resource
/// - `workspace_favorites`, `workspace_projects`, `workspace_templates`, `workspace_tags`:
///   GID of the workspace
/// - `my_tasks`: GID of the workspace to get user's assigned tasks from
/// - `project_tasks`: GID of the project or portfolio to get tasks from
/// - `task_subtasks`, `task_comments`: GID of the parent task
/// - `status_update`: GID of the status update
/// - `status_updates`: GID of the project, portfolio, or goal
/// - `project_sections`: GID of the project
/// - `all_workspaces`: GID is ignored
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ResourceType {
    /// Get a single project by GID
    Project,
    /// Get a portfolio with nested items (use depth parameter)
    Portfolio,
    /// Get a task with context (use include_* flags)
    Task,
    /// Get user's favorites from a workspace (gid = workspace GID or empty for default)
    #[serde(rename = "workspace_favorites", alias = "favorites")]
    WorkspaceFavorites,
    /// Get all tasks from a project or portfolio (gid = project/portfolio GID)
    #[serde(rename = "project_tasks", alias = "tasks")]
    ProjectTasks,
    /// Get subtasks of a task (gid = parent task GID)
    #[serde(rename = "task_subtasks", alias = "subtasks")]
    TaskSubtasks,
    /// Get comments on a task (gid = task GID)
    #[serde(rename = "task_comments", alias = "comments")]
    TaskComments,
    /// Get a single status update by its own GID (gid = status update GID)
    #[serde(rename = "status_update")]
    StatusUpdate,
    /// List all status updates posted on a project, portfolio, or goal (gid = parent GID)
    #[serde(rename = "status_updates")]
    StatusUpdates,
    /// List all workspaces (gid is ignored)
    #[serde(rename = "all_workspaces", alias = "workspaces")]
    AllWorkspaces,
    /// Get a single workspace by GID
    Workspace,
    /// List project templates (gid = team GID for team templates, or empty for all accessible)
    #[serde(rename = "workspace_templates", alias = "project_templates")]
    WorkspaceTemplates,
    /// Get a single project template by GID
    ProjectTemplate,
    /// List sections in a project (gid = project GID)
    #[serde(rename = "project_sections", alias = "sections")]
    ProjectSections,
    /// Get a single section by GID
    Section,
    /// List tags in a workspace (gid = workspace GID)
    #[serde(rename = "workspace_tags", alias = "tags")]
    WorkspaceTags,
    /// Get a single tag by GID
    Tag,
    /// Get tasks assigned to the current user in a workspace (gid = workspace GID)
    #[serde(rename = "my_tasks", alias = "my_assigned_tasks")]
    MyTasks,
    /// List all projects in a workspace (gid = workspace GID)
    #[serde(rename = "workspace_projects", alias = "projects")]
    WorkspaceProjects,
    /// Get the current authenticated user (gid is ignored)
    #[serde(alias = "current_user")]
    Me,
    /// Get a user by GID
    User,
    /// List all users in a workspace (gid = workspace GID)
    #[serde(rename = "workspace_users", alias = "users")]
    WorkspaceUsers,
    /// Get a team by GID
    Team,
    /// List all teams in an organization/workspace (gid = workspace GID)
    #[serde(rename = "workspace_teams", alias = "teams")]
    WorkspaceTeams,
    /// List users in a team (gid = team GID)
    TeamUsers,
    /// Get custom field settings for a project (gid = project GID)
    #[serde(rename = "project_custom_fields", alias = "custom_fields")]
    ProjectCustomFields,
    /// Get project brief by brief GID. This is the "Key Resources" content on the project Overview tab.
    /// NOTE: This is NOT the "Note" tab feature - that is a separate Asana feature without public API access.
    #[serde(rename = "project_brief")]
    ProjectBrief,
    /// Get project's brief via project GID. Returns the brief (including its GID) embedded in project.
    /// Use this when you have a project GID but need to discover the brief's GID.
    /// NOTE: This is NOT the "Note" tab feature - that is a separate Asana feature without public API access.
    #[serde(rename = "project_project_brief")]
    ProjectProjectBrief,
}

/// Parameters for the universal get tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetParams {
    /// The type of resource to fetch
    pub resource_type: ResourceType,
    /// The GID of the resource. Optional for: all_workspaces, me, and workspace-based operations
    /// (which fall back to ASANA_DEFAULT_WORKSPACE). Required for resource-specific operations.
    #[serde(default)]
    pub gid: Option<String>,
    /// Portfolio/task traversal depth: -1 = unlimited, 0 = none, N = N levels
    #[serde(default)]
    pub depth: Option<i32>,
    /// Subtask expansion depth: -1 = unlimited, 0 = none (default), N = N levels
    #[serde(default)]
    pub subtask_depth: Option<i32>,
    /// Include subtasks when fetching a task (default: true)
    #[serde(default)]
    pub include_subtasks: Option<bool>,
    /// Include dependencies/dependents when fetching a task (default: true)
    #[serde(default)]
    pub include_dependencies: Option<bool>,
    /// Include comments when fetching a task (default: true)
    #[serde(default)]
    pub include_comments: Option<bool>,
    /// Level of detail: "minimal" (gid/name only) or "default" (curated fields).
    /// Use minimal to reduce response size when you just need to identify resources.
    #[serde(default)]
    pub detail_level: DetailLevel,
    /// Additional fields to include beyond the detail_level base set.
    /// Example: ["due_on", "assignee.name"] adds these to minimal or default fields.
    #[serde(default)]
    pub extra_fields: Option<Vec<String>>,
    /// Explicit field list - overrides detail_level and extra_fields entirely.
    /// Use this for full control over exactly which fields are returned.
    /// Example: ["gid", "name", "completed", "assignee.name"]
    #[serde(default)]
    pub opt_fields: Option<Vec<String>>,
}

/// The type of resource to create.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CreateResourceType {
    /// Create a new task
    Task,
    /// Create a subtask under a parent task
    Subtask,
    /// Create a new project
    Project,
    /// Create a project from a template
    #[serde(rename = "project_from_template")]
    ProjectFromTemplate,
    /// Create a new portfolio
    Portfolio,
    /// Create a section in a project
    Section,
    /// Create a comment on a task
    Comment,
    /// Create a status update on a project/portfolio
    #[serde(rename = "status_update")]
    StatusUpdate,
    /// Create a new tag
    Tag,
    /// Duplicate an existing project
    #[serde(rename = "project_duplicate")]
    ProjectDuplicate,
    /// Duplicate an existing task
    #[serde(rename = "task_duplicate")]
    TaskDuplicate,
    /// Create a project brief (the "Key Resources" section on the project Overview tab).
    /// NOTE: This is NOT the "Note" tab feature - that is a separate Asana feature without public API access.
    #[serde(rename = "project_brief")]
    ProjectBrief,
}

/// Date variable for template instantiation.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct DateVariableParam {
    /// The GID of the date variable from the template
    pub gid: String,
    /// The date value in YYYY-MM-DD format
    pub value: String,
}

/// Role assignment for template instantiation.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct RoleAssignmentParam {
    /// The GID of the role from the template
    pub gid: String,
    /// The user GID to assign to this role
    pub value: String,
}

/// Parameters for the create tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateParams {
    /// The type of resource to create
    pub resource_type: CreateResourceType,
    /// Workspace GID (required for task without project, portfolio, tag)
    #[serde(default)]
    pub workspace_gid: Option<String>,
    /// Project GID (for task creation, section creation)
    #[serde(default)]
    pub project_gid: Option<String>,
    /// Task GID (for subtask or comment creation)
    #[serde(default)]
    pub task_gid: Option<String>,
    /// Team GID (for project creation)
    #[serde(default)]
    pub team_gid: Option<String>,
    /// Parent GID (for status update - project or portfolio)
    #[serde(default)]
    pub parent_gid: Option<String>,
    /// Template GID (for project_from_template)
    #[serde(default)]
    pub template_gid: Option<String>,
    /// Date variables for template instantiation
    #[serde(default)]
    pub requested_dates: Option<Vec<DateVariableParam>>,
    /// Role assignments for template instantiation
    #[serde(default)]
    pub requested_roles: Option<Vec<RoleAssignmentParam>>,
    /// Name of the resource
    #[serde(default)]
    pub name: Option<String>,
    /// Plain text notes/description
    #[serde(default)]
    pub notes: Option<String>,
    /// HTML notes/description
    #[serde(default)]
    pub html_notes: Option<String>,
    /// Color (for project, portfolio, tag)
    #[serde(default)]
    pub color: Option<String>,
    /// Due date in YYYY-MM-DD format
    #[serde(default)]
    pub due_on: Option<String>,
    /// Start date in YYYY-MM-DD format
    #[serde(default)]
    pub start_on: Option<String>,
    /// Assignee user GID (for task)
    #[serde(default)]
    pub assignee: Option<String>,
    /// Privacy setting (for project): "public_to_workspace" or "private_to_team"
    #[serde(default)]
    pub privacy_setting: Option<String>,
    /// Whether the resource is public
    #[serde(default)]
    pub public: Option<bool>,
    /// Status type for status_update: "on_track", "at_risk", "off_track", etc.
    #[serde(default)]
    pub status_type: Option<String>,
    /// Title (for status_update)
    #[serde(default)]
    pub title: Option<String>,
    /// Text content (for comment, status_update, project_brief)
    #[serde(default)]
    pub text: Option<String>,
    /// HTML text content (for comment or project_brief - use html_notes for tasks/projects).
    /// Cannot be used together with text; provide one or the other.
    #[serde(default)]
    pub html_text: Option<String>,
    /// Custom field values as {field_gid: value}
    #[serde(default)]
    pub custom_fields: Option<HashMap<String, serde_json::Value>>,
    /// Source GID (for project_duplicate, task_duplicate - the resource to copy)
    #[serde(default)]
    pub source_gid: Option<String>,
    /// What to include when duplicating. For project: members, notes, task_notes, task_assignee,
    /// task_subtasks, task_attachments, task_dates, task_dependencies, task_followers, task_tags.
    /// For task: notes, assignee, subtasks, attachments, tags, followers, projects, dates, dependencies, parent.
    #[serde(default)]
    pub include: Option<Vec<String>>,
    /// Override default fields returned in response. If not provided, returns minimal confirmation.
    /// Example: ["gid", "name", "permalink_url"]
    #[serde(default)]
    pub opt_fields: Option<Vec<String>>,
}

/// Parameters for task search (rich filtering for tasks only).
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct TaskSearchParams {
    /// Workspace GID to search in (uses ASANA_DEFAULT_WORKSPACE if not provided)
    #[serde(default)]
    pub workspace_gid: Option<String>,
    /// Search for tasks containing this text in name or notes
    #[serde(default)]
    pub text: Option<String>,
    /// Filter by assignee user GID (use "me" for current user, "null" for unassigned)
    #[serde(default)]
    pub assignee: Option<String>,
    /// Filter by project GID(s)
    #[serde(default)]
    pub projects: Option<Vec<String>>,
    /// Filter by tag GID(s)
    #[serde(default)]
    pub tags: Option<Vec<String>>,
    /// Filter by section GID(s)
    #[serde(default)]
    pub sections: Option<Vec<String>>,
    /// Filter by completion status
    #[serde(default)]
    pub completed: Option<bool>,
    /// Filter by tasks due on this date (YYYY-MM-DD)
    #[serde(default)]
    pub due_on: Option<String>,
    /// Filter by tasks due on or before this date
    #[serde(default)]
    pub due_on_before: Option<String>,
    /// Filter by tasks due on or after this date
    #[serde(default)]
    pub due_on_after: Option<String>,
    /// Filter by tasks starting on this date
    #[serde(default)]
    pub start_on: Option<String>,
    /// Filter by tasks starting on or before this date
    #[serde(default)]
    pub start_on_before: Option<String>,
    /// Filter by tasks starting on or after this date
    #[serde(default)]
    pub start_on_after: Option<String>,
    /// Filter by tasks modified on or after this datetime (ISO 8601)
    #[serde(default)]
    pub modified_at_after: Option<String>,
    /// Filter by tasks modified on or before this datetime (ISO 8601)
    #[serde(default)]
    pub modified_at_before: Option<String>,
    /// Filter by tasks in portfolios (GID)
    #[serde(default)]
    pub portfolios: Option<Vec<String>>,
    /// Sort by: due_date, created_at, completed_at, likes, modified_at
    #[serde(default)]
    pub sort_by: Option<String>,
    /// Sort order: asc or desc
    #[serde(default)]
    pub sort_ascending: Option<bool>,
    /// Level of detail: "minimal" (gid/name only) or "default" (curated fields).
    /// Use minimal to reduce response size when you just need to identify tasks.
    #[serde(default)]
    pub detail_level: DetailLevel,
    /// Additional fields to include beyond the detail_level base set.
    /// Example: ["due_on", "assignee.name"] adds these to minimal or default fields.
    #[serde(default)]
    pub extra_fields: Option<Vec<String>>,
    /// Explicit field list - overrides detail_level and extra_fields entirely.
    /// Use this for full control over exactly which fields are returned.
    /// Example: ["gid", "name", "completed", "assignee.name", "due_on"]
    #[serde(default)]
    pub opt_fields: Option<Vec<String>>,
}

/// Resource types that can be searched via typeahead.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SearchableResourceType {
    /// Search for projects
    Project,
    /// Search for project templates
    #[serde(rename = "project_template")]
    ProjectTemplate,
    /// Search for portfolios
    Portfolio,
    /// Search for users/people
    User,
    /// Search for teams
    Team,
    /// Search for tags
    Tag,
    /// Search for goals
    Goal,
}

impl SearchableResourceType {
    /// Get the API string for this resource type.
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Project => "project",
            Self::ProjectTemplate => "project_template",
            Self::Portfolio => "portfolio",
            Self::User => "user",
            Self::Team => "team",
            Self::Tag => "tag",
            Self::Goal => "goal",
        }
    }
}

/// Parameters for resource search (typeahead-based search across resource types).
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ResourceSearchParams {
    /// The search query (searches resource names)
    pub query: Option<String>,
    /// The type of resource to search for
    pub resource_type: SearchableResourceType,
    /// Workspace GID to search in (uses ASANA_DEFAULT_WORKSPACE if not provided)
    #[serde(default)]
    pub workspace_gid: Option<String>,
    /// Maximum number of results to return (default 20, max 100)
    #[serde(default)]
    pub count: Option<u32>,
}

/// The type of resource to update.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum UpdateResourceType {
    /// Update a task
    Task,
    /// Update a project
    Project,
    /// Update a portfolio
    Portfolio,
    /// Update a section
    Section,
    /// Update a tag
    Tag,
    /// Update a comment/story
    Comment,
    /// Update a status update
    #[serde(rename = "status_update")]
    StatusUpdate,
    /// Update a project brief (the "Key Resources" section on the project Overview tab).
    /// NOTE: This is NOT the "Note" tab feature - that is a separate Asana feature without public API access.
    #[serde(rename = "project_brief")]
    ProjectBrief,
}

/// Parameters for the update tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateParams {
    /// The type of resource to update
    pub resource_type: UpdateResourceType,
    /// The GID of the resource to update
    pub gid: String,
    /// New name
    #[serde(default)]
    pub name: Option<String>,
    /// New plain text notes/description
    #[serde(default)]
    pub notes: Option<String>,
    /// New HTML notes/description
    #[serde(default)]
    pub html_notes: Option<String>,
    /// Mark task as completed/incomplete
    #[serde(default)]
    pub completed: Option<bool>,
    /// New due date in YYYY-MM-DD format
    #[serde(default)]
    pub due_on: Option<String>,
    /// New start date in YYYY-MM-DD format
    #[serde(default)]
    pub start_on: Option<String>,
    /// New assignee user GID
    #[serde(default)]
    pub assignee: Option<String>,
    /// New color
    #[serde(default)]
    pub color: Option<String>,
    /// Archive/unarchive project
    #[serde(default)]
    pub archived: Option<bool>,
    /// New privacy setting
    #[serde(default)]
    pub privacy_setting: Option<String>,
    /// Make public/private
    #[serde(default)]
    pub public: Option<bool>,
    /// New text content (for comment, status_update, project_brief)
    #[serde(default)]
    pub text: Option<String>,
    /// New HTML text content (for comment or project_brief - use html_notes for tasks/projects).
    /// Cannot be used together with text; provide one or the other.
    #[serde(default)]
    pub html_text: Option<String>,
    /// New title (for status_update)
    #[serde(default)]
    pub title: Option<String>,
    /// New status type (for status_update): "on_track", "at_risk", "off_track", etc.
    #[serde(default)]
    pub status_type: Option<String>,
    /// Updated custom field values
    #[serde(default)]
    pub custom_fields: Option<HashMap<String, serde_json::Value>>,
    /// Override default fields returned in response. If not provided, returns curated fields.
    /// Example: ["gid", "name", "modified_at"]
    #[serde(default)]
    pub opt_fields: Option<Vec<String>>,
}

/// The type of resource to delete.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DeleteResourceType {
    /// Delete a task
    Task,
    /// Delete a project
    Project,
    /// Delete a portfolio
    Portfolio,
    /// Delete a section
    Section,
    /// Delete a tag
    Tag,
    /// Delete a comment/story
    Comment,
    /// Delete a status update
    #[serde(rename = "status_update")]
    StatusUpdate,
    /// Delete a project brief (the "Key Resources" section on the project Overview tab).
    /// NOTE: This is NOT the "Note" tab feature - that is a separate Asana feature without public API access.
    #[serde(rename = "project_brief")]
    ProjectBrief,
}

impl DeleteResourceType {
    /// Get the API endpoint path segment for this resource type.
    pub fn endpoint(&self) -> &'static str {
        match self {
            Self::Task => "tasks",
            Self::Project => "projects",
            Self::Portfolio => "portfolios",
            Self::Section => "sections",
            Self::Tag => "tags",
            Self::Comment => "stories",
            Self::StatusUpdate => "status_updates",
            Self::ProjectBrief => "project_briefs",
        }
    }

    /// Get the human-readable display name for this resource type.
    pub fn display_name(&self) -> &'static str {
        match self {
            Self::Task => "task",
            Self::Project => "project",
            Self::Portfolio => "portfolio",
            Self::Section => "section",
            Self::Tag => "tag",
            Self::Comment => "comment",
            Self::StatusUpdate => "status update",
            Self::ProjectBrief => "project brief",
        }
    }
}

/// Parameters for the delete tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DeleteParams {
    /// The type of resource to delete
    pub resource_type: DeleteResourceType,
    /// The GID of the resource to delete
    pub gid: String,
}

/// The action to perform on a relationship.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum LinkAction {
    /// Add a relationship
    Add,
    /// Remove a relationship
    Remove,
}

/// The type of relationship to manage.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RelationshipType {
    /// Task <-> Project membership
    #[serde(rename = "task_project")]
    TaskProject,
    /// Task <-> Tag association
    #[serde(rename = "task_tag")]
    TaskTag,
    /// Task parent-child relationship
    #[serde(rename = "task_parent")]
    TaskParent,
    /// Task dependency (blocking) relationship
    #[serde(rename = "task_dependency")]
    TaskDependency,
    /// Task dependent (blocked by) relationship
    #[serde(rename = "task_dependent")]
    TaskDependent,
    /// Task follower
    #[serde(rename = "task_follower")]
    TaskFollower,
    /// Portfolio <-> Project/Portfolio item
    #[serde(rename = "portfolio_item")]
    PortfolioItem,
    /// Portfolio member
    #[serde(rename = "portfolio_member")]
    PortfolioMember,
    /// Project member
    #[serde(rename = "project_member")]
    ProjectMember,
    /// Project follower
    #[serde(rename = "project_follower")]
    ProjectFollower,
}

/// Parameters for the link tool.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct LinkParams {
    /// Whether to add or remove the relationship
    pub action: LinkAction,
    /// The type of relationship to manage
    pub relationship: RelationshipType,
    /// The GID of the target resource (task, project, or portfolio)
    pub target_gid: String,
    /// Single item GID for the relationship
    #[serde(default)]
    pub item_gid: Option<String>,
    /// Multiple item GIDs for bulk operations
    #[serde(default)]
    pub item_gids: Option<Vec<String>>,
    /// Section GID for task-project relationships
    #[serde(default)]
    pub section_gid: Option<String>,
    /// Insert before this GID (for ordering)
    #[serde(default)]
    pub insert_before: Option<String>,
    /// Insert after this GID (for ordering)
    #[serde(default)]
    pub insert_after: Option<String>,
}