Skip to content

Commit 4ce8aa8

Browse files
Merge branch 'main' into issues/1449
2 parents b8a6978 + 16972b2 commit 4ce8aa8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1643
-1375
lines changed

.github/workflows/issue-triage.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: Claude Issue Triage
2+
description: Run Claude Code for issue triage in GitHub Actions
3+
on:
4+
issues:
5+
types: [opened]
6+
7+
jobs:
8+
triage-issue:
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 10
11+
permissions:
12+
contents: read
13+
issues: write
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
18+
with:
19+
fetch-depth: 0
20+
21+
- name: Setup GitHub MCP Server
22+
run: |
23+
mkdir -p /tmp/mcp-config
24+
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
25+
{
26+
"mcpServers": {
27+
"github": {
28+
"command": "docker",
29+
"args": [
30+
"run",
31+
"-i",
32+
"--rm",
33+
"-e",
34+
"GITHUB_PERSONAL_ACCESS_TOKEN",
35+
"ghcr.io/github/github-mcp-server:sha-efef8ae"
36+
],
37+
"env": {
38+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
39+
}
40+
}
41+
}
42+
}
43+
EOF
44+
45+
- name: Create triage prompt
46+
run: |
47+
mkdir -p /tmp/claude-prompts
48+
cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF'
49+
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
50+
51+
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
52+
53+
Issue Information:
54+
- REPO: ${{ github.repository }}
55+
- ISSUE_NUMBER: ${{ github.event.issue.number }}
56+
57+
TASK OVERVIEW:
58+
59+
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
60+
61+
2. Next, use the GitHub tools to get context about the issue:
62+
- You have access to these tools:
63+
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
64+
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
65+
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
66+
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
67+
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
68+
- Start by using mcp__github__get_issue to get the issue details
69+
70+
3. Analyze the issue content, considering:
71+
- The issue title and description
72+
- The type of issue (bug report, feature request, question, etc.)
73+
- Technical areas mentioned
74+
- Severity or priority indicators
75+
- User impact
76+
- Components affected
77+
78+
4. Select appropriate labels from the available labels list provided above:
79+
- Choose labels that accurately reflect the issue's nature
80+
- Be specific but comprehensive
81+
- Select priority labels if you can determine urgency (p0, p1, or p2)
82+
- Consider platform labels (kubernetes) if applicable
83+
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
84+
85+
5. Apply the selected labels:
86+
- Use mcp__github__update_issue to apply your selected labels
87+
- DO NOT post any comments explaining your decision
88+
- DO NOT communicate directly with users
89+
- If no labels are clearly applicable, do not apply any labels
90+
91+
IMPORTANT GUIDELINES:
92+
- Be thorough in your analysis
93+
- Only select labels from the provided list above
94+
- DO NOT post any comments to the issue
95+
- Your ONLY action should be to apply labels using mcp__github__update_issue
96+
- It's okay to not add any labels if none are clearly applicable
97+
EOF
98+
99+
- name: Run Claude Code for Issue Triage
100+
uses: anthropics/claude-code-base-action@beta
101+
with:
102+
prompt_file: /tmp/claude-prompts/triage-prompt.txt
103+
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
104+
mcp_config: /tmp/mcp-config/mcp-servers.json
105+
timeout_minutes: "5"
106+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
107+
env:
108+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/verify-docgen.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ jobs:
1515
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
1616
with:
1717
go-version-file: go.mod
18+
- name: Install swag
19+
run: go install github.com/swaggo/swag/v2/cmd/swag@latest
1820
- run: ./cmd/help/verify.sh

cmd/help/verify.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
#!/usr/bin/env bash
22
set -e
33

4-
# Verify that generated Markdown docs are up-to-date.
4+
# Verify that generated CLI docs are up-to-date.
55
tmpdir=$(mktemp -d)
66
go run cmd/help/main.go --dir "$tmpdir"
77
diff -Naur -I "^ date:" "$tmpdir" docs/cli/
8+
9+
# Generate API docs in temp directory that mimics the final structure
10+
api_tmpdir=$(mktemp -d)
11+
mkdir -p "$api_tmpdir/server"
12+
swag init -g pkg/api/server.go --v3.1 -o "$api_tmpdir/server"
13+
# Exclude README.md from diff as it's manually maintained
14+
diff -Naur --exclude="README.md" "$api_tmpdir/server" docs/server/
15+
816
echo "######################################################################################"
917
echo "If diffs are found, please run: \`task docs\` to regenerate the docs."
1018
echo "######################################################################################"

