Skip to content

Commit 88752f6

Browse files
committed
Merged main.
Signed-off-by: Roddie Kieley <[email protected]>
2 parents 77c9100 + 6c8d3e2 commit 88752f6

37 files changed

+3340
-368
lines changed

.github/workflows/claude.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Claude PR Assistant
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
pull_request_review_comment:
7+
types: [created]
8+
issues:
9+
types: [opened, assigned]
10+
pull_request_review:
11+
types: [submitted]
12+
13+
jobs:
14+
claude-code-action:
15+
if: |
16+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19+
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
pull-requests: read
24+
issues: read
25+
id-token: write
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
29+
with:
30+
fetch-depth: 1
31+
32+
- name: Run Claude PR Action
33+
uses: anthropics/claude-code-action@beta
34+
with:
35+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
36+
# Or use OAuth token instead:
37+
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38+
timeout_minutes: "20"
39+
# mode: tag # Default: responds to @claude mentions
40+
# Optional: Restrict network access to specific domains only
41+
# experimental_allowed_domains: |
42+
# .anthropic.com
43+
# .github.com
44+
# api.github.com
45+
# .githubusercontent.com
46+
# bun.sh
47+
# registry.npmjs.org
48+
# .blob.core.windows.net

.github/workflows/run-on-main.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ jobs:
1919
e2e-tests:
2020
name: E2E Tests
2121
uses: ./.github/workflows/e2e-tests.yml
22-
swagger:
23-
name: Swagger
24-
uses: ./.github/workflows/verify-swagger.yml
22+
codegen:
23+
name: Codegen
24+
uses: ./.github/workflows/verify-gen.yml
2525
image-build-and-push:
2626
name: Build and Sign Image
27-
needs: [ linting, tests, e2e-tests, swagger ]
27+
needs: [ linting, tests, e2e-tests, codegen ]
2828
permissions:
2929
contents: write
3030
packages: write

.github/workflows/run-on-pr.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ jobs:
2626
docs:
2727
name: Docs
2828
uses: ./.github/workflows/verify-docgen.yml
29-
swagger:
30-
name: Swagger
31-
uses: ./.github/workflows/verify-swagger.yml
29+
codegen:
30+
name: Codegen
31+
uses: ./.github/workflows/verify-gen.yml
3232
operator-ci:
3333
name: Operator CI
3434
permissions:

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,9 @@ Follow conventional commit format:
193193

194194
## Development Best Practices
195195

196-
- **Linting**:
196+
- **Linting**:
197197
- Prefer `lint-fix` to `lint` since `lint-fix` will fix problems automatically.
198+
- **Commit messages and PR titles**:
199+
- Refer to the `CONTRIBUTING.md` file for guidelines on commit message format
200+
conventions.
201+
- Do not use "Conventional Commits", e.g. starting with `feat`, `fix`, `chore`, etc.

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"k8s.io/apimachinery/pkg/runtime"
2323
"k8s.io/apimachinery/pkg/types"
2424
"k8s.io/apimachinery/pkg/util/intstr"
25+
"k8s.io/utils/ptr"
2526
ctrl "sigs.k8s.io/controller-runtime"
2627
"sigs.k8s.io/controller-runtime/pkg/client"
2728
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -556,6 +557,23 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
556557
}
557558
}
558559

560+
// Prepare ProxyRunner's pod and container security context
561+
proxyRunnerPodSecurityContext := &corev1.PodSecurityContext{
562+
RunAsNonRoot: ptr.To(true),
563+
RunAsUser: ptr.To(int64(1000)),
564+
RunAsGroup: ptr.To(int64(1000)),
565+
FSGroup: ptr.To(int64(1000)),
566+
}
567+
568+
proxyRunnerContainerSecurityContext := &corev1.SecurityContext{
569+
Privileged: ptr.To(false),
570+
RunAsNonRoot: ptr.To(true),
571+
RunAsUser: ptr.To(int64(1000)),
572+
RunAsGroup: ptr.To(int64(1000)),
573+
AllowPrivilegeEscalation: ptr.To(false),
574+
ReadOnlyRootFilesystem: ptr.To(true),
575+
}
576+
559577
env = ensureRequiredEnvVars(env)
560578

561579
dep := &appsv1.Deployment{
@@ -612,8 +630,10 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
612630
TimeoutSeconds: 3,
613631
FailureThreshold: 3,
614632
},
633+
SecurityContext: proxyRunnerContainerSecurityContext,
615634
}},
616-
Volumes: volumes,
635+
Volumes: volumes,
636+
SecurityContext: proxyRunnerPodSecurityContext,
617637
},
618638
},
619639
},

