diff --git a/backend/__tests__/fixtures/hostedMcp/linear-tools-list-2026-10-19.json b/backend/__tests__/fixtures/hostedMcp/linear-tools-list-2026-10-19.json new file mode 100644 index 0000000000..03092e494a --- /dev/null +++ b/backend/__tests__/fixtures/hostedMcp/linear-tools-list-2026-10-19.json @@ -0,0 +1,4628 @@ +{ + "server": "https://mcp.linear.app/mcp", + "capturedAt": "2026-10-19T07:00:52.954Z", + "scopeRequested": "read write openid", + "tokenShape": { + "keys": [ + "access_token", + "expires_in", + "id_token", + "issuer", + "refresh_token", + "scope", + "token_type" + ], + "tokenType": "bearer", + "grantedScope": "read write openid", + "expiresIn": 86100, + "hasRefreshToken": true + }, + "toolCount": 68, + "tools": [ + { + "name": "get_attachment", + "title": "Get attachment", + "description": "Retrieve an issue attachment by ID. Use format=url to get its title and download URL without fetching the file content. Linear upload URLs are signed for five minutes; call again to refresh. External URLs are returned unchanged. Files embedded in comments have no issue attachment ID; use the URLs in list_comments attachments instead. Download and process documents with your own file tools.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Issue attachment ID from get_issue's attachments array (attachments[].id)" + }, + "format": { + "description": "Omit or use content to fetch a Linear-hosted file. Use url for attachment metadata without fetching the file; also supports external links", + "type": "string", + "enum": [ + "content", + "url" + ] + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "prepare_attachment_upload", + "title": "Prepare attachment upload", + "description": "Prepare a direct Linear file upload for an existing issue.\nUploaded files appear in the issue's Resources; they are not native Linear Documents. For written findings or proposals, prefer updating the issue description or using save_document with issue to create an editable Linear Document, unless the user requests a file. The upload workflow creates the Resources link; do not also add the upload URL to save_issue's links field. To revise a file, upload a replacement. To remove its Resources link, use delete_attachment with the attachment ID returned by get_issue. These tools do not edit uploaded file contents, permanently delete stored files, or inspect or change per-file permissions. Explain these limits when relevant.\n\nWorkflow:\n1. Call this tool with issue, filename, contentType, and size.\n2. Upload raw bytes with PUT to uploadRequest.url outside MCP.\n3. All headers in uploadRequest.headers are part of the signed request, so send them verbatim.\n4. After PUT succeeds, call create_attachment_from_upload with assetUrl to link it to the issue.\n\nDo not base64-encode or transform the file. Use curl --data-binary @path or fetch(url, { method: 'PUT', body: blob }).\nOmitting or modifying any signed header, including casing, will return HTTP 403.\nThe signed URL must be used within 60 seconds or it will expire.\n\nUpload sequencing:\nPrepare, PUT, and finalize one file before calling this tool for another file.\nDo not batch multiple prepare_attachment_upload calls before starting the PUTs because earlier signed URLs can expire while later files are prepared.\n\nExample:\ncurl -X PUT --data-binary @file.png \\\n -H \"content-type: image/png\" \\\n -H \"x-goog-content-length-range: N,N\" \\\n -H \"cache-control: public, max-age=31536000\" \\\n -H 'Content-Disposition: attachment; filename=\"file.png\"' \\\n \"\"", + "inputSchema": { + "type": "object", + "properties": { + "issue": { + "type": "string", + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues" + }, + "filename": { + "type": "string", + "description": "Filename for the upload, e.g. screenshot.png" + }, + "contentType": { + "type": "string", + "description": "MIME type, e.g. image/png or application/pdf" + }, + "size": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "Exact file size in bytes. Must be smaller than 2 GB." + }, + "title": { + "description": "Suggested attachment title for the finalize step", + "type": "string" + }, + "subtitle": { + "description": "Suggested attachment subtitle for the finalize step", + "type": "string" + } + }, + "required": [ + "issue", + "filename", + "contentType", + "size" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "create_attachment_from_upload", + "title": "Create attachment from upload", + "description": "Link an already-uploaded Linear assetUrl to an existing issue as an attachment.\nUploaded files appear in the issue's Resources; they are not native Linear Documents. For written findings or proposals, prefer updating the issue description or using save_document with issue to create an editable Linear Document, unless the user requests a file. The upload workflow creates the Resources link; do not also add the upload URL to save_issue's links field. To revise a file, upload a replacement. To remove its Resources link, use delete_attachment with the attachment ID returned by get_issue. These tools do not edit uploaded file contents, permanently delete stored files, or inspect or change per-file permissions. Explain these limits when relevant.\n\nUse this only after:\n1. prepare_attachment_upload returned an assetUrl and uploadRequest.\n2. The client successfully PUT raw file bytes to uploadRequest.url.\n\nThis tool does not upload file content. It only creates the Linear attachment row.\nIf the direct upload failed or the signed URL expired, rerun prepare_attachment_upload and upload again.", + "inputSchema": { + "type": "object", + "properties": { + "issue": { + "type": "string", + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues" + }, + "assetUrl": { + "type": "string", + "format": "uri", + "description": "Linear upload assetUrl returned by prepare_attachment_upload" + }, + "title": { + "description": "Attachment title. Defaults to filename or asset URL", + "type": "string" + }, + "subtitle": { + "description": "Optional attachment subtitle", + "type": "string" + } + }, + "required": [ + "issue", + "assetUrl" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "create_attachment", + "title": "Create attachment (deprecated)", + "description": "Deprecated fallback for tiny files only. Accepts base64 file content, verifies SHA-256 checksum, and uploads it through the MCP worker.\nPrefer `prepare_attachment_upload` plus direct PUT plus `create_attachment_from_upload`.\nUploaded files appear in the issue's Resources; they are not native Linear Documents. For written findings or proposals, prefer updating the issue description or using save_document with issue to create an editable Linear Document, unless the user requests a file. The upload workflow creates the Resources link; do not also add the upload URL to save_issue's links field. To revise a file, upload a replacement. To remove its Resources link, use delete_attachment with the attachment ID returned by get_issue. These tools do not edit uploaded file contents, permanently delete stored files, or inspect or change per-file permissions. Explain these limits when relevant.\n\nCRITICAL: Do not print base64Content and then copy it into this tool call.\nOpaque base64 copied through model-visible text is easy to corrupt.\n\nGenerate base64Content mechanically from the source bytes and pass it through a programmatic argument construction path whenever available. Before calling this tool, verify the values are consistent:\n\nUnix-like shells:\nfile=\"/path/to/file\"\nbase64Content=$(base64 < \"$file\" | tr -d '\\n\\r')\nsha256=$(shasum -a 256 \"$file\" | awk '{print $1}')\ndecodedSha256=$(printf '%s' \"$base64Content\" | base64 -d | shasum -a 256 | awk '{print $1}')\nsize=$(wc -c < \"$file\" | tr -d ' ') # optional; stat can also get file byte size\ndecodedSize=$(printf '%s' \"$base64Content\" | base64 -d | wc -c | tr -d ' ')\n\nPowerShell:\n$file = 'C:\\path\\to\\file'\n$bytes = [IO.File]::ReadAllBytes($file)\n$base64Content = [Convert]::ToBase64String($bytes)\n$sha256 = (Get-FileHash -Algorithm SHA256 -Path $file).Hash.ToLower()\n$size = $bytes.Length # optional\n$decoded = [Convert]::FromBase64String($base64Content)\n$decodedStream = [IO.MemoryStream]::new($decoded)\n$decodedSha256 = (Get-FileHash -Algorithm SHA256 -InputStream $decodedStream).Hash.ToLower()\n$decodedSize = $decoded.Length\n\ndecodedSha256 must equal sha256. If you pass size, decodedSize must equal size.\nPass `sha256`. Passing `size` is optional, but recommended for clearer mismatch errors.", + "inputSchema": { + "type": "object", + "properties": { + "issue": { + "type": "string", + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues" + }, + "base64Content": { + "type": "string", + "description": "Deprecated base64-encoded file content to upload" + }, + "filename": { + "type": "string", + "description": "Filename for the upload (e.g., 'screenshot.png')" + }, + "contentType": { + "type": "string", + "description": "MIME type for the upload (e.g., 'image/png', 'application/pdf')" + }, + "size": { + "description": "Optional expected decoded file size in bytes. Rejects the upload if it does not match.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "sha256": { + "type": "string", + "pattern": "^[a-fA-F0-9]{64}$", + "description": "Expected SHA-256 hex digest of the decoded file bytes." + }, + "title": { + "description": "Optional title for the attachment", + "type": "string" + }, + "subtitle": { + "description": "Optional subtitle for the attachment", + "type": "string" + } + }, + "required": [ + "issue", + "base64Content", + "filename", + "contentType", + "sha256" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "delete_attachment", + "title": "Delete attachment", + "description": "Remove an attachment link from an issue's Resources. This does not delete the stored file. Use the attachment ID from get_issue's attachments array, not a UUID from the upload URL.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Issue attachment ID from get_issue's attachments array (attachments[].id)" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_agent_skills", + "title": "List agent skills", + "description": "List Linear Agent skills available to the authenticated user.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_agent_skill", + "title": "Get agent skill", + "description": "Retrieve a Linear Agent skill by ID, including its full markdown instructions.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Agent skill ID" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_comments", + "title": "List comments", + "description": "List comments on a Linear issue, project, initiative, document, project milestone, or project/initiative status update. Provide exactly one of `issueId`, `projectId`, `initiativeId`, `documentId`, `milestoneId`, or `statusUpdateId`. For issues, projects, and initiatives this returns both top-level discussion threads and inline description comments. Inline (anchored) comments carry a non-null `quotedText` set to the snippet of description text they reference. Returned comments include `author` and nullable `onBehalfOf`; when set, `onBehalfOf` identifies the user the agent author acted for. Files uploaded in comment bodies are returned in `attachments` with titles and download URLs, not issue attachment IDs. Signed upload URLs expire after five minutes; call list_comments again to refresh them. Download documents using these URLs and process them with your own file tools.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "issueId": { + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues (provide exactly one parent)", + "type": "string" + }, + "projectId": { + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug (provide exactly one parent)", + "type": "string" + }, + "initiativeId": { + "description": "Initiative name, ID, identifier (e.g., I-123), or slug (provide exactly one parent)", + "type": "string" + }, + "documentId": { + "description": "Document ID or slug (provide exactly one parent)", + "type": "string" + }, + "milestoneId": { + "description": "Milestone UUID (provide exactly one parent). Resolve milestone names via `list_milestones` first.", + "type": "string" + }, + "statusUpdateId": { + "description": "Status update UUID (provide exactly one parent). Resolve status updates via `get_status_updates` first.", + "type": "string" + }, + "statusUpdateType": { + "description": "Type of status update named by `statusUpdateId`, as returned by `get_status_updates`. Only valid together with `statusUpdateId`; omit to check both project and initiative status updates.", + "type": "string", + "enum": [ + "project", + "initiative" + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_comment", + "title": "Save comment", + "description": "Create or update a comment on a Linear issue, project, initiative, document, project milestone, or project/initiative status update. If `id` is provided, updates the existing comment; otherwise creates a new one. To start a new thread, pass `body` and exactly one of `issueId`, `projectId`, `initiativeId`, `documentId`, `milestoneId`, or `statusUpdateId` — comments on issues/projects/initiatives become top-level discussion threads; comments on documents/milestones become description comments. A status update holds a single thread, so a comment on an update that already has one is added to it as a reply. To reply to an existing thread, pass `parentId` and `body`; the reply inherits the parent's thread type, so no entity reference is needed. Parent reference fields are ignored when `id` or `parentId` is provided (`statusUpdateType` is rejected instead).", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "description": "Comment ID. If provided, updates the existing comment", + "type": "string" + }, + "issueId": { + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues (provide exactly one parent)", + "type": "string" + }, + "projectId": { + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug (provide exactly one parent)", + "type": "string" + }, + "initiativeId": { + "description": "Initiative name, ID, identifier (e.g., I-123), or slug (provide exactly one parent)", + "type": "string" + }, + "documentId": { + "description": "Document ID or slug (provide exactly one parent)", + "type": "string" + }, + "milestoneId": { + "description": "Milestone UUID (provide exactly one parent). Resolve milestone names via `list_milestones` first.", + "type": "string" + }, + "statusUpdateId": { + "description": "Status update UUID (provide exactly one parent). Resolve status updates via `get_status_updates` first.", + "type": "string" + }, + "statusUpdateType": { + "description": "Type of status update named by `statusUpdateId`, as returned by `get_status_updates`. Only valid together with `statusUpdateId`; omit to check both project and initiative status updates.", + "type": "string", + "enum": [ + "project", + "initiative" + ] + }, + "parentId": { + "description": "Parent comment ID (for replies, only when creating)", + "type": "string" + }, + "body": { + "type": "string", + "description": "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)" + } + }, + "required": [ + "body" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "delete_comment", + "title": "Delete comment", + "description": "Delete a Linear comment. Inline description comments (those with non-null `quotedText`) anchor a mark in the editor, so their root cannot be deleted — delete the replies individually or resolve the thread instead.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Comment ID" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_cycles", + "title": "List cycles", + "description": "List a team's cycles with their IDs, numbers, dates, and issue and scope histories", + "inputSchema": { + "type": "object", + "properties": { + "teamId": { + "type": "string", + "description": "Team UUID from list_teams or get_team; names and keys are not accepted" + }, + "type": { + "description": "Filter relative to the team's current cycle. Omit to return cycles without this filter", + "type": "string", + "enum": [ + "current", + "previous", + "next" + ] + } + }, + "required": [ + "teamId" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_custom_views", + "title": "List custom views", + "description": "Find saved issue, project, and initiative views accessible to the user, including personal, workspace, and team views. Excludes archived views and views scoped to a project or initiative. Returns view metadata, not the records matching its filters.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Search custom view names", + "type": "string" + }, + "type": { + "description": "Entity type displayed by the view", + "type": "string", + "enum": [ + "Issue", + "Project", + "Initiative" + ] + }, + "team": { + "description": "Team name or ID", + "type": "string" + }, + "shared": { + "description": "True for shared views, false for personal views", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_document", + "title": "Get document", + "description": "Retrieve a Linear document by ID or slug", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Document ID or slug" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_documents", + "title": "List documents", + "description": "List documents in the user's Linear workspace", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Search query", + "type": "string" + }, + "projectId": { + "description": "Filter by project ID, identifier (e.g., 'P-ENG-123'), or slug", + "type": "string" + }, + "initiativeId": { + "description": "Filter by initiative ID, identifier (e.g., 'I-123'), or slug", + "type": "string" + }, + "teamId": { + "description": "Filter by team ID", + "type": "string" + }, + "creatorId": { + "description": "Filter by creator ID", + "type": "string" + }, + "createdAt": { + "description": "Created after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "updatedAt": { + "description": "Updated after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + }, + "fields": { + "description": "Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "id", + "title", + "content", + "url", + "createdAt", + "updatedAt", + "archivedAt", + "creator", + "updatedBy", + "project", + "initiative", + "team", + "issue" + ] + } + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_document", + "title": "Save document", + "description": "Create or update a Linear document. If `id` is provided, edits the existing document; otherwise creates a new one. When creating, `title` is required and exactly one parent (`project`, `issue`, `initiative`, `cycle`, or `team`) must be specified. On update, passing a parent reparents the document. To change parts of the content without resending all of it, pass `patch` instead of `content`.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "description": "Document ID or slug to update. Omit to create a new document.", + "type": "string" + }, + "title": { + "description": "Document title (required when creating)", + "type": "string" + }, + "content": { + "description": "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + "type": "string" + }, + "patch": { + "description": "Partial edits applied to the current content, in order and atomically (one failing operation aborts the whole save). Every anchor string must match the current content exactly once. Only valid on update, in place of the full content/description field", + "minItems": 1, + "maxItems": 50, + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "replace" + }, + "old_string": { + "type": "string", + "minLength": 1, + "description": "Exact text to replace. Must match the current content exactly once" + }, + "new_string": { + "type": "string", + "description": "Replacement text. Empty string deletes the match" + }, + "replace_all": { + "description": "Replace every occurrence instead of requiring a unique match", + "type": "boolean" + } + }, + "required": [ + "op", + "old_string", + "new_string" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "insert_before" + }, + "anchor": { + "type": "string", + "minLength": 1, + "description": "Exact text to insert before. Must match the current content exactly once" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert directly before the anchor. Include separators (e.g. \"\\n\\n\")" + } + }, + "required": [ + "op", + "anchor", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "insert_after" + }, + "anchor": { + "type": "string", + "minLength": 1, + "description": "Exact text to insert after. Must match the current content exactly once" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert directly after the anchor. Include separators (e.g. \"\\n\\n\")" + } + }, + "required": [ + "op", + "anchor", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "prepend" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert at the very start of the content" + } + }, + "required": [ + "op", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "append" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert at the very end of the content" + } + }, + "required": [ + "op", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "replace_range" + }, + "from": { + "type": "string", + "minLength": 1, + "description": "Exact text where the range starts (inclusive). Must match exactly once" + }, + "to": { + "type": "string", + "minLength": 1, + "description": "Exact text where the range ends (exclusive, stays in place). Must match exactly once after `from`" + }, + "new_string": { + "type": "string", + "description": "Text replacing the range. Empty string deletes it" + } + }, + "required": [ + "op", + "from", + "to", + "new_string" + ] + } + ] + } + }, + "project": { + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug", + "type": "string" + }, + "issue": { + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues", + "type": "string" + }, + "initiative": { + "description": "Initiative name, ID, identifier (e.g., I-123), or slug", + "type": "string" + }, + "cycle": { + "description": "Cycle name, number, or ID. When passing a name or number, also pass `team` to disambiguate.", + "type": "string" + }, + "team": { + "description": "Team name or ID. Attaches the document to the team, unless `cycle` is also passed, in which case it disambiguates the cycle.", + "type": "string" + }, + "icon": { + "description": "Icon name or emoji code (e.g. \"Rocket\" or \":eagle:\"), not a raw Unicode emoji", + "type": "string" + }, + "color": { + "description": "Hex color", + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "extract_images", + "title": "Extract images", + "description": "Extract and fetch images from markdown content. Use this to view screenshots, diagrams, or other images embedded in Linear issues, comments, or documents. Pass the markdown content (e.g., issue description) and receive the images as viewable data.", + "inputSchema": { + "type": "object", + "properties": { + "markdown": { + "type": "string", + "description": "Markdown content containing image references (e.g., issue description, comment body)" + } + }, + "required": [ + "markdown" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_issue", + "title": "Get issue", + "description": "Retrieve detailed information about an issue by ID, including attachments, git branch name, and active Triage Intelligence suggestions when the issue is in triage. A nonempty fields selection overrides the include options; request optional fields directly in fields.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues" + }, + "fields": { + "description": "Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "id", + "uuid", + "title", + "description", + "projectMilestone", + "priority", + "estimate", + "url", + "gitBranchName", + "createdAt", + "updatedAt", + "archivedAt", + "completedAt", + "startedAt", + "canceledAt", + "startedTriageAt", + "triagedAt", + "dueDate", + "slaStartedAt", + "slaMediumRiskAt", + "slaHighRiskAt", + "slaBreachesAt", + "slaType", + "status", + "statusType", + "labels", + "triageIntel", + "createdBy", + "createdById", + "assignee", + "assigneeId", + "delegate", + "delegateId", + "project", + "projectId", + "parentId", + "team", + "teamId", + "cycleId", + "attachments", + "documents", + "stateHistory", + "relations", + "customerNeeds", + "releases" + ] + } + }, + "includeRelations": { + "default": false, + "description": "Include blocking/related/duplicate relations", + "type": "boolean" + }, + "includeCustomerNeeds": { + "default": false, + "description": "Include associated customer needs", + "type": "boolean" + }, + "includeReleases": { + "default": false, + "description": "Include associated releases", + "type": "boolean" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_issues", + "title": "List issues", + "description": "List issues in the user's Linear workspace, including active Triage Intelligence suggestions for issues in triage. For issues assigned to me, use \"me\" as the assignee. For issues created by me, use \"me\" as the creator. Use \"null\" for no assignee.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Search issue title or description. Cannot be combined with customView", + "type": "string" + }, + "customView": { + "description": "Saved view ID, URL, slug, or exact name. Use list_custom_views to discover views. Applies saved filters and scope; other filters narrow the results. Uses list ordering, not saved display settings", + "type": "string", + "minLength": 1 + }, + "team": { + "description": "Team name or ID", + "type": "string" + }, + "includeSubTeams": { + "default": false, + "description": "Include results from the selected team and all descendants when team is set. Team ID filters already include descendants; this also expands name and key filters", + "type": "boolean" + }, + "state": { + "description": "State type, name, or ID", + "type": "string" + }, + "cycle": { + "description": "Cycle name, number, or ID", + "type": "string" + }, + "label": { + "description": "Label name or ID", + "type": "string" + }, + "assignee": { + "description": "User ID, name, email, or \"me\"", + "type": [ + "string", + "null" + ] + }, + "creator": { + "description": "User ID, name, email, or \"me\"", + "type": "string" + }, + "delegate": { + "description": "Agent name or ID. When the user asks to delegate to \"Linear\" or \"the Linear agent\", this refers to the \"Linear\" app user specifically", + "type": "string" + }, + "project": { + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug", + "type": "string" + }, + "release": { + "description": "Release ID or slug", + "type": "string" + }, + "priority": { + "description": "0=None, 1=Urgent, 2=High, 3=Medium, 4=Low", + "type": "number" + }, + "parentId": { + "description": "Parent issue ID or identifier (e.g., LIN-123)", + "type": "string" + }, + "fields": { + "description": "Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "id", + "uuid", + "title", + "description", + "projectMilestone", + "priority", + "estimate", + "url", + "gitBranchName", + "createdAt", + "updatedAt", + "archivedAt", + "completedAt", + "startedAt", + "canceledAt", + "startedTriageAt", + "triagedAt", + "dueDate", + "slaStartedAt", + "slaMediumRiskAt", + "slaHighRiskAt", + "slaBreachesAt", + "slaType", + "status", + "statusType", + "labels", + "triageIntel", + "createdBy", + "createdById", + "assignee", + "assigneeId", + "delegate", + "delegateId", + "project", + "projectId", + "parentId", + "team", + "teamId", + "cycleId" + ] + } + }, + "createdAt": { + "description": "Created after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "updatedAt": { + "description": "Updated after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "triagedAt": { + "description": "Left triage after: ISO-8601 date/duration (e.g., -P1D). Issues still in triage have no triagedAt; list them with state \"triage\"", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_issue", + "title": "Save issue", + "description": "Create or update a Linear issue. If `id` is provided, updates the existing issue; otherwise creates a new one. When creating, `team` is required, and `title` is required unless `template` is set. Note: use `assignee` (not `assigneeId`) to set the assignee — it accepts a user ID, name, email, or \"me\".", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "description": "Only for updating an existing issue. Pass the issue ID or identifier (e.g., LIN-123). Do NOT pass this parameter when creating a new issue.", + "type": "string" + }, + "title": { + "description": "Issue title (required when creating, unless template is set)", + "type": "string" + }, + "description": { + "description": "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + "type": "string" + }, + "patch": { + "description": "Partial edits applied to the current content, in order and atomically (one failing operation aborts the whole save). Every anchor string must match the current content exactly once. Only valid on update, in place of the full content/description field", + "minItems": 1, + "maxItems": 50, + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "replace" + }, + "old_string": { + "type": "string", + "minLength": 1, + "description": "Exact text to replace. Must match the current content exactly once" + }, + "new_string": { + "type": "string", + "description": "Replacement text. Empty string deletes the match" + }, + "replace_all": { + "description": "Replace every occurrence instead of requiring a unique match", + "type": "boolean" + } + }, + "required": [ + "op", + "old_string", + "new_string" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "insert_before" + }, + "anchor": { + "type": "string", + "minLength": 1, + "description": "Exact text to insert before. Must match the current content exactly once" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert directly before the anchor. Include separators (e.g. \"\\n\\n\")" + } + }, + "required": [ + "op", + "anchor", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "insert_after" + }, + "anchor": { + "type": "string", + "minLength": 1, + "description": "Exact text to insert after. Must match the current content exactly once" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert directly after the anchor. Include separators (e.g. \"\\n\\n\")" + } + }, + "required": [ + "op", + "anchor", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "prepend" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert at the very start of the content" + } + }, + "required": [ + "op", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "append" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert at the very end of the content" + } + }, + "required": [ + "op", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "replace_range" + }, + "from": { + "type": "string", + "minLength": 1, + "description": "Exact text where the range starts (inclusive). Must match exactly once" + }, + "to": { + "type": "string", + "minLength": 1, + "description": "Exact text where the range ends (exclusive, stays in place). Must match exactly once after `from`" + }, + "new_string": { + "type": "string", + "description": "Text replacing the range. Empty string deletes it" + } + }, + "required": [ + "op", + "from", + "to", + "new_string" + ] + } + ] + } + }, + "team": { + "description": "Team name or ID (required when creating)", + "type": "string" + }, + "template": { + "description": "Template name or ID. Applied on create only. The template fills in the content, fields, and sub-issues server-side. A `description` replaces the template body rather than merging with it, but an empty `description` keeps the template body. Labels merge with the template's own", + "type": "string" + }, + "cycle": { + "description": "Cycle name, number, or ID. Null to remove", + "type": [ + "string", + "null" + ] + }, + "milestone": { + "description": "Milestone name or ID", + "type": "string" + }, + "priority": { + "description": "0=None, 1=Urgent, 2=High, 3=Medium, 4=Low", + "type": "number" + }, + "project": { + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug. Null to remove", + "type": [ + "string", + "null" + ] + }, + "state": { + "description": "State type, name, or ID", + "type": "string" + }, + "assignee": { + "description": "User ID, name, email, or \"me\". Null to remove", + "type": [ + "string", + "null" + ] + }, + "delegate": { + "description": "Agent name or ID. When the user asks to delegate to \"Linear\" or \"the Linear agent\", this refers to the \"Linear\" app user specifically. Null to remove", + "type": [ + "string", + "null" + ] + }, + "labels": { + "description": "Label names or IDs as a JSON array of strings (e.g. [\"Bug\", \"Urgent\"]). Replaces the full label set; existing labels not included are removed. Omit to leave labels unchanged. Cannot be combined with addLabels/removeLabels; prefer those for incremental changes", + "type": "array", + "items": { + "type": "string" + } + }, + "addLabels": { + "description": "Label names or IDs as a JSON array of strings (e.g. [\"Bug\", \"Urgent\"]) to add. Append-only; existing labels are never removed. Cannot be combined with a team change", + "type": "array", + "items": { + "type": "string" + } + }, + "removeLabels": { + "description": "Label names or IDs as a JSON array of strings (e.g. [\"Bug\", \"Urgent\"]) to remove. Only valid when updating an existing issue. Cannot be combined with a team change", + "type": "array", + "items": { + "type": "string" + } + }, + "dueDate": { + "description": "Due date (ISO format). On update, pass null to remove the due date", + "type": [ + "string", + "null" + ] + }, + "slaBreachesAt": { + "description": "ISO-8601 timestamp when the SLA will breach. On update, pass null to remove the SLA", + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + { + "type": "null" + } + ] + }, + "slaType": { + "description": "SLA day counting type: \"all\" or \"onlyBusinessDays\". Only use with slaBreachesAt; null means unset", + "anyOf": [ + { + "type": "string", + "enum": [ + "all", + "onlyBusinessDays" + ] + }, + { + "type": "null" + } + ] + }, + "parentId": { + "description": "Parent issue ID or identifier (e.g., LIN-123). Null to remove", + "type": [ + "string", + "null" + ] + }, + "estimate": { + "description": "Issue estimate value. On create, pass null or omit for no estimate. On update, pass null to clear the estimate; omitting leaves it unchanged. 0 is a real estimate only on teams that allow zero estimates.", + "type": [ + "number", + "null" + ] + }, + "links": { + "description": "Link attachments to add [{url, title}]. Append-only; existing links are never removed", + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "url", + "title" + ] + } + }, + "setReleases": { + "description": "Replace all releases on the issue with these. Cannot be combined with addReleases/removeReleases", + "type": "array", + "items": { + "type": "string" + } + }, + "addReleases": { + "description": "Release IDs or slugs to add. Append-only; existing releases are never removed", + "type": "array", + "items": { + "type": "string" + } + }, + "removeReleases": { + "description": "Release IDs or slugs to remove. Only valid when updating an existing issue", + "type": "array", + "items": { + "type": "string" + } + }, + "blocks": { + "description": "Issue IDs/identifiers this blocks. Append-only; existing relations are never removed", + "type": "array", + "items": { + "type": "string" + } + }, + "blockedBy": { + "description": "Issue IDs/identifiers blocking this. Append-only; existing relations are never removed", + "type": "array", + "items": { + "type": "string" + } + }, + "relatedTo": { + "description": "Related issue IDs/identifiers. Append-only; existing relations are never removed", + "type": "array", + "items": { + "type": "string" + } + }, + "duplicateOf": { + "description": "Duplicate of issue ID/identifier. Null to remove", + "type": [ + "string", + "null" + ] + }, + "removeBlocks": { + "description": "Issue IDs/identifiers to stop blocking", + "type": "array", + "items": { + "type": "string" + } + }, + "removeBlockedBy": { + "description": "Issue IDs/identifiers to remove as blockers of this issue", + "type": "array", + "items": { + "type": "string" + } + }, + "removeRelatedTo": { + "description": "Related issue IDs/identifiers to remove", + "type": "array", + "items": { + "type": "string" + } + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_issue_statuses", + "title": "List issue statuses", + "description": "List available issue statuses in a Linear team", + "inputSchema": { + "type": "object", + "properties": { + "team": { + "type": "string", + "description": "Team name or ID" + } + }, + "required": [ + "team" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_issue_status", + "title": "Get issue status", + "description": "Retrieve detailed information about an issue status in Linear by name or ID", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Status ID" + }, + "name": { + "type": "string", + "description": "Status name" + }, + "team": { + "type": "string", + "description": "Team name or ID" + } + }, + "required": [ + "id", + "name", + "team" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_issue_labels", + "title": "List issue labels", + "description": "List available issue labels in a Linear workspace or team. A retired label carries `retiredAt` and cannot be applied to new issues.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "name": { + "description": "Filter by name", + "type": "string" + }, + "team": { + "description": "Team name or ID", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + }, + "includeGroups": { + "default": false, + "description": "Include label groups in the results, marked with `isGroup`. A group cannot be applied directly; retiring or restoring one cascades to its child labels", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_issue_label", + "title": "Save issue label", + "description": "Create or update a Linear issue label. If `id` is provided, updates the existing label; otherwise creates a new one. When creating, `name` is required.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "description": "Label name or ID. If provided, updates the existing label", + "type": "string" + }, + "name": { + "description": "Label name (required when creating)", + "type": "string" + }, + "description": { + "description": "Label description. Null to clear it", + "type": [ + "string", + "null" + ] + }, + "color": { + "description": "Hex color code", + "type": "string" + }, + "teamId": { + "description": "Team UUID (omit for workspace label). When updating, only disambiguates `id` by name; a label's team cannot be changed", + "type": "string" + }, + "parent": { + "description": "Parent label group name or ID. Null to remove from its group", + "type": [ + "string", + "null" + ] + }, + "isGroup": { + "description": "Whether the label is a group", + "type": "boolean" + }, + "groupType": { + "description": "Selection mode for label groups. Defaults to singleSelect when creating; omit to keep it unchanged when updating", + "type": "string", + "enum": [ + "singleSelect", + "multiSelect" + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "create_issue_label", + "title": "Create issue label (deprecated)", + "description": "Create a new Linear issue label. Deprecated: use `save_issue_label`, which can also update labels.", + "inputSchema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Label name" + }, + "description": { + "description": "Label description", + "type": "string" + }, + "color": { + "description": "Hex color code", + "type": "string" + }, + "teamId": { + "description": "Team UUID (omit for workspace label)", + "type": "string" + }, + "parent": { + "description": "Parent label group name or ID", + "type": "string" + }, + "isGroup": { + "default": false, + "description": "Whether the label is a group", + "type": "boolean" + } + }, + "required": [ + "name" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "retire_issue_label", + "title": "Retire issue label", + "description": "Retire a Linear issue label so it cannot be applied to new issues. The label stays visible on issues that already have it. Retiring a label group also retires its child labels. Undo with `restore_issue_label`.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Label or label group ID" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "restore_issue_label", + "title": "Restore issue label", + "description": "Restore a retired Linear issue label, making it available for use on issues again. Restoring a label group also restores every child label, including any that were retired separately.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Label or label group ID" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_projects", + "title": "List projects", + "description": "List projects in the user's Linear workspace", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Max results (default 50, max 50)", + "type": "number", + "maximum": 50 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "customView": { + "description": "Saved view ID, URL, slug, or exact name. Use list_custom_views to discover views. Applies saved filters and scope; other filters narrow the results. Uses list ordering, not saved display settings", + "type": "string", + "minLength": 1 + }, + "query": { + "description": "Search project name", + "type": "string" + }, + "state": { + "description": "State type, name, or ID", + "type": "string" + }, + "initiative": { + "description": "Initiative name, ID, identifier (e.g., I-123), or slug", + "type": "string" + }, + "team": { + "description": "Team name or ID", + "type": "string" + }, + "includeSubTeams": { + "default": false, + "description": "Include results from the selected team and all descendants when team is set. Team ID filters already include descendants; this also expands name and key filters", + "type": "boolean" + }, + "member": { + "description": "User ID, name, email, or \"me\"", + "type": "string" + }, + "label": { + "description": "Label name or ID", + "type": "string" + }, + "createdAt": { + "description": "Created after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "updatedAt": { + "description": "Updated after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "includeMilestones": { + "default": false, + "description": "Include milestones", + "type": "boolean" + }, + "includeMembers": { + "default": false, + "description": "Include project members", + "type": "boolean" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + }, + "fields": { + "description": "Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "id", + "uuid", + "name", + "summary", + "description", + "url", + "trashed", + "createdAt", + "updatedAt", + "startedAt", + "completedAt", + "canceledAt", + "startDate", + "startDateResolution", + "targetDate", + "targetDateResolution", + "priority", + "sortOrder", + "labels", + "initiatives", + "lead", + "leadTeam", + "status", + "teams", + "members", + "milestones" + ] + } + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_project", + "title": "Get project", + "description": "Retrieve details of a specific project in Linear", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug" + }, + "includeCustomerNeeds": { + "default": false, + "description": "Include one page of customer needs attached directly to the project", + "type": "boolean" + }, + "customerNeedsLimit": { + "default": 10, + "description": "Customer needs per page when includeCustomerNeeds is true (default 10, max 50)", + "type": "integer", + "minimum": 1, + "maximum": 50 + }, + "customerNeedsCursor": { + "description": "Next customer-needs page: pass customerNeedsPageInfo.endCursor with includeCustomerNeeds true", + "type": "string" + }, + "includeMilestones": { + "default": false, + "description": "Include milestones", + "type": "boolean" + }, + "includeMembers": { + "default": false, + "description": "Include project members", + "type": "boolean" + }, + "includeResources": { + "default": false, + "description": "Include resources (documents, links, attachments)", + "type": "boolean" + } + }, + "required": [ + "query" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_project", + "title": "Save project", + "description": "Create or update a Linear project. If `id` is provided, updates the existing project; otherwise creates a new one. When creating, `name` and at least one team (via `addTeams` or `setTeams`) are required. Pass `template` to create the project from a project template. To change parts of the description without resending all of it, pass `patch` instead of `description`. `state` resolves against the project statuses of the lead team, so pass `leadTeam` when creating a multi-team project whose teams use different statuses.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug. If provided, updates the existing project", + "type": "string" + }, + "name": { + "description": "Project name (required when creating)", + "type": "string" + }, + "template": { + "description": "Template name or ID. Applied on create only. The template fills in the content, fields, and sub-issues server-side. A `description` replaces the template body rather than merging with it, but an empty `description` keeps the template body. Labels merge with the template's own", + "type": "string" + }, + "icon": { + "description": "Icon name or emoji code (e.g. \"Rocket\" or \":eagle:\"), not a raw Unicode emoji", + "type": "string" + }, + "color": { + "description": "Hex color", + "type": "string" + }, + "summary": { + "description": "Short summary (max 255 chars)", + "type": "string" + }, + "description": { + "description": "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + "type": "string" + }, + "patch": { + "description": "Partial edits applied to the current content, in order and atomically (one failing operation aborts the whole save). Every anchor string must match the current content exactly once. Only valid on update, in place of the full content/description field", + "minItems": 1, + "maxItems": 50, + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "replace" + }, + "old_string": { + "type": "string", + "minLength": 1, + "description": "Exact text to replace. Must match the current content exactly once" + }, + "new_string": { + "type": "string", + "description": "Replacement text. Empty string deletes the match" + }, + "replace_all": { + "description": "Replace every occurrence instead of requiring a unique match", + "type": "boolean" + } + }, + "required": [ + "op", + "old_string", + "new_string" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "insert_before" + }, + "anchor": { + "type": "string", + "minLength": 1, + "description": "Exact text to insert before. Must match the current content exactly once" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert directly before the anchor. Include separators (e.g. \"\\n\\n\")" + } + }, + "required": [ + "op", + "anchor", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "insert_after" + }, + "anchor": { + "type": "string", + "minLength": 1, + "description": "Exact text to insert after. Must match the current content exactly once" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert directly after the anchor. Include separators (e.g. \"\\n\\n\")" + } + }, + "required": [ + "op", + "anchor", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "prepend" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert at the very start of the content" + } + }, + "required": [ + "op", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "append" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert at the very end of the content" + } + }, + "required": [ + "op", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "replace_range" + }, + "from": { + "type": "string", + "minLength": 1, + "description": "Exact text where the range starts (inclusive). Must match exactly once" + }, + "to": { + "type": "string", + "minLength": 1, + "description": "Exact text where the range ends (exclusive, stays in place). Must match exactly once after `from`" + }, + "new_string": { + "type": "string", + "description": "Text replacing the range. Empty string deletes it" + } + }, + "required": [ + "op", + "from", + "to", + "new_string" + ] + } + ] + } + }, + "state": { + "description": "Project status name, type, or ID. Names resolve within the lead team's project statuses. If the lead team is unknown, standard labels resolve as status types; custom names require a status ID or leadTeam", + "type": "string" + }, + "startDate": { + "description": "Start date (ISO format). Pair with startDateResolution to indicate precision (e.g. month, quarter). Null to remove", + "type": [ + "string", + "null" + ] + }, + "startDateResolution": { + "description": "Start date resolution. Null for an exact day. When omitted, the current resolution stays and the date snaps to it", + "anyOf": [ + { + "type": "string", + "enum": [ + "halfYear", + "month", + "quarter", + "year" + ] + }, + { + "type": "null" + } + ] + }, + "targetDate": { + "description": "Target date (ISO format). Pair with targetDateResolution to indicate precision (e.g. month, quarter). Null to remove", + "type": [ + "string", + "null" + ] + }, + "targetDateResolution": { + "description": "Target date resolution. Null for an exact day. When omitted, the current resolution stays and the date snaps to it", + "anyOf": [ + { + "type": "string", + "enum": [ + "halfYear", + "month", + "quarter", + "year" + ] + }, + { + "type": "null" + } + ] + }, + "priority": { + "description": "0=None, 1=Urgent, 2=High, 3=Medium, 4=Low", + "type": "integer", + "minimum": 0, + "maximum": 4 + }, + "addTeams": { + "description": "Team name or ID to add", + "type": "array", + "items": { + "type": "string" + } + }, + "removeTeams": { + "description": "Team name or ID to remove", + "type": "array", + "items": { + "type": "string" + } + }, + "setTeams": { + "description": "Replace all teams with these. Cannot combine with addTeams/removeTeams", + "type": "array", + "items": { + "type": "string" + } + }, + "leadTeam": { + "description": "Team that leads the project. Team name or ID. Overrides the default lead team; added to the project's teams when not among them", + "type": "string" + }, + "labels": { + "description": "Label names or IDs as a JSON array of strings (e.g. [\"Bug\", \"Urgent\"]). Replaces the full label set; existing labels not included are removed. Omit to leave labels unchanged", + "type": "array", + "items": { + "type": "string" + } + }, + "lead": { + "description": "User ID, name, email, or \"me\". Null to remove, except on create with a template that names a lead", + "type": [ + "string", + "null" + ] + }, + "addInitiatives": { + "description": "Initiatives to add. Initiative name, ID, identifier (e.g., I-123), or slug", + "type": "array", + "items": { + "type": "string" + } + }, + "removeInitiatives": { + "description": "Initiatives to remove. Initiative name, ID, identifier (e.g., I-123), or slug", + "type": "array", + "items": { + "type": "string" + } + }, + "setInitiatives": { + "description": "Replace all initiatives with these. Initiative name, ID, identifier (e.g., I-123), or slug. Cannot combine with addInitiatives/removeInitiatives", + "type": "array", + "items": { + "type": "string" + } + }, + "links": { + "description": "External resource links to add [{url, title}]. Append-only; existing links are never removed", + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "url", + "title" + ] + } + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_project_labels", + "title": "List project labels", + "description": "List available project labels in a Linear workspace or team. Returns workspace labels by default; pass `team` to also include that team's labels. A team-scoped label carries a `team` field; different teams can hold same-named labels, so check it before acting on an ID. A retired label carries `retiredAt` and cannot be applied to new projects.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "name": { + "description": "Filter by name", + "type": "string" + }, + "team": { + "description": "Team name or ID", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + }, + "includeGroups": { + "default": false, + "description": "Include label groups in the results, marked with `isGroup`. A group cannot be applied directly; retiring or restoring one cascades to its child labels", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "retire_project_label", + "title": "Retire project label", + "description": "Retire a Linear project label so it cannot be applied to new projects. The label stays visible on projects that already have it. Retiring a label group also retires its child labels. Undo with `restore_project_label`.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Label or label group ID" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "restore_project_label", + "title": "Restore project label", + "description": "Restore a retired Linear project label, making it available for use on projects again. Restoring a label group also restores every child label, including any that were retired separately.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Label or label group ID" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_project_label", + "title": "Save project label", + "description": "Create or update a Linear project label. If `id` is provided, updates the existing label; otherwise creates a new one. When creating, `name` is required.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "description": "Label name or ID. If provided, updates the existing label", + "type": "string" + }, + "name": { + "description": "Label name (required when creating)", + "type": "string" + }, + "description": { + "description": "Label description. Null to clear it", + "type": [ + "string", + "null" + ] + }, + "color": { + "description": "Hex color code", + "type": "string" + }, + "teamId": { + "description": "Team UUID (omit for workspace label). When updating, only disambiguates `id` by name; a label's team cannot be changed", + "type": "string" + }, + "parent": { + "description": "Parent label group name or ID. Null to remove from its group", + "type": [ + "string", + "null" + ] + }, + "isGroup": { + "description": "Whether the label is a group", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_release_pipelines", + "title": "List release pipelines", + "description": "List release pipelines in the workspace.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Search pipeline name", + "type": "string" + }, + "team": { + "description": "Team name or ID", + "type": "string" + }, + "type": { + "description": "Pipeline type: continuous | scheduled", + "type": "string", + "enum": [ + "continuous", + "scheduled" + ] + }, + "isProduction": { + "description": "Filter by production pipeline flag", + "type": "boolean" + }, + "includeStages": { + "default": false, + "description": "Include each pipeline's stages", + "type": "boolean" + }, + "includeTeams": { + "default": false, + "description": "Include each pipeline's teams", + "type": "boolean" + }, + "createdAt": { + "description": "Created after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "updatedAt": { + "description": "Updated after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_releases", + "title": "List releases", + "description": "List releases in the workspace, with optional filtering by pipeline, stage, version, and text.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Search release name or version", + "type": "string" + }, + "pipeline": { + "description": "Release pipeline ID, slug, or exact name", + "type": "string" + }, + "stage": { + "description": "Release stage ID or exact name", + "type": "string" + }, + "stageType": { + "description": "Filter by stage lifecycle type", + "type": "string", + "enum": [ + "planned", + "started", + "completed", + "canceled" + ] + }, + "version": { + "description": "Exact version match", + "type": "string" + }, + "hasReleaseNotes": { + "description": "Filter to releases that do (true) or do not (false) have release notes", + "type": "boolean" + }, + "includeReleaseNotes": { + "default": false, + "description": "Include associated release notes", + "type": "boolean" + }, + "createdAt": { + "description": "Created after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "updatedAt": { + "description": "Updated after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_release", + "title": "Get release", + "description": "Retrieve details of a release by ID or slug.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Release ID or slug" + }, + "includeReleaseNotes": { + "default": false, + "description": "Include associated release notes", + "type": "boolean" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_release", + "title": "Save release", + "description": "Create or update a release. If `id` is provided, updates the existing release; otherwise creates a new one. When creating, `name` and `pipeline` are required. Release status is modeled as the release pipeline stage.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "description": "Release ID or slug to update. Omit to create a new release.", + "type": "string" + }, + "name": { + "description": "Release name (required when creating)", + "type": "string" + }, + "description": { + "description": "Release description", + "type": "string" + }, + "version": { + "description": "Version identifier", + "type": "string" + }, + "pipeline": { + "description": "Release pipeline ID, slug, or exact name (required when creating)", + "type": "string" + }, + "stage": { + "description": "Release stage ID, exact name, or lifecycle type within the release pipeline", + "type": "string" + }, + "startDate": { + "description": "Estimated start date (ISO YYYY-MM-DD, null to remove)", + "type": [ + "string", + "null" + ] + }, + "targetDate": { + "description": "Estimated completion date (ISO YYYY-MM-DD, null to remove)", + "type": [ + "string", + "null" + ] + }, + "createdAt": { + "description": "Import/create timestamp (ISO DateTime)", + "type": "string" + }, + "startedAt": { + "description": "Started timestamp (ISO DateTime, null to remove)", + "type": [ + "string", + "null" + ] + }, + "completedAt": { + "description": "Completed timestamp (ISO DateTime, null to remove)", + "type": [ + "string", + "null" + ] + }, + "commitSha": { + "description": "Commit SHA associated with the release", + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_release_notes", + "title": "List release notes", + "description": "List release notes in the workspace, optionally filtered by pipeline or covered release.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Search release notes title", + "type": "string" + }, + "pipeline": { + "description": "Release pipeline ID, slug, or exact name", + "type": "string" + }, + "release": { + "description": "Release ID or slug", + "type": "string" + }, + "includeContent": { + "default": false, + "description": "Include markdown release notes content", + "type": "boolean" + }, + "includeReleases": { + "default": false, + "description": "Include associated releases", + "type": "boolean" + }, + "createdAt": { + "description": "Created after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "updatedAt": { + "description": "Updated after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_release_note", + "title": "Get release notes", + "description": "Retrieve release notes by ID or slug, including markdown content.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Release notes ID or slug" + }, + "includeReleases": { + "default": false, + "description": "Include associated releases", + "type": "boolean" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_release_note", + "title": "Save release notes", + "description": "Create or update release notes. If `id` is provided, updates the existing release notes; otherwise creates a new one. When creating, `pipeline` and either `releases` or a release range are required. To change parts of the content without resending all of it, pass `patch` instead of `content`.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "description": "Release notes ID or slug to update. Omit to create new release notes.", + "type": "string" + }, + "pipeline": { + "description": "Release pipeline ID, slug, or exact name (required when creating)", + "type": "string" + }, + "title": { + "description": "Release notes title", + "type": "string" + }, + "content": { + "description": "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + "type": "string" + }, + "patch": { + "description": "Partial edits applied to the current content, in order and atomically (one failing operation aborts the whole save). Every anchor string must match the current content exactly once. Only valid on update, in place of the full content/description field", + "minItems": 1, + "maxItems": 50, + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "replace" + }, + "old_string": { + "type": "string", + "minLength": 1, + "description": "Exact text to replace. Must match the current content exactly once" + }, + "new_string": { + "type": "string", + "description": "Replacement text. Empty string deletes the match" + }, + "replace_all": { + "description": "Replace every occurrence instead of requiring a unique match", + "type": "boolean" + } + }, + "required": [ + "op", + "old_string", + "new_string" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "insert_before" + }, + "anchor": { + "type": "string", + "minLength": 1, + "description": "Exact text to insert before. Must match the current content exactly once" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert directly before the anchor. Include separators (e.g. \"\\n\\n\")" + } + }, + "required": [ + "op", + "anchor", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "insert_after" + }, + "anchor": { + "type": "string", + "minLength": 1, + "description": "Exact text to insert after. Must match the current content exactly once" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert directly after the anchor. Include separators (e.g. \"\\n\\n\")" + } + }, + "required": [ + "op", + "anchor", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "prepend" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert at the very start of the content" + } + }, + "required": [ + "op", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "append" + }, + "text": { + "type": "string", + "minLength": 1, + "description": "Text to insert at the very end of the content" + } + }, + "required": [ + "op", + "text" + ] + }, + { + "type": "object", + "properties": { + "op": { + "type": "string", + "const": "replace_range" + }, + "from": { + "type": "string", + "minLength": 1, + "description": "Exact text where the range starts (inclusive). Must match exactly once" + }, + "to": { + "type": "string", + "minLength": 1, + "description": "Exact text where the range ends (exclusive, stays in place). Must match exactly once after `from`" + }, + "new_string": { + "type": "string", + "description": "Text replacing the range. Empty string deletes it" + } + }, + "required": [ + "op", + "from", + "to", + "new_string" + ] + } + ] + } + }, + "releases": { + "description": "Release IDs or slugs to include in the note", + "type": "array", + "items": { + "type": "string" + } + }, + "rangeFromRelease": { + "description": "Oldest release ID or slug in the note range", + "type": "string" + }, + "rangeToRelease": { + "description": "Newest release ID or slug in the note range", + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "share_issue", + "title": "Share issue", + "description": "Share an issue with a workspace user who cannot otherwise access it. Use this tool only when the user explicitly asks to share the issue.", + "inputSchema": { + "type": "object", + "properties": { + "issue": { + "type": "string", + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues" + }, + "user": { + "type": "string", + "description": "User ID, name, email, or \"me\" Must be an email address or UUID." + } + }, + "required": [ + "issue", + "user" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "unshare_issue", + "title": "Unshare issue", + "description": "Remove a workspace user's shared access to an issue. Use this tool only when the user explicitly asks to stop sharing the issue.", + "inputSchema": { + "type": "object", + "properties": { + "issue": { + "type": "string", + "description": "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues" + }, + "user": { + "type": "string", + "description": "User ID, name, email, or \"me\" Must be an email address or UUID." + } + }, + "required": [ + "issue", + "user" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_milestones", + "title": "List project milestones", + "description": "List all milestones in a Linear project", + "inputSchema": { + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug" + } + }, + "required": [ + "project" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_milestone", + "title": "Get milestone", + "description": "Retrieve details of a specific milestone by ID or name", + "inputSchema": { + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug" + }, + "query": { + "type": "string", + "description": "Milestone name or ID" + } + }, + "required": [ + "project", + "query" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_milestone", + "title": "Save milestone", + "description": "Create or update a milestone in a Linear project. If `id` is provided, updates the existing milestone; otherwise creates a new one. When creating, `name` is required.", + "inputSchema": { + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug" + }, + "id": { + "description": "Milestone name or ID", + "type": "string" + }, + "name": { + "description": "Milestone name (required when creating)", + "type": "string" + }, + "description": { + "description": "Milestone description", + "type": "string" + }, + "targetDate": { + "description": "Target completion date (ISO format, null to remove)", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "project" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_notifications", + "title": "Get notifications", + "description": "Get a page of notifications from the authenticated user's Linear inbox.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "unreadOnly": { + "default": false, + "description": "Only return unread notifications", + "type": "boolean" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "mark_notification", + "title": "Mark notification", + "description": "Mark a Linear inbox notification as read or unread, or snooze or unsnooze it.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Notification ID" + }, + "read": { + "description": "Whether the notification should be marked as read", + "type": "boolean" + }, + "snoozedUntilAt": { + "description": "ISO-8601 time to snooze until, or null to unsnooze", + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_teams", + "title": "List teams", + "description": "List teams in the user's Linear workspace, including their parent IDs and hierarchy names", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Search query", + "type": "string" + }, + "parentTeam": { + "description": "Only direct sub-teams of this team (name, key, or ID)", + "type": "string" + }, + "ancestorTeam": { + "description": "All descendant teams of this team, at any depth, excluding itself (name, key, or ID)", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + }, + "createdAt": { + "description": "Created after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "updatedAt": { + "description": "Updated after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_team", + "title": "Get team", + "description": "Retrieve details of a specific Linear team, including its parent ID and hierarchy name. Use list_teams with parentTeam or ancestorTeam to find its sub-teams", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Team UUID, key, or name" + } + }, + "required": [ + "query" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_triage_responsibility", + "title": "Get triage responsibility", + "description": "Retrieve who is responsible for a team's triage: the person on triage now, what happens when an issue enters triage, and the upcoming shifts of the team's triage rotation. Pass `user` to get only that person's shifts, for example to find when their next triage duty starts, even when it is weeks away.", + "inputSchema": { + "type": "object", + "properties": { + "team": { + "type": "string", + "description": "Team name or ID" + }, + "user": { + "description": "Only return shifts of this user: User ID, name, email, or \"me\"", + "type": "string" + }, + "limit": { + "default": 10, + "description": "Max upcoming shifts to return (default 10, max 50)", + "type": "integer", + "minimum": 1, + "maximum": 50 + } + }, + "required": [ + "team" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_templates", + "title": "List templates", + "description": "List the Linear issue, project, and document templates available to the authenticated user. Use this to find the template a team expects, then call get_template for its content.", + "inputSchema": { + "type": "object", + "properties": { + "type": { + "description": "Filter by template type. Omit to return every type", + "type": "string", + "enum": [ + "issue", + "project", + "document" + ] + }, + "team": { + "description": "Team name or ID. Returns that team's templates plus workspace-level ones", + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_template", + "title": "Get template", + "description": "Retrieve a Linear template by ID or name: the content it pre-fills, the ids it applies, its sub-issues, and its form fields. Pass the template to save_issue or save_project to apply it. References come back as ids, so read a name with the matching tool when you need one. A form template collects its answers through a form, so applying one here creates an issue with the form unanswered.", + "inputSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Template ID or name" + } + }, + "required": [ + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_users", + "title": "List users", + "description": "List workspace users with their IDs, names, emails, and admin, guest, and active flags. Use get_user for team memberships.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Match part of a name or email. Omit to list users without a name or email filter", + "type": "string" + }, + "team": { + "description": "Team name or ID. Only return members of this team; omit to search the workspace", + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_user", + "title": "Get user", + "description": "Retrieve one user's profile, email, admin, guest, and active flags, plus team IDs, names, and keys. Use list_users to find matching users.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "User ID, name, email, or \"me\"" + } + }, + "required": [ + "query" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_workspace", + "title": "Get workspace", + "description": "Retrieve the connected workspace's ID, name, and URL to confirm which workspace you are using", + "inputSchema": { + "type": "object", + "properties": {} + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "search_documentation", + "title": "Search documentation", + "description": "Search Linear's product documentation for feature and usage guidance. Returns up to five results with titles, URLs, and content snippets per page.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Feature name, topic, or question to search for in Linear's product documentation" + }, + "page": { + "default": 0, + "description": "Zero-based page number. Increment by one to fetch the next page for the same query", + "type": "number" + } + }, + "required": [ + "query" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_diff", + "title": "Get diff", + "description": "Exact lookup for a Linear diff. Use with review URLs, GitHub PR URLs, Linear full identifiers, UUIDs, or slugs.", + "inputSchema": { + "type": "object", + "properties": { + "urlOrId": { + "type": "string", + "minLength": 1, + "description": "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL" + } + }, + "required": [ + "urlOrId" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "list_diffs", + "title": "List diffs", + "description": "List Linear diff pull requests visible to the authenticated user", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "query": { + "description": "Broad search by title, branch, PR number, or bare slug", + "type": "string" + }, + "owner": { + "description": "Filter returned diffs by repository owner", + "type": "string" + }, + "repo": { + "description": "Filter returned diffs by repository name", + "type": "string" + }, + "status": { + "description": "Filter returned diffs by pull request status", + "type": "string" + }, + "author": { + "description": "Filter by author ID, name, email, or \"me\"", + "type": "string" + }, + "reviewer": { + "description": "Filter by reviewer ID, name, email, or \"me\"", + "type": "string" + }, + "reviewState": { + "description": "Reviewer state; requires reviewer and defaults to pending when reviewer is set", + "type": "string", + "enum": [ + "pending", + "approved", + "changesRequested", + "commented", + "dismissed" + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_diff_threads", + "title": "Get diff threads", + "description": "Exact lookup for diff threads and the authenticated user's drafts. Use with review URLs, GitHub PR URLs, Linear full identifiers, UUIDs, or slugs.", + "inputSchema": { + "type": "object", + "properties": { + "urlOrId": { + "type": "string", + "minLength": 1, + "description": "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL" + }, + "threadId": { + "description": "Optional Linear comment UUID of any comment in the thread (not its `externalThreadId`); a reply's UUID resolves to its thread root", + "type": "string" + }, + "resolved": { + "description": "Filter returned threads by resolved state", + "type": "boolean" + }, + "orderBy": { + "default": "updatedAt", + "description": "Sort: createdAt | updatedAt", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + } + }, + "required": [ + "urlOrId" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_diff_comment", + "title": "Save diff comment", + "description": "Create, reply to, or edit a comment or persisted draft on a Linear diff. Set draft to true to save without submitting. Provide draftId to edit or submit a persisted draft, and commentId only to edit a submitted comment. Submitting an inline draft reuses its saved anchor; submitting a reply draft requires parentId again.", + "inputSchema": { + "type": "object", + "properties": { + "urlOrId": { + "description": "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", + "type": "string", + "minLength": 1 + }, + "commentId": { + "description": "Existing comment ID to edit", + "type": "string" + }, + "draftId": { + "description": "Existing persisted draft ID to edit or submit", + "type": "string" + }, + "draft": { + "default": false, + "description": "Save as a private persisted draft without submitting", + "type": "boolean" + }, + "parentId": { + "description": "Top-level comment ID to reply to; also required when submitting a persisted reply draft", + "type": "string" + }, + "body": { + "type": "string", + "minLength": 1, + "description": "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences" + }, + "anchor": { + "description": "Optional comment anchor copied from a diff thread or built for a file or diff hunk", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "anchorContent": { + "description": "Exact source text of the anchor's end line; stored with an inline draft so it can be re-anchored after the diff receives new revisions", + "type": "string" + } + }, + "required": [ + "body" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "resolve_diff_thread", + "title": "Resolve diff thread", + "description": "Resolve or reopen a top-level comment thread on a Linear diff", + "inputSchema": { + "type": "object", + "properties": { + "threadId": { + "type": "string", + "minLength": 1, + "description": "Top-level diff thread/comment ID" + }, + "resolved": { + "default": true, + "description": "True to resolve the thread; false to reopen it", + "type": "boolean" + } + }, + "required": [ + "threadId" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "delete_diff_comment", + "title": "Delete diff comment", + "description": "Delete a comment or persisted draft from a Linear diff", + "inputSchema": { + "type": "object", + "properties": { + "commentId": { + "description": "Submitted diff comment ID to delete", + "type": "string", + "minLength": 1 + }, + "draftId": { + "description": "Persisted diff comment draft ID to delete", + "type": "string", + "minLength": 1 + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "submit_diff_review", + "title": "Submit diff review", + "description": "Approve a Linear diff, request changes, or submit a review comment", + "inputSchema": { + "type": "object", + "properties": { + "urlOrId": { + "type": "string", + "minLength": 1, + "description": "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL" + }, + "decision": { + "type": "string", + "enum": [ + "approved", + "changesRequested", + "commented" + ], + "description": "Review decision: approved, changesRequested, or commented" + }, + "body": { + "description": "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. Required when requesting changes or commenting", + "type": "string", + "minLength": 1 + } + }, + "required": [ + "urlOrId", + "decision" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "update_diff", + "title": "Update diff", + "description": "Update a Linear diff's title, description, reviewer requests, issue links, or status. Provide at least one update. Combined updates can partially succeed; inspect the result before retrying. Use merge_diff to merge and submit_diff_review to approve or request changes.", + "inputSchema": { + "type": "object", + "properties": { + "urlOrId": { + "type": "string", + "minLength": 1, + "description": "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL" + }, + "title": { + "description": "New diff title", + "type": "string", + "minLength": 1 + }, + "description": { + "description": "New description as Markdown; an empty string clears it", + "type": "string" + }, + "addedReviewRequests": { + "description": "Request or re-request reviews; existing pending requests are unchanged", + "minItems": 1, + "maxItems": 25, + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "user": { + "type": "string", + "minLength": 1, + "description": "Workspace user ID, name, email, or \"me\"" + } + }, + "required": [ + "user" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "externalUserId": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "description": "External user ID from get_diff reviewer metadata" + } + }, + "required": [ + "externalUserId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "githubTeamId": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "description": "GitHub team ID from get_diff reviewer metadata" + } + }, + "required": [ + "githubTeamId" + ], + "additionalProperties": false + } + ] + } + }, + "removedReviewRequests": { + "description": "Withdraw pending review requests; submitted reviews are preserved", + "minItems": 1, + "maxItems": 25, + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "user": { + "type": "string", + "minLength": 1, + "description": "Workspace user ID, name, email, or \"me\"" + } + }, + "required": [ + "user" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "externalUserId": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "description": "External user ID from get_diff reviewer metadata" + } + }, + "required": [ + "externalUserId" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "githubTeamId": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "description": "GitHub team ID from get_diff reviewer metadata" + } + }, + "required": [ + "githubTeamId" + ], + "additionalProperties": false + } + ] + } + }, + "addedIssueLinks": { + "description": "Add issue links or change the relationship of an existing link", + "minItems": 1, + "maxItems": 25, + "type": "array", + "items": { + "type": "object", + "properties": { + "issue": { + "type": "string", + "minLength": 1, + "description": "Linear issue identifier or UUID" + }, + "type": { + "type": "string", + "enum": [ + "closes", + "contributes", + "links", + "reopens" + ], + "description": "Relationship: closes, contributes, links, or reopens" + } + }, + "required": [ + "issue", + "type" + ], + "additionalProperties": false + } + }, + "removedIssueLinks": { + "description": "Linear issue identifiers or UUIDs to unlink", + "minItems": 1, + "maxItems": 25, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "statusAction": { + "description": "Status transition to perform after metadata is updated", + "type": "string", + "enum": [ + "markReadyForReview", + "convertToDraft", + "close", + "reopen" + ] + } + }, + "required": [ + "urlOrId" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "merge_diff", + "title": "Merge diff", + "description": "Merge a Linear diff or add it to the repository's merge queue", + "inputSchema": { + "type": "object", + "properties": { + "urlOrId": { + "type": "string", + "minLength": 1, + "description": "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL" + }, + "mergeMethod": { + "description": "Merge method; omit to use the repository default", + "type": "string", + "enum": [ + "MERGE", + "REBASE", + "SQUASH" + ] + } + }, + "required": [ + "urlOrId" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "get_status_updates", + "title": "Get status updates", + "description": "List or get project/initiative status updates with their body, health, author, and parent project or initiative. Pass id to get one update and ignore list filters; omit id to list.", + "inputSchema": { + "type": "object", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of results to return per page", + "type": "number", + "maximum": 250 + }, + "cursor": { + "description": "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + "type": "string" + }, + "orderBy": { + "default": "updatedAt", + "description": "Timestamp to sort results by: creation time or last update time", + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ] + }, + "type": { + "type": "string", + "enum": [ + "project", + "initiative" + ], + "description": "Type of status update" + }, + "id": { + "description": "Update ID from get_status_updates results (statusUpdates[].id) or save_status_update's id", + "type": "string" + }, + "project": { + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug. Filters project updates only; omit to include all accessible projects", + "type": "string" + }, + "initiative": { + "description": "Initiative name, ID, identifier (e.g., I-123), or slug. Filters initiative updates only; omit to include all accessible initiatives", + "type": "string" + }, + "user": { + "description": "Filter by update author: User ID, name, email, or \"me\". Omit to include all authors", + "type": "string" + }, + "createdAt": { + "description": "Created after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "updatedAt": { + "description": "Updated after: ISO-8601 date/duration (e.g., -P1D)", + "type": "string" + }, + "includeArchived": { + "default": false, + "description": "Include archived items", + "type": "boolean" + } + }, + "required": [ + "type" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "save_status_update", + "title": "Save status update", + "description": "Create or update a project/initiative status update and return the saved update. To create, omit id and provide body plus project or initiative matching type. To update, provide id; project and initiative are ignored.", + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "project", + "initiative" + ], + "description": "Type of status update" + }, + "id": { + "description": "Update ID from get_status_updates results (statusUpdates[].id) or save_status_update's id. Omit to create", + "type": "string" + }, + "project": { + "description": "Project name, ID, identifier (e.g., P-ENG-123), or slug", + "type": "string" + }, + "initiative": { + "description": "Initiative name, ID, identifier (e.g., I-123), or slug", + "type": "string" + }, + "body": { + "description": "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe). Required on create; replaces the body on update. Omit on update to leave the body unchanged", + "type": "string" + }, + "health": { + "description": "onTrack | atRisk | offTrack", + "type": "string", + "enum": [ + "onTrack", + "atRisk", + "offTrack" + ] + }, + "isDiffHidden": { + "description": "Deprecated. Hide diff with previous update (create only)", + "type": "boolean" + } + }, + "required": [ + "type" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + }, + { + "name": "delete_status_update", + "title": "Delete status update", + "description": "Delete (archive) a project or initiative status update.", + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "project", + "initiative" + ], + "description": "Type of status update" + }, + "id": { + "type": "string", + "description": "Update ID from get_status_updates results (statusUpdates[].id) or save_status_update's id; type must match the update" + } + }, + "required": [ + "type", + "id" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false + }, + "_meta": { + "microsoft/allowedDomains": [ + "linear.app" + ] + } + } + ] +} diff --git a/backend/__tests__/unit/routes/grants.mint.test.js b/backend/__tests__/unit/routes/grants.mint.test.js index daca837174..38207d78df 100644 --- a/backend/__tests__/unit/routes/grants.mint.test.js +++ b/backend/__tests__/unit/routes/grants.mint.test.js @@ -274,7 +274,7 @@ describe('POST /api/grants on a hosted-mcp connection', () => { }); test('write is refused on a hosted row, and write-with-confirm is not', async () => { - Integration.findById.mockResolvedValue(hostedRow()); + Integration.findById.mockResolvedValue(hostedRow({ grantedScope: 'read write openid' })); const refused = await withCatalog([ENTRY], () => hostedMint({ writeMode: 'write' })); expect(refused.status).toBe(403); expect(refused.body).toMatchObject({ error: 'write_requires_confirm' }); @@ -286,6 +286,16 @@ describe('POST /api/grants on a hosted-mcp connection', () => { expect(allowed.status).toBe(201); }); + test.each(['read openid', '', undefined, 'read overwrite openid'])('Linear scope %s cannot mint confirmation mode', async (grantedScope) => { + Integration.findById.mockResolvedValue(hostedRow({ grantedScope })); + const res = await withCatalog([ENTRY], () => hostedMint({ writeMode: 'write-with-confirm' })); + expect(res.status).toBe(403); + expect(res.body.error).toBe('write_scope_required'); + expect(await RoomGrant.countDocuments({})).toBe(0); + const read = await withCatalog([ENTRY], () => hostedMint({ writeMode: 'read' })); + expect(read.status).toBe(201); + }); + test('a github-app write grant is refused by the CTO pod-eligibility ruling', async () => { outsiderGrantPodRefusal.mockResolvedValue('write_mode_unconfined'); const res = await mint({ writeMode: 'write' }); diff --git a/backend/__tests__/unit/routes/hostedMcpConnect.callback.test.js b/backend/__tests__/unit/routes/hostedMcpConnect.callback.test.js index 03cb8d4358..d59b627cb4 100644 --- a/backend/__tests__/unit/routes/hostedMcpConnect.callback.test.js +++ b/backend/__tests__/unit/routes/hostedMcpConnect.callback.test.js @@ -86,7 +86,7 @@ const intake = require('../../../services/hostedMcpIntakeService'); const dcr = require('../../../services/hostedMcpDcrRegistry'); const { HOSTED_MCP_ENTRIES: SHIPPED_ENTRIES } = jest.requireActual('../../../integrations/hostedMcp/entries'); -const LINEAR_SCOPE_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list-2026-10-01.json'); +const LINEAR_SCOPE_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list-2026-10-19.json'); const ATLASSIAN_SCOPE_CAPTURE = require('../../fixtures/hostedMcp/atlassian-tools-list-2026-10-19.json'); const AIRTABLE_SCOPE_CAPTURE = require('../../fixtures/hostedMcp/airtable-tools-list-2026-10-09.json'); const GOOGLE_CALENDAR_CAPTURE = require('../../fixtures/hostedMcp/google-calendar-tools-list-2026-10-01.json'); @@ -337,7 +337,7 @@ describe('hosted-mcp connect: callback', () => { }); it('the captured Linear, Atlassian and Airtable grants match their requested scope sets', () => { const captures = [ - [LINEAR_SCOPE_CAPTURE, 'scope'], + [LINEAR_SCOPE_CAPTURE, 'grantedScope'], [ATLASSIAN_SCOPE_CAPTURE, 'grantedScope'], [AIRTABLE_SCOPE_CAPTURE, 'grantedScope'], ]; diff --git a/backend/__tests__/unit/services/hostedMcpEntry.test.js b/backend/__tests__/unit/services/hostedMcpEntry.test.js index 4b5511cd3d..702d426d93 100644 --- a/backend/__tests__/unit/services/hostedMcpEntry.test.js +++ b/backend/__tests__/unit/services/hostedMcpEntry.test.js @@ -18,9 +18,10 @@ const { } = require('../../../integrations/hostedMcp/entries'); const GOOGLE_CALENDAR_CAPTURE = require('../../fixtures/hostedMcp/google-calendar-tools-list-2026-10-01.json'); // The first Linear capture (2026-10-01) is the one the entry was built from; the -// second (2026-10-14) is the one it is pinned to now, after five schemas drifted. +// second (2026-10-14) records the earlier five-schema drift; 10-19 adds writes. const LINEAR_FIRST_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list-2026-10-01.json'); -const LINEAR_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list-2026-10-14.json'); +const LINEAR_READ_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list-2026-10-14.json'); +const LINEAR_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list-2026-10-19.json'); const ATLASSIAN_CAPTURE = require('../../fixtures/hostedMcp/atlassian-tools-list-2026-10-19.json'); const AIRTABLE_CAPTURE = require('../../fixtures/hostedMcp/airtable-tools-list-2026-10-09.json'); const SENTRY_CAPTURE = require('../../fixtures/hostedMcp/sentry-tools-list-2026-10-19.json'); @@ -590,20 +591,21 @@ describe('the shipped catalogue cannot land half-wired', () => { expect(findHostedMcpEntry(HOSTED_MCP_ENTRIES, 'google-calendar').title).toBe('Google Calendar'); }); - test('the shipped Linear entry is read-only, fully annotated and names a revoke page', () => { + test('the shipped Linear entry pins approval-required writers and names a revoke page', () => { const [entry] = HOSTED_MCP_ENTRIES; - // 38 tools were measured at scope `read`; two caller-chosen fetch paths are - // excluded by the v1 target-fetch rule, leaving the reviewed 36-tool pin. - expect(entry.tools).toHaveLength(36); + // 68 captured tools minus two external fetches and four excluded writers. + expect(entry.tools).toHaveLength(62); expect(entry.tools.map((tool) => tool.name)).not.toContain('get_attachment'); expect(entry.tools.map((tool) => tool.name)).not.toContain('extract_images'); - expect(new Set(entry.tools.map((tool) => tool.class))).toEqual(new Set(['read'])); - expect(entry.tools.every((tool) => tool.annotations?.readOnlyHint === true)).toBe(true); - expect(entry.tools.every((tool) => tool.annotations?.destructiveHint === false)).toBe(true); + expect(entry.tools.filter((tool) => tool.class === 'read')).toHaveLength(36); + expect(entry.tools.filter((tool) => tool.class === 'write')).toHaveLength(26); + expect(entry.tools.every((tool) => tool.annotations?.readOnlyHint === (tool.class === 'read'))).toBe(true); + expect(entry.tools.filter((tool) => tool.class === 'read').every((tool) => tool.annotations?.destructiveHint === false)).toBe(true); + expect(entryToolProjections(entry).filter((tool) => tool.requiredWriteMode === 'write-with-confirm')).toHaveLength(26); // Every name an agent sees is the upstream name, which the entry load // requires and the broker calls with. expect(entry.tools.every((tool) => tool.name === tool.upstreamName)).toBe(true); - expect(new Set(entry.tools.map((tool) => `${entry.id}.${tool.name}`)).size).toBe(36); + expect(new Set(entry.tools.map((tool) => `${entry.id}.${tool.name}`)).size).toBe(62); // The person's own "Authorized applications" list, where the grant shows as // "MCP · Read · Revoke" (fresh-account walk, 2026-10-15). `/settings/security` // is the workspace admin page and has no per-person app list. @@ -615,7 +617,7 @@ describe('the shipped catalogue cannot land half-wired', () => { expect(entry.resource).toBe('https://mcp.linear.app/mcp'); expect(entry.issuer).toBe('https://mcp.linear.app'); expect(entry.client).toBe('cimd'); - expect([...entry.scopes].sort()).toEqual(['openid', 'read']); + expect([...entry.scopes].sort()).toEqual(['openid', 'read', 'write']); }); test('the Google Calendar entry is pre-registered and pins only its five read tools', () => { @@ -812,8 +814,8 @@ describe('the shipped catalogue cannot land half-wired', () => { } }); - test('the Linear pin is the measured list minus caller-chosen fetch paths', () => { - // The latest capture `connector-ops` took (2026-10-14) is committed beside + test('the Linear pin is the measured list minus the six ruled exclusions', () => { + // The latest capture `connector-ops` took (2026-10-19) is committed beside // this suite, so the pin's schemas are compared with the measurement rather // than with themselves. Drift is checked against the LIVE list at call time; this // is the other end, catching a hand-edit of the entry that would otherwise @@ -821,30 +823,54 @@ describe('the shipped catalogue cannot land half-wired', () => { const capture = LINEAR_CAPTURE; const [entry] = HOSTED_MCP_ENTRIES; expect(capture.server).toBe(entry.resource); - expect(capture.requestedScope.split(' ').sort()).toEqual([...entry.scopes].sort()); + expect(capture.scopeRequested.split(' ').sort()).toEqual([...entry.scopes].sort()); const measured = new Map(capture.tools.map((tool) => [tool.name, tool])); - const excludedForCallerChosenFetch = new Set(['get_attachment', 'extract_images']); + const excludedByPinRuling = new Set(['get_attachment', 'extract_images', 'prepare_attachment_upload', + 'create_attachment_from_upload', 'create_attachment', 'create_issue_label']); expect(entry.tools.map((tool) => tool.name)).toEqual( - capture.tools.filter((tool) => !excludedForCallerChosenFetch.has(tool.name)).map((tool) => tool.name), + capture.tools.filter((tool) => !excludedByPinRuling.has(tool.name)).map((tool) => tool.name), ); expect(capture.tools.filter((tool) => !entry.tools.some((pinned) => pinned.name === tool.name)) - .map((tool) => tool.name).sort()).toEqual([...excludedForCallerChosenFetch].sort()); + .map((tool) => tool.name).sort()).toEqual([...excludedByPinRuling].sort()); for (const tool of entry.tools) { const found = measured.get(tool.name); expect(found).toBeDefined(); expect(canonicalJson(tool.inputSchema)).toBe(canonicalJson(found.inputSchema)); + expect(tool.class).toBe(found.annotations.readOnlyHint ? 'read' : 'write'); expect(tool.annotations?.readOnlyHint).toBe(found.annotations.readOnlyHint); expect(tool.annotations?.destructiveHint).toBe(found.annotations.destructiveHint); } - // One annotation set across the whole list, which is what the header claims - // and what makes a single `READ_ANNOTATIONS` honest. Compared canonically: - // the 10-14 list serialises one tool's identical set in a different key order. - expect(new Set(capture.tools.map((tool) => canonicalJson(tool.annotations))).size).toBe(1); - expect(capture.tools).toHaveLength(38); + // Both read and write annotations come from the consenting capture. + expect(capture.tools.filter((tool) => tool.annotations.readOnlyHint)).toHaveLength(38); + expect(capture.tools).toHaveLength(68); expect(entry.tools.find((tool) => tool.name === 'get_diff').description).toContain('not fetch targets'); expect(entry.tools.find((tool) => tool.name === 'get_diff_threads').description).toContain('not fetch targets'); }); + test('Linear writers use local summaries and the 19 reviewed irreversible flags', () => { + const entry = findHostedMcpEntry(HOSTED_MCP_ENTRIES, 'linear'); + const measured = new Map(LINEAR_CAPTURE.tools.map((tool) => [tool.name, tool])); + const irreversible = [ + 'save_issue', 'save_comment', 'save_document', 'save_project', 'save_status_update', + 'save_release_note', 'save_diff_comment', 'submit_diff_review', 'update_diff', 'merge_diff', + 'share_issue', 'delete_comment', 'delete_diff_comment', 'delete_status_update', 'delete_attachment', + 'save_milestone', 'save_release', 'save_issue_label', 'save_project_label', + ]; + const writers = entry.tools.filter((tool) => tool.class === 'write'); + expect(writers).toHaveLength(26); + expect(writers.filter((tool) => tool.irreversible).map((tool) => tool.name).sort()) + .toEqual(irreversible.sort()); + for (const tool of writers) { + expect(tool.description).not.toBe(measured.get(tool.name).description.split('\n')[0]); + expect(tool.irreversible).toBe(irreversible.includes(tool.name)); + } + const issue = writers.find((tool) => tool.name === 'save_issue'); + expect(issue.description).toContain('connection owner'); + expect(issue.description).toContain('agent installed in that Linear workspace'); + expect(issue.inputSchema.properties.delegate).toEqual(measured.get('save_issue').inputSchema.properties.delegate); + expect(assessEntryTools(entry, LINEAR_CAPTURE.tools).every((item) => item.verdict === 'ok')).toBe(true); + }); + test('the 10-01 pins remain compatible with exactly five changes in the 10-14 capture', () => { // Real vendor data on both sides: the schemas the entry shipped with, assessed // against the list Linear served thirteen days later. Every change was an @@ -854,9 +880,9 @@ describe('the shipped catalogue cannot land half-wired', () => { const first = new Map(LINEAR_FIRST_CAPTURE.tools.map((tool) => [tool.name, tool])); const asFirstPinned = { ...entry, - tools: entry.tools.map((tool) => ({ ...tool, inputSchema: first.get(tool.name).inputSchema })), + tools: entry.tools.filter((tool) => tool.class === 'read').map((tool) => ({ ...tool, inputSchema: first.get(tool.name).inputSchema })), }; - const assessed = assessEntryTools(asFirstPinned, LINEAR_CAPTURE.tools); + const assessed = assessEntryTools(asFirstPinned, LINEAR_READ_CAPTURE.tools); expect(assessed.every((assessment) => assessment.verdict === 'ok')).toBe(true); const changed = assessed.filter((assessment) => assessment.detail); expect(changed.map((assessment) => assessment.name).sort()) diff --git a/backend/__tests__/unit/services/installableCatalogService.test.js b/backend/__tests__/unit/services/installableCatalogService.test.js index ac0f05b4c4..e2d7033890 100644 --- a/backend/__tests__/unit/services/installableCatalogService.test.js +++ b/backend/__tests__/unit/services/installableCatalogService.test.js @@ -295,7 +295,7 @@ it("a hosted entry's tool row carries its connect descriptor and only its own co expect(linear.tools).toEqual(linearEntry.tools.map((tool) => expect.objectContaining({ name: `linear.${tool.name}`, description: tool.description, - requiredWriteMode: 'read', + requiredWriteMode: tool.class === 'write' ? 'write-with-confirm' : 'read', irreversible: Boolean(tool.irreversible), }))); expect(acmeRow.tools).toEqual([{ name: 'acme.list_tickets', description: 'List tickets', requiredWriteMode: 'read', irreversible: false }]); diff --git a/backend/integrations/hostedMcp/linear.ts b/backend/integrations/hostedMcp/linear.ts index bd8762bf11..39710d8db8 100644 --- a/backend/integrations/hostedMcp/linear.ts +++ b/backend/integrations/hostedMcp/linear.ts @@ -1,1570 +1,4017 @@ /** - * Linear — the first hosted-MCP catalogue entry (TASK-172, scope §3; build-order - * step 7). + * Linear — TASK-238. Pins transcribed from connector-ops's consenting + * 2026-10-19T07:00:52.954Z tools/list at read write openid (68 tools). + * 36 reads and 26 writes, per Wren's 2026-10-24 pin ruling. Every writer maps + * to write-with-confirm, with the exact arguments approved by the credential owner before a vendor call. + * Existing read-scoped connections must reconnect after deployment. * - * Pinned from a real `tools/list` taken on a consenting account at - * 2026-10-01T11:18:49Z against `https://mcp.linear.app/mcp` (`Linear MCP` - * 1.0.0), with the token at scope `read openid`. The measurement and the raw - * capture are connector-ops's; the `inputSchema` of every pinned tool below is - * transcribed from it unchanged, because drift compares the pin with the live - * list — a schema that is "close enough" would refuse every call as - * `tool_drift` rather than approve args under what upstream accepts. + * get_attachment and extract_images remain excluded: they can fetch external + * caller-chosen targets into agent context. Upload preparation exposes upload + * credentials, its asset-link partner depends on that flow, base64 attachment + * creation hides bytes from approval, and create_issue_label is deprecated. + * Those four writers are excluded. Named diff review/merge remains included. + * Nineteen writers are marked irreversible. The other seven have pinned + * reversals. The flag records the ruling even though all hosted writes park. + * Schemas and both security annotations are pinned unchanged from the capture. + * Descriptions are local copy. No outputSchema was captured. * - * Re-pinned 2026-10-14 from a second capture (same row, same scope, 38 tools, - * same names and annotations) after the live drift check withheld five tools: - * `list_teams` gained `parentTeam` and `ancestorTeam`, `list_issues` and - * `list_projects` gained `includeSubTeams`, `get_issue` gained `fields`, and the - * `fields` description on the three list tools gained one sentence. Each new - * property names a team or a result field, or is a boolean — none is a - * provider-side fetch target, so §3's egress exclusion is unchanged. Only those - * five schemas moved; every other pin already equalled the second capture. - * - * Read tools only, and the credential itself carries only `read`: Linear's docs - * say a `read` token "can't reach write APIs", so §5's attribution rule has - * nothing to guard in v1 — the fence is the vendor's, and the entry's class - * agrees with it rather than being the only thing standing. - * - * All 38 measured tools arrived carrying ONE annotation set — - * `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, - * `openWorldHint: false` — so nothing in the capture is unannotated, and neither - * exclusion below is for a missing hint. The entry load refuses a `read` pin that does not - * carry `readOnlyHint: true`; `idempotentHint` and `openWorldHint` are recorded - * as they arrived and are not read by the drift comparison, which only asks - * whether a claim the pin made has been withdrawn. - * - * v1 excludes `extract_images` and `get_attachment`. The former accepts free - * markdown and fetches image URLs from it; the latter takes an attachment id - * and, with `format: content`, can fetch an external URL named by that - * attachment. These provider-side fetches can return results into agent context - * from caller-chosen targets, outside the seat's confinement. Annotations do - * not constrain that behavior, and the pinned schema cannot narrow one - * argument without drifting from the live schema. - * - * One fact about this vendor is NOT in this file, because it is the live - * walk's to settle (§10 step 7): that a removal's revoke took at Linear (shown - * by something other than the 200 the shared token/revocation endpoint answers - * either way). The other, the Settings URL a person lands on, the 2026-10-15 - * fresh-account walk settled: `/settings/security` is the WORKSPACE security - * page (admin controls, no per-person app list), while the approved grant is - * listed under Settings → Security & access → "Authorized applications" as - * "MCP · Read · Revoke", which is `/settings/account/security`. Linear - * redirects both to the signed-in workspace's slug. - * - * No tool returned an `outputSchema` in the capture, which is why none is - * pinned: nothing arrives to pin, and the field is not part of `HostedMcpPinnedTool`. - */ -import type { HostedMcpEntry, HostedMcpPinnedTool } from '../../services/hostedMcpEntryService'; - -/** - * The vendor's annotations as the pin recorded them, in one place because they - * are one set: a review of the entry should be able to see at a glance that no - * tool carries a different claim, and a tool that ever does will have to say so - * here rather than in a line 900 further down. + * CIMD remains the registration mode. openid binds providerSubject during + * reconnect. The revoke page is the signed-in Authorized applications page + * verified in the 2026-10-15 walk, not the workspace security settings page. */ -const READ_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false }; - -/** One read tool: the name an agent sees is the upstream name, checked by the entry load. */ -const readTool = ( - name: string, - description: string, - inputSchema: Record, -): HostedMcpPinnedTool => ({ - name, - upstreamName: name, - description, - class: 'read', - inputSchema, - annotations: { ...READ_ANNOTATIONS }, -}); +import type { HostedMcpEntry } from '../../services/hostedMcpEntryService'; export const LINEAR_ENTRY: HostedMcpEntry = { - id: 'linear', - title: 'Linear', - description: 'Issues, projects, cycles and releases in your own Linear workspace.', - resource: 'https://mcp.linear.app/mcp', - issuer: 'https://mcp.linear.app', - // Linear advertises `client_id_metadata_document_supported: true`, so nothing - // is registered and no secret is stored: the client id is this instance's own - // metadata URL. §4's `cimd` is the preferred mode, not the fallback. - client: 'cimd', - // `openid` only for `providerSubject` (§2's account-change rule): the token - // response carries an ID token, measured at the same time as the tool list. - // `write` is deliberately absent — see the header. - scopes: ['read', 'openid'], - revoke: { page: 'https://linear.app/settings/account/security' }, + id: "linear", + title: "Linear", + description: "Read Linear issues and projects; changes require your approval.", + resource: "https://mcp.linear.app/mcp", + issuer: "https://mcp.linear.app", + client: "cimd", + scopes: [ + "read", + "write", + "openid", + ], + revoke: { + page: "https://linear.app/settings/account/security", + }, tools: [ - readTool( - 'list_agent_skills', - 'List the Linear Agent skills this workspace can use.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], + name: "delete_attachment", + upstreamName: "delete_attachment", + description: "Remove an attachment link from a Linear issue's Resources; the stored file remains.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Issue attachment ID from get_issue's attachments array (attachments[].id)", + }, }, + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + irreversible: true, }, - ), - readTool( - 'get_agent_skill', - 'Read one Linear Agent skill by id, with its full instructions.', { - type: 'object', - properties: { - id: { - type: 'string', - description: 'Agent skill ID', + name: "list_agent_skills", + upstreamName: "list_agent_skills", + description: "List the Linear Agent skills this workspace can use.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['id'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_comments', - 'List comments, optionally narrowed to one issue, project, initiative, document, milestone or status update.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - issueId: { - description: 'Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues (provide exactly one parent)', - type: 'string', - }, - projectId: { - description: 'Project name, ID, identifier (e.g., P-ENG-123), or slug (provide exactly one parent)', - type: 'string', - }, - initiativeId: { - description: 'Initiative name, ID, identifier (e.g., I-123), or slug (provide exactly one parent)', - type: 'string', - }, - documentId: { - description: 'Document ID or slug (provide exactly one parent)', - type: 'string', - }, - milestoneId: { - description: 'Milestone UUID (provide exactly one parent). Resolve milestone names via `list_milestones` first.', - type: 'string', - }, - statusUpdateId: { - description: 'Status update UUID (provide exactly one parent). Resolve status updates via `get_status_updates` first.', - type: 'string', - }, - statusUpdateType: { - description: 'Type of status update named by `statusUpdateId`, as returned by `get_status_updates`. Only valid together with `statusUpdateId`; omit to check both project and initiative status updates.', - type: 'string', - enum: ['project', 'initiative'], + name: "get_agent_skill", + upstreamName: "get_agent_skill", + description: "Read one Linear Agent skill by id, with its full instructions.", + class: "read", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Agent skill ID", + }, }, + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_cycles', - 'List one team\'s cycles.', { - type: 'object', - properties: { - teamId: { - type: 'string', - description: 'Team ID', - }, - type: { - description: 'Filter: current, previous, next, or all', - type: 'string', - enum: ['current', 'previous', 'next'], + name: "list_comments", + upstreamName: "list_comments", + description: "List comments, optionally narrowed to one issue, project, initiative, document, milestone or status update.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + issueId: { + description: "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues (provide exactly one parent)", + type: "string", + }, + projectId: { + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug (provide exactly one parent)", + type: "string", + }, + initiativeId: { + description: "Initiative name, ID, identifier (e.g., I-123), or slug (provide exactly one parent)", + type: "string", + }, + documentId: { + description: "Document ID or slug (provide exactly one parent)", + type: "string", + }, + milestoneId: { + description: "Milestone UUID (provide exactly one parent). Resolve milestone names via `list_milestones` first.", + type: "string", + }, + statusUpdateId: { + description: "Status update UUID (provide exactly one parent). Resolve status updates via `get_status_updates` first.", + type: "string", + }, + statusUpdateType: { + description: "Type of status update named by `statusUpdateId`, as returned by `get_status_updates`. Only valid together with `statusUpdateId`; omit to check both project and initiative status updates.", + type: "string", + enum: [ + "project", + "initiative", + ], + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['teamId'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_custom_views', - 'List the saved issue, project and initiative views this account can see. The views come back, not the records they match.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - query: { - description: 'Search custom view names', - type: 'string', - }, - type: { - description: 'Entity type displayed by the view', - type: 'string', - enum: ['Issue', 'Project', 'Initiative'], - }, - team: { - description: 'Team name or ID', - type: 'string', - }, - shared: { - description: 'True for shared views, false for personal views', - type: 'boolean', + name: "save_comment", + upstreamName: "save_comment", + description: "Create or edit a comment on a Linear issue, project, initiative, document, milestone or status update.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + description: "Comment ID. If provided, updates the existing comment", + type: "string", + }, + issueId: { + description: "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues (provide exactly one parent)", + type: "string", + }, + projectId: { + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug (provide exactly one parent)", + type: "string", + }, + initiativeId: { + description: "Initiative name, ID, identifier (e.g., I-123), or slug (provide exactly one parent)", + type: "string", + }, + documentId: { + description: "Document ID or slug (provide exactly one parent)", + type: "string", + }, + milestoneId: { + description: "Milestone UUID (provide exactly one parent). Resolve milestone names via `list_milestones` first.", + type: "string", + }, + statusUpdateId: { + description: "Status update UUID (provide exactly one parent). Resolve status updates via `get_status_updates` first.", + type: "string", + }, + statusUpdateType: { + description: "Type of status update named by `statusUpdateId`, as returned by `get_status_updates`. Only valid together with `statusUpdateId`; omit to check both project and initiative status updates.", + type: "string", + enum: [ + "project", + "initiative", + ], + }, + parentId: { + description: "Parent comment ID (for replies, only when creating)", + type: "string", + }, + body: { + type: "string", + description: "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + }, }, + required: [ + "body", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, }, - ), - readTool( - 'get_document', - 'Read one document by id.', { - type: 'object', - properties: { - id: { - type: 'string', - description: 'Document ID or slug', + name: "delete_comment", + upstreamName: "delete_comment", + description: "Delete a Linear comment identified by its ID.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Comment ID", + }, }, + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, }, - required: ['id'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, }, - ), - readTool( - 'list_documents', - 'List documents, optionally filtered by project, initiative, team, creator or a date range.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - query: { - description: 'Search query', - type: 'string', - }, - projectId: { - description: 'Filter by project ID, identifier (e.g., \'P-ENG-123\'), or slug', - type: 'string', - }, - initiativeId: { - description: 'Filter by initiative ID, identifier (e.g., \'I-123\'), or slug', - type: 'string', - }, - teamId: { - description: 'Filter by team ID', - type: 'string', - }, - creatorId: { - description: 'Filter by creator ID', - type: 'string', - }, - createdAt: { - description: 'Created after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - updatedAt: { - description: 'Updated after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', - }, - fields: { - description: 'Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.', - type: 'array', - items: { - type: 'string', + name: "list_cycles", + upstreamName: "list_cycles", + description: "List one team's cycles.", + class: "read", + inputSchema: { + type: "object", + properties: { + teamId: { + type: "string", + description: "Team UUID from list_teams or get_team; names and keys are not accepted", + }, + type: { + description: "Filter relative to the team's current cycle. Omit to return cycles without this filter", + type: "string", enum: [ - 'id', - 'title', - 'content', - 'url', - 'createdAt', - 'updatedAt', - 'archivedAt', - 'creator', - 'updatedBy', - 'project', - 'initiative', - 'team', - 'issue', + "current", + "previous", + "next", ], }, }, + required: [ + "teamId", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_issue', - 'Read one issue by id or identifier, optionally with its relations, customer needs and releases.', { - type: 'object', - properties: { - id: { - type: 'string', - description: 'Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues', - }, - fields: { - description: 'Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.', - type: 'array', - items: { - type: 'string', + name: "list_custom_views", + upstreamName: "list_custom_views", + description: "List the saved issue, project and initiative views this account can see. The views come back, not the records they match.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", enum: [ - 'id', - 'uuid', - 'title', - 'description', - 'projectMilestone', - 'priority', - 'estimate', - 'url', - 'gitBranchName', - 'createdAt', - 'updatedAt', - 'archivedAt', - 'completedAt', - 'startedAt', - 'canceledAt', - 'startedTriageAt', - 'triagedAt', - 'dueDate', - 'slaStartedAt', - 'slaMediumRiskAt', - 'slaHighRiskAt', - 'slaBreachesAt', - 'slaType', - 'status', - 'statusType', - 'labels', - 'triageIntel', - 'createdBy', - 'createdById', - 'assignee', - 'assigneeId', - 'delegate', - 'delegateId', - 'project', - 'projectId', - 'parentId', - 'team', - 'teamId', - 'cycleId', - 'attachments', - 'documents', - 'stateHistory', - 'relations', - 'customerNeeds', - 'releases', + "createdAt", + "updatedAt", ], }, + query: { + description: "Search custom view names", + type: "string", + }, + type: { + description: "Entity type displayed by the view", + type: "string", + enum: [ + "Issue", + "Project", + "Initiative", + ], + }, + team: { + description: "Team name or ID", + type: "string", + }, + shared: { + description: "True for shared views, false for personal views", + type: "boolean", + }, }, - includeRelations: { - default: false, - description: 'Include blocking/related/duplicate relations', - type: 'boolean', - }, - includeCustomerNeeds: { - default: false, - description: 'Include associated customer needs', - type: 'boolean', - }, - includeReleases: { - default: false, - description: 'Include associated releases', - type: 'boolean', - }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['id'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_issues', - 'List issues, optionally filtered by team, state, cycle, label, assignee, creator, delegate, project, release, priority, parent or a search query.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], + name: "get_document", + upstreamName: "get_document", + description: "Read one document by id.", + class: "read", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Document ID or slug", + }, }, - query: { - description: 'Search issue title or description. Cannot be combined with customView', - type: 'string', + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "list_documents", + upstreamName: "list_documents", + description: "List documents, optionally filtered by project, initiative, team, creator or a date range.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + query: { + description: "Search query", + type: "string", + }, + projectId: { + description: "Filter by project ID, identifier (e.g., 'P-ENG-123'), or slug", + type: "string", + }, + initiativeId: { + description: "Filter by initiative ID, identifier (e.g., 'I-123'), or slug", + type: "string", + }, + teamId: { + description: "Filter by team ID", + type: "string", + }, + creatorId: { + description: "Filter by creator ID", + type: "string", + }, + createdAt: { + description: "Created after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + updatedAt: { + description: "Updated after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, + fields: { + description: "Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.", + type: "array", + items: { + type: "string", + enum: [ + "id", + "title", + "content", + "url", + "createdAt", + "updatedAt", + "archivedAt", + "creator", + "updatedBy", + "project", + "initiative", + "team", + "issue", + ], + }, + }, }, - customView: { - description: 'Saved view ID, URL, slug, or exact name. Use list_custom_views to discover views. Applies saved filters and scope; other filters narrow the results. Uses list ordering, not saved display settings', - type: 'string', - minLength: 1, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "save_document", + upstreamName: "save_document", + description: "Create or edit a Linear document and its links to workspace records.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + description: "Document ID or slug to update. Omit to create a new document.", + type: "string", + }, + title: { + description: "Document title (required when creating)", + type: "string", + }, + content: { + description: "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + type: "string", + }, + patch: { + description: "Partial edits applied to the current content, in order and atomically (one failing operation aborts the whole save). Every anchor string must match the current content exactly once. Only valid on update, in place of the full content/description field", + minItems: 1, + maxItems: 50, + type: "array", + items: { + oneOf: [ + { + type: "object", + properties: { + op: { + type: "string", + const: "replace", + }, + old_string: { + type: "string", + minLength: 1, + description: "Exact text to replace. Must match the current content exactly once", + }, + new_string: { + type: "string", + description: "Replacement text. Empty string deletes the match", + }, + replace_all: { + description: "Replace every occurrence instead of requiring a unique match", + type: "boolean", + }, + }, + required: [ + "op", + "old_string", + "new_string", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "insert_before", + }, + anchor: { + type: "string", + minLength: 1, + description: "Exact text to insert before. Must match the current content exactly once", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert directly before the anchor. Include separators (e.g. \"\\n\\n\")", + }, + }, + required: [ + "op", + "anchor", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "insert_after", + }, + anchor: { + type: "string", + minLength: 1, + description: "Exact text to insert after. Must match the current content exactly once", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert directly after the anchor. Include separators (e.g. \"\\n\\n\")", + }, + }, + required: [ + "op", + "anchor", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "prepend", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert at the very start of the content", + }, + }, + required: [ + "op", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "append", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert at the very end of the content", + }, + }, + required: [ + "op", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "replace_range", + }, + from: { + type: "string", + minLength: 1, + description: "Exact text where the range starts (inclusive). Must match exactly once", + }, + to: { + type: "string", + minLength: 1, + description: "Exact text where the range ends (exclusive, stays in place). Must match exactly once after `from`", + }, + new_string: { + type: "string", + description: "Text replacing the range. Empty string deletes it", + }, + }, + required: [ + "op", + "from", + "to", + "new_string", + ], + }, + ], + }, + }, + project: { + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug", + type: "string", + }, + issue: { + description: "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues", + type: "string", + }, + initiative: { + description: "Initiative name, ID, identifier (e.g., I-123), or slug", + type: "string", + }, + cycle: { + description: "Cycle name, number, or ID. When passing a name or number, also pass `team` to disambiguate.", + type: "string", + }, + team: { + description: "Team name or ID. Attaches the document to the team, unless `cycle` is also passed, in which case it disambiguates the cycle.", + type: "string", + }, + icon: { + description: "Icon name or emoji code (e.g. \"Rocket\" or \":eagle:\"), not a raw Unicode emoji", + type: "string", + }, + color: { + description: "Hex color", + type: "string", + }, }, - team: { - description: 'Team name or ID', - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, + }, + { + name: "get_issue", + upstreamName: "get_issue", + description: "Read one issue by id or identifier, optionally with its relations, customer needs and releases.", + class: "read", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues", + }, + fields: { + description: "Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.", + type: "array", + items: { + type: "string", + enum: [ + "id", + "uuid", + "title", + "description", + "projectMilestone", + "priority", + "estimate", + "url", + "gitBranchName", + "createdAt", + "updatedAt", + "archivedAt", + "completedAt", + "startedAt", + "canceledAt", + "startedTriageAt", + "triagedAt", + "dueDate", + "slaStartedAt", + "slaMediumRiskAt", + "slaHighRiskAt", + "slaBreachesAt", + "slaType", + "status", + "statusType", + "labels", + "triageIntel", + "createdBy", + "createdById", + "assignee", + "assigneeId", + "delegate", + "delegateId", + "project", + "projectId", + "parentId", + "team", + "teamId", + "cycleId", + "attachments", + "documents", + "stateHistory", + "relations", + "customerNeeds", + "releases", + ], + }, + }, + includeRelations: { + default: false, + description: "Include blocking/related/duplicate relations", + type: "boolean", + }, + includeCustomerNeeds: { + default: false, + description: "Include associated customer needs", + type: "boolean", + }, + includeReleases: { + default: false, + description: "Include associated releases", + type: "boolean", + }, }, - includeSubTeams: { - default: false, - description: 'Include results from the selected team and all descendants when team is set. Team ID filters already include descendants; this also expands name and key filters', - type: 'boolean', + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "list_issues", + upstreamName: "list_issues", + description: "List issues, optionally filtered by team, state, cycle, label, assignee, creator, delegate, project, release, priority, parent or a search query.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + query: { + description: "Search issue title or description. Cannot be combined with customView", + type: "string", + }, + customView: { + description: "Saved view ID, URL, slug, or exact name. Use list_custom_views to discover views. Applies saved filters and scope; other filters narrow the results. Uses list ordering, not saved display settings", + type: "string", + minLength: 1, + }, + team: { + description: "Team name or ID", + type: "string", + }, + includeSubTeams: { + default: false, + description: "Include results from the selected team and all descendants when team is set. Team ID filters already include descendants; this also expands name and key filters", + type: "boolean", + }, + state: { + description: "State type, name, or ID", + type: "string", + }, + cycle: { + description: "Cycle name, number, or ID", + type: "string", + }, + label: { + description: "Label name or ID", + type: "string", + }, + assignee: { + description: "User ID, name, email, or \"me\"", + type: [ + "string", + "null", + ], + }, + creator: { + description: "User ID, name, email, or \"me\"", + type: "string", + }, + delegate: { + description: "Agent name or ID. When the user asks to delegate to \"Linear\" or \"the Linear agent\", this refers to the \"Linear\" app user specifically", + type: "string", + }, + project: { + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug", + type: "string", + }, + release: { + description: "Release ID or slug", + type: "string", + }, + priority: { + description: "0=None, 1=Urgent, 2=High, 3=Medium, 4=Low", + type: "number", + }, + parentId: { + description: "Parent issue ID or identifier (e.g., LIN-123)", + type: "string", + }, + fields: { + description: "Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.", + type: "array", + items: { + type: "string", + enum: [ + "id", + "uuid", + "title", + "description", + "projectMilestone", + "priority", + "estimate", + "url", + "gitBranchName", + "createdAt", + "updatedAt", + "archivedAt", + "completedAt", + "startedAt", + "canceledAt", + "startedTriageAt", + "triagedAt", + "dueDate", + "slaStartedAt", + "slaMediumRiskAt", + "slaHighRiskAt", + "slaBreachesAt", + "slaType", + "status", + "statusType", + "labels", + "triageIntel", + "createdBy", + "createdById", + "assignee", + "assigneeId", + "delegate", + "delegateId", + "project", + "projectId", + "parentId", + "team", + "teamId", + "cycleId", + ], + }, + }, + createdAt: { + description: "Created after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + updatedAt: { + description: "Updated after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + triagedAt: { + description: "Left triage after: ISO-8601 date/duration (e.g., -P1D). Issues still in triage have no triagedAt; list them with state \"triage\"", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, }, - state: { - description: 'State type, name, or ID', - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "save_issue", + upstreamName: "save_issue", + description: "Create or edit a Linear issue. Assignee \"me\" means the connection owner; delegate assigns work to an agent installed in that Linear workspace.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + description: "Only for updating an existing issue. Pass the issue ID or identifier (e.g., LIN-123). Do NOT pass this parameter when creating a new issue.", + type: "string", + }, + title: { + description: "Issue title (required when creating, unless template is set)", + type: "string", + }, + description: { + description: "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + type: "string", + }, + patch: { + description: "Partial edits applied to the current content, in order and atomically (one failing operation aborts the whole save). Every anchor string must match the current content exactly once. Only valid on update, in place of the full content/description field", + minItems: 1, + maxItems: 50, + type: "array", + items: { + oneOf: [ + { + type: "object", + properties: { + op: { + type: "string", + const: "replace", + }, + old_string: { + type: "string", + minLength: 1, + description: "Exact text to replace. Must match the current content exactly once", + }, + new_string: { + type: "string", + description: "Replacement text. Empty string deletes the match", + }, + replace_all: { + description: "Replace every occurrence instead of requiring a unique match", + type: "boolean", + }, + }, + required: [ + "op", + "old_string", + "new_string", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "insert_before", + }, + anchor: { + type: "string", + minLength: 1, + description: "Exact text to insert before. Must match the current content exactly once", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert directly before the anchor. Include separators (e.g. \"\\n\\n\")", + }, + }, + required: [ + "op", + "anchor", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "insert_after", + }, + anchor: { + type: "string", + minLength: 1, + description: "Exact text to insert after. Must match the current content exactly once", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert directly after the anchor. Include separators (e.g. \"\\n\\n\")", + }, + }, + required: [ + "op", + "anchor", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "prepend", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert at the very start of the content", + }, + }, + required: [ + "op", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "append", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert at the very end of the content", + }, + }, + required: [ + "op", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "replace_range", + }, + from: { + type: "string", + minLength: 1, + description: "Exact text where the range starts (inclusive). Must match exactly once", + }, + to: { + type: "string", + minLength: 1, + description: "Exact text where the range ends (exclusive, stays in place). Must match exactly once after `from`", + }, + new_string: { + type: "string", + description: "Text replacing the range. Empty string deletes it", + }, + }, + required: [ + "op", + "from", + "to", + "new_string", + ], + }, + ], + }, + }, + team: { + description: "Team name or ID (required when creating)", + type: "string", + }, + template: { + description: "Template name or ID. Applied on create only. The template fills in the content, fields, and sub-issues server-side. A `description` replaces the template body rather than merging with it, but an empty `description` keeps the template body. Labels merge with the template's own", + type: "string", + }, + cycle: { + description: "Cycle name, number, or ID. Null to remove", + type: [ + "string", + "null", + ], + }, + milestone: { + description: "Milestone name or ID", + type: "string", + }, + priority: { + description: "0=None, 1=Urgent, 2=High, 3=Medium, 4=Low", + type: "number", + }, + project: { + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug. Null to remove", + type: [ + "string", + "null", + ], + }, + state: { + description: "State type, name, or ID", + type: "string", + }, + assignee: { + description: "User ID, name, email, or \"me\". Null to remove", + type: [ + "string", + "null", + ], + }, + delegate: { + description: "Agent name or ID. When the user asks to delegate to \"Linear\" or \"the Linear agent\", this refers to the \"Linear\" app user specifically. Null to remove", + type: [ + "string", + "null", + ], + }, + labels: { + description: "Label names or IDs as a JSON array of strings (e.g. [\"Bug\", \"Urgent\"]). Replaces the full label set; existing labels not included are removed. Omit to leave labels unchanged. Cannot be combined with addLabels/removeLabels; prefer those for incremental changes", + type: "array", + items: { + type: "string", + }, + }, + addLabels: { + description: "Label names or IDs as a JSON array of strings (e.g. [\"Bug\", \"Urgent\"]) to add. Append-only; existing labels are never removed. Cannot be combined with a team change", + type: "array", + items: { + type: "string", + }, + }, + removeLabels: { + description: "Label names or IDs as a JSON array of strings (e.g. [\"Bug\", \"Urgent\"]) to remove. Only valid when updating an existing issue. Cannot be combined with a team change", + type: "array", + items: { + type: "string", + }, + }, + dueDate: { + description: "Due date (ISO format). On update, pass null to remove the due date", + type: [ + "string", + "null", + ], + }, + slaBreachesAt: { + description: "ISO-8601 timestamp when the SLA will breach. On update, pass null to remove the SLA", + anyOf: [ + { + type: "string", + format: "date-time", + pattern: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + }, + { + type: "null", + }, + ], + }, + slaType: { + description: "SLA day counting type: \"all\" or \"onlyBusinessDays\". Only use with slaBreachesAt; null means unset", + anyOf: [ + { + type: "string", + enum: [ + "all", + "onlyBusinessDays", + ], + }, + { + type: "null", + }, + ], + }, + parentId: { + description: "Parent issue ID or identifier (e.g., LIN-123). Null to remove", + type: [ + "string", + "null", + ], + }, + estimate: { + description: "Issue estimate value. On create, pass null or omit for no estimate. On update, pass null to clear the estimate; omitting leaves it unchanged. 0 is a real estimate only on teams that allow zero estimates.", + type: [ + "number", + "null", + ], + }, + links: { + description: "Link attachments to add [{url, title}]. Append-only; existing links are never removed", + type: "array", + items: { + type: "object", + properties: { + url: { + type: "string", + format: "uri", + }, + title: { + type: "string", + minLength: 1, + }, + }, + required: [ + "url", + "title", + ], + }, + }, + setReleases: { + description: "Replace all releases on the issue with these. Cannot be combined with addReleases/removeReleases", + type: "array", + items: { + type: "string", + }, + }, + addReleases: { + description: "Release IDs or slugs to add. Append-only; existing releases are never removed", + type: "array", + items: { + type: "string", + }, + }, + removeReleases: { + description: "Release IDs or slugs to remove. Only valid when updating an existing issue", + type: "array", + items: { + type: "string", + }, + }, + blocks: { + description: "Issue IDs/identifiers this blocks. Append-only; existing relations are never removed", + type: "array", + items: { + type: "string", + }, + }, + blockedBy: { + description: "Issue IDs/identifiers blocking this. Append-only; existing relations are never removed", + type: "array", + items: { + type: "string", + }, + }, + relatedTo: { + description: "Related issue IDs/identifiers. Append-only; existing relations are never removed", + type: "array", + items: { + type: "string", + }, + }, + duplicateOf: { + description: "Duplicate of issue ID/identifier. Null to remove", + type: [ + "string", + "null", + ], + }, + removeBlocks: { + description: "Issue IDs/identifiers to stop blocking", + type: "array", + items: { + type: "string", + }, + }, + removeBlockedBy: { + description: "Issue IDs/identifiers to remove as blockers of this issue", + type: "array", + items: { + type: "string", + }, + }, + removeRelatedTo: { + description: "Related issue IDs/identifiers to remove", + type: "array", + items: { + type: "string", + }, + }, }, - cycle: { - description: 'Cycle name, number, or ID', - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, + }, + { + name: "list_issue_statuses", + upstreamName: "list_issue_statuses", + description: "List the workflow statuses of one team.", + class: "read", + inputSchema: { + type: "object", + properties: { + team: { + type: "string", + description: "Team name or ID", + }, }, - label: { - description: 'Label name or ID', - type: 'string', + required: [ + "team", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "get_issue_status", + upstreamName: "get_issue_status", + description: "Read one workflow status by id, or by name within a team.", + class: "read", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Status ID", + }, + name: { + type: "string", + description: "Status name", + }, + team: { + type: "string", + description: "Team name or ID", + }, }, - assignee: { - description: 'User ID, name, email, or "me"', - type: ['string', 'null'], + required: [ + "id", + "name", + "team", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "list_issue_labels", + upstreamName: "list_issue_labels", + description: "List issue labels, optionally filtered by name or team, and optionally including label groups.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + name: { + description: "Filter by name", + type: "string", + }, + team: { + description: "Team name or ID", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, + includeGroups: { + default: false, + description: "Include label groups in the results, marked with `isGroup`. A group cannot be applied directly; retiring or restoring one cascades to its child labels", + type: "boolean", + }, }, - creator: { - description: 'User ID, name, email, or "me"', - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "save_issue_label", + upstreamName: "save_issue_label", + description: "Create or edit an issue label in Linear, including its grouping and team.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + description: "Label name or ID. If provided, updates the existing label", + type: "string", + }, + name: { + description: "Label name (required when creating)", + type: "string", + }, + description: { + description: "Label description. Null to clear it", + type: [ + "string", + "null", + ], + }, + color: { + description: "Hex color code", + type: "string", + }, + teamId: { + description: "Team UUID (omit for workspace label). When updating, only disambiguates `id` by name; a label's team cannot be changed", + type: "string", + }, + parent: { + description: "Parent label group name or ID. Null to remove from its group", + type: [ + "string", + "null", + ], + }, + isGroup: { + description: "Whether the label is a group", + type: "boolean", + }, + groupType: { + description: "Selection mode for label groups. Defaults to singleSelect when creating; omit to keep it unchanged when updating", + type: "string", + enum: [ + "singleSelect", + "multiSelect", + ], + }, }, - delegate: { - description: 'Agent name or ID. When the user asks to delegate to "Linear" or "the Linear agent", this refers to the "Linear" app user specifically', - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, + }, + { + name: "retire_issue_label", + upstreamName: "retire_issue_label", + description: "Retire a Linear issue label from active use.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Label or label group ID", + }, }, - project: { - description: 'Project name, ID, identifier (e.g., P-ENG-123), or slug', - type: 'string', + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: false, + }, + { + name: "restore_issue_label", + upstreamName: "restore_issue_label", + description: "Restore a retired Linear issue label to active use.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Label or label group ID", + }, }, - release: { - description: 'Release ID or slug', - type: 'string', + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: false, + }, + { + name: "list_projects", + upstreamName: "list_projects", + description: "List projects, optionally filtered by state, initiative, team, member, label or a search query.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Max results (default 50, max 50)", + type: "number", + maximum: 50, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + customView: { + description: "Saved view ID, URL, slug, or exact name. Use list_custom_views to discover views. Applies saved filters and scope; other filters narrow the results. Uses list ordering, not saved display settings", + type: "string", + minLength: 1, + }, + query: { + description: "Search project name", + type: "string", + }, + state: { + description: "State type, name, or ID", + type: "string", + }, + initiative: { + description: "Initiative name, ID, identifier (e.g., I-123), or slug", + type: "string", + }, + team: { + description: "Team name or ID", + type: "string", + }, + includeSubTeams: { + default: false, + description: "Include results from the selected team and all descendants when team is set. Team ID filters already include descendants; this also expands name and key filters", + type: "boolean", + }, + member: { + description: "User ID, name, email, or \"me\"", + type: "string", + }, + label: { + description: "Label name or ID", + type: "string", + }, + createdAt: { + description: "Created after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + updatedAt: { + description: "Updated after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + includeMilestones: { + default: false, + description: "Include milestones", + type: "boolean", + }, + includeMembers: { + default: false, + description: "Include project members", + type: "boolean", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, + fields: { + description: "Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.", + type: "array", + items: { + type: "string", + enum: [ + "id", + "uuid", + "name", + "summary", + "description", + "url", + "trashed", + "createdAt", + "updatedAt", + "startedAt", + "completedAt", + "canceledAt", + "startDate", + "startDateResolution", + "targetDate", + "targetDateResolution", + "priority", + "sortOrder", + "labels", + "initiatives", + "lead", + "leadTeam", + "status", + "teams", + "members", + "milestones", + ], + }, + }, }, - priority: { - description: '0=None, 1=Urgent, 2=High, 3=Medium, 4=Low', - type: 'number', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "get_project", + upstreamName: "get_project", + description: "Read one project by name or id, optionally with its milestones, members, resources and customer needs.", + class: "read", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug", + }, + includeCustomerNeeds: { + default: false, + description: "Include one page of customer needs attached directly to the project", + type: "boolean", + }, + customerNeedsLimit: { + default: 10, + description: "Customer needs per page when includeCustomerNeeds is true (default 10, max 50)", + type: "integer", + minimum: 1, + maximum: 50, + }, + customerNeedsCursor: { + description: "Next customer-needs page: pass customerNeedsPageInfo.endCursor with includeCustomerNeeds true", + type: "string", + }, + includeMilestones: { + default: false, + description: "Include milestones", + type: "boolean", + }, + includeMembers: { + default: false, + description: "Include project members", + type: "boolean", + }, + includeResources: { + default: false, + description: "Include resources (documents, links, attachments)", + type: "boolean", + }, }, - parentId: { - description: 'Parent issue ID or identifier (e.g., LIN-123)', - type: 'string', + required: [ + "query", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "save_project", + upstreamName: "save_project", + description: "Create or edit a Linear project, including its teams, dates, labels and links.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug. If provided, updates the existing project", + type: "string", + }, + name: { + description: "Project name (required when creating)", + type: "string", + }, + template: { + description: "Template name or ID. Applied on create only. The template fills in the content, fields, and sub-issues server-side. A `description` replaces the template body rather than merging with it, but an empty `description` keeps the template body. Labels merge with the template's own", + type: "string", + }, + icon: { + description: "Icon name or emoji code (e.g. \"Rocket\" or \":eagle:\"), not a raw Unicode emoji", + type: "string", + }, + color: { + description: "Hex color", + type: "string", + }, + summary: { + description: "Short summary (max 255 chars)", + type: "string", + }, + description: { + description: "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + type: "string", + }, + patch: { + description: "Partial edits applied to the current content, in order and atomically (one failing operation aborts the whole save). Every anchor string must match the current content exactly once. Only valid on update, in place of the full content/description field", + minItems: 1, + maxItems: 50, + type: "array", + items: { + oneOf: [ + { + type: "object", + properties: { + op: { + type: "string", + const: "replace", + }, + old_string: { + type: "string", + minLength: 1, + description: "Exact text to replace. Must match the current content exactly once", + }, + new_string: { + type: "string", + description: "Replacement text. Empty string deletes the match", + }, + replace_all: { + description: "Replace every occurrence instead of requiring a unique match", + type: "boolean", + }, + }, + required: [ + "op", + "old_string", + "new_string", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "insert_before", + }, + anchor: { + type: "string", + minLength: 1, + description: "Exact text to insert before. Must match the current content exactly once", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert directly before the anchor. Include separators (e.g. \"\\n\\n\")", + }, + }, + required: [ + "op", + "anchor", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "insert_after", + }, + anchor: { + type: "string", + minLength: 1, + description: "Exact text to insert after. Must match the current content exactly once", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert directly after the anchor. Include separators (e.g. \"\\n\\n\")", + }, + }, + required: [ + "op", + "anchor", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "prepend", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert at the very start of the content", + }, + }, + required: [ + "op", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "append", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert at the very end of the content", + }, + }, + required: [ + "op", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "replace_range", + }, + from: { + type: "string", + minLength: 1, + description: "Exact text where the range starts (inclusive). Must match exactly once", + }, + to: { + type: "string", + minLength: 1, + description: "Exact text where the range ends (exclusive, stays in place). Must match exactly once after `from`", + }, + new_string: { + type: "string", + description: "Text replacing the range. Empty string deletes it", + }, + }, + required: [ + "op", + "from", + "to", + "new_string", + ], + }, + ], + }, + }, + state: { + description: "Project status name, type, or ID. Names resolve within the lead team's project statuses. If the lead team is unknown, standard labels resolve as status types; custom names require a status ID or leadTeam", + type: "string", + }, + startDate: { + description: "Start date (ISO format). Pair with startDateResolution to indicate precision (e.g. month, quarter). Null to remove", + type: [ + "string", + "null", + ], + }, + startDateResolution: { + description: "Start date resolution. Null for an exact day. When omitted, the current resolution stays and the date snaps to it", + anyOf: [ + { + type: "string", + enum: [ + "halfYear", + "month", + "quarter", + "year", + ], + }, + { + type: "null", + }, + ], + }, + targetDate: { + description: "Target date (ISO format). Pair with targetDateResolution to indicate precision (e.g. month, quarter). Null to remove", + type: [ + "string", + "null", + ], + }, + targetDateResolution: { + description: "Target date resolution. Null for an exact day. When omitted, the current resolution stays and the date snaps to it", + anyOf: [ + { + type: "string", + enum: [ + "halfYear", + "month", + "quarter", + "year", + ], + }, + { + type: "null", + }, + ], + }, + priority: { + description: "0=None, 1=Urgent, 2=High, 3=Medium, 4=Low", + type: "integer", + minimum: 0, + maximum: 4, + }, + addTeams: { + description: "Team name or ID to add", + type: "array", + items: { + type: "string", + }, + }, + removeTeams: { + description: "Team name or ID to remove", + type: "array", + items: { + type: "string", + }, + }, + setTeams: { + description: "Replace all teams with these. Cannot combine with addTeams/removeTeams", + type: "array", + items: { + type: "string", + }, + }, + leadTeam: { + description: "Team that leads the project. Team name or ID. Overrides the default lead team; added to the project's teams when not among them", + type: "string", + }, + labels: { + description: "Label names or IDs as a JSON array of strings (e.g. [\"Bug\", \"Urgent\"]). Replaces the full label set; existing labels not included are removed. Omit to leave labels unchanged", + type: "array", + items: { + type: "string", + }, + }, + lead: { + description: "User ID, name, email, or \"me\". Null to remove, except on create with a template that names a lead", + type: [ + "string", + "null", + ], + }, + addInitiatives: { + description: "Initiatives to add. Initiative name, ID, identifier (e.g., I-123), or slug", + type: "array", + items: { + type: "string", + }, + }, + removeInitiatives: { + description: "Initiatives to remove. Initiative name, ID, identifier (e.g., I-123), or slug", + type: "array", + items: { + type: "string", + }, + }, + setInitiatives: { + description: "Replace all initiatives with these. Initiative name, ID, identifier (e.g., I-123), or slug. Cannot combine with addInitiatives/removeInitiatives", + type: "array", + items: { + type: "string", + }, + }, + links: { + description: "External resource links to add [{url, title}]. Append-only; existing links are never removed", + type: "array", + items: { + type: "object", + properties: { + url: { + type: "string", + format: "uri", + }, + title: { + type: "string", + minLength: 1, + }, + }, + required: [ + "url", + "title", + ], + }, + }, }, - fields: { - description: 'Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.', - type: 'array', - items: { - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, + }, + { + name: "list_project_labels", + upstreamName: "list_project_labels", + description: "List project labels, optionally filtered by name or team, and optionally including label groups.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", enum: [ - 'id', - 'uuid', - 'title', - 'description', - 'projectMilestone', - 'priority', - 'estimate', - 'url', - 'gitBranchName', - 'createdAt', - 'updatedAt', - 'archivedAt', - 'completedAt', - 'startedAt', - 'canceledAt', - 'startedTriageAt', - 'triagedAt', - 'dueDate', - 'slaStartedAt', - 'slaMediumRiskAt', - 'slaHighRiskAt', - 'slaBreachesAt', - 'slaType', - 'status', - 'statusType', - 'labels', - 'triageIntel', - 'createdBy', - 'createdById', - 'assignee', - 'assigneeId', - 'delegate', - 'delegateId', - 'project', - 'projectId', - 'parentId', - 'team', - 'teamId', - 'cycleId', + "createdAt", + "updatedAt", ], }, + name: { + description: "Filter by name", + type: "string", + }, + team: { + description: "Team name or ID", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, + includeGroups: { + default: false, + description: "Include label groups in the results, marked with `isGroup`. A group cannot be applied directly; retiring or restoring one cascades to its child labels", + type: "boolean", + }, }, - createdAt: { - description: 'Created after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - updatedAt: { - description: 'Updated after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "retire_project_label", + upstreamName: "retire_project_label", + description: "Retire a Linear project label from active use.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Label or label group ID", + }, }, - triagedAt: { - description: 'Left triage after: ISO-8601 date/duration (e.g., -P1D). Issues still in triage have no triagedAt; list them with state "triage"', - type: 'string', + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: false, + }, + { + name: "restore_project_label", + upstreamName: "restore_project_label", + description: "Restore a retired Linear project label to active use.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Label or label group ID", + }, }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: false, + }, + { + name: "save_project_label", + upstreamName: "save_project_label", + description: "Create or edit a Linear project label, including its grouping and team.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + description: "Label name or ID. If provided, updates the existing label", + type: "string", + }, + name: { + description: "Label name (required when creating)", + type: "string", + }, + description: { + description: "Label description. Null to clear it", + type: [ + "string", + "null", + ], + }, + color: { + description: "Hex color code", + type: "string", + }, + teamId: { + description: "Team UUID (omit for workspace label). When updating, only disambiguates `id` by name; a label's team cannot be changed", + type: "string", + }, + parent: { + description: "Parent label group name or ID. Null to remove from its group", + type: [ + "string", + "null", + ], + }, + isGroup: { + description: "Whether the label is a group", + type: "boolean", + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, }, - ), - readTool( - 'list_issue_statuses', - 'List the workflow statuses of one team.', { - type: 'object', - properties: { - team: { - type: 'string', - description: 'Team name or ID', + name: "list_release_pipelines", + upstreamName: "list_release_pipelines", + description: "List release pipelines, optionally narrowed to one team or to the production pipelines.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + query: { + description: "Search pipeline name", + type: "string", + }, + team: { + description: "Team name or ID", + type: "string", + }, + type: { + description: "Pipeline type: continuous | scheduled", + type: "string", + enum: [ + "continuous", + "scheduled", + ], + }, + isProduction: { + description: "Filter by production pipeline flag", + type: "boolean", + }, + includeStages: { + default: false, + description: "Include each pipeline's stages", + type: "boolean", + }, + includeTeams: { + default: false, + description: "Include each pipeline's teams", + type: "boolean", + }, + createdAt: { + description: "Created after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + updatedAt: { + description: "Updated after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['team'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_issue_status', - 'Read one workflow status by id, or by name within a team.', { - type: 'object', - properties: { - id: { - type: 'string', - description: 'Status ID', + name: "list_releases", + upstreamName: "list_releases", + description: "List releases, optionally filtered by pipeline, stage, stage type, version or a search query.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + query: { + description: "Search release name or version", + type: "string", + }, + pipeline: { + description: "Release pipeline ID, slug, or exact name", + type: "string", + }, + stage: { + description: "Release stage ID or exact name", + type: "string", + }, + stageType: { + description: "Filter by stage lifecycle type", + type: "string", + enum: [ + "planned", + "started", + "completed", + "canceled", + ], + }, + version: { + description: "Exact version match", + type: "string", + }, + hasReleaseNotes: { + description: "Filter to releases that do (true) or do not (false) have release notes", + type: "boolean", + }, + includeReleaseNotes: { + default: false, + description: "Include associated release notes", + type: "boolean", + }, + createdAt: { + description: "Created after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + updatedAt: { + description: "Updated after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, }, - name: { - type: 'string', - description: 'Status name', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "get_release", + upstreamName: "get_release", + description: "Read one release by id, optionally with its release notes.", + class: "read", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Release ID or slug", + }, + includeReleaseNotes: { + default: false, + description: "Include associated release notes", + type: "boolean", + }, }, - team: { - type: 'string', - description: 'Team name or ID', + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "save_release", + upstreamName: "save_release", + description: "Create or edit a release in a Linear release pipeline.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + description: "Release ID or slug to update. Omit to create a new release.", + type: "string", + }, + name: { + description: "Release name (required when creating)", + type: "string", + }, + description: { + description: "Release description", + type: "string", + }, + version: { + description: "Version identifier", + type: "string", + }, + pipeline: { + description: "Release pipeline ID, slug, or exact name (required when creating)", + type: "string", + }, + stage: { + description: "Release stage ID, exact name, or lifecycle type within the release pipeline", + type: "string", + }, + startDate: { + description: "Estimated start date (ISO YYYY-MM-DD, null to remove)", + type: [ + "string", + "null", + ], + }, + targetDate: { + description: "Estimated completion date (ISO YYYY-MM-DD, null to remove)", + type: [ + "string", + "null", + ], + }, + createdAt: { + description: "Import/create timestamp (ISO DateTime)", + type: "string", + }, + startedAt: { + description: "Started timestamp (ISO DateTime, null to remove)", + type: [ + "string", + "null", + ], + }, + completedAt: { + description: "Completed timestamp (ISO DateTime, null to remove)", + type: [ + "string", + "null", + ], + }, + commitSha: { + description: "Commit SHA associated with the release", + type: "string", + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, }, - required: ['id', 'name', 'team'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + irreversible: true, }, - ), - readTool( - 'list_issue_labels', - 'List issue labels, optionally filtered by name or team, and optionally including label groups.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, + name: "list_release_notes", + upstreamName: "list_release_notes", + description: "List release notes, optionally filtered by pipeline, release or a search query.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + query: { + description: "Search release notes title", + type: "string", + }, + pipeline: { + description: "Release pipeline ID, slug, or exact name", + type: "string", + }, + release: { + description: "Release ID or slug", + type: "string", + }, + includeContent: { + default: false, + description: "Include markdown release notes content", + type: "boolean", + }, + includeReleases: { + default: false, + description: "Include associated releases", + type: "boolean", + }, + createdAt: { + description: "Created after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + updatedAt: { + description: "Updated after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, }, - cursor: { - description: 'Next page cursor', - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "get_release_note", + upstreamName: "get_release_note", + description: "Read one release note by id or slug, including its markdown.", + class: "read", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Release notes ID or slug", + }, + includeReleases: { + default: false, + description: "Include associated releases", + type: "boolean", + }, }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "save_release_note", + upstreamName: "save_release_note", + description: "Create or edit published release-note text and its associated releases in Linear.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + description: "Release notes ID or slug to update. Omit to create new release notes.", + type: "string", + }, + pipeline: { + description: "Release pipeline ID, slug, or exact name (required when creating)", + type: "string", + }, + title: { + description: "Release notes title", + type: "string", + }, + content: { + description: "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe)", + type: "string", + }, + patch: { + description: "Partial edits applied to the current content, in order and atomically (one failing operation aborts the whole save). Every anchor string must match the current content exactly once. Only valid on update, in place of the full content/description field", + minItems: 1, + maxItems: 50, + type: "array", + items: { + oneOf: [ + { + type: "object", + properties: { + op: { + type: "string", + const: "replace", + }, + old_string: { + type: "string", + minLength: 1, + description: "Exact text to replace. Must match the current content exactly once", + }, + new_string: { + type: "string", + description: "Replacement text. Empty string deletes the match", + }, + replace_all: { + description: "Replace every occurrence instead of requiring a unique match", + type: "boolean", + }, + }, + required: [ + "op", + "old_string", + "new_string", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "insert_before", + }, + anchor: { + type: "string", + minLength: 1, + description: "Exact text to insert before. Must match the current content exactly once", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert directly before the anchor. Include separators (e.g. \"\\n\\n\")", + }, + }, + required: [ + "op", + "anchor", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "insert_after", + }, + anchor: { + type: "string", + minLength: 1, + description: "Exact text to insert after. Must match the current content exactly once", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert directly after the anchor. Include separators (e.g. \"\\n\\n\")", + }, + }, + required: [ + "op", + "anchor", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "prepend", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert at the very start of the content", + }, + }, + required: [ + "op", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "append", + }, + text: { + type: "string", + minLength: 1, + description: "Text to insert at the very end of the content", + }, + }, + required: [ + "op", + "text", + ], + }, + { + type: "object", + properties: { + op: { + type: "string", + const: "replace_range", + }, + from: { + type: "string", + minLength: 1, + description: "Exact text where the range starts (inclusive). Must match exactly once", + }, + to: { + type: "string", + minLength: 1, + description: "Exact text where the range ends (exclusive, stays in place). Must match exactly once after `from`", + }, + new_string: { + type: "string", + description: "Text replacing the range. Empty string deletes it", + }, + }, + required: [ + "op", + "from", + "to", + "new_string", + ], + }, + ], + }, + }, + releases: { + description: "Release IDs or slugs to include in the note", + type: "array", + items: { + type: "string", + }, + }, + rangeFromRelease: { + description: "Oldest release ID or slug in the note range", + type: "string", + }, + rangeToRelease: { + description: "Newest release ID or slug in the note range", + type: "string", + }, }, - name: { - description: 'Filter by name', - type: 'string', + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, + }, + { + name: "share_issue", + upstreamName: "share_issue", + description: "Give the specified user access to a Linear issue.", + class: "write", + inputSchema: { + type: "object", + properties: { + issue: { + type: "string", + description: "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues", + }, + user: { + type: "string", + description: "User ID, name, email, or \"me\" Must be an email address or UUID.", + }, }, - team: { - description: 'Team name or ID', - type: 'string', + required: [ + "issue", + "user", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, + }, + { + name: "unshare_issue", + upstreamName: "unshare_issue", + description: "Remove the specified user from the sharing list of a Linear issue.", + class: "write", + inputSchema: { + type: "object", + properties: { + issue: { + type: "string", + description: "Issue ID or identifier (e.g., LIN-123). P-prefixed identifiers (e.g., P-ENG-123) are projects, not issues", + }, + user: { + type: "string", + description: "User ID, name, email, or \"me\" Must be an email address or UUID.", + }, }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', + required: [ + "issue", + "user", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: false, + }, + { + name: "list_milestones", + upstreamName: "list_milestones", + description: "List the milestones of one project.", + class: "read", + inputSchema: { + type: "object", + properties: { + project: { + type: "string", + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug", + }, }, - includeGroups: { - default: false, - description: 'Include label groups in the results, marked with `isGroup`. A group cannot be applied directly; retiring or restoring one cascades to its child labels', - type: 'boolean', + required: [ + "project", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + }, + }, + { + name: "get_milestone", + upstreamName: "get_milestone", + description: "Read one project milestone by id or name.", + class: "read", + inputSchema: { + type: "object", + properties: { + project: { + type: "string", + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug", + }, + query: { + type: "string", + description: "Milestone name or ID", + }, }, + required: [ + "project", + "query", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_projects', - 'List projects, optionally filtered by state, initiative, team, member, label or a search query.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 50)', - type: 'number', - maximum: 50, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - customView: { - description: 'Saved view ID, URL, slug, or exact name. Use list_custom_views to discover views. Applies saved filters and scope; other filters narrow the results. Uses list ordering, not saved display settings', - type: 'string', - minLength: 1, - }, - query: { - description: 'Search project name', - type: 'string', - }, - state: { - description: 'State type, name, or ID', - type: 'string', - }, - initiative: { - description: 'Initiative name, ID, identifier (e.g., I-123), or slug', - type: 'string', - }, - team: { - description: 'Team name or ID', - type: 'string', - }, - includeSubTeams: { - default: false, - description: 'Include results from the selected team and all descendants when team is set. Team ID filters already include descendants; this also expands name and key filters', - type: 'boolean', - }, - member: { - description: 'User ID, name, email, or "me"', - type: 'string', - }, - label: { - description: 'Label name or ID', - type: 'string', - }, - createdAt: { - description: 'Created after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - updatedAt: { - description: 'Updated after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - includeMilestones: { - default: false, - description: 'Include milestones', - type: 'boolean', - }, - includeMembers: { - default: false, - description: 'Include project members', - type: 'boolean', - }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', - }, - fields: { - description: 'Fields to include in each result. Explicitly requested fields with no value return null. `id` is always included. Omit or pass an empty array for the default response.', - type: 'array', - items: { - type: 'string', - enum: [ - 'id', - 'uuid', - 'name', - 'summary', - 'description', - 'url', - 'trashed', - 'createdAt', - 'updatedAt', - 'startedAt', - 'completedAt', - 'canceledAt', - 'startDate', - 'startDateResolution', - 'targetDate', - 'targetDateResolution', - 'priority', - 'sortOrder', - 'labels', - 'initiatives', - 'lead', - 'leadTeam', - 'status', - 'teams', - 'members', - 'milestones', + name: "save_milestone", + upstreamName: "save_milestone", + description: "Create or edit a milestone in a Linear project.", + class: "write", + inputSchema: { + type: "object", + properties: { + project: { + type: "string", + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug", + }, + id: { + description: "Milestone name or ID", + type: "string", + }, + name: { + description: "Milestone name (required when creating)", + type: "string", + }, + description: { + description: "Milestone description", + type: "string", + }, + targetDate: { + description: "Target completion date (ISO format, null to remove)", + type: [ + "string", + "null", ], }, }, + required: [ + "project", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, }, - ), - readTool( - 'get_project', - 'Read one project by name or id, optionally with its milestones, members, resources and customer needs.', { - type: 'object', - properties: { - query: { - type: 'string', - description: 'Project name, ID, identifier (e.g., P-ENG-123), or slug', - }, - includeCustomerNeeds: { - default: false, - description: 'Include one page of customer needs attached directly to the project', - type: 'boolean', - }, - customerNeedsLimit: { - default: 10, - description: 'Customer needs per page when includeCustomerNeeds is true (default 10, max 50)', - type: 'integer', - minimum: 1, - maximum: 50, - }, - customerNeedsCursor: { - description: 'Next customer-needs page: pass customerNeedsPageInfo.endCursor with includeCustomerNeeds true', - type: 'string', - }, - includeMilestones: { - default: false, - description: 'Include milestones', - type: 'boolean', - }, - includeMembers: { - default: false, - description: 'Include project members', - type: 'boolean', - }, - includeResources: { - default: false, - description: 'Include resources (documents, links, attachments)', - type: 'boolean', + name: "get_notifications", + upstreamName: "get_notifications", + description: "Read a page of the connected account's notifications, optionally the unread ones only.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + unreadOnly: { + default: false, + description: "Only return unread notifications", + type: "boolean", + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['query'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_project_labels', - 'List project labels, optionally filtered by name or team, and optionally including label groups.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - name: { - description: 'Filter by name', - type: 'string', - }, - team: { - description: 'Team name or ID', - type: 'string', - }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', - }, - includeGroups: { - default: false, - description: 'Include label groups in the results, marked with `isGroup`. A group cannot be applied directly; retiring or restoring one cascades to its child labels', - type: 'boolean', + name: "mark_notification", + upstreamName: "mark_notification", + description: "Change a Linear notification to read or unread, or change when it is snoozed until.", + class: "write", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Notification ID", + }, + read: { + description: "Whether the notification should be marked as read", + type: "boolean", + }, + snoozedUntilAt: { + description: "ISO-8601 time to snooze until, or null to unsnooze", + anyOf: [ + { + type: "string", + format: "date-time", + pattern: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$", + }, + { + type: "null", + }, + ], + }, }, + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + irreversible: false, }, - ), - readTool( - 'list_release_pipelines', - 'List release pipelines, optionally narrowed to one team or to the production pipelines.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - query: { - description: 'Search pipeline name', - type: 'string', - }, - team: { - description: 'Team name or ID', - type: 'string', - }, - type: { - description: 'Pipeline type: continuous | scheduled', - type: 'string', - enum: ['continuous', 'scheduled'], - }, - isProduction: { - description: 'Filter by production pipeline flag', - type: 'boolean', - }, - includeStages: { - default: false, - description: 'Include each pipeline\'s stages', - type: 'boolean', - }, - includeTeams: { - default: false, - description: 'Include each pipeline\'s teams', - type: 'boolean', - }, - createdAt: { - description: 'Created after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - updatedAt: { - description: 'Updated after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', + name: "list_teams", + upstreamName: "list_teams", + description: "List the workspace's teams.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + query: { + description: "Search query", + type: "string", + }, + parentTeam: { + description: "Only direct sub-teams of this team (name, key, or ID)", + type: "string", + }, + ancestorTeam: { + description: "All descendant teams of this team, at any depth, excluding itself (name, key, or ID)", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, + createdAt: { + description: "Created after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + updatedAt: { + description: "Updated after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_releases', - 'List releases, optionally filtered by pipeline, stage, stage type, version or a search query.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - query: { - description: 'Search release name or version', - type: 'string', - }, - pipeline: { - description: 'Release pipeline ID, slug, or exact name', - type: 'string', - }, - stage: { - description: 'Release stage ID or exact name', - type: 'string', - }, - stageType: { - description: 'Filter by stage lifecycle type', - type: 'string', - enum: ['planned', 'started', 'completed', 'canceled'], - }, - version: { - description: 'Exact version match', - type: 'string', - }, - hasReleaseNotes: { - description: 'Filter to releases that do (true) or do not (false) have release notes', - type: 'boolean', - }, - includeReleaseNotes: { - default: false, - description: 'Include associated release notes', - type: 'boolean', - }, - createdAt: { - description: 'Created after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - updatedAt: { - description: 'Updated after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', + name: "get_team", + upstreamName: "get_team", + description: "Read one team by name or id.", + class: "read", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Team UUID, key, or name", + }, }, + required: [ + "query", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_release', - 'Read one release by id, optionally with its release notes.', { - type: 'object', - properties: { - id: { - type: 'string', - description: 'Release ID or slug', - }, - includeReleaseNotes: { - default: false, - description: 'Include associated release notes', - type: 'boolean', + name: "get_triage_responsibility", + upstreamName: "get_triage_responsibility", + description: "Read a team's triage rotation: who is on triage now, what triage does, and the upcoming shifts.", + class: "read", + inputSchema: { + type: "object", + properties: { + team: { + type: "string", + description: "Team name or ID", + }, + user: { + description: "Only return shifts of this user: User ID, name, email, or \"me\"", + type: "string", + }, + limit: { + default: 10, + description: "Max upcoming shifts to return (default 10, max 50)", + type: "integer", + minimum: 1, + maximum: 50, + }, }, + required: [ + "team", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['id'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_release_notes', - 'List release notes, optionally filtered by pipeline, release or a search query.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - query: { - description: 'Search release notes title', - type: 'string', - }, - pipeline: { - description: 'Release pipeline ID, slug, or exact name', - type: 'string', - }, - release: { - description: 'Release ID or slug', - type: 'string', - }, - includeContent: { - default: false, - description: 'Include markdown release notes content', - type: 'boolean', - }, - includeReleases: { - default: false, - description: 'Include associated releases', - type: 'boolean', - }, - createdAt: { - description: 'Created after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - updatedAt: { - description: 'Updated after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', + name: "list_templates", + upstreamName: "list_templates", + description: "List issue and project templates, optionally for one team.", + class: "read", + inputSchema: { + type: "object", + properties: { + type: { + description: "Filter by template type. Omit to return every type", + type: "string", + enum: [ + "issue", + "project", + "document", + ], + }, + team: { + description: "Team name or ID. Returns that team's templates plus workspace-level ones", + type: "string", + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_release_note', - 'Read one release note by id or slug, including its markdown.', { - type: 'object', - properties: { - id: { - type: 'string', - description: 'Release notes ID or slug', - }, - includeReleases: { - default: false, - description: 'Include associated releases', - type: 'boolean', + name: "get_template", + upstreamName: "get_template", + description: "Read one template by id.", + class: "read", + inputSchema: { + type: "object", + properties: { + id: { + type: "string", + description: "Template ID or name", + }, }, + required: [ + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['id'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_milestones', - 'List the milestones of one project.', { - type: 'object', - properties: { - project: { - type: 'string', - description: 'Project name, ID, identifier (e.g., P-ENG-123), or slug', + name: "list_users", + upstreamName: "list_users", + description: "List workspace users, optionally filtered by team or a search query.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + query: { + description: "Match part of a name or email. Omit to list users without a name or email filter", + type: "string", + }, + team: { + description: "Team name or ID. Only return members of this team; omit to search the workspace", + type: "string", + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['project'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_milestone', - 'Read one project milestone by id or name.', { - type: 'object', - properties: { - project: { - type: 'string', - description: 'Project name, ID, identifier (e.g., P-ENG-123), or slug', - }, - query: { - type: 'string', - description: 'Milestone name or ID', + name: "get_user", + upstreamName: "get_user", + description: "Read one user by name, email or id.", + class: "read", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "User ID, name, email, or \"me\"", + }, }, + required: [ + "query", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['project', 'query'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_notifications', - 'Read a page of the connected account\'s notifications, optionally the unread ones only.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - unreadOnly: { - default: false, - description: 'Only return unread notifications', - type: 'boolean', - }, + name: "get_workspace", + upstreamName: "get_workspace", + description: "Read the connected workspace.", + class: "read", + inputSchema: { + type: "object", + properties: {}, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_teams', - 'List the workspace\'s teams.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - query: { - description: 'Search query', - type: 'string', - }, - parentTeam: { - description: 'Only direct sub-teams of this team (name, key, or ID)', - type: 'string', - }, - ancestorTeam: { - description: 'All descendant teams of this team, at any depth, excluding itself (name, key, or ID)', - type: 'string', - }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', - }, - createdAt: { - description: 'Created after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - updatedAt: { - description: 'Updated after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', + name: "search_documentation", + upstreamName: "search_documentation", + description: "Search Linear's documentation for how a feature works.", + class: "read", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Feature name, topic, or question to search for in Linear's product documentation", + }, + page: { + default: 0, + description: "Zero-based page number. Increment by one to fetch the next page for the same query", + type: "number", + }, }, + required: [ + "query", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_team', - 'Read one team by name or id.', { - type: 'object', - properties: { - query: { - type: 'string', - description: 'Team UUID, key, or name', + name: "get_diff", + upstreamName: "get_diff", + description: "Exact lookup for a Linear diff. Review URLs and GitHub PR URLs are lookup keys in Linear, not fetch targets.", + class: "read", + inputSchema: { + type: "object", + properties: { + urlOrId: { + type: "string", + minLength: 1, + description: "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", + }, }, + required: [ + "urlOrId", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['query'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_triage_responsibility', - 'Read a team\'s triage rotation: who is on triage now, what triage does, and the upcoming shifts.', { - type: 'object', - properties: { - team: { - type: 'string', - description: 'Team name or ID', - }, - user: { - description: 'Only return shifts of this user: User ID, name, email, or "me"', - type: 'string', - }, - limit: { - default: 10, - description: 'Max upcoming shifts to return (default 10, max 50)', - type: 'integer', - minimum: 1, - maximum: 50, + name: "list_diffs", + upstreamName: "list_diffs", + description: "List the diffs this account can see, optionally filtered by owner, repository, status, author or reviewer.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + query: { + description: "Broad search by title, branch, PR number, or bare slug", + type: "string", + }, + owner: { + description: "Filter returned diffs by repository owner", + type: "string", + }, + repo: { + description: "Filter returned diffs by repository name", + type: "string", + }, + status: { + description: "Filter returned diffs by pull request status", + type: "string", + }, + author: { + description: "Filter by author ID, name, email, or \"me\"", + type: "string", + }, + reviewer: { + description: "Filter by reviewer ID, name, email, or \"me\"", + type: "string", + }, + reviewState: { + description: "Reviewer state; requires reviewer and defaults to pending when reviewer is set", + type: "string", + enum: [ + "pending", + "approved", + "changesRequested", + "commented", + "dismissed", + ], + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - required: ['team'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'list_templates', - 'List issue and project templates, optionally for one team.', { - type: 'object', - properties: { - type: { - description: 'Filter by template type. Omit to return every type', - type: 'string', - enum: ['issue', 'project', 'document'], - }, - team: { - description: 'Team name or ID. Returns that team\'s templates plus workspace-level ones', - type: 'string', + name: "get_diff_threads", + upstreamName: "get_diff_threads", + description: "Read review threads for the exact Linear diff lookup; supplied URLs are lookup keys, not fetch targets.", + class: "read", + inputSchema: { + type: "object", + properties: { + urlOrId: { + type: "string", + minLength: 1, + description: "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", + }, + threadId: { + description: "Optional Linear comment UUID of any comment in the thread (not its `externalThreadId`); a reply's UUID resolves to its thread root", + type: "string", + }, + resolved: { + description: "Filter returned threads by resolved state", + type: "boolean", + }, + orderBy: { + default: "updatedAt", + description: "Sort: createdAt | updatedAt", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, }, + required: [ + "urlOrId", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_template', - 'Read one template by id.', { - type: 'object', - properties: { - id: { - type: 'string', - description: 'Template ID or name', + name: "save_diff_comment", + upstreamName: "save_diff_comment", + description: "Create or edit a comment or draft on a Linear diff, including inline anchors and replies.", + class: "write", + inputSchema: { + type: "object", + properties: { + urlOrId: { + description: "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", + type: "string", + minLength: 1, + }, + commentId: { + description: "Existing comment ID to edit", + type: "string", + }, + draftId: { + description: "Existing persisted draft ID to edit or submit", + type: "string", + }, + draft: { + default: false, + description: "Save as a private persisted draft without submitting", + type: "boolean", + }, + parentId: { + description: "Top-level comment ID to reply to; also required when submitting a persisted reply draft", + type: "string", + }, + body: { + type: "string", + minLength: 1, + description: "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences", + }, + anchor: { + description: "Optional comment anchor copied from a diff thread or built for a file or diff hunk", + type: "object", + propertyNames: { + type: "string", + }, + additionalProperties: {}, + }, + anchorContent: { + description: "Exact source text of the anchor's end line; stored with an inline draft so it can be re-anchored after the diff receives new revisions", + type: "string", + }, }, + required: [ + "body", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, }, - required: ['id'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + irreversible: true, }, - ), - readTool( - 'list_users', - 'List workspace users, optionally filtered by team or a search query.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - query: { - description: 'Filter by name or email', - type: 'string', - }, - team: { - description: 'Team name or ID', - type: 'string', + name: "resolve_diff_thread", + upstreamName: "resolve_diff_thread", + description: "Resolve or reopen a discussion thread on a Linear diff.", + class: "write", + inputSchema: { + type: "object", + properties: { + threadId: { + type: "string", + minLength: 1, + description: "Top-level diff thread/comment ID", + }, + resolved: { + default: true, + description: "True to resolve the thread; false to reopen it", + type: "boolean", + }, }, + required: [ + "threadId", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: false, }, - ), - readTool( - 'get_user', - 'Read one user by name, email or id.', { - type: 'object', - properties: { - query: { - type: 'string', - description: 'User ID, name, email, or "me"', + name: "delete_diff_comment", + upstreamName: "delete_diff_comment", + description: "Delete a comment or draft from a Linear diff.", + class: "write", + inputSchema: { + type: "object", + properties: { + commentId: { + description: "Submitted diff comment ID to delete", + type: "string", + minLength: 1, + }, + draftId: { + description: "Persisted diff comment draft ID to delete", + type: "string", + minLength: 1, + }, }, + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, }, - required: ['query'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + irreversible: true, }, - ), - readTool( - 'get_workspace', - 'Read the connected workspace.', { - type: 'object', - properties: {}, + name: "submit_diff_review", + upstreamName: "submit_diff_review", + description: "Publish a review of a Linear diff, with approval, requested changes or a comment.", + class: "write", + inputSchema: { + type: "object", + properties: { + urlOrId: { + type: "string", + minLength: 1, + description: "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", + }, + decision: { + type: "string", + enum: [ + "approved", + "changesRequested", + "commented", + ], + description: "Review decision: approved, changesRequested, or commented", + }, + body: { + description: "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. Required when requesting changes or commenting", + type: "string", + minLength: 1, + }, + }, + required: [ + "urlOrId", + "decision", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, }, - ), - readTool( - 'search_documentation', - 'Search Linear\'s documentation for how a feature works.', { - type: 'object', - properties: { - query: { - type: 'string', - description: 'Search query', - }, - page: { - default: 0, - description: 'Page number', - type: 'number', + name: "update_diff", + upstreamName: "update_diff", + description: "Edit a Linear diff and its review requests, issue links or status.", + class: "write", + inputSchema: { + type: "object", + properties: { + urlOrId: { + type: "string", + minLength: 1, + description: "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", + }, + title: { + description: "New diff title", + type: "string", + minLength: 1, + }, + description: { + description: "New description as Markdown; an empty string clears it", + type: "string", + }, + addedReviewRequests: { + description: "Request or re-request reviews; existing pending requests are unchanged", + minItems: 1, + maxItems: 25, + type: "array", + items: { + anyOf: [ + { + type: "object", + properties: { + user: { + type: "string", + minLength: 1, + description: "Workspace user ID, name, email, or \"me\"", + }, + }, + required: [ + "user", + ], + additionalProperties: false, + }, + { + type: "object", + properties: { + externalUserId: { + type: "string", + format: "uuid", + pattern: "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + description: "External user ID from get_diff reviewer metadata", + }, + }, + required: [ + "externalUserId", + ], + additionalProperties: false, + }, + { + type: "object", + properties: { + githubTeamId: { + type: "string", + format: "uuid", + pattern: "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + description: "GitHub team ID from get_diff reviewer metadata", + }, + }, + required: [ + "githubTeamId", + ], + additionalProperties: false, + }, + ], + }, + }, + removedReviewRequests: { + description: "Withdraw pending review requests; submitted reviews are preserved", + minItems: 1, + maxItems: 25, + type: "array", + items: { + anyOf: [ + { + type: "object", + properties: { + user: { + type: "string", + minLength: 1, + description: "Workspace user ID, name, email, or \"me\"", + }, + }, + required: [ + "user", + ], + additionalProperties: false, + }, + { + type: "object", + properties: { + externalUserId: { + type: "string", + format: "uuid", + pattern: "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + description: "External user ID from get_diff reviewer metadata", + }, + }, + required: [ + "externalUserId", + ], + additionalProperties: false, + }, + { + type: "object", + properties: { + githubTeamId: { + type: "string", + format: "uuid", + pattern: "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + description: "GitHub team ID from get_diff reviewer metadata", + }, + }, + required: [ + "githubTeamId", + ], + additionalProperties: false, + }, + ], + }, + }, + addedIssueLinks: { + description: "Add issue links or change the relationship of an existing link", + minItems: 1, + maxItems: 25, + type: "array", + items: { + type: "object", + properties: { + issue: { + type: "string", + minLength: 1, + description: "Linear issue identifier or UUID", + }, + type: { + type: "string", + enum: [ + "closes", + "contributes", + "links", + "reopens", + ], + description: "Relationship: closes, contributes, links, or reopens", + }, + }, + required: [ + "issue", + "type", + ], + additionalProperties: false, + }, + }, + removedIssueLinks: { + description: "Linear issue identifiers or UUIDs to unlink", + minItems: 1, + maxItems: 25, + type: "array", + items: { + type: "string", + minLength: 1, + }, + }, + statusAction: { + description: "Status transition to perform after metadata is updated", + type: "string", + enum: [ + "markReadyForReview", + "convertToDraft", + "close", + "reopen", + ], + }, }, + required: [ + "urlOrId", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, }, - required: ['query'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + irreversible: true, }, - ), - readTool( - 'get_diff', - 'Exact lookup for a Linear diff. Review URLs and GitHub PR URLs are lookup keys in Linear, not fetch targets.', { - type: 'object', - properties: { - urlOrId: { - type: 'string', - minLength: 1, - description: 'Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL', + name: "merge_diff", + upstreamName: "merge_diff", + description: "Merge a Linear diff or place it in its repository's merge queue.", + class: "write", + inputSchema: { + type: "object", + properties: { + urlOrId: { + type: "string", + minLength: 1, + description: "Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL", + }, + mergeMethod: { + description: "Merge method; omit to use the repository default", + type: "string", + enum: [ + "MERGE", + "REBASE", + "SQUASH", + ], + }, }, + required: [ + "urlOrId", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, }, - required: ['urlOrId'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, }, - ), - readTool( - 'list_diffs', - 'List the diffs this account can see, optionally filtered by owner, repository, status, author or reviewer.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - query: { - description: 'Broad search by title, branch, PR number, or bare slug', - type: 'string', - }, - owner: { - description: 'Filter returned diffs by repository owner', - type: 'string', - }, - repo: { - description: 'Filter returned diffs by repository name', - type: 'string', - }, - status: { - description: 'Filter returned diffs by pull request status', - type: 'string', - }, - author: { - description: 'Filter by author ID, name, email, or "me"', - type: 'string', - }, - reviewer: { - description: 'Filter by reviewer ID, name, email, or "me"', - type: 'string', - }, - reviewState: { - description: 'Reviewer state; requires reviewer and defaults to pending when reviewer is set', - type: 'string', - enum: ['pending', 'approved', 'changesRequested', 'commented', 'dismissed'], + name: "get_status_updates", + upstreamName: "get_status_updates", + description: "List project status updates of one type, optionally for a project, initiative or user, or one update by id.", + class: "read", + inputSchema: { + type: "object", + properties: { + limit: { + default: 50, + description: "Maximum number of results to return per page", + type: "number", + maximum: 250, + }, + cursor: { + description: "Omit for the first page. To continue, pass the previous response's cursor when hasNextPage is true", + type: "string", + }, + orderBy: { + default: "updatedAt", + description: "Timestamp to sort results by: creation time or last update time", + type: "string", + enum: [ + "createdAt", + "updatedAt", + ], + }, + type: { + type: "string", + enum: [ + "project", + "initiative", + ], + description: "Type of status update", + }, + id: { + description: "Update ID from get_status_updates results (statusUpdates[].id) or save_status_update's id", + type: "string", + }, + project: { + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug. Filters project updates only; omit to include all accessible projects", + type: "string", + }, + initiative: { + description: "Initiative name, ID, identifier (e.g., I-123), or slug. Filters initiative updates only; omit to include all accessible initiatives", + type: "string", + }, + user: { + description: "Filter by update author: User ID, name, email, or \"me\". Omit to include all authors", + type: "string", + }, + createdAt: { + description: "Created after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + updatedAt: { + description: "Updated after: ISO-8601 date/duration (e.g., -P1D)", + type: "string", + }, + includeArchived: { + default: false, + description: "Include archived items", + type: "boolean", + }, }, + required: [ + "type", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, }, - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, }, - ), - readTool( - 'get_diff_threads', - 'Read review threads for the exact Linear diff lookup; supplied URLs are lookup keys, not fetch targets.', { - type: 'object', - properties: { - urlOrId: { - type: 'string', - minLength: 1, - description: 'Linear review URL, diff slug, pull request ID, Linear full identifier, or GitHub PR URL', - }, - threadId: { - description: 'Optional Linear comment UUID of any comment in the thread (not its `externalThreadId`); a reply\'s UUID resolves to its thread root', - type: 'string', - }, - resolved: { - description: 'Filter returned threads by resolved state', - type: 'boolean', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], + name: "save_status_update", + upstreamName: "save_status_update", + description: "Create or edit a project or initiative status update in Linear.", + class: "write", + inputSchema: { + type: "object", + properties: { + type: { + type: "string", + enum: [ + "project", + "initiative", + ], + description: "Type of status update", + }, + id: { + description: "Update ID from get_status_updates results (statusUpdates[].id) or save_status_update's id. Omit to create", + type: "string", + }, + project: { + description: "Project name, ID, identifier (e.g., P-ENG-123), or slug", + type: "string", + }, + initiative: { + description: "Initiative name, ID, identifier (e.g., I-123), or slug", + type: "string", + }, + body: { + description: "Content as Markdown. Do not escape the string — use literal newlines and special characters, not escape sequences. To mention a user, use @displayName (e.g., @johndoe). Required on create; replaces the body on update. Omit on update to leave the body unchanged", + type: "string", + }, + health: { + description: "onTrack | atRisk | offTrack", + type: "string", + enum: [ + "onTrack", + "atRisk", + "offTrack", + ], + }, + isDiffHidden: { + description: "Deprecated. Hide diff with previous update (create only)", + type: "boolean", + }, }, + required: [ + "type", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, }, - required: ['urlOrId'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + annotations: { + readOnlyHint: false, + destructiveHint: true, + }, + irreversible: true, }, - ), - readTool( - 'get_status_updates', - 'List project status updates of one type, optionally for a project, initiative or user, or one update by id.', { - type: 'object', - properties: { - limit: { - default: 50, - description: 'Max results (default 50, max 250)', - type: 'number', - maximum: 250, - }, - cursor: { - description: 'Next page cursor', - type: 'string', - }, - orderBy: { - default: 'updatedAt', - description: 'Sort: createdAt | updatedAt', - type: 'string', - enum: ['createdAt', 'updatedAt'], - }, - type: { - type: 'string', - enum: ['project', 'initiative'], - description: 'Type of status update', - }, - id: { - description: 'Status update ID - if provided, returns this specific update', - type: 'string', - }, - project: { - description: 'Project name, ID, identifier (e.g., P-ENG-123), or slug', - type: 'string', - }, - initiative: { - description: 'Initiative name, ID, identifier (e.g., I-123), or slug', - type: 'string', - }, - user: { - description: 'User ID, name, email, or "me"', - type: 'string', - }, - createdAt: { - description: 'Created after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - updatedAt: { - description: 'Updated after: ISO-8601 date/duration (e.g., -P1D)', - type: 'string', - }, - includeArchived: { - default: false, - description: 'Include archived items', - type: 'boolean', + name: "delete_status_update", + upstreamName: "delete_status_update", + description: "Delete a project or initiative status update in Linear.", + class: "write", + inputSchema: { + type: "object", + properties: { + type: { + type: "string", + enum: [ + "project", + "initiative", + ], + description: "Type of status update", + }, + id: { + type: "string", + description: "Update ID from get_status_updates results (statusUpdates[].id) or save_status_update's id; type must match the update", + }, }, + required: [ + "type", + "id", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + additionalProperties: false, + }, + annotations: { + readOnlyHint: false, + destructiveHint: true, }, - required: ['type'], - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, + irreversible: true, }, - ), ], }; diff --git a/backend/routes/grants.ts b/backend/routes/grants.ts index cb06f4a97a..169b50c8da 100644 --- a/backend/routes/grants.ts +++ b/backend/routes/grants.ts @@ -340,6 +340,16 @@ router.post('/', grantRateLimit, auth, async (req: AuthenticatedRequest, res: ex message: 'a hosted-mcp grant cannot hold write mode; use write-with-confirm', }); } + // Linear's earlier tokens carry read/openid only. Approval cannot upgrade + // that vendor authorization; require a reconnect before minting this mode. + if (hosted && connection.config?.entryId === 'linear' && body.writeMode === 'write-with-confirm' + && !(typeof connection.config?.grantedScope === 'string' ? connection.config.grantedScope : '') + .split(/\s+/).includes('write')) { + return res.status(403).json({ + error: 'write_scope_required', + message: 'reconnect Linear to enable writes before granting write-with-confirm', + }); + } const target = body.target; if (!target || (target.kind !== 'pod' && target.kind !== 'seat')) { diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 9350553c83..6cab51af52 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2220,6 +2220,7 @@ "loadFailed": "Could not load artifacts." }, "tools": { + "linearWriteReconnect": "Linear connections made before write access was added need to be removed and connected again to enable writes. Each write requires your approval.", "makePodInviteOnly": "Make this pod invite-only", "podInviteOnlyNow": "This pod is invite-only. Select Grant to continue.", "joinPolicyError": "Could not make this pod invite-only.", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 2301cbdc4b..71d9ef1fb7 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2211,6 +2211,7 @@ "loadFailed": "无法加载产物。" }, "tools": { + "linearWriteReconnect": "在增加写入权限前连接的 Linear 需要先移除再重新连接,才能启用写入。每次写入都需要你批准。", "makePodInviteOnly": "将此 Pod 设为仅限邀请", "podInviteOnlyNow": "此 Pod 现已设为仅限邀请。再次选择“授权”继续。", "joinPolicyError": "无法将此 Pod 设为仅限邀请。", diff --git a/frontend/src/v2/__tests__/V2ConnectorTools.test.tsx b/frontend/src/v2/__tests__/V2ConnectorTools.test.tsx index 6d75cf05da..b026d83822 100644 --- a/frontend/src/v2/__tests__/V2ConnectorTools.test.tsx +++ b/frontend/src/v2/__tests__/V2ConnectorTools.test.tsx @@ -76,6 +76,20 @@ const linearEntry = { }; const grantLinear = { ...grantLive, grantId: 'grant_lin', installationId: 'inst-2', writeMode: 'read', tools: ['linear.list_issues'] }; +test.each(['en', 'zh-CN'])('connected Linear explains the write reconnect requirement in %s', async (language) => { + await i18nReady; + await act(async () => { await i18n.changeLanguage(language); }); + try { + mockTwoEntries({ catalog: [linearEntry], grants: { p1: [grantLinear], p2: [] } }); + render(); + const notice = await screen.findByRole('note'); + expect(notice).toHaveTextContent(language === 'en' ? 'connected again to enable writes' : '重新连接'); + expect(notice).toHaveTextContent(language === 'en' ? 'Each write requires your approval' : '每次写入都需要你批准'); + } finally { + await act(async () => { await i18n.changeLanguage('en'); }); + } +}); + test('Atlassian shows its read-in-testing state on an existing grant', async () => { const atlassian = { ...linearEntry, installableId: 'atlassian', entryId: 'atlassian', label: 'Atlassian', tools: [{ name: 'atlassian.get_accessible_atlassian_resources', requiredWriteMode: 'read', irreversible: false }] }; diff --git a/frontend/src/v2/components/V2ConnectorTools.tsx b/frontend/src/v2/components/V2ConnectorTools.tsx index 5c7d9d6eca..32c722370a 100644 --- a/frontend/src/v2/components/V2ConnectorTools.tsx +++ b/frontend/src/v2/components/V2ConnectorTools.tsx @@ -1087,6 +1087,12 @@ const V2ConnectorTools: React.FC = ({ pods, manualRevokeNotice = null })

{error}

)} {removedHiddenNotice &&

{t('tools.removedReviewProvider', { defaultValue: 'Connection removed. Review Commonly access at {{label}}.', label: removedHiddenNotice.label })} {t('tools.openProviderSettings', { defaultValue: 'Open provider settings' })}

} + {catalog.some((entry) => entry.connectionType === 'hosted-mcp' + && entry.entryId === 'linear' && entry.connections.length > 0) && ( +

+ {t('tools.linearWriteReconnect', { defaultValue: 'Linear connections made before write access was added need to be removed and connected again to enable writes. Each write requires your approval.' })} +

+ )}
{liveRows.map(renderRow)} {notYet.map(renderNotYet)}