cmd/thv/app/client.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,10 @@ func init() {
102102
clientCmd.AddCommand(clientRemoveCmd)
103103
clientCmd.AddCommand(clientListRegisteredCmd)
104104

105-
// TODO: Re-enable when group functionality is complete
106-
//clientRegisterCmd.Flags().StringSliceVar(
107-
// &groupAddNames, "group", []string{groups.DefaultGroup}, "Only register workloads from specified groups")
108-
//clientRemoveCmd.Flags().StringSliceVar(
109-
// &groupRmNames, "group", []string{}, "Remove client from specified groups (if not set, removes all workloads from the client)")
105+
clientRegisterCmd.Flags().StringSliceVar(
106+
&groupAddNames, "group", []string{groups.DefaultGroup}, "Only register workloads from specified groups")
107+
clientRemoveCmd.Flags().StringSliceVar(
108+
&groupRmNames, "group", []string{}, "Remove client from specified groups (if not set, removes all workloads from the client)")
110109
}
111110

112111
func clientStatusCmdFunc(cmd *cobra.Command, _ []string) error {
@@ -157,11 +156,11 @@ func clientSetupCmdFunc(cmd *cobra.Command, _ []string) error {
157156
return registerSelectedClients(cmd, selectedClients, selectedGroups)
158157
}
159158

160-
// Helper to get available (installed but unregistered) clients
159+
// Helper to get available (installed) clients
161160
func getAvailableClients(statuses []client.MCPClientStatus) []client.MCPClientStatus {
162161
var available []client.MCPClientStatus
163162
for _, s := range statuses {
164-
if s.Installed && !s.Registered {
163+
if s.Installed {
165164
available = append(available, s)
166165
}
167166
}

cmd/thv/app/commands.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func NewRootCmd(enableUpdates bool) *cobra.Command {
5454
rootCmd.AddCommand(newSecretCommand())
5555
rootCmd.AddCommand(inspectorCommand())
5656
rootCmd.AddCommand(newMCPCommand())
57-
// rootCmd.AddCommand(groupCmd) // TODO: add back in once we have a working group command, and update the docs
57+
rootCmd.AddCommand(groupCmd)
5858

5959
// Silence printing the usage on error
6060
rootCmd.SilenceUsage = true

cmd/thv/app/common.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/stacklok/toolhive/pkg/config"
1010
"github.com/stacklok/toolhive/pkg/secrets"
11+
"github.com/stacklok/toolhive/pkg/validation"
1112
"github.com/stacklok/toolhive/pkg/workloads"
1213
)
1314

@@ -149,7 +150,7 @@ func completeLogsArgs(cmd *cobra.Command, args []string, _ string) ([]string, co
149150

150151
// ValidateGroupFlag returns a cobra PreRunE-compatible function
151152
// that validates the --group flag *if provided*.
152-
/*func validateGroupFlag() func(cmd *cobra.Command, args []string) error {
153+
func validateGroupFlag() func(cmd *cobra.Command, args []string) error {
153154
return func(cmd *cobra.Command, _ []string) error {
154155
groupName, err := cmd.Flags().GetString("group")
155156
if err != nil {
@@ -168,4 +169,4 @@ func completeLogsArgs(cmd *cobra.Command, args []string, _ string) ([]string, co
168169

169170
return nil
170171
}
171-
}*/
172+
}

cmd/thv/app/list.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ func init() {
3232
listCmd.Flags().BoolVarP(&listAll, "all", "a", false, "Show all workloads (default shows just running)")
3333
listCmd.Flags().StringVar(&listFormat, "format", FormatText, "Output format (json, text, or mcpservers)")
3434
listCmd.Flags().StringArrayVarP(&listLabelFilter, "label", "l", []string{}, "Filter workloads by labels (format: key=value)")
35-
// TODO: Re-enable when group functionality is complete
36-
// listCmd.Flags().StringVar(&listGroupFilter, "group", "", "Filter workloads by group")
35+
listCmd.Flags().StringVar(&listGroupFilter, "group", "", "Filter workloads by group")
3736

38-
// listCmd.PreRunE = validateGroupFlag()
37+
listCmd.PreRunE = validateGroupFlag()
3938
}
4039

4140
func listCmdFunc(cmd *cobra.Command, _ []string) error {

cmd/thv/app/mcp.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/stacklok/toolhive/pkg/transport/streamable"
2020
"github.com/stacklok/toolhive/pkg/transport/types"
2121
"github.com/stacklok/toolhive/pkg/versions"
22+
"github.com/stacklok/toolhive/pkg/workloads"
2223
)
2324

2425
var (
@@ -86,7 +87,7 @@ func newMCPCommand() *cobra.Command {
8687
}
8788

8889
func addMCPFlags(cmd *cobra.Command) {
89-
cmd.Flags().StringVar(&mcpServerURL, "server", "", "MCP server URL (required)")
90+
cmd.Flags().StringVar(&mcpServerURL, "server", "", "MCP server URL or name from ToolHive registry (required)")
9091
cmd.Flags().StringVar(&mcpFormat, "format", FormatText, "Output format (json or text)")
9192
cmd.Flags().DurationVar(&mcpTimeout, "timeout", 30*time.Second, "Connection timeout")
9293
cmd.Flags().StringVar(&mcpTransport, "transport", "auto", "Transport type (auto, sse, streamable-http)")
@@ -98,7 +99,13 @@ func mcpListCmdFunc(cmd *cobra.Command, _ []string) error {
9899
ctx, cancel := context.WithTimeout(cmd.Context(), mcpTimeout)
99100
defer cancel()
100101

101-
mcpClient, err := createMCPClient()
102+
// Resolve server URL if it's a name
103+
serverURL, err := resolveServerURL(ctx, mcpServerURL)
104+
if err != nil {
105+
return err
106+
}
107+
108+
mcpClient, err := createMCPClient(serverURL)
102109
if err != nil {
103110
return err
104111
}
@@ -143,7 +150,13 @@ func mcpListToolsCmdFunc(cmd *cobra.Command, _ []string) error {
143150
ctx, cancel := context.WithTimeout(cmd.Context(), mcpTimeout)
144151
defer cancel()
145152

146-
mcpClient, err := createMCPClient()
153+
// Resolve server URL if it's a name
154+
serverURL, err := resolveServerURL(ctx, mcpServerURL)
155+
if err != nil {
156+
return err
157+
}
158+
159+
mcpClient, err := createMCPClient(serverURL)
147160
if err != nil {
148161
return err
149162
}
@@ -166,7 +179,13 @@ func mcpListResourcesCmdFunc(cmd *cobra.Command, _ []string) error {
166179
ctx, cancel := context.WithTimeout(cmd.Context(), mcpTimeout)
167180
defer cancel()
168181

169-
mcpClient, err := createMCPClient()
182+
// Resolve server URL if it's a name
183+
serverURL, err := resolveServerURL(ctx, mcpServerURL)
184+
if err != nil {
185+
return err
186+
}
187+
188+
mcpClient, err := createMCPClient(serverURL)
170189
if err != nil {
171190
return err
172191
}
@@ -189,7 +208,13 @@ func mcpListPromptsCmdFunc(cmd *cobra.Command, _ []string) error {
189208
ctx, cancel := context.WithTimeout(cmd.Context(), mcpTimeout)
190209
defer cancel()
191210

192-
mcpClient, err := createMCPClient()
211+
// Resolve server URL if it's a name
212+
serverURL, err := resolveServerURL(ctx, mcpServerURL)
213+
if err != nil {
214+
return err
215+
}
216+
217+
mcpClient, err := createMCPClient(serverURL)
193218
if err != nil {
194219
return err
195220
}
@@ -207,19 +232,48 @@ func mcpListPromptsCmdFunc(cmd *cobra.Command, _ []string) error {
207232
return outputMCPData(map[string]interface{}{"prompts": result.Prompts}, mcpFormat)
208233
}
209234

235+
// resolveServerURL resolves a server name to a URL or returns the URL if it's already a URL
236+
func resolveServerURL(ctx context.Context, serverInput string) (string, error) {
237+
// Check if it's already a URL
238+
if strings.HasPrefix(serverInput, "http://") || strings.HasPrefix(serverInput, "https://") {
239+
return serverInput, nil
240+
}
241+
242+
// Try to get the workload by name
243+
manager, err := workloads.NewManager(ctx)
244+
if err != nil {
245+
return "", fmt.Errorf("failed to create workload manager: %w", err)
246+
}
247+
248+
workload, err := manager.GetWorkload(ctx, serverInput)
249+
if err != nil {
250+
return "", fmt.Errorf(
251+
"server '%s' not found in running workloads. "+
252+
"Please ensure the server is running or provide a valid URL", serverInput)
253+
}
254+
255+
// Check if the workload is running
256+
if workload.Status != "running" {
257+
return "", fmt.Errorf("server '%s' is not running (status: %s). "+
258+
"Please start it first using 'thv run %s'", serverInput, workload.Status, serverInput)
259+
}
260+
261+
return workload.URL, nil
262+
}
263+
210264
// createMCPClient creates an MCP client based on the server URL and transport type
211-
func createMCPClient() (*client.Client, error) {
212-
transportType := determineTransportType(mcpServerURL, mcpTransport)
265+
func createMCPClient(serverURL string) (*client.Client, error) {
266+
transportType := determineTransportType(serverURL, mcpTransport)
213267

214268
switch transportType {
215269
case types.TransportTypeSSE:
216-
mcpClient, err := client.NewSSEMCPClient(mcpServerURL)
270+
mcpClient, err := client.NewSSEMCPClient(serverURL)
217271
if err != nil {
218272
return nil, fmt.Errorf("failed to create SSE MCP client: %w", err)
219273
}
220274
return mcpClient, nil
221275
case types.TransportTypeStreamableHTTP:
222-
mcpClient, err := client.NewStreamableHttpClient(mcpServerURL)
276+
mcpClient, err := client.NewStreamableHttpClient(serverURL)
223277
if err != nil {
224278
return nil, fmt.Errorf("failed to create Streamable HTTP MCP client: %w", err)
225279
}

cmd/thv/app/restart.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,12 @@ var restartCmd = &cobra.Command{
2929
func init() {
3030
restartCmd.Flags().BoolVarP(&restartAll, "all", "a", false, "Restart all MCP servers")
3131
restartCmd.Flags().BoolVarP(&restartForeground, "foreground", "f", false, "Run the restarted workload in foreground mode")
32-
// TODO: Uncomment when groups are fully supported
33-
//restartCmd.Flags().StringVarP(&restartGroup, "group", "g", "", "Restart all MCP servers in a specific group")
34-
//
35-
//// Mark the flags as mutually exclusive
36-
//restartCmd.MarkFlagsMutuallyExclusive("all", "group")
32+
restartCmd.Flags().StringVarP(&restartGroup, "group", "g", "", "Restart all MCP servers in a specific group")
3733

38-
// restartCmd.PreRunE = validateGroupFlag()
34+
// Mark the flags as mutually exclusive
35+
restartCmd.MarkFlagsMutuallyExclusive("all", "group")
36+
37+
restartCmd.PreRunE = validateGroupFlag()
3938
}
4039

4140
func restartCmdFunc(cmd *cobra.Command, args []string) error {

cmd/thv/app/rm.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ var rmCmd = &cobra.Command{
2020
}
2121

2222
func init() {
23-
// TODO: Re-enable when group functionality is complete
24-
// rmCmd.Flags().String("group", "", "Delete all workloads in the specified group")
23+
rmCmd.Flags().String("group", "", "Delete all workloads in the specified group")
2524

26-
// rmCmd.PreRunE = validateGroupFlag()
25+
rmCmd.PreRunE = validateGroupFlag()
2726
}
2827

2928
//nolint:gocyclo // This function is complex but manageable

0 commit comments

Comments
 (0)