cmd/thv-operator/controllers/mcpserver_pod_template_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,53 @@ func TestDeploymentForMCPServerWithEnvVars(t *testing.T) {
344344
assert.True(t, debugModeArgFound, "DEBUG_MODE should be passed as --env flag")
345345
}
346346

347+
func TestProxyRunnerSecurityContext(t *testing.T) {
348+
t.Parallel()
349+
350+
// Create a test MCPServer
351+
mcpServer := &mcpv1alpha1.MCPServer{
352+
ObjectMeta: metav1.ObjectMeta{
353+
Name: "test-mcp-server-env",
354+
Namespace: "default",
355+
},
356+
Spec: mcpv1alpha1.MCPServerSpec{
357+
Image: "test-image:latest",
358+
Transport: "stdio",
359+
Port: 8080,
360+
},
361+
}
362+
363+
// Register the scheme
364+
s := scheme.Scheme
365+
s.AddKnownTypes(mcpv1alpha1.GroupVersion, &mcpv1alpha1.MCPServer{})
366+
s.AddKnownTypes(mcpv1alpha1.GroupVersion, &mcpv1alpha1.MCPServerList{})
367+
368+
// Create a reconciler with the scheme
369+
r := &MCPServerReconciler{
370+
Scheme: s,
371+
}
372+
373+
// Generate the deployment
374+
deployment := r.deploymentForMCPServer(mcpServer)
375+
require.NotNil(t, deployment, "Deployment should not be nil")
376+
377+
// Check that the ProxyRunner's pod and container security context are set
378+
proxyRunnerPodSecurityContext := deployment.Spec.Template.Spec.SecurityContext
379+
require.NotNil(t, proxyRunnerPodSecurityContext, "ProxyRunner pod security context should not be nil")
380+
assert.True(t, *proxyRunnerPodSecurityContext.RunAsNonRoot, "ProxyRunner pod RunAsNonRoot should be true")
381+
assert.Equal(t, int64(1000), *proxyRunnerPodSecurityContext.RunAsUser, "ProxyRunner pod RunAsUser should be 1000")
382+
assert.Equal(t, int64(1000), *proxyRunnerPodSecurityContext.RunAsGroup, "ProxyRunner pod RunAsGroup should be 1000")
383+
assert.Equal(t, int64(1000), *proxyRunnerPodSecurityContext.FSGroup, "ProxyRunner pod FSGroup should be 1000")
384+
385+
proxyRunnerContainerSecurityContext := deployment.Spec.Template.Spec.Containers[0].SecurityContext
386+
require.NotNil(t, proxyRunnerContainerSecurityContext, "ProxyRunner container security context should not be nil")
387+
assert.False(t, *proxyRunnerContainerSecurityContext.Privileged, "ProxyRunner container Privileged should be false")
388+
assert.True(t, *proxyRunnerContainerSecurityContext.RunAsNonRoot, "ProxyRunner container RunAsNonRoot should be true")
389+
assert.Equal(t, int64(1000), *proxyRunnerContainerSecurityContext.RunAsUser, "ProxyRunner container RunAsUser should be 1000")
390+
assert.Equal(t, int64(1000), *proxyRunnerContainerSecurityContext.RunAsGroup, "ProxyRunner container RunAsGroup should be 1000")
391+
assert.False(t, *proxyRunnerContainerSecurityContext.AllowPrivilegeEscalation, "ProxyRunner container AllowPrivilegeEscalation should be false")
392+
}
393+
347394
// Helper functions
348395
func boolPtr(b bool) *bool {
349396
return &b

cmd/thv-proxyrunner/app/run.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/spf13/cobra"
99

1010
"github.com/stacklok/toolhive/pkg/container"
11+
"github.com/stacklok/toolhive/pkg/environment"
1112
"github.com/stacklok/toolhive/pkg/logger"
1213
"github.com/stacklok/toolhive/pkg/registry"
1314
"github.com/stacklok/toolhive/pkg/runner"
@@ -237,6 +238,12 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
237238

238239
var imageMetadata *registry.ImageMetadata
239240

241+
// Parse environment variables from slice to map
242+
envVarsMap, err := environment.ParseEnvironmentVariables(runEnv)
243+
if err != nil {
244+
return fmt.Errorf("failed to parse environment variables: %v", err)
245+
}
246+
240247
// Initialize a new RunConfig with values from command-line flags
241248
runConfig, err := runner.NewRunConfigBuilder().
242249
WithRuntime(rt).
@@ -261,7 +268,7 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
261268
WithTelemetryConfig(finalOtelEndpoint, runOtelEnablePrometheusMetricsPath, runOtelServiceName,
262269
finalOtelSamplingRate, runOtelHeaders, runOtelInsecure, finalOtelEnvironmentVariables).
263270
WithToolsFilter(runToolsFilter).
264-
Build(ctx, imageMetadata, runEnv, envVarValidator)
271+
Build(ctx, imageMetadata, envVarsMap, envVarValidator)
265272
if err != nil {
266273
return fmt.Errorf("failed to create RunConfig: %v", err)
267274
}

cmd/thv/app/mcp_serve_helpers.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ func buildServerConfig(
6666
builder = builder.WithTransportAndPorts(transport, 0, 0)
6767

6868
// Prepare environment variables
69-
envVarsList := prepareEnvironmentVariables(imageMetadata, args.Env)
69+
envVars := prepareEnvironmentVariables(imageMetadata, args.Env)
7070

7171
// Build the configuration
7272
envVarValidator := &runner.DetachedEnvVarValidator{}
73-
return builder.Build(ctx, imageMetadata, envVarsList, envVarValidator)
73+
return builder.Build(ctx, imageMetadata, envVars, envVarValidator)
7474
}
7575

7676
// configureTransport sets up transport configuration from metadata
@@ -88,7 +88,7 @@ func configureTransport(builder *runner.RunConfigBuilder, imageMetadata *registr
8888
}
8989

9090
// prepareEnvironmentVariables merges default and user environment variables
91-
func prepareEnvironmentVariables(imageMetadata *registry.ImageMetadata, userEnv map[string]string) []string {
91+
func prepareEnvironmentVariables(imageMetadata *registry.ImageMetadata, userEnv map[string]string) map[string]string {
9292
envVarsMap := make(map[string]string)
9393

9494
// Add default environment variables from metadata
@@ -105,13 +105,7 @@ func prepareEnvironmentVariables(imageMetadata *registry.ImageMetadata, userEnv
105105
envVarsMap[k] = v
106106
}
107107

108-
// Convert map to []string format
109-
var envVarsList []string
110-
for k, v := range envVarsMap {
111-
envVarsList = append(envVarsList, fmt.Sprintf("%s=%s", k, v))
112-
}
113-
114-
return envVarsList
108+
return envVarsMap
115109
}
116110

117111
// saveAndRunServer saves the configuration and runs the server

cmd/thv/app/proxy.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,10 @@ func init() {
171171
if err := proxyCmd.MarkFlagRequired("target-uri"); err != nil {
172172
logger.Warnf("Warning: Failed to mark flag as required: %v", err)
173173
}
174-
175-
// Attach the subcommand to the main proxy command
174+
// Attach the subcommands to the main proxy command
176175
proxyCmd.AddCommand(proxyTunnelCmd)
176+
proxyCmd.AddCommand(proxyStdioCmd)
177+
177178
}
178179

179180
func proxyCmdFunc(cmd *cobra.Command, args []string) error {

cmd/thv/app/proxy_stdio.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package app
2+
3+
import (
4+
"fmt"
5+
"os/signal"
6+
"syscall"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/stacklok/toolhive/pkg/logger"
11+
"github.com/stacklok/toolhive/pkg/transport"
12+
"github.com/stacklok/toolhive/pkg/workloads"
13+
)
14+
15+
var proxyStdioCmd = &cobra.Command{
16+
Use: "stdio WORKLOAD-NAME",
17+
Short: "Create a stdio-based proxy for an MCP server",
18+
Long: `Create a stdio-based proxy that connects stdin/stdout to a target MCP server.
19+
20+
Example:
21+
thv proxy stdio my-workload
22+
`,
23+
Args: cobra.ExactArgs(1),
24+
RunE: proxyStdioCmdFunc,
25+
}
26+
27+
func proxyStdioCmdFunc(cmd *cobra.Command, args []string) error {
28+
ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
29+
defer cancel()
30+
31+
workloadName := args[0]
32+
workloadManager, err := workloads.NewManager(ctx)
33+
if err != nil {
34+
return fmt.Errorf("failed to create workload manager: %w", err)
35+
}
36+
stdioWorkload, err := workloadManager.GetWorkload(ctx, workloadName)
37+
if err != nil {
38+
return fmt.Errorf("failed to get workload %q: %w", workloadName, err)
39+
}
40+
logger.Infof("Starting stdio proxy for workload=%q", workloadName)
41+
42+
bridge, err := transport.NewStdioBridge(workloadName, stdioWorkload.URL, stdioWorkload.TransportType)
43+
if err != nil {
44+
return fmt.Errorf("failed to create stdio bridge: %w", err)
45+
}
46+
bridge.Start(ctx)
47+
48+
// Consume until interrupt
49+
<-ctx.Done()
50+
logger.Info("Shutting down bridge")
51+
bridge.Shutdown()
52+
return nil
53+
}

0 commit comments

Comments
 (0)