use crate::LinearTools;
use crate::models::ArchiveIssueResult;
use crate::models::CommentResult;
use crate::models::CreateIssueResult;
use crate::models::GetMetadataResult;
use crate::models::IssueDetails;
use crate::models::IssueResult;
use crate::models::SearchResult;
use crate::models::SetRelationResult;
use agentic_tools_core::Tool;
use agentic_tools_core::ToolContext;
use agentic_tools_core::ToolError;
use agentic_tools_core::ToolRegistry;
use futures::future::BoxFuture;
use schemars::JsonSchema;
use serde::Deserialize;
use std::sync::Arc;
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SearchIssuesInput {
#[serde(default)]
pub query: Option<String>,
#[serde(default)]
pub include_comments: Option<bool>,
#[serde(default)]
pub priority: Option<i32>,
#[serde(default)]
pub state_id: Option<String>,
#[serde(default)]
pub assignee_id: Option<String>,
#[serde(default)]
pub creator_id: Option<String>,
#[serde(default)]
pub team_id: Option<String>,
#[serde(default)]
pub project_id: Option<String>,
#[serde(default)]
pub created_after: Option<String>,
#[serde(default)]
pub created_before: Option<String>,
#[serde(default)]
pub updated_after: Option<String>,
#[serde(default)]
pub updated_before: Option<String>,
#[serde(default)]
pub first: Option<i32>,
#[serde(default)]
pub after: Option<String>,
}
#[derive(Clone)]
pub struct SearchIssuesTool {
linear: Arc<LinearTools>,
}
impl SearchIssuesTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for SearchIssuesTool {
type Input = SearchIssuesInput;
type Output = SearchResult;
const NAME: &'static str = "linear_search_issues";
const DESCRIPTION: &'static str = "Search Linear issues using full-text search and/or filters";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.search_issues(
input.query,
input.include_comments,
input.priority,
input.state_id,
input.assignee_id,
input.creator_id,
input.team_id,
input.project_id,
input.created_after,
input.created_before,
input.updated_after,
input.updated_before,
input.first,
input.after,
)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct ReadIssueInput {
pub issue: String,
}
#[derive(Clone)]
pub struct ReadIssueTool {
linear: Arc<LinearTools>,
}
impl ReadIssueTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for ReadIssueTool {
type Input = ReadIssueInput;
type Output = IssueDetails;
const NAME: &'static str = "linear_read_issue";
const DESCRIPTION: &'static str =
"Read a Linear issue by ID, identifier (e.g., ENG-245), or URL";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.read_issue(input.issue)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct CreateIssueInput {
pub team_id: String,
pub title: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub priority: Option<i32>,
#[serde(default)]
pub assignee_id: Option<String>,
#[serde(default)]
pub project_id: Option<String>,
#[serde(default)]
pub state_id: Option<String>,
#[serde(default)]
pub parent_id: Option<String>,
#[serde(default)]
pub label_ids: Vec<String>,
}
#[derive(Clone)]
pub struct CreateIssueTool {
linear: Arc<LinearTools>,
}
impl CreateIssueTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for CreateIssueTool {
type Input = CreateIssueInput;
type Output = CreateIssueResult;
const NAME: &'static str = "linear_create_issue";
const DESCRIPTION: &'static str = "Create a new Linear issue in a team";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.create_issue(
input.team_id,
input.title,
input.description,
input.priority,
input.assignee_id,
input.project_id,
input.state_id,
input.parent_id,
input.label_ids,
)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct AddCommentInput {
pub issue: String,
pub body: String,
#[serde(default)]
pub parent_id: Option<String>,
}
#[derive(Clone)]
pub struct AddCommentTool {
linear: Arc<LinearTools>,
}
impl AddCommentTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for AddCommentTool {
type Input = AddCommentInput;
type Output = CommentResult;
const NAME: &'static str = "linear_add_comment";
const DESCRIPTION: &'static str = "Add a comment to a Linear issue";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.add_comment(input.issue, input.body, input.parent_id)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct GetIssueCommentsInput {
pub issue: String,
}
#[derive(Clone)]
pub struct GetIssueCommentsTool {
linear: Arc<LinearTools>,
}
impl GetIssueCommentsTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for GetIssueCommentsTool {
type Input = GetIssueCommentsInput;
type Output = crate::models::CommentsResult;
const NAME: &'static str = "linear_get_issue_comments";
const DESCRIPTION: &'static str = "Get comments on a Linear issue. Returns 10 comments per call with implicit pagination - call again with the same issue to get more comments.";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.get_issue_comments(input.issue)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct ArchiveIssueInput {
pub issue: String,
}
#[derive(Clone)]
pub struct ArchiveIssueTool {
linear: Arc<LinearTools>,
}
impl ArchiveIssueTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for ArchiveIssueTool {
type Input = ArchiveIssueInput;
type Output = ArchiveIssueResult;
const NAME: &'static str = "linear_archive_issue";
const DESCRIPTION: &'static str =
"Archive a Linear issue by ID, identifier (e.g., ENG-245), or URL";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.archive_issue(input.issue)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct UpdateIssueInput {
pub issue: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub priority: Option<i32>,
#[serde(default)]
pub assignee_id: Option<String>,
#[serde(default)]
pub state_id: Option<String>,
#[serde(default)]
pub project_id: Option<String>,
#[serde(default)]
pub parent_id: Option<String>,
#[serde(default)]
pub label_ids: Option<Vec<String>>,
#[serde(default)]
pub added_label_ids: Option<Vec<String>>,
#[serde(default)]
pub removed_label_ids: Option<Vec<String>>,
#[serde(default)]
pub due_date: Option<String>,
}
#[derive(Clone)]
pub struct UpdateIssueTool {
linear: Arc<LinearTools>,
}
impl UpdateIssueTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for UpdateIssueTool {
type Input = UpdateIssueInput;
type Output = IssueResult;
const NAME: &'static str = "linear_update_issue";
const DESCRIPTION: &'static str = "Update an existing Linear issue. Use linear_get_metadata to look up user IDs, state IDs, project IDs, and label IDs.";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.update_issue(
input.issue,
input.title,
input.description,
input.priority,
input.assignee_id,
input.state_id,
input.project_id,
input.parent_id,
input.label_ids,
input.added_label_ids,
input.removed_label_ids,
input.due_date,
)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SetRelationInput {
pub issue: String,
pub related_issue: String,
#[serde(default)]
pub relation_type: Option<String>,
}
#[derive(Clone)]
pub struct SetRelationTool {
linear: Arc<LinearTools>,
}
impl SetRelationTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for SetRelationTool {
type Input = SetRelationInput;
type Output = SetRelationResult;
const NAME: &'static str = "linear_set_relation";
const DESCRIPTION: &'static str = "Set or remove a relation between two issues. Provide relation_type to create (blocks/duplicate/related), or omit/null to remove any existing relation.";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.set_relation(input.issue, input.related_issue, input.relation_type)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct GetMetadataInput {
pub kind: crate::models::MetadataKind,
#[serde(default)]
pub search: Option<String>,
#[serde(default)]
pub team_id: Option<String>,
#[serde(default)]
pub first: Option<i32>,
#[serde(default)]
pub after: Option<String>,
}
#[derive(Clone)]
pub struct GetMetadataTool {
linear: Arc<LinearTools>,
}
impl GetMetadataTool {
pub fn new(linear: Arc<LinearTools>) -> Self {
Self { linear }
}
}
impl Tool for GetMetadataTool {
type Input = GetMetadataInput;
type Output = GetMetadataResult;
const NAME: &'static str = "linear_get_metadata";
const DESCRIPTION: &'static str = "Look up Linear metadata: users, teams, projects, workflow states, or labels. Use this to discover IDs for filtering and updating issues.";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
let linear = self.linear.clone();
Box::pin(async move {
linear
.get_metadata(
input.kind,
input.search,
input.team_id,
input.first,
input.after,
)
.await
.map_err(map_anyhow_to_tool_error)
})
}
}
pub fn build_registry(linear: Arc<LinearTools>) -> ToolRegistry {
ToolRegistry::builder()
.register::<SearchIssuesTool, ()>(SearchIssuesTool::new(linear.clone()))
.register::<ReadIssueTool, ()>(ReadIssueTool::new(linear.clone()))
.register::<CreateIssueTool, ()>(CreateIssueTool::new(linear.clone()))
.register::<AddCommentTool, ()>(AddCommentTool::new(linear.clone()))
.register::<GetIssueCommentsTool, ()>(GetIssueCommentsTool::new(linear.clone()))
.register::<ArchiveIssueTool, ()>(ArchiveIssueTool::new(linear.clone()))
.register::<UpdateIssueTool, ()>(UpdateIssueTool::new(linear.clone()))
.register::<SetRelationTool, ()>(SetRelationTool::new(linear.clone()))
.register::<GetMetadataTool, ()>(GetMetadataTool::new(linear))
.finish()
}
fn map_anyhow_to_tool_error(e: anyhow::Error) -> ToolError {
let msg = e.to_string();
let lc = msg.to_lowercase();
if lc.contains("permission") || lc.contains("401") || lc.contains("403") {
ToolError::Permission(msg)
} else if lc.contains("not found") || lc.contains("404") {
ToolError::NotFound(msg)
} else if lc.contains("invalid") || lc.contains("bad request") {
ToolError::InvalidInput(msg)
} else if lc.contains("timeout") || lc.contains("network") || lc.contains("rate limit") {
ToolError::External(msg)
} else {
ToolError::Internal(msg)
}
}