@bretwardjames/ghp-mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @bretwardjames/ghp-mcp@0.4.0 build /home/bretwardjames/IdeaProjects/ghp/packages/mcp
2
+ > @bretwardjames/ghp-mcp@0.5.0 build /home/bretwardjames/IdeaProjects/ghp/packages/mcp
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@ CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 97.52 KB
11
- ESM ⚡️ Build success in 30ms
10
+ ESM dist/index.js 103.19 KB
11
+ ESM ⚡️ Build success in 35ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 1029ms
13
+ DTS ⚡️ Build success in 1054ms
14
14
  DTS dist/index.d.ts 20.00 B
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @bretwardjames/ghp-mcp
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add field discovery, hotfix branching, and named flags
8
+
9
+ - `ghp fields` command to discover project fields and valid values
10
+ - `--hotfix [ref]` flag on `ghp start` to branch from tags/commits
11
+ - `--priority` and `--size` named flags on `ghp add`
12
+ - `--body-file` and `--body-stdin` flags for issue body input
13
+ - Verbose creation summary showing field assignments
14
+ - Fix silent flag dropping after positional title arg
15
+ - `get_fields` and `get_tags` MCP tools
16
+ - `validateRefString()` for shell injection prevention in git refs
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+ - @bretwardjames/ghp-core@0.9.0
22
+
3
23
  ## 0.4.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -73,8 +73,10 @@ The server uses the same GitHub authentication as the CLI. Run `ghp auth` to aut
73
73
  | `update_issue` | Update an issue's title and/or body |
74
74
  | `move_issue` | Move an issue to a different status |
75
75
  | `mark_done` | Mark an issue as done |
76
- | `start_work` | Start working on an issue |
76
+ | `start_work` | Start working on an issue (supports `hotfix` param for branching from tags) |
77
77
  | `create_worktree` | Create a worktree for parallel development |
78
+ | `get_fields` | List all project fields and their valid values |
79
+ | `get_tags` | List git tags sorted newest first (for hotfix discovery) |
78
80
  | `assign_issue` | Assign users to an issue |
79
81
  | `add_comment` | Add a comment to an issue |
80
82
  | `set_field` | Set a field value on an issue |
package/dist/index.js CHANGED
@@ -674,7 +674,8 @@ import * as z5 from "zod";
674
674
  import {
675
675
  getCurrentBranch,
676
676
  executeHooksForEvent,
677
- hasHooksForEvent
677
+ hasHooksForEvent,
678
+ resolveRef
678
679
  } from "@bretwardjames/ghp-core";
