Skip to content

Commit abd7a82

Browse files
committed
fix .gitignore, add panic recovery
1 parent 371cde3 commit abd7a82

File tree

4 files changed

+228
-90
lines changed

4 files changed

+228
-90
lines changed

.gitignore

Lines changed: 9 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,14 @@
1-
# Environment variables
2-
.env
1+
.idea
2+
cmd/harness-mcp-server/harness-mcp-server
33

4-
# Virtual Environment
5-
venv/
6-
env/
7-
ENV/
8-
bin/
4+
# VSCode
5+
.vscode/*
6+
!.vscode/launch.json
97

10-
.venv/
11-
src/harness_mcp/__pycache__/
128

13-
# Python
14-
__pycache__/
15-
*.py[cod]
16-
*$py.class
17-
*.so
18-
.Python
19-
build/
20-
develop-eggs/
9+
# Added by goreleaser init:
2110
dist/
22-
downloads/
23-
eggs/
24-
.eggs/
25-
lib/
26-
lib64/
27-
parts/
28-
sdist/
29-
var/
30-
wheels/
31-
*.egg-info/
32-
.installed.cfg
33-
*.egg
11+
__debug_bin*
3412

35-
# Distribution / packaging
36-
.Python
37-
build/
38-
develop-eggs/
39-
dist/
40-
downloads/
41-
eggs/
42-
.eggs/
43-
lib/
44-
lib64/
45-
parts/
46-
sdist/
47-
var/
48-
wheels/
49-
share/python-wheels/
50-
*.egg-info/
51-
.installed.cfg
52-
*.egg
53-
MANIFEST
54-
55-
# PyInstaller
56-
*.manifest
57-
*.spec
58-
59-
# Installer logs
60-
pip-log.txt
61-
pip-delete-this-directory.txt
62-
63-
# Unit test / coverage reports
64-
htmlcov/
65-
.tox/
66-
.nox/
67-
.coverage
68-
.coverage.*
69-
.cache
70-
nosetests.xml
71-
coverage.xml
72-
*.cover
73-
*.py,cover
74-
.hypothesis/
75-
.pytest_cache/
76-
cover/
77-
78-
# Jupyter Notebook
79-
.ipynb_checkpoints
80-
81-
# IDEs and editors
82-
.idea/
83-
.vscode/
84-
*.swp
85-
*.swo
86-
*~
87-
88-
# Go binaries
89-
harness-mcp-server
90-
*.exe
13+
# Go
14+
vendor
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package config
2+
3+
type Config struct {
4+
Version string
5+
BaseURL string
6+
AccountID string
7+
OrgID string
8+
ProjectID string
9+
APIKey string
10+
ReadOnly bool
11+
Toolsets []string
12+
LogFilePath string
13+
}

cmd/harness-mcp-server/main.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"log"
8+
"log/slog"
9+
"os"
10+
"os/signal"
11+
"syscall"
12+
13+
"github.com/harness/harness-mcp/client"
14+
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
15+
"github.com/harness/harness-mcp/pkg/harness"
16+
"github.com/mark3labs/mcp-go/mcp"
17+
"github.com/mark3labs/mcp-go/server"
18+
"github.com/spf13/cobra"
19+
"github.com/spf13/viper"
20+
)
21+
22+
var version = "0.1.0"
23+
var commit = "dev"
24+
var date = "unknown"
25+
26+
var (
27+
rootCmd = &cobra.Command{
28+
Use: "harness-mcp-server",
29+
Short: "Harness MCP Server",
30+
Long: `A Harness MCP server that handles various tools and resources.`,
31+
Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version, commit, date),
32+
}
33+
34+
stdioCmd = &cobra.Command{
35+
Use: "stdio",
36+
Short: "Start stdio server",
37+
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
38+
RunE: func(_ *cobra.Command, _ []string) error {
39+
token := viper.GetString("api_key")
40+
if token == "" {
41+
return fmt.Errorf("API key not provided")
42+
}
43+
44+
var toolsets []string
45+
err := viper.UnmarshalKey("toolsets", &toolsets)
46+
if err != nil {
47+
return fmt.Errorf("Failed to unmarshal toolsets: %w", err)
48+
}
49+
50+
cfg := config.Config{
51+
Version: version,
52+
BaseURL: viper.GetString("base_url"),
53+
AccountID: viper.GetString("account_id"),
54+
OrgID: viper.GetString("org_id"),
55+
ProjectID: viper.GetString("project_id"),
56+
APIKey: viper.GetString("api_key"),
57+
ReadOnly: viper.GetBool("read_only"),
58+
Toolsets: toolsets,
59+
LogFilePath: viper.GetString("log_file"),
60+
}
61+
62+
if err := runStdioServer(cfg); err != nil {
63+
return fmt.Errorf("failed to run stdio server: %w", err)
64+
}
65+
return nil
66+
},
67+
}
68+
)
69+
70+
func init() {
71+
cobra.OnInitialize(initConfig)
72+
73+
rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n")
74+
75+
// Add global flags
76+
rootCmd.PersistentFlags().StringSlice("toolsets", harness.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all")
77+
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
78+
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
79+
rootCmd.PersistentFlags().Bool("enable-command-logging", true, "When enabled, the server will log all command requests and responses to the log file")
80+
rootCmd.PersistentFlags().String("base-url", "https://app.harness.io", "Base URL for Harness")
81+
rootCmd.PersistentFlags().String("api-key", "", "API key for authentication")
82+
rootCmd.PersistentFlags().String("account-id", "", "Account ID to use")
83+
rootCmd.PersistentFlags().String("org-id", "", "(Optional) org ID to use")
84+
rootCmd.PersistentFlags().String("project-id", "", "(Optional) project ID to use")
85+
86+
// Bind flags to viper
87+
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
88+
_ = viper.BindPFlag("read_only", rootCmd.PersistentFlags().Lookup("read-only"))
89+
_ = viper.BindPFlag("log_file", rootCmd.PersistentFlags().Lookup("log-file"))
90+
_ = viper.BindPFlag("enable_command_logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
91+
_ = viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url"))
92+
_ = viper.BindPFlag("api_key", rootCmd.PersistentFlags().Lookup("api-key"))
93+
_ = viper.BindPFlag("account_id", rootCmd.PersistentFlags().Lookup("account-id"))
94+
_ = viper.BindPFlag("org_id", rootCmd.PersistentFlags().Lookup("org-id"))
95+
_ = viper.BindPFlag("project_id", rootCmd.PersistentFlags().Lookup("project-id"))
96+
97+
// Add subcommands
98+
rootCmd.AddCommand(stdioCmd)
99+
}
100+
101+
func initConfig() {
102+
// Initialize Viper configuration
103+
viper.SetEnvPrefix("harness")
104+
viper.AutomaticEnv()
105+
}
106+
107+
func initLogger(outPath string) error {
108+
if outPath == "" {
109+
return nil
110+
}
111+
112+
file, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
113+
if err != nil {
114+
return fmt.Errorf("failed to open log file: %w", err)
115+
}
116+
117+
logger := slog.New(slog.NewTextHandler(file, nil))
118+
slog.SetDefault(logger)
119+
return nil
120+
}
121+
122+
type runConfig struct {
123+
readOnly bool
124+
logger *log.Logger
125+
logCommands bool
126+
enabledToolsets []string
127+
}
128+
129+
func runStdioServer(config config.Config) error {
130+
// Create app context
131+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
132+
defer stop()
133+
134+
err := initLogger(config.LogFilePath)
135+
if err != nil {
136+
return fmt.Errorf("failed to initialize logger: %w", err)
137+
}
138+
139+
slog.Info("Starting server", "url", config.BaseURL)
140+
141+
// Define beforeInit function to add client info to user agent
142+
beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) {
143+
slog.Info("Client connected", "name", message.Params.ClientInfo.Name, "version", message.Params.ClientInfo.Version)
144+
}
145+
146+
// Setup server hooks
147+
hooks := &server.Hooks{
148+
OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit},
149+
}
150+
151+
// Create server
152+
// WithRecovery makes sure panics are logged and don't crash the server
153+
harnessServer := harness.NewServer(version, server.WithHooks(hooks), server.WithRecovery())
154+
155+
client, err := client.NewWithToken(config.BaseURL, config.APIKey)
156+
if err != nil {
157+
slog.Error("Failed to create client", "error", err)
158+
return fmt.Errorf("failed to create client: %w", err)
159+
}
160+
161+
// Initialize toolsets
162+
toolsets, err := harness.InitToolsets(client, &config)
163+
if err != nil {
164+
slog.Error("Failed to initialize toolsets", "error", err)
165+
}
166+
167+
// Register the tools with the server
168+
toolsets.RegisterTools(harnessServer)
169+
170+
// Create stdio server
171+
stdioServer := server.NewStdioServer(harnessServer)
172+
173+
// Set error logger
174+
stdioServer.SetErrorLogger(slog.NewLogLogger(slog.Default().Handler(), slog.LevelError))
175+
176+
// Start listening for messages
177+
errC := make(chan error, 1)
178+
go func() {
179+
in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
180+
181+
errC <- stdioServer.Listen(ctx, in, out)
182+
}()
183+
184+
// Output startup message
185+
slog.Info("Harness MCP Server running on stdio", "version", version)
186+
187+
// Wait for shutdown signal
188+
select {
189+
case <-ctx.Done():
190+
slog.Info("shutting down server...")
191+
case err := <-errC:
192+
if err != nil {
193+
slog.Error("error running server", "error", err)
194+
return fmt.Errorf("error running server: %w", err)
195+
}
196+
}
197+
198+
return nil
199+
}
200+
201+
func main() {
202+
if err := rootCmd.Execute(); err != nil {
203+
fmt.Println(err)
204+
os.Exit(1)
205+
}
206+
}

pkg/harness/pipelines.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"log/slog"
87

98
"github.com/harness/harness-mcp/client"
109
"github.com/harness/harness-mcp/client/dto"
@@ -67,15 +66,11 @@ func ListPipelinesTool(config *config.Config, client *client.Client) (tool mcp.T
6766
return mcp.NewToolResultError(err.Error()), nil
6867
}
6968

70-
slog.Info("request", request)
71-
7269
page, size, err := fetchPagination(request)
7370
if err != nil {
7471
return mcp.NewToolResultError(err.Error()), nil
7572
}
7673

77-
slog.Info("page", page, "size", size)
78-
7974
searchTerm, err := OptionalParam[string](request, "search_term")
8075
if err != nil {
8176
return mcp.NewToolResultError(err.Error()), nil

0 commit comments

Comments
 (0)