diff --git a/toolkit-docs-generator/data/toolkits/github.json b/toolkit-docs-generator/data/toolkits/github.json index 04454499f..0ec6e4885 100644 --- a/toolkit-docs-generator/data/toolkits/github.json +++ b/toolkit-docs-generator/data/toolkits/github.json @@ -1652,45 +1652,6 @@ "extras": null } }, - { - "name": "GetNotificationSummary", - "qualifiedName": "Github.GetNotificationSummary", - "fullyQualifiedName": "Github.GetNotificationSummary@2.0.1", - "description": "Get a summary of GitHub notifications.\n\nReturns counts grouped by reason, repository, and type without full notification details.", - "parameters": [], - "auth": { - "providerId": "github", - "providerType": "oauth2", - "scopes": [] - }, - "secrets": [ - "GITHUB_SERVER_URL", - "GITHUB_CLASSIC_PERSONAL_ACCESS_TOKEN" - ], - "secretsInfo": [ - { - "name": "GITHUB_SERVER_URL", - "type": "unknown" - }, - { - "name": "GITHUB_CLASSIC_PERSONAL_ACCESS_TOKEN", - "type": "token" - } - ], - "output": { - "type": "json", - "description": "Summary of user notifications" - }, - "documentationChunks": [], - "codeExample": { - "toolName": "Github.GetNotificationSummary", - "parameters": {}, - "requiresAuth": true, - "authProvider": "github", - "tabLabel": "Call the Tool with User Authorization" - }, - "metadata": null - }, { "name": "GetPullRequest", "qualifiedName": "Github.GetPullRequest", @@ -2259,135 +2220,6 @@ "extras": null } }, - { - "name": "ListNotifications", - "qualifiedName": "Github.ListNotifications", - "fullyQualifiedName": "Github.ListNotifications@2.0.1", - "description": "List GitHub notifications with pagination.\n\nReturns notifications sorted chronologically by most recent first.", - "parameters": [ - { - "name": "page", - "type": "integer", - "required": false, - "description": "Page number to fetch. Default is 1.", - "enum": null, - "inferrable": true - }, - { - "name": "per_page", - "type": "integer", - "required": false, - "description": "Number of notifications per page (max 100). Default is 30.", - "enum": null, - "inferrable": true - }, - { - "name": "all_notifications", - "type": "boolean", - "required": false, - "description": "Include read notifications. Default is False.", - "enum": null, - "inferrable": true - }, - { - "name": "participating", - "type": "boolean", - "required": false, - "description": "Only show notifications user is participating in. Default is False.", - "enum": null, - "inferrable": true - }, - { - "name": "repository_full_name", - "type": "string", - "required": false, - "description": "Filter notifications to owner/name repository. Default is None.", - "enum": null, - "inferrable": true - }, - { - "name": "subject_types", - "type": "array", - "innerType": "string", - "required": false, - "description": "List of notification subject types to include. Default is None (all types).", - "enum": [ - "Issue", - "PullRequest", - "Release", - "Commit", - "Discussion" - ], - "inferrable": true - } - ], - "auth": { - "providerId": "github", - "providerType": "oauth2", - "scopes": [] - }, - "secrets": [ - "GITHUB_SERVER_URL", - "GITHUB_CLASSIC_PERSONAL_ACCESS_TOKEN" - ], - "secretsInfo": [ - { - "name": "GITHUB_SERVER_URL", - "type": "unknown" - }, - { - "name": "GITHUB_CLASSIC_PERSONAL_ACCESS_TOKEN", - "type": "token" - } - ], - "output": { - "type": "json", - "description": "Paginated list of notifications" - }, - "documentationChunks": [], - "codeExample": { - "toolName": "Github.ListNotifications", - "parameters": { - "page": { - "value": 2, - "type": "integer", - "required": false - }, - "per_page": { - "value": 50, - "type": "integer", - "required": false - }, - "all_notifications": { - "value": true, - "type": "boolean", - "required": false - }, - "participating": { - "value": false, - "type": "boolean", - "required": false - }, - "repository_full_name": { - "value": "octocat/Hello-World", - "type": "string", - "required": false - }, - "subject_types": { - "value": [ - "Issue", - "PullRequest" - ], - "type": "array", - "required": false - } - }, - "requiresAuth": true, - "authProvider": "github", - "tabLabel": "Call the Tool with User Authorization" - }, - "metadata": null - }, { "name": "ListOrgRepositories", "qualifiedName": "Github.ListOrgRepositories", @@ -5934,6 +5766,5 @@ "import { Callout, Tabs } from \"nextra/components\";" ], "subPages": [], - "generatedAt": "2026-03-24T11:25:03.361Z", - "summary": "Arcade.dev's GitHub toolkit provides authenticated access to GitHub resources, enabling automated repository, issue, PR, review, project, and notification workflows. It exposes high-level actions to create, modify, query, and manage code and project artifacts while handling pagination, fuzzy matching, and diff-aware operations.\n\n**Capabilities**\n- Automate repository and branch operations, file and content edits, and PR lifecycle management.\n- Manage issues, comments, labels, reviewers, and review threads with contextual controls.\n- Query activity, notifications, stargazers, collaborators, projects, and user-centric views with pagination and filters.\n\n**OAuth**\nProvider: github\nScopes: None\n\n**Secrets**\nTypes: unknown, token. Examples: GITHUB_SERVER_URL, GITHUB_CLASSIC_PERSONAL_ACCESS_TOKEN" + "generatedAt": "2026-04-17T23:26:32.708Z" } \ No newline at end of file diff --git a/toolkit-docs-generator/data/toolkits/index.json b/toolkit-docs-generator/data/toolkits/index.json index 77809a1f2..87cfdcb37 100644 --- a/toolkit-docs-generator/data/toolkits/index.json +++ b/toolkit-docs-generator/data/toolkits/index.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-04-16T11:32:31.519Z", + "generatedAt": "2026-04-17T23:26:33.110Z", "version": "1.0.0", "toolkits": [ { @@ -242,7 +242,7 @@ "version": "3.1.3", "category": "development", "type": "arcade", - "toolCount": 44, + "toolCount": 42, "authType": "oauth2" }, { @@ -503,13 +503,13 @@ "version": "3.1.2", "category": "productivity", "type": "auth", - "toolCount": 45, + "toolCount": 43, "authType": "oauth2" }, { "id": "Linear", "label": "Linear", - "version": "3.3.2", + "version": "3.3.3", "category": "productivity", "type": "arcade", "toolCount": 39, diff --git a/toolkit-docs-generator/data/toolkits/jira.json b/toolkit-docs-generator/data/toolkits/jira.json index 1c99f4e79..2a1b2a06a 100644 --- a/toolkit-docs-generator/data/toolkits/jira.json +++ b/toolkit-docs-generator/data/toolkits/jira.json @@ -2746,90 +2746,6 @@ "extras": null } }, - { - "name": "ListPrioritiesAssociatedWithAPriorityScheme", - "qualifiedName": "Jira.ListPrioritiesAssociatedWithAPriorityScheme", - "fullyQualifiedName": "Jira.ListPrioritiesAssociatedWithAPriorityScheme@2.5.0", - "description": "Browse the priorities associated with a priority scheme.", - "parameters": [ - { - "name": "scheme_id", - "type": "string", - "required": true, - "description": "The ID of the priority scheme to retrieve priorities for.", - "enum": null, - "inferrable": true - }, - { - "name": "limit", - "type": "integer", - "required": false, - "description": "The maximum number of priority schemes to return. Min of 1, max of 50. Defaults to 50.", - "enum": null, - "inferrable": true - }, - { - "name": "offset", - "type": "integer", - "required": false, - "description": "The number of priority schemes to skip. Defaults to 0 (start from the first scheme).", - "enum": null, - "inferrable": true - }, - { - "name": "atlassian_cloud_id", - "type": "string", - "required": false, - "description": "The ID of the Atlassian Cloud to use (defaults to None). If not provided and the user has a single cloud authorized, the tool will use that. Otherwise, an error will be raised.", - "enum": null, - "inferrable": true - } - ], - "auth": { - "providerId": "atlassian", - "providerType": "oauth2", - "scopes": [ - "manage:jira-configuration", - "read:jira-user" - ] - }, - "secrets": [], - "secretsInfo": [], - "output": { - "type": "json", - "description": "The priorities associated with the priority scheme" - }, - "documentationChunks": [], - "codeExample": { - "toolName": "Jira.ListPrioritiesAssociatedWithAPriorityScheme", - "parameters": { - "scheme_id": { - "value": "10001", - "type": "string", - "required": true - }, - "limit": { - "value": 25, - "type": "integer", - "required": false - }, - "offset": { - "value": 0, - "type": "integer", - "required": false - }, - "atlassian_cloud_id": { - "value": "abcd1234-cloud-id", - "type": "string", - "required": false - } - }, - "requiresAuth": true, - "authProvider": "atlassian", - "tabLabel": "Call the Tool with User Authorization" - }, - "metadata": null - }, { "name": "ListPrioritiesAvailableToAnIssue", "qualifiedName": "Jira.ListPrioritiesAvailableToAnIssue", @@ -3281,103 +3197,6 @@ "extras": null } }, - { - "name": "ListProjectsAssociatedWithAPriorityScheme", - "qualifiedName": "Jira.ListProjectsAssociatedWithAPriorityScheme", - "fullyQualifiedName": "Jira.ListProjectsAssociatedWithAPriorityScheme@2.5.0", - "description": "Browse the projects associated with a priority scheme.", - "parameters": [ - { - "name": "scheme_id", - "type": "string", - "required": true, - "description": "The ID of the priority scheme to retrieve projects for.", - "enum": null, - "inferrable": true - }, - { - "name": "project", - "type": "string", - "required": false, - "description": "Filter by project ID, key or name. Defaults to None (returns all projects).", - "enum": null, - "inferrable": true - }, - { - "name": "limit", - "type": "integer", - "required": false, - "description": "The maximum number of projects to return. Min of 1, max of 50. Defaults to 50.", - "enum": null, - "inferrable": true - }, - { - "name": "offset", - "type": "integer", - "required": false, - "description": "The number of projects to skip. Defaults to 0 (start from the first project).", - "enum": null, - "inferrable": true - }, - { - "name": "atlassian_cloud_id", - "type": "string", - "required": false, - "description": "The ID of the Atlassian Cloud to use (defaults to None). If not provided and the user has a single cloud authorized, the tool will use that. Otherwise, an error will be raised.", - "enum": null, - "inferrable": true - } - ], - "auth": { - "providerId": "atlassian", - "providerType": "oauth2", - "scopes": [ - "manage:jira-configuration", - "read:jira-user" - ] - }, - "secrets": [], - "secretsInfo": [], - "output": { - "type": "json", - "description": "The projects associated with the priority scheme" - }, - "documentationChunks": [], - "codeExample": { - "toolName": "Jira.ListProjectsAssociatedWithAPriorityScheme", - "parameters": { - "scheme_id": { - "value": "10001", - "type": "string", - "required": true - }, - "project": { - "value": "PROJKEY", - "type": "string", - "required": false - }, - "limit": { - "value": 25, - "type": "integer", - "required": false - }, - "offset": { - "value": 0, - "type": "integer", - "required": false - }, - "atlassian_cloud_id": { - "value": "abcd1234-cloudid", - "type": "string", - "required": false - } - }, - "requiresAuth": true, - "authProvider": "atlassian", - "tabLabel": "Call the Tool with User Authorization" - }, - "metadata": null - }, { "name": "ListProjectsByScheme", "qualifiedName": "Jira.ListProjectsByScheme", @@ -4756,6 +4575,5 @@ "relativePath": "environment-variables/page.mdx" } ], - "generatedAt": "2026-03-24T11:24:59.816Z", - "summary": "Arcade.dev's Jira toolkit enables LLM-driven interactions with Atlassian Jira, letting agents create and update issues, manage sprints and boards, handle attachments and comments, and query projects and users. It's designed for programmatic issue lifecycle and Agile workflows.\n\n**Capabilities**\n- Manage issues and workflows: create/update issues, transitions, labels, priorities, and comments.\n- Sprint and board operations: list sprints/boards, add/remove issues from sprints, backlog management with batching and date filters.\n- File and attachment handling: attach, download, and list metadata for files.\n- Scalable search & browsing: parameterized searches, project/user/priorities listing with pagination and single-call batching.\n\n**OAuth**\nProvider: atlassian\nScopes: manage:jira-configuration, read:board-scope.admin:jira-software, read:board-scope:jira-software, read:issue-details:jira, read:jira-user, read:jira-work, read:jql:jira, read:project:jira, read:sprint:jira-software, write:board-scope:jira-software, write:jira-work, write:sprint:jira-software" + "generatedAt": "2026-04-17T23:26:32.708Z" } \ No newline at end of file diff --git a/toolkit-docs-generator/data/toolkits/linear.json b/toolkit-docs-generator/data/toolkits/linear.json index bfcdc7ec5..579b9211e 100644 --- a/toolkit-docs-generator/data/toolkits/linear.json +++ b/toolkit-docs-generator/data/toolkits/linear.json @@ -1,7 +1,7 @@ { "id": "Linear", "label": "Linear", - "version": "3.3.2", + "version": "3.3.3", "description": "Arcade tools designed for LLMs to interact with Linear", "metadata": { "category": "productivity", @@ -26,7 +26,7 @@ { "name": "AddComment", "qualifiedName": "Linear.AddComment", - "fullyQualifiedName": "Linear.AddComment@3.3.2", + "fullyQualifiedName": "Linear.AddComment@3.3.3", "description": "Add a comment to an issue.", "parameters": [ { @@ -99,7 +99,7 @@ { "name": "AddProjectComment", "qualifiedName": "Linear.AddProjectComment", - "fullyQualifiedName": "Linear.AddProjectComment@3.3.2", + "fullyQualifiedName": "Linear.AddProjectComment@3.3.3", "description": "Add a comment to a project's document content.\n\nIMPORTANT: Due to Linear API limitations, comments created via the API will NOT\nappear visually anchored inline in the document (no yellow highlight on text).\nThe comment will be stored and can be retrieved via list_project_comments, but\nit will appear in the comments panel rather than inline in the document.\n\nFor true inline comments that are visually anchored to text, users should create\nthem directly in the Linear UI by selecting text and adding a comment.\n\nThe quoted_text parameter stores metadata about what text the comment references,\nwhich is useful for context even though the comment won't be visually anchored.", "parameters": [ { @@ -198,7 +198,7 @@ { "name": "AddProjectToInitiative", "qualifiedName": "Linear.AddProjectToInitiative", - "fullyQualifiedName": "Linear.AddProjectToInitiative@3.3.2", + "fullyQualifiedName": "Linear.AddProjectToInitiative@3.3.3", "description": "Link a project to an initiative.\n\nBoth initiative and project can be specified by ID or name.\nIf a name is provided, fuzzy matching is used to resolve it.", "parameters": [ { @@ -284,7 +284,7 @@ { "name": "ArchiveInitiative", "qualifiedName": "Linear.ArchiveInitiative", - "fullyQualifiedName": "Linear.ArchiveInitiative@3.3.2", + "fullyQualifiedName": "Linear.ArchiveInitiative@3.3.3", "description": "Archive an initiative.\n\nArchived initiatives are hidden from default views but can be restored.", "parameters": [ { @@ -357,7 +357,7 @@ { "name": "ArchiveIssue", "qualifiedName": "Linear.ArchiveIssue", - "fullyQualifiedName": "Linear.ArchiveIssue@3.3.2", + "fullyQualifiedName": "Linear.ArchiveIssue@3.3.3", "description": "Archive an issue.\n\nArchived issues are hidden from default views but can be restored.", "parameters": [ { @@ -417,7 +417,7 @@ { "name": "ArchiveProject", "qualifiedName": "Linear.ArchiveProject", - "fullyQualifiedName": "Linear.ArchiveProject@3.3.2", + "fullyQualifiedName": "Linear.ArchiveProject@3.3.3", "description": "Archive a project.\n\nArchived projects are hidden from default views but can be restored.", "parameters": [ { @@ -490,7 +490,7 @@ { "name": "CreateInitiative", "qualifiedName": "Linear.CreateInitiative", - "fullyQualifiedName": "Linear.CreateInitiative@3.3.2", + "fullyQualifiedName": "Linear.CreateInitiative@3.3.3", "description": "Create a new Linear initiative.\n\nInitiatives are high-level strategic goals that group related projects.", "parameters": [ { @@ -596,7 +596,7 @@ { "name": "CreateIssue", "qualifiedName": "Linear.CreateIssue", - "fullyQualifiedName": "Linear.CreateIssue@3.3.2", + "fullyQualifiedName": "Linear.CreateIssue@3.3.3", "description": "Create a new Linear issue with validation.\n\nWhen assignee is None or '@me', the issue is assigned to the authenticated user.\nAll entity references (team, assignee, labels, state, project, cycle, parent)\nare validated before creation. If an entity is not found, suggestions are\nreturned to help correct the input.", "parameters": [ { @@ -762,7 +762,7 @@ { "name": "CreateIssueRelation", "qualifiedName": "Linear.CreateIssueRelation", - "fullyQualifiedName": "Linear.CreateIssueRelation@3.3.2", + "fullyQualifiedName": "Linear.CreateIssueRelation@3.3.3", "description": "Create a relation between two issues.\n\nRelation types define the relationship from the source issue's perspective:\n- blocks: Source issue blocks the related issue\n- blockedBy: Source issue is blocked by the related issue\n- duplicate: Source issue is a duplicate of the related issue\n- related: Issues are related (bidirectional)", "parameters": [ { @@ -853,7 +853,7 @@ { "name": "CreateProject", "qualifiedName": "Linear.CreateProject", - "fullyQualifiedName": "Linear.CreateProject@3.3.2", + "fullyQualifiedName": "Linear.CreateProject@3.3.3", "description": "Create a new Linear project.\n\nTeam is validated before creation. If team is not found, suggestions are\nreturned to help correct the input. Lead is validated if provided.", "parameters": [ { @@ -1024,7 +1024,7 @@ { "name": "CreateProjectUpdate", "qualifiedName": "Linear.CreateProjectUpdate", - "fullyQualifiedName": "Linear.CreateProjectUpdate@3.3.2", + "fullyQualifiedName": "Linear.CreateProjectUpdate@3.3.3", "description": "Create a project status update.\n\nProject updates are posts that communicate progress, blockers, or status\nchanges to stakeholders. They appear in the project's Updates tab and\ncan include a health status indicator.", "parameters": [ { @@ -1114,7 +1114,7 @@ { "name": "GetCycle", "qualifiedName": "Linear.GetCycle", - "fullyQualifiedName": "Linear.GetCycle@3.3.2", + "fullyQualifiedName": "Linear.GetCycle@3.3.3", "description": "Get detailed information about a specific Linear cycle.", "parameters": [ { @@ -1174,7 +1174,7 @@ { "name": "GetInitiative", "qualifiedName": "Linear.GetInitiative", - "fullyQualifiedName": "Linear.GetInitiative@3.3.2", + "fullyQualifiedName": "Linear.GetInitiative@3.3.3", "description": "Get detailed information about a specific Linear initiative.\n\nSupports lookup by ID or name (with fuzzy matching for name).", "parameters": [ { @@ -1276,7 +1276,7 @@ { "name": "GetInitiativeDescription", "qualifiedName": "Linear.GetInitiativeDescription", - "fullyQualifiedName": "Linear.GetInitiativeDescription@3.3.2", + "fullyQualifiedName": "Linear.GetInitiativeDescription@3.3.3", "description": "Get an initiative's full description with pagination support.\n\nUse this tool when you need the complete description of an initiative that\nwas truncated in the get_initiative response. Supports chunked reading for\nvery large descriptions.", "parameters": [ { @@ -1362,7 +1362,7 @@ { "name": "GetIssue", "qualifiedName": "Linear.GetIssue", - "fullyQualifiedName": "Linear.GetIssue@3.3.2", + "fullyQualifiedName": "Linear.GetIssue@3.3.3", "description": "Get detailed information about a specific Linear issue.\n\nAccepts either the issue UUID or the human-readable identifier (like TOO-123).", "parameters": [ { @@ -1474,7 +1474,7 @@ { "name": "GetNotifications", "qualifiedName": "Linear.GetNotifications", - "fullyQualifiedName": "Linear.GetNotifications@3.3.2", + "fullyQualifiedName": "Linear.GetNotifications@3.3.3", "description": "Get the authenticated user's notifications.\n\nReturns notifications including issue mentions, comments, assignments,\nand state changes.", "parameters": [ { @@ -1560,7 +1560,7 @@ { "name": "GetProject", "qualifiedName": "Linear.GetProject", - "fullyQualifiedName": "Linear.GetProject@3.3.2", + "fullyQualifiedName": "Linear.GetProject@3.3.3", "description": "Get detailed information about a specific Linear project.\n\nSupports lookup by ID, slug_id, or name (with fuzzy matching for name).", "parameters": [ { @@ -1676,7 +1676,7 @@ { "name": "GetProjectDescription", "qualifiedName": "Linear.GetProjectDescription", - "fullyQualifiedName": "Linear.GetProjectDescription@3.3.2", + "fullyQualifiedName": "Linear.GetProjectDescription@3.3.3", "description": "Get a project's full description with pagination support.\n\nUse this tool when you need the complete description of a project that\nwas truncated in the get_project response. Supports chunked reading for\nvery large descriptions.", "parameters": [ { @@ -1762,7 +1762,7 @@ { "name": "GetRecentActivity", "qualifiedName": "Linear.GetRecentActivity", - "fullyQualifiedName": "Linear.GetRecentActivity@3.3.2", + "fullyQualifiedName": "Linear.GetRecentActivity@3.3.3", "description": "Get the authenticated user's recent issue activity.\n\nReturns issues the user has recently created or been assigned to\nwithin the specified time period.", "parameters": [ { @@ -1835,7 +1835,7 @@ { "name": "GetTeam", "qualifiedName": "Linear.GetTeam", - "fullyQualifiedName": "Linear.GetTeam@3.3.2", + "fullyQualifiedName": "Linear.GetTeam@3.3.3", "description": "Get detailed information about a specific Linear team.\n\nSupports lookup by ID, key (like TOO, ENG), or name (with fuzzy matching).", "parameters": [ { @@ -1925,7 +1925,7 @@ { "name": "LinkGithubToIssue", "qualifiedName": "Linear.LinkGithubToIssue", - "fullyQualifiedName": "Linear.LinkGithubToIssue@3.3.2", + "fullyQualifiedName": "Linear.LinkGithubToIssue@3.3.3", "description": "Link a GitHub PR, commit, or issue to a Linear issue.\n\nAutomatically detects the artifact type from the URL and generates\nan appropriate title if not provided.", "parameters": [ { @@ -2011,7 +2011,7 @@ { "name": "ListComments", "qualifiedName": "Linear.ListComments", - "fullyQualifiedName": "Linear.ListComments@3.3.2", + "fullyQualifiedName": "Linear.ListComments@3.3.3", "description": "List comments on an issue.\n\nReturns comments with user info, timestamps, and reply threading info.", "parameters": [ { @@ -2097,7 +2097,7 @@ { "name": "ListCycles", "qualifiedName": "Linear.ListCycles", - "fullyQualifiedName": "Linear.ListCycles@3.3.2", + "fullyQualifiedName": "Linear.ListCycles@3.3.3", "description": "List Linear cycles, optionally filtered by team and status.\n\nCycles are time-boxed iterations (like sprints) for organizing work.", "parameters": [ { @@ -2209,7 +2209,7 @@ { "name": "ListInitiatives", "qualifiedName": "Linear.ListInitiatives", - "fullyQualifiedName": "Linear.ListInitiatives@3.3.2", + "fullyQualifiedName": "Linear.ListInitiatives@3.3.3", "description": "List Linear initiatives, optionally filtered by keywords and other criteria.\n\nReturns all initiatives when no filters provided, or filtered results when\nkeywords or other filters are specified.", "parameters": [ { @@ -2315,7 +2315,7 @@ { "name": "ListIssues", "qualifiedName": "Linear.ListIssues", - "fullyQualifiedName": "Linear.ListIssues@3.3.2", + "fullyQualifiedName": "Linear.ListIssues@3.3.3", "description": "List Linear issues, optionally filtered by keywords and other criteria.\n\nReturns all issues when no filters provided, or filtered results when\nkeywords or other filters are specified.", "parameters": [ { @@ -2498,7 +2498,7 @@ { "name": "ListLabels", "qualifiedName": "Linear.ListLabels", - "fullyQualifiedName": "Linear.ListLabels@3.3.2", + "fullyQualifiedName": "Linear.ListLabels@3.3.3", "description": "List available issue labels in the workspace.\n\nReturns labels that can be applied to issues. Use label IDs or names\nwhen creating or updating issues.", "parameters": [ { @@ -2558,7 +2558,7 @@ { "name": "ListProjectComments", "qualifiedName": "Linear.ListProjectComments", - "fullyQualifiedName": "Linear.ListProjectComments@3.3.2", + "fullyQualifiedName": "Linear.ListProjectComments@3.3.3", "description": "List comments on a project's document content.\n\nReturns comments with user info, timestamps, quoted text for inline comments,\nand reply threading info. Replies are nested under their parent comments.\n\nUse comment_filter to control which comments are returned:\n- only_quoted (default): Only comments attached to a quote in the text\n- only_unquoted: Only comments not attached to a particular quote\n- all: All comments regardless of being attached to a quote or not", "parameters": [ { @@ -2687,7 +2687,7 @@ { "name": "ListProjects", "qualifiedName": "Linear.ListProjects", - "fullyQualifiedName": "Linear.ListProjects@3.3.2", + "fullyQualifiedName": "Linear.ListProjects@3.3.3", "description": "List Linear projects, optionally filtered by keywords and other criteria.\n\nReturns all projects when no filters provided, or filtered results when\nkeywords or other filters are specified.", "parameters": [ { @@ -2812,7 +2812,7 @@ { "name": "ListTeams", "qualifiedName": "Linear.ListTeams", - "fullyQualifiedName": "Linear.ListTeams@3.3.2", + "fullyQualifiedName": "Linear.ListTeams@3.3.3", "description": "List Linear teams, optionally filtered by keywords and other criteria.\n\nReturns all teams when no filters provided, or filtered results when\nkeywords or other filters are specified.", "parameters": [ { @@ -2924,7 +2924,7 @@ { "name": "ListWorkflowStates", "qualifiedName": "Linear.ListWorkflowStates", - "fullyQualifiedName": "Linear.ListWorkflowStates@3.3.2", + "fullyQualifiedName": "Linear.ListWorkflowStates@3.3.3", "description": "List available workflow states in the workspace.\n\nReturns workflow states that can be used for issue transitions.\nStates are team-specific and have different types.", "parameters": [ { @@ -3017,7 +3017,7 @@ { "name": "ManageIssueSubscription", "qualifiedName": "Linear.ManageIssueSubscription", - "fullyQualifiedName": "Linear.ManageIssueSubscription@3.3.2", + "fullyQualifiedName": "Linear.ManageIssueSubscription@3.3.3", "description": "Subscribe to or unsubscribe from an issue's notifications.", "parameters": [ { @@ -3090,7 +3090,7 @@ { "name": "ReplyToComment", "qualifiedName": "Linear.ReplyToComment", - "fullyQualifiedName": "Linear.ReplyToComment@3.3.2", + "fullyQualifiedName": "Linear.ReplyToComment@3.3.3", "description": "Reply to an existing comment on an issue.\n\nCreates a threaded reply to the specified parent comment.", "parameters": [ { @@ -3176,7 +3176,7 @@ { "name": "ReplyToProjectComment", "qualifiedName": "Linear.ReplyToProjectComment", - "fullyQualifiedName": "Linear.ReplyToProjectComment@3.3.2", + "fullyQualifiedName": "Linear.ReplyToProjectComment@3.3.3", "description": "Reply to an existing comment on a project document.\n\nCreates a threaded reply to the specified parent comment.", "parameters": [ { @@ -3275,7 +3275,7 @@ { "name": "TransitionIssueState", "qualifiedName": "Linear.TransitionIssueState", - "fullyQualifiedName": "Linear.TransitionIssueState@3.3.2", + "fullyQualifiedName": "Linear.TransitionIssueState@3.3.3", "description": "Transition a Linear issue to a new workflow state.\n\nThe target state is validated against the team's available states.", "parameters": [ { @@ -3361,7 +3361,7 @@ { "name": "UpdateComment", "qualifiedName": "Linear.UpdateComment", - "fullyQualifiedName": "Linear.UpdateComment@3.3.2", + "fullyQualifiedName": "Linear.UpdateComment@3.3.3", "description": "Update an existing comment.", "parameters": [ { @@ -3434,7 +3434,7 @@ { "name": "UpdateInitiative", "qualifiedName": "Linear.UpdateInitiative", - "fullyQualifiedName": "Linear.UpdateInitiative@3.3.2", + "fullyQualifiedName": "Linear.UpdateInitiative@3.3.3", "description": "Update a Linear initiative with partial updates.\n\nOnly fields that are explicitly provided will be updated.", "parameters": [ { @@ -3553,7 +3553,7 @@ { "name": "UpdateIssue", "qualifiedName": "Linear.UpdateIssue", - "fullyQualifiedName": "Linear.UpdateIssue@3.3.2", + "fullyQualifiedName": "Linear.UpdateIssue@3.3.3", "description": "Update a Linear issue with partial updates.\n\nOnly fields that are explicitly provided will be updated. All entity\nreferences are validated before update.", "parameters": [ { @@ -3584,7 +3584,7 @@ "name": "assignee", "type": "string", "required": false, - "description": "New assignee name or email. Use '@me' for current user. Must be a team member. Only updated if provided.", + "description": "New assignee name or email. Use '@me' for current user. Use one of the following strings to clear assignee: '', 'none', 'null', or 'unassigned'. Only updated if provided.", "enum": null, "inferrable": true }, @@ -3808,7 +3808,7 @@ { "name": "UpdateProject", "qualifiedName": "Linear.UpdateProject", - "fullyQualifiedName": "Linear.UpdateProject@3.3.2", + "fullyQualifiedName": "Linear.UpdateProject@3.3.3", "description": "Update a Linear project with partial updates.\n\nOnly fields that are explicitly provided will be updated. All entity\nreferences are validated before update.\n\nIMPORTANT: Updating the 'content' field will break any existing inline\ncomment anchoring. The comments will still exist and be retrievable via\nlist_project_comments, but they will no longer appear visually anchored\nto text in the Linear UI. The 'description' field can be safely updated\nwithout affecting inline comments.", "parameters": [ { @@ -4012,7 +4012,7 @@ { "name": "WhoAmI", "qualifiedName": "Linear.WhoAmI", - "fullyQualifiedName": "Linear.WhoAmI@3.3.2", + "fullyQualifiedName": "Linear.WhoAmI@3.3.3", "description": "Get the authenticated user's profile and team memberships.\n\nReturns the current user's information including their name, email,\norganization, and the teams they belong to.", "parameters": [], "auth": { @@ -4066,5 +4066,5 @@ ], "customImports": [], "subPages": [], - "generatedAt": "2026-04-10T11:26:08.882Z" + "generatedAt": "2026-04-17T23:26:32.818Z" } \ No newline at end of file diff --git a/toolkit-docs-generator/src/sources/toolkit-data-source.ts b/toolkit-docs-generator/src/sources/toolkit-data-source.ts index 52e85c386..3ee4e1fcf 100644 --- a/toolkit-docs-generator/src/sources/toolkit-data-source.ts +++ b/toolkit-docs-generator/src/sources/toolkit-data-source.ts @@ -108,28 +108,45 @@ export class CombinedToolkitDataSource implements IToolkitDataSource { this.metadataSource = config.metadataSource; } + /** + * Apply the "*Api" provider-id fallback when the direct metadata lookup + * missed. Used by both `fetchToolkitData` and `fetchAllToolkitsData` so + * they resolve metadata the same way. + */ + private async resolveProviderMetadata( + toolkitId: string, + tools: readonly ToolDefinition[], + directMetadata: ToolkitMetadata | null + ): Promise { + if (directMetadata || !normalizeId(toolkitId).endsWith("api")) { + return directMetadata; + } + + const providerId = tools.find((t) => t.auth?.providerId)?.auth?.providerId; + if (!providerId) { + return null; + } + + return ( + (await this.metadataSource.getToolkitMetadata(`${providerId}Api`)) ?? + (await this.metadataSource.getToolkitMetadata(providerId)) + ); + } + async fetchToolkitData( toolkitId: string, version?: string ): Promise { // Fetch tools and metadata in parallel - const [tools, fetchedMetadata] = await Promise.all([ + const [tools, directMetadata] = await Promise.all([ this.toolSource.fetchToolsByToolkit(toolkitId), this.metadataSource.getToolkitMetadata(toolkitId), ]); - - // If the toolkit isn't in the Design System under its exact ID, try to match - // based on the auth provider for "*Api" toolkits (e.g. MailchimpMarketingApi -> MailchimpApi). - let metadata = fetchedMetadata; - if (!metadata && normalizeId(toolkitId).endsWith("api")) { - const providerId = tools.find((t) => t.auth?.providerId)?.auth - ?.providerId; - if (providerId) { - metadata = - (await this.metadataSource.getToolkitMetadata(`${providerId}Api`)) ?? - (await this.metadataSource.getToolkitMetadata(providerId)); - } - } + const metadata = await this.resolveProviderMetadata( + toolkitId, + tools, + directMetadata + ); // Filter tools by version if specified, otherwise keep only the highest // version to drop stale tools from older releases that Engine still serves. @@ -184,10 +201,16 @@ export class CombinedToolkitDataSource implements IToolkitDataSource { // matching the behaviour of fetchToolkitData. const result = new Map(); for (const [toolkitId, tools] of toolkitGroups) { - const directMetadata = metadataMap.get(toolkitId) ?? null; - const metadata = - directMetadata ?? + // Prefer the batch lookup; only fall back to per-toolkit lookup + // (and then the provider-id fallback) when the map misses. + const directMetadata = + metadataMap.get(toolkitId) ?? (await this.metadataSource.getToolkitMetadata(toolkitId)); + const metadata = await this.resolveProviderMetadata( + toolkitId, + tools, + directMetadata + ); result.set(toolkitId, { tools, metadata }); } diff --git a/toolkit-docs-generator/src/utils/version-coherence.ts b/toolkit-docs-generator/src/utils/version-coherence.ts index 47ffedc82..458ed0273 100644 --- a/toolkit-docs-generator/src/utils/version-coherence.ts +++ b/toolkit-docs-generator/src/utils/version-coherence.ts @@ -1,71 +1,142 @@ import type { ToolDefinition } from "../types/index.js"; import { extractVersion } from "./fp.js"; +interface ParsedSemver { + readonly core: readonly number[]; + readonly prerelease: readonly (number | string)[] | null; +} + /** - * Parse the numeric MAJOR.MINOR.PATCH tuple from a semver string, - * stripping pre-release (`-alpha.1`) and build metadata (`+build.456`). - * - * Handles formats produced by arcade-mcp's normalize_version(): - * "3.1.3", "1.2.3-beta.1", "1.2.3+build.456", "1.2.3-rc.1+build.789" + * Parse a semver-ish string into numeric core + optional pre-release parts. + * Build metadata (+...) is ignored for ordering. */ -const parseNumericVersion = (version: string): number[] => { - // Strip build metadata (after +) then pre-release (after -) - const core = version.split("+")[0]?.split("-")[0] ?? version; - return core.split(".").map((s) => { - const n = Number(s); +const parseSemver = (version: string): ParsedSemver => { + const withoutBuild = version.split("+")[0] ?? version; + const prereleaseIndex = withoutBuild.indexOf("-"); + const coreText = + prereleaseIndex >= 0 + ? withoutBuild.slice(0, prereleaseIndex) + : withoutBuild; + const prereleaseText = + prereleaseIndex >= 0 ? withoutBuild.slice(prereleaseIndex + 1) : ""; + + const core = coreText.split(".").map((segment) => { + const n = Number(segment); return Number.isNaN(n) ? 0 : n; }); + + const prerelease = + prereleaseText.length > 0 + ? prereleaseText.split(".").map((identifier) => { + const n = Number(identifier); + return Number.isNaN(n) ? identifier : n; + }) + : null; + + return { core, prerelease }; +}; + +const compareCoreVersions = ( + aCore: readonly number[], + bCore: readonly number[] +): number => { + const len = Math.max(aCore.length, bCore.length); + for (let i = 0; i < len; i++) { + const diff = (aCore[i] ?? 0) - (bCore[i] ?? 0); + if (diff !== 0) return diff; + } + return 0; }; /** - * Compare two semver version strings numerically by MAJOR.MINOR.PATCH. - * Pre-release and build metadata are ignored for ordering purposes - * (they are unlikely to appear in Engine API responses, but we handle - * them defensively since arcade-mcp's semver allows them). - * - * Returns a positive number if a > b, negative if a < b, 0 if equal. + * Compare two prerelease identifiers following SemVer 2.0.0 §11.4.2: + * numeric identifiers always rank lower than alphanumeric identifiers, + * numeric identifiers compare numerically, and alphanumeric identifiers + * compare by ASCII byte order (not locale). */ -const compareVersions = (a: string, b: string): number => { - const aParts = parseNumericVersion(a); - const bParts = parseNumericVersion(b); - const len = Math.max(aParts.length, bParts.length); +const comparePrereleaseIdentifier = ( + aPart: number | string, + bPart: number | string +): number => { + if (typeof aPart === "number" && typeof bPart === "number") { + return aPart - bPart; + } + if (typeof aPart === "number") return -1; + if (typeof bPart === "number") return 1; + if (aPart < bPart) return -1; + if (aPart > bPart) return 1; + return 0; +}; + +/** + * Compare prerelease identifier arrays following SemVer 2.0.0 §11.4: + * a prerelease version has lower precedence than the associated normal + * version, and longer identifier lists with matching prefix rank higher. + */ +const comparePrerelease = ( + aPrerelease: readonly (number | string)[] | null, + bPrerelease: readonly (number | string)[] | null +): number => { + if (!(aPrerelease || bPrerelease)) return 0; + if (!aPrerelease) return 1; + if (!bPrerelease) return -1; + + const len = Math.max(aPrerelease.length, bPrerelease.length); for (let i = 0; i < len; i++) { - const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0); + const aPart = aPrerelease[i]; + const bPart = bPrerelease[i]; + if (aPart === undefined) return -1; + if (bPart === undefined) return 1; + const diff = comparePrereleaseIdentifier(aPart, bPart); if (diff !== 0) return diff; } + return 0; }; +/** + * Compare two semver version strings using SemVer 2.0.0 precedence: + * - MAJOR.MINOR.PATCH compared numerically + * - Stable release > prerelease when core is equal + * - Numeric prerelease identifiers compare numerically + * - Alphanumeric prerelease identifiers compare by ASCII byte order + * - Build metadata is ignored for precedence + * + * Returns a positive number if a > b, negative if a < b, 0 if equal. + */ +export const compareVersions = (a: string, b: string): number => { + const aSemver = parseSemver(a); + const bSemver = parseSemver(b); + const coreDiff = compareCoreVersions(aSemver.core, bSemver.core); + if (coreDiff !== 0) return coreDiff; + return comparePrerelease(aSemver.prerelease, bSemver.prerelease); +}; + /** * Find the highest version among all tools in a toolkit. - * This is the version we keep — stale tools from older releases are dropped. + * Returns null when no tool carries a usable version. */ export const getHighestVersion = ( tools: readonly ToolDefinition[] ): string | null => { - if (tools.length === 0) { - return null; - } - - let best = ""; + let best: string | null = null; for (const tool of tools) { const version = extractVersion(tool.fullyQualifiedName); - if (best === "" || compareVersions(version, best) > 0) { + if (best === null || compareVersions(version, best) > 0) { best = version; } } - - return best || null; + return best; }; /** - * Keep only tools whose @version matches the highest version for - * their toolkit. If all tools share the same version (the common - * case), returns the original array unchanged. + * Keep only tools whose @version is semver-equivalent to the highest + * version for their toolkit. If all tools share the highest version + * (the common case), returns the original array unchanged. * - * This drops stale tools from older releases that Engine still serves, - * while always preserving the newest version — even if it has fewer tools - * (e.g. tools were removed/consolidated in the new release). + * Tools are compared via `compareVersions`, so build metadata is + * ignored when deciding equivalence — a tool at `@1.0.0+build.1` + * survives alongside `@1.0.0` instead of being silently dropped. */ export const filterToolsByHighestVersion = ( tools: readonly ToolDefinition[] @@ -75,13 +146,14 @@ export const filterToolsByHighestVersion = ( return tools; } - // Fast path: if every tool is already at the highest version, skip filtering const allSame = tools.every( - (t) => extractVersion(t.fullyQualifiedName) === highest + (t) => compareVersions(extractVersion(t.fullyQualifiedName), highest) === 0 ); if (allSame) { return tools; } - return tools.filter((t) => extractVersion(t.fullyQualifiedName) === highest); + return tools.filter( + (t) => compareVersions(extractVersion(t.fullyQualifiedName), highest) === 0 + ); }; diff --git a/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts b/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts index 5b014cba6..6e75e049d 100644 --- a/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts +++ b/toolkit-docs-generator/tests/sources/toolkit-data-source.test.ts @@ -10,6 +10,7 @@ import { InMemoryMetadataSource, InMemoryToolDataSource, } from "../../src/sources/in-memory.js"; +import type { IMetadataSource } from "../../src/sources/internal.js"; import { createCombinedToolkitDataSource } from "../../src/sources/toolkit-data-source.js"; import type { ToolDefinition, ToolkitMetadata } from "../../src/types/index.js"; @@ -277,4 +278,131 @@ describe("CombinedToolkitDataSource", () => { const result = await dataSource.fetchToolkitData("MailchimpMarketingApi"); expect(result.metadata?.label).toBe("Mailchimp API"); }); + + it("should use providerId fallback consistently in fetchAllToolkitsData", async () => { + const toolSource = new InMemoryToolDataSource([ + createTool({ + qualifiedName: "MailchimpMarketingApi.CreateCampaign", + fullyQualifiedName: "MailchimpMarketingApi.CreateCampaign@1.0.0", + auth: { providerId: "mailchimp", providerType: "oauth2", scopes: [] }, + }), + ]); + + const metadataSource = new InMemoryMetadataSource([ + createMetadata({ + id: "MailchimpApi", + label: "Mailchimp API", + category: "productivity", + type: "arcade_starter", + }), + ]); + + const dataSource = createCombinedToolkitDataSource({ + toolSource, + metadataSource, + }); + + const single = await dataSource.fetchToolkitData("MailchimpMarketingApi"); + const all = await dataSource.fetchAllToolkitsData(); + const listed = all.get("MailchimpMarketingApi"); + + expect(single.metadata?.label).toBe("Mailchimp API"); + expect(listed?.metadata?.label).toBe("Mailchimp API"); + }); + + it("fetchToolkitData does not re-fetch metadata when direct lookup is already null", async () => { + const toolSource = new InMemoryToolDataSource([ + createTool({ + qualifiedName: "Github.CreateIssue", + fullyQualifiedName: "Github.CreateIssue@1.0.0", + }), + ]); + + let metadataLookupCalls = 0; + const metadataSource = { + async getToolkitMetadata(_toolkitId) { + metadataLookupCalls += 1; + return null; + }, + async getAllToolkitsMetadata() { + return []; + }, + async listToolkitIds() { + return []; + }, + } satisfies IMetadataSource; + + const dataSource = createCombinedToolkitDataSource({ + toolSource, + metadataSource, + }); + + await dataSource.fetchToolkitData("Github"); + expect(metadataLookupCalls).toBe(1); + }); + + it("fetchAllToolkitsData performs toolkit-id lookup when metadata map misses", async () => { + const toolSource = new InMemoryToolDataSource([ + createTool({ + qualifiedName: "Github.CreateIssue", + fullyQualifiedName: "Github.CreateIssue@1.0.0", + }), + ]); + + const lookedUpIds: string[] = []; + const metadataSource = { + async getToolkitMetadata(toolkitId) { + lookedUpIds.push(toolkitId); + return null; + }, + async getAllToolkitsMetadata() { + return [ + createMetadata({ id: "Slack", label: "Slack", category: "social" }), + ]; + }, + async listToolkitIds() { + return ["Slack"]; + }, + } satisfies IMetadataSource; + + const dataSource = createCombinedToolkitDataSource({ + toolSource, + metadataSource, + }); + + await dataSource.fetchAllToolkitsData(); + expect(lookedUpIds).toEqual(["Github"]); + }); + + it("fetchAllToolkitsData does not re-lookup metadata when the map already has a direct hit", async () => { + const toolSource = new InMemoryToolDataSource([ + createTool({ + qualifiedName: "Github.CreateIssue", + fullyQualifiedName: "Github.CreateIssue@1.0.0", + }), + ]); + + const lookedUpIds: string[] = []; + const metadataSource = { + async getToolkitMetadata(toolkitId) { + lookedUpIds.push(toolkitId); + return null; + }, + async getAllToolkitsMetadata() { + return [createMetadata()]; + }, + async listToolkitIds() { + return ["Github"]; + }, + } satisfies IMetadataSource; + + const dataSource = createCombinedToolkitDataSource({ + toolSource, + metadataSource, + }); + + const result = await dataSource.fetchAllToolkitsData(); + expect(result.get("Github")?.metadata?.label).toBe("GitHub"); + expect(lookedUpIds).toEqual([]); + }); }); diff --git a/toolkit-docs-generator/tests/utils/version-coherence.test.ts b/toolkit-docs-generator/tests/utils/version-coherence.test.ts index e399c859b..dfcf9a762 100644 --- a/toolkit-docs-generator/tests/utils/version-coherence.test.ts +++ b/toolkit-docs-generator/tests/utils/version-coherence.test.ts @@ -110,6 +110,81 @@ describe("getHighestVersion", () => { ]; expect(getHighestVersion(tools)).toBe("4.0.0-beta.1+build.123"); }); + + it("prefers stable release over pre-release with same core version", () => { + const tools = [ + createTool("Github.CreateIssue@1.23-rc.1"), + createTool("Github.SetStarred@1.23"), + ]; + expect(getHighestVersion(tools)).toBe("1.23"); + }); + + it("orders numeric pre-release identifiers numerically", () => { + const tools = [ + createTool("Github.CreateIssue@1.23-rc.2"), + createTool("Github.SetStarred@1.23-rc.10"), + ]; + expect(getHighestVersion(tools)).toBe("1.23-rc.10"); + }); + + it("treats non-numeric pre-release identifiers as higher than numeric ones", () => { + const tools = [ + createTool("Github.CreateIssue@1.23-rc.1"), + createTool("Github.SetStarred@1.23-rc.beta"), + ]; + expect(getHighestVersion(tools)).toBe("1.23-rc.beta"); + }); + + it("treats longer pre-release identifier lists as newer when prefix matches", () => { + const tools = [ + createTool("Github.CreateIssue@1.23-rc"), + createTool("Github.SetStarred@1.23-rc.1"), + ]; + expect(getHighestVersion(tools)).toBe("1.23-rc.1"); + }); + + it("distinguishes versions that differ only in the patch component", () => { + const tools = [ + createTool("Github.CreateIssue@1.2.3"), + createTool("Github.SetStarred@1.2.4"), + ]; + expect(getHighestVersion(tools)).toBe("1.2.4"); + }); + + it("orders alphanumeric pre-release identifiers by ASCII byte order", () => { + // SemVer §11.4.2: alphanumeric identifiers compare by ASCII, not locale. + // 'B' (66) < 'a' (97), so "1.0.0-Beta" < "1.0.0-alpha". + const tools = [ + createTool("Github.CreateIssue@1.0.0-Beta"), + createTool("Github.SetStarred@1.0.0-alpha"), + ]; + expect(getHighestVersion(tools)).toBe("1.0.0-alpha"); + }); + + it("orders alphanumeric pre-release identifiers when both are non-numeric", () => { + const tools = [ + createTool("Github.CreateIssue@1.0.0-alpha"), + createTool("Github.SetStarred@1.0.0-beta"), + ]; + expect(getHighestVersion(tools)).toBe("1.0.0-beta"); + }); + + it("returns null when every tool lacks an @version", () => { + const tools = [ + { + ...createTool("X.A@0.0.0"), + fullyQualifiedName: "X.A", + }, + { + ...createTool("X.B@0.0.0"), + fullyQualifiedName: "X.B", + }, + ]; + // extractVersion defaults missing "@" to "0.0.0", so "highest" is + // well-defined here. This test pins that contract so future changes + // to extractVersion surface in the coherence layer. + expect(getHighestVersion(tools)).toBe("0.0.0"); + }); }); describe("filterToolsByHighestVersion", () => { @@ -187,4 +262,41 @@ describe("filterToolsByHighestVersion", () => { true ); }); + + it("drops matching pre-release tools when stable release exists", () => { + const tools = [ + createTool("Github.CreateIssue@1.23-rc.1"), + createTool("Github.SetStarred@1.23"), + ]; + const result = filterToolsByHighestVersion(tools); + expect(result).toHaveLength(1); + expect(result[0]?.fullyQualifiedName).toBe("Github.SetStarred@1.23"); + }); + + it("keeps only the highest numeric pre-release", () => { + const tools = [ + createTool("Github.CreateIssue@1.23-rc.2"), + createTool("Github.SetStarred@1.23-rc.10"), + createTool("Github.ListPullRequests@1.23-rc.1"), + ]; + const result = filterToolsByHighestVersion(tools); + expect(result).toHaveLength(1); + expect(result[0]?.fullyQualifiedName).toBe("Github.SetStarred@1.23-rc.10"); + }); + + it("keeps tools with equivalent core versions even when build metadata differs", () => { + // Build metadata is ignored by semver precedence, so two tools tagged + // `@1.0.0` and `@1.0.0+build.1` are the same release and both survive. + const tools = [ + createTool("Github.CreateIssue@1.0.0"), + createTool("Github.SetStarred@1.0.0+build.1"), + createTool("Github.ListPullRequests@0.9.0"), + ]; + const result = filterToolsByHighestVersion(tools); + expect(result).toHaveLength(2); + expect(result.map((t) => t.fullyQualifiedName)).toEqual([ + "Github.CreateIssue@1.0.0", + "Github.SetStarred@1.0.0+build.1", + ]); + }); });