679
680
  var meta5 = {
680
681
  name: "start_work",
@@ -688,10 +689,11 @@ function register5(server, context) {
688
689
  description: 'Start working on an issue by setting its status to "In Progress". Note: Branch creation is not supported in MCP context - use the CLI for that.',
689
690
  inputSchema: {
690
691
  issue: z5.number().describe("Issue number to start working on"),
691
- updateStatus: z5.boolean().optional().describe("Whether to update the status (default: true)")
692
+ updateStatus: z5.boolean().optional().describe("Whether to update the status (default: true)"),
693
+ hotfix: z5.string().optional().describe("Branch from a specific tag or commit for hotfix branches. Use get_tags tool to discover available tags.")
692
694
  }
693
695
  },
694
- async ({ issue, updateStatus = true }) => {
696
+ async ({ issue, updateStatus = true, hotfix }) => {
695
697
  const authenticated = await context.ensureAuthenticated();
696
698
  if (!authenticated) {
697
699
  return {
@@ -717,6 +719,24 @@ function register5(server, context) {
717
719
  };
718
720
  }
719
721
  try {
722
+ let hotfixInfo = "";
723
+ if (hotfix) {
724
+ const resolved = await resolveRef(hotfix);
725
+ if (!resolved) {
726
+ return {
727
+ content: [
728
+ {
729
+ type: "text",
730
+ text: `Error: Hotfix ref "${hotfix}" does not exist. Use get_tags to discover available tags.`
731
+ }
732
+ ],
733
+ isError: true
734
+ };
735
+ }
736
+ hotfixInfo = `
737
+
738
+ Hotfix: Branch from "${hotfix}" (${resolved.substring(0, 7)}). Use the CLI to create the branch: ghp start ${issue} --hotfix ${hotfix}`;
739
+ }
720
740
  const item = await context.api.findItemByNumber(repo, issue);
721
741
  if (!item) {
722
742
  return {
@@ -738,7 +758,7 @@ WARNING: This issue is blocked by: ${openBlockers.map((b) => `#${b.number} (${b.
738
758
  content: [
739
759
  {
740
760
  type: "text",
741
- text: `Started work on issue #${issue} "${item.title}" (status not updated).${blockingWarning}`
761
+ text: `Started work on issue #${issue} "${item.title}" (status not updated).${blockingWarning}${hotfixInfo}`
742
762
  }
743
763
  ]
744
764
  };
@@ -820,7 +840,7 @@ Hooks: ${successCount} succeeded`;
820
840
  content: [
821
841
  {
822
842
  type: "text",
823
- text: `Started work on issue #${issue} "${item.title}" - status set to "${inProgressOption.name}".${blockingWarning}${hookInfo}`
843
+ text: `Started work on issue #${issue} "${item.title}" - status set to "${inProgressOption.name}".${blockingWarning}${hotfixInfo}${hookInfo}`
824
844
  }
825
845
  ]
826
846
  };
@@ -1700,7 +1720,7 @@ function register13(server, context) {
1700
1720
  deleteHead: z13.boolean().optional().describe("Delete head branch after merge (default: true)")
1701
1721
  }
1702
1722
  },
1703
- async ({ number: number19, method = "squash", deleteHead = true }) => {
1723
+ async ({ number: number20, method = "squash", deleteHead = true }) => {
1704
1724
  const authenticated = await context.ensureAuthenticated();
1705
1725
  if (!authenticated) {
1706
1726
  return {
@@ -1726,7 +1746,7 @@ function register13(server, context) {
1726
1746
  };
1727
1747
  }
1728
1748
  try {
1729
- const safePrNumber = validateNumericInput2(number19, "PR number");
1749
+ const safePrNumber = validateNumericInput2(number20, "PR number");
1730
1750
  const args = ["pr", "merge", String(safePrNumber)];
1731
1751
  if (method === "squash") {
1732
1752
  args.push("--squash");
@@ -1755,7 +1775,7 @@ function register13(server, context) {
1755
1775
  isError: true
1756
1776
  };
1757
1777
  }
1758
- let message = `Merged PR #${number19} using ${method} strategy.`;
1778
+ let message = `Merged PR #${number20} using ${method} strategy.`;
1759
1779
  if (deleteHead) {
1760
1780
  message += " Head branch deleted.";
1761
1781
  }
@@ -2993,6 +3013,191 @@ function register24(server, context) {
2993
3013
  );
2994
3014
  }
2995
3015
 
3016
+ // src/tools/fields.ts
3017
+ var fields_exports = {};
3018
+ __export(fields_exports, {
3019
+ meta: () => meta25,
3020
+ register: () => register25
3021
+ });
3022
+ import * as z24 from "zod";
3023
+ var meta25 = {
3024
+ name: "get_fields",
3025
+ category: "read"
3026
+ };
3027
+ function register25(server, context) {
3028
+ server.registerTool(
3029
+ "get_fields",
3030
+ {
3031
+ title: "Get Project Fields",
3032
+ description: "Get all project fields and their valid values. Useful for discovering what values can be used with set_field, move_issue, or add_issue --field flags.",
3033
+ inputSchema: {
3034
+ project: z24.string().optional().describe("Project name to query (defaults to first project)")
3035
+ }
3036
+ },
3037
+ async ({ project: projectName }) => {
3038
+ const authenticated = await context.ensureAuthenticated();
3039
+ if (!authenticated) {
3040
+ return {
3041
+ content: [
3042
+ {
3043
+ type: "text",
3044
+ text: "Error: Not authenticated. Please ensure gh CLI is authenticated or set GITHUB_TOKEN."
3045
+ }
3046
+ ],
3047
+ isError: true
3048
+ };
3049
+ }
3050
+ const repo = await context.getRepo();
3051
+ if (!repo) {
3052
+ return {
3053
+ content: [
3054
+ {
3055
+ type: "text",
3056
+ text: "Error: Not in a git repository with a GitHub remote."
3057
+ }
3058
+ ],
3059
+ isError: true
3060
+ };
3061
+ }
3062
+ try {
3063
+ const projects = await context.api.getProjects(repo);
3064
+ if (projects.length === 0) {
3065
+ return {
3066
+ content: [
3067
+ {
3068
+ type: "text",
3069
+ text: "No projects found for this repository."
3070
+ }
3071
+ ],
3072
+ isError: true
3073
+ };
3074
+ }
3075
+ let selectedProject = projects[0];
3076
+ if (projectName) {
3077
+ const match = projects.find(
3078
+ (p) => p.title.toLowerCase() === projectName.toLowerCase()
3079
+ );
3080
+ if (!match) {
3081
+ const available = projects.map((p) => p.title).join(", ");
3082
+ return {
3083
+ content: [
3084
+ {
3085
+ type: "text",
3086
+ text: `Project "${projectName}" not found. Available projects: ${available}`
3087
+ }
3088
+ ],
3089
+ isError: true
3090
+ };
3091
+ }
3092
+ selectedProject = match;
3093
+ }
3094
+ const fields = await context.api.getProjectFields(selectedProject.id);
3095
+ const result = {
3096
+ project: selectedProject.title,
3097
+ fields: fields.map((f) => ({
3098
+ name: f.name,
3099
+ type: f.type || "Text",
3100
+ options: f.options?.map((o) => o.name)
3101
+ }))
3102
+ };
3103
+ return {
3104
+ content: [
3105
+ {
3106
+ type: "text",
3107
+ text: JSON.stringify(result, null, 2)
3108
+ }
3109
+ ]
3110
+ };
3111
+ } catch (error) {
3112
+ return {
3113
+ content: [
3114
+ {
3115
+ type: "text",
3116
+ text: `Error fetching project fields: ${error instanceof Error ? error.message : String(error)}`
3117
+ }
3118
+ ],
3119
+ isError: true
3120
+ };
3121
+ }
3122
+ }
3123
+ );
3124
+ }
3125
+
3126
+ // src/tools/tags.ts
3127
+ var tags_exports = {};
3128
+ __export(tags_exports, {
3129
+ meta: () => meta26,
3130
+ register: () => register26
3131
+ });
3132
+ import * as z25 from "zod";
3133
+ import { listTags } from "@bretwardjames/ghp-core";
3134
+ var meta26 = {
3135
+ name: "get_tags",
3136
+ category: "read"
3137
+ };
3138
+ function register26(server, context) {
3139
+ server.registerTool(
3140
+ "get_tags",
3141
+ {
3142
+ title: "Get Tags",
3143
+ description: "List git tags in the repository, sorted newest first. Useful for discovering available tags for hotfix branches with start_work.",
3144
+ inputSchema: {
3145
+ limit: z25.number().optional().describe("Maximum number of tags to return (default: 20)")
3146
+ }
3147
+ },
3148
+ async ({ limit = 20 }) => {
3149
+ const repo = await context.getRepo();
3150
+ if (!repo) {
3151
+ return {
3152
+ content: [
3153
+ {
3154
+ type: "text",
3155
+ text: "Error: Not in a git repository with a GitHub remote."
3156
+ }
3157
+ ],
3158
+ isError: true
3159
+ };
3160
+ }
3161
+ try {
3162
+ const tags = await listTags();
3163
+ const limited = tags.slice(0, limit);
3164
+ if (limited.length === 0) {
3165
+ return {
3166
+ content: [
3167
+ {
3168
+ type: "text",
3169
+ text: "No tags found in this repository."
3170
+ }
3171
+ ]
3172
+ };
3173
+ }
3174
+ return {
3175
+ content: [
3176
+ {
3177
+ type: "text",
3178
+ text: JSON.stringify({
3179
+ total: tags.length,
3180
+ showing: limited.length,
3181
+ tags: limited
3182
+ }, null, 2)
3183
+ }
3184
+ ]
3185
+ };
3186
+ } catch (error) {
3187
+ return {
3188
+ content: [
3189
+ {
3190
+ type: "text",
3191
+ text: `Error listing tags: ${error instanceof Error ? error.message : String(error)}`
3192
+ }
3193
+ ],
3194
+ isError: true
3195
+ };
3196
+ }
3197
+ }
3198
+ );
3199
+ }
3200
+
2996
3201
  // src/tool-registry.ts
2997
3202
  var TOOLS = [
2998
3203
  // Read tools
@@ -3002,6 +3207,8 @@ var TOOLS = [
3002
3207
  get_progress_exports,
3003
3208
  get_issue_exports,
3004
3209
  standup_exports,
3210
+ fields_exports,
3211
+ tags_exports,
3005
3212
  // Action tools
3006
3213
  move_exports,
3007
3214
  done_exports,
@@ -3334,7 +3541,7 @@ function registerIssueResource(server, context) {
3334
3541
  description: "Detailed information about a specific issue including comments",
3335
3542
  mimeType: "application/json"
3336
3543
  },
3337
- async (uri, { number: number19 }) => {
3544
+ async (uri, { number: number20 }) => {
3338
3545
  const authenticated = await context.ensureAuthenticated();
3339
3546
  if (!authenticated) {
3340
3547
  return {
@@ -3360,7 +3567,7 @@ function registerIssueResource(server, context) {
3360
3567
  };
3361
3568
  }
3362
3569
  try {
3363
- const issueNumber = parseInt(number19, 10);
3570
+ const issueNumber = parseInt(number20, 10);
3364
3571
  if (isNaN(issueNumber)) {
3365
3572
  return {
3366
3573
  contents: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bretwardjames/ghp-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for ghp (GitHub Projects)",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "@modelcontextprotocol/sdk": "^1.0.0",
31
31
  "zod": "^3.22.0",
32
- "@bretwardjames/ghp-core": "0.8.0"
32
+ "@bretwardjames/ghp-core": "0.9.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^20.10.0",
@@ -35,6 +35,8 @@ import * as unlinkBranchTool from './tools/unlink-branch.js';
35
35
  // Phase 3: Lower Priority Tools
36
36
  import * as getIssueTool from './tools/get-issue.js';
37
37
  import * as standupTool from './tools/standup.js';
38
+ import * as fieldsTool from './tools/fields.js';
39
+ import * as tagsTool from './tools/tags.js';
38
40
 
39
41
  // Re-export types
40
42
  export type { ToolCategory, McpConfig, McpToolsConfig } from './types.js';
@@ -58,6 +60,8 @@ const TOOLS: ToolModule[] = [
58
60
  getProgressTool,
59
61
  getIssueTool,
60
62
  standupTool,
63
+ fieldsTool,
64
+ tagsTool,
61
65
  // Action tools
62
66
  moveTool,
63
67
  doneTool,
@@ -0,0 +1,124 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import * as z from 'zod';
3
+ import type { ServerContext } from '../server.js';
4
+ import type { ToolMeta } from '../types.js';
5
+
6
+ /** Tool metadata for registry */
7
+ export const meta: ToolMeta = {
8
+ name: 'get_fields',
9
+ category: 'read',
10
+ };
11
+
12
+ /**
13
+ * Registers the get_fields tool.
14
+ * Returns project fields and their valid values (options for single-select, type for others).
15
+ */
16
+ export function register(server: McpServer, context: ServerContext): void {
17
+ server.registerTool(
18
+ 'get_fields',
19
+ {
20
+ title: 'Get Project Fields',
21
+ description:
22
+ 'Get all project fields and their valid values. Useful for discovering what values can be used with set_field, move_issue, or add_issue --field flags.',
23
+ inputSchema: {
24
+ project: z
25
+ .string()
26
+ .optional()
27
+ .describe('Project name to query (defaults to first project)'),
28
+ },
29
+ },
30
+ async ({ project: projectName }) => {
31
+ const authenticated = await context.ensureAuthenticated();
32
+ if (!authenticated) {
33
+ return {
34
+ content: [
35
+ {
36
+ type: 'text',
37
+ text: 'Error: Not authenticated. Please ensure gh CLI is authenticated or set GITHUB_TOKEN.',
38
+ },
39
+ ],
40
+ isError: true,
41
+ };
42
+ }
43
+
44
+ const repo = await context.getRepo();
45
+ if (!repo) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: 'text',
50
+ text: 'Error: Not in a git repository with a GitHub remote.',
51
+ },
52
+ ],
53
+ isError: true,
54
+ };
55
+ }
56
+
57
+ try {
58
+ const projects = await context.api.getProjects(repo);
59
+ if (projects.length === 0) {
60
+ return {
61
+ content: [
62
+ {
63
+ type: 'text',
64
+ text: 'No projects found for this repository.',
65
+ },
66
+ ],
67
+ isError: true,
68
+ };
69
+ }
70
+
71
+ // Select project by name if provided, otherwise first
72
+ let selectedProject = projects[0];
73
+ if (projectName) {
74
+ const match = projects.find(
75
+ p => p.title.toLowerCase() === projectName.toLowerCase()
76
+ );
77
+ if (!match) {
78
+ const available = projects.map(p => p.title).join(', ');
79
+ return {
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: `Project "${projectName}" not found. Available projects: ${available}`,
84
+ },
85
+ ],
86
+ isError: true,
87
+ };
88
+ }
89
+ selectedProject = match;
90
+ }
91
+
92
+ const fields = await context.api.getProjectFields(selectedProject.id);
93
+
94
+ const result = {
95
+ project: selectedProject.title,
96
+ fields: fields.map(f => ({
97
+ name: f.name,
98
+ type: f.type || 'Text',
99
+ options: f.options?.map(o => o.name),
100
+ })),
101
+ };
102
+
103
+ return {
104
+ content: [
105
+ {
106
+ type: 'text',
107
+ text: JSON.stringify(result, null, 2),
108
+ },
109
+ ],
110
+ };
111
+ } catch (error) {
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text',
116
+ text: `Error fetching project fields: ${error instanceof Error ? error.message : String(error)}`,
117
+ },
118
+ ],
119
+ isError: true,
120
+ };
121
+ }
122
+ }
123
+ );
124
+ }
@@ -27,6 +27,8 @@ import { register as registerLinkBranch } from './link-branch.js';
27
27
  import { register as registerUnlinkBranch } from './unlink-branch.js';
28
28
  // Phase 3: Lower Priority Tools
29
29
  import { register as registerGetIssue } from './get-issue.js';
30
+ import { register as registerGetFields } from './fields.js';
31
+ import { register as registerGetTags } from './tags.js';
30
32
 
31
33
  /**
32
34
  * @deprecated Use registerEnabledTools from '../tool-registry.js' instead.
@@ -39,6 +41,8 @@ export function registerAllTools(server: McpServer, context: ServerContext): voi
39
41
  registerListWorktrees(server, context);
40
42
  registerGetProgress(server, context);
41
43
  registerGetIssue(server, context);
44
+ registerGetFields(server, context);
45
+ registerGetTags(server, context);
42
46
 
43
47
  // Action tools
44
48
  registerMove(server, context);
@@ -7,6 +7,7 @@ import {
7
7
  getCurrentBranch,
8
8
  executeHooksForEvent,
9
9
  hasHooksForEvent,
10
+ resolveRef,
10
11
  type IssueStartedPayload,
11
12
  } from '@bretwardjames/ghp-core';
12
13
 
@@ -33,9 +34,13 @@ export function register(server: McpServer, context: ServerContext): void {
33
34
  .boolean()
34
35
  .optional()
35
36
  .describe('Whether to update the status (default: true)'),
37
+ hotfix: z
38
+ .string()
39
+ .optional()
40
+ .describe('Branch from a specific tag or commit for hotfix branches. Use get_tags tool to discover available tags.'),
36
41
  },
37
42
  },
38
- async ({ issue, updateStatus = true }) => {
43
+ async ({ issue, updateStatus = true, hotfix }) => {
39
44
  const authenticated = await context.ensureAuthenticated();
40
45
  if (!authenticated) {
41
46
  return {
@@ -63,6 +68,24 @@ export function register(server: McpServer, context: ServerContext): void {
63
68
  }
64
69
 
65
70
  try {
71
+ // Validate hotfix ref if provided
72
+ let hotfixInfo = '';
73
+ if (hotfix) {
74
+ const resolved = await resolveRef(hotfix);
75
+ if (!resolved) {
76
+ return {
77
+ content: [
78
+ {
79
+ type: 'text',
80
+ text: `Error: Hotfix ref "${hotfix}" does not exist. Use get_tags to discover available tags.`,
81
+ },
82
+ ],
83
+ isError: true,
84
+ };
85
+ }
86
+ hotfixInfo = `\n\nHotfix: Branch from "${hotfix}" (${resolved.substring(0, 7)}). Use the CLI to create the branch: ghp start ${issue} --hotfix ${hotfix}`;
87
+ }
88
+
66
89
  // Find the issue in projects
67
90
  const item = await context.api.findItemByNumber(repo, issue);
68
91
  if (!item) {
@@ -88,7 +111,7 @@ export function register(server: McpServer, context: ServerContext): void {
88
111
  content: [
89
112
  {
90
113
  type: 'text',
91
- text: `Started work on issue #${issue} "${item.title}" (status not updated).${blockingWarning}`,
114
+ text: `Started work on issue #${issue} "${item.title}" (status not updated).${blockingWarning}${hotfixInfo}`,
92
115
  },
93
116
  ],
94
117
  };
@@ -182,7 +205,7 @@ export function register(server: McpServer, context: ServerContext): void {
182
205
  content: [
183
206
  {
184
207
  type: 'text',
185
- text: `Started work on issue #${issue} "${item.title}" - status set to "${inProgressOption.name}".${blockingWarning}${hookInfo}`,
208
+ text: `Started work on issue #${issue} "${item.title}" - status set to "${inProgressOption.name}".${blockingWarning}${hotfixInfo}${hookInfo}`,
186
209
  },
187
210
  ],
188
211
  };
@@ -0,0 +1,85 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import * as z from 'zod';
3
+ import type { ServerContext } from '../server.js';
4
+ import type { ToolMeta } from '../types.js';
5
+ import { listTags } from '@bretwardjames/ghp-core';
6
+
7
+ /** Tool metadata for registry */
8
+ export const meta: ToolMeta = {
9
+ name: 'get_tags',
10
+ category: 'read',
11
+ };
12
+
13
+ /**
14
+ * Registers the get_tags tool.
15
+ * Lists git tags in the repository, sorted newest first.
16
+ */
17
+ export function register(server: McpServer, context: ServerContext): void {
18
+ server.registerTool(
19
+ 'get_tags',
20
+ {
21
+ title: 'Get Tags',
22
+ description:
23
+ 'List git tags in the repository, sorted newest first. Useful for discovering available tags for hotfix branches with start_work.',
24
+ inputSchema: {
25
+ limit: z
26
+ .number()
27
+ .optional()
28
+ .describe('Maximum number of tags to return (default: 20)'),
29
+ },
30
+ },
31
+ async ({ limit = 20 }) => {
32
+ const repo = await context.getRepo();
33
+ if (!repo) {
34
+ return {
35
+ content: [
36
+ {
37
+ type: 'text',
38
+ text: 'Error: Not in a git repository with a GitHub remote.',
39
+ },
40
+ ],
41
+ isError: true,
42
+ };
43
+ }
44
+
45
+ try {
46
+ const tags = await listTags();
47
+ const limited = tags.slice(0, limit);
48
+
49
+ if (limited.length === 0) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: 'No tags found in this repository.',
55
+ },
56
+ ],
57
+ };
58
+ }
59
+
60
+ return {
61
+ content: [
62
+ {
63
+ type: 'text',
64
+ text: JSON.stringify({
65
+ total: tags.length,
66
+ showing: limited.length,
67
+ tags: limited,
68
+ }, null, 2),
69
+ },
70
+ ],
71
+ };
72
+ } catch (error) {
73
+ return {
74
+ content: [
75
+ {
76
+ type: 'text',
77
+ text: `Error listing tags: ${error instanceof Error ? error.message : String(error)}`,
78
+ },
79
+ ],
80
+ isError: true,
81
+ };
82
+ }
83
+ }
84
+ );
85
+ }