@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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +20 -0
- package/README.md +3 -1
- package/dist/index.js +217 -10
- package/package.json +2 -2
- package/src/tool-registry.ts +4 -0
- package/src/tools/fields.ts +124 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/start.ts +26 -3
- package/src/tools/tags.ts +85 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @bretwardjames/ghp-mcp@0.
|
|
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
|
|
11
|
-
ESM ⚡️ Build success in
|
|
10
|
+
ESM dist/index.js 103.19 KB
|
|
11
|
+
ESM ⚡️ Build success in 35ms
|
|
12
12
|
DTS Build start
|
|
13
|
-
DTS ⚡️ Build success in
|
|
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:
|
|
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(
|
|
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 #${
|
|
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:
|
|
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(
|
|
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.
|
|
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.
|
|
32
|
+
"@bretwardjames/ghp-core": "0.9.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^20.10.0",
|
package/src/tool-registry.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -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);
|
package/src/tools/start.ts
CHANGED
|
@@ -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
|
+
}
|