Skip to content

Commit ed0b11c

Browse files
David-KreinerHarness
authored andcommitted
feat: [ML-1118]: Init chatbot service for internal MCP usage (#11)
* feat: [ML-1118]: Init chatbot service for internal MCP usage
1 parent 692431e commit ed0b11c

File tree

6 files changed

+161
-0
lines changed

6 files changed

+161
-0
lines changed

client/chatbot.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/harness/harness-mcp/client/dto"
8+
)
9+
10+
// There are no external paths for the chatbot-service
11+
const (
12+
chatPath = "chat"
13+
)
14+
15+
type ChatbotService struct {
16+
Client *Client
17+
}
18+
19+
func (c *ChatbotService) SendChatMessage(ctx context.Context, scope dto.Scope, request *dto.ChatRequest) (string, error) {
20+
path := chatPath
21+
params := make(map[string]string)
22+
23+
// Only add non-empty scope parameters
24+
if scope.AccountID != "" {
25+
params["accountIdentifier"] = scope.AccountID
26+
}
27+
28+
if scope.OrgID != "" {
29+
params["orgIdentifier"] = scope.OrgID
30+
}
31+
32+
if scope.ProjectID != "" {
33+
params["projectIdentifier"] = scope.ProjectID
34+
}
35+
36+
var response string
37+
err := c.Client.Post(ctx, path, params, request, &response)
38+
if err != nil {
39+
return "", fmt.Errorf("failed to send message to chatbot: %w", err)
40+
}
41+
42+
return response, nil
43+
}

client/dto/chatbot.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dto
2+
3+
type ChatHistoryItem struct {
4+
Question string `json:"question"`
5+
Answer string `json:"answer"`
6+
}
7+
8+
type ChatRequest struct {
9+
Question string `json:"question"`
10+
ChatHistory []ChatHistoryItem `json:"chat_history,omitempty"`
11+
}

cmd/harness-mcp-server/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ type Config struct {
2323
PipelineSvcSecret string
2424
NgManagerBaseURL string
2525
NgManagerSecret string
26+
ChatbotBaseURL string
27+
ChatbotSecret string
2628
McpSvcSecret string
2729
}

cmd/harness-mcp-server/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ var (
137137
PipelineSvcSecret: viper.GetString("pipeline_svc_secret"),
138138
NgManagerBaseURL: viper.GetString("ng_manager_base_url"),
139139
NgManagerSecret: viper.GetString("ng_manager_secret"),
140+
ChatbotBaseURL: viper.GetString("chatbot_base_url"),
141+
ChatbotSecret: viper.GetString("chatbot_secret"),
140142
McpSvcSecret: viper.GetString("mcp_svc_secret"),
141143
}
142144

@@ -174,6 +176,8 @@ func init() {
174176
internalCmd.Flags().String("pipeline-svc-secret", "", "Secret for pipeline service")
175177
internalCmd.Flags().String("ng-manager-base-url", "", "Base URL for NG manager")
176178
internalCmd.Flags().String("ng-manager-secret", "", "Secret for NG manager")
179+
internalCmd.Flags().String("chatbot-base-url", "", "Base URL for chatbot service")
180+
internalCmd.Flags().String("chatbot-secret", "", "Secret for chatbot service")
177181
internalCmd.Flags().String("mcp-svc-secret", "", "Secret for MCP service")
178182

179183
// Bind global flags to viper
@@ -194,6 +198,8 @@ func init() {
194198
_ = viper.BindPFlag("pipeline_svc_secret", internalCmd.Flags().Lookup("pipeline-svc-secret"))
195199
_ = viper.BindPFlag("ng_manager_base_url", internalCmd.Flags().Lookup("ng-manager-base-url"))
196200
_ = viper.BindPFlag("ng_manager_secret", internalCmd.Flags().Lookup("ng-manager-secret"))
201+
_ = viper.BindPFlag("chatbot_base_url", internalCmd.Flags().Lookup("chatbot-base-url"))
202+
_ = viper.BindPFlag("chatbot_secret", internalCmd.Flags().Lookup("chatbot-secret"))
197203
_ = viper.BindPFlag("mcp_svc_secret", internalCmd.Flags().Lookup("mcp-svc-secret"))
198204

199205
// Add subcommands

pkg/harness/chatbot.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package harness
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/harness/harness-mcp/client"
8+
"github.com/harness/harness-mcp/client/dto"
9+
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
func AskChatbotTool(config *config.Config, client *client.ChatbotService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
15+
return mcp.NewTool("ask_chatbot",
16+
mcp.WithDescription("Ask a question about Harness products and documentation to the Harness Documentation Bot. The bot uses AI to retrieve and summarize relevant information from Harness documentation."),
17+
mcp.WithString("question",
18+
mcp.Required(),
19+
mcp.Description("The question to ask the chatbot"),
20+
),
21+
mcp.WithArray("chat_history",
22+
mcp.Description("Optional chat history for context"),
23+
),
24+
WithScope(config, false),
25+
),
26+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
27+
question, err := requiredParam[string](request, "question")
28+
if err != nil {
29+
return mcp.NewToolResultError(err.Error()), nil
30+
}
31+
32+
scope, err := fetchScope(config, request, false)
33+
if err != nil {
34+
return mcp.NewToolResultError(err.Error()), nil
35+
}
36+
37+
var chatHistory []dto.ChatHistoryItem
38+
chatHistoryRaw, err := OptionalParam[[]interface{}](request, "chat_history")
39+
if err == nil && len(chatHistoryRaw) > 0 {
40+
for _, historyItem := range chatHistoryRaw {
41+
if historyItemMap, ok := historyItem.(map[string]interface{}); ok {
42+
questionStr, _ := historyItemMap["question"].(string)
43+
answerStr, _ := historyItemMap["answer"].(string)
44+
chatHistory = append(chatHistory, dto.ChatHistoryItem{
45+
Question: questionStr,
46+
Answer: answerStr,
47+
})
48+
}
49+
}
50+
}
51+
52+
chatRequest := &dto.ChatRequest{
53+
Question: question,
54+
ChatHistory: chatHistory,
55+
}
56+
57+
response, err := client.SendChatMessage(ctx, scope, chatRequest)
58+
if err != nil {
59+
return nil, fmt.Errorf("failed to send message to chatbot: %w", err)
60+
}
61+
62+
return mcp.NewToolResultText(response), nil
63+
}
64+
}

pkg/harness/tools.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ func InitToolsets(config *config.Config) (*toolsets.ToolsetGroup, error) {
3333
return nil, err
3434
}
3535

36+
// Register chatbot
37+
if err := registerChatbot(config, tsg); err != nil {
38+
return nil, err
39+
}
40+
3641
// TODO: support internal mode for other endpoints as well eventually
3742
if err := registerPullRequests(config, tsg); err != nil {
3843
return nil, err
@@ -221,6 +226,36 @@ func registerRegistries(config *config.Config, tsg *toolsets.ToolsetGroup) error
221226
return nil
222227
}
223228

229+
// registerChatbot registers the chatbot toolset
230+
func registerChatbot(config *config.Config, tsg *toolsets.ToolsetGroup) error {
231+
// Skip registration for external mode (no external service exposed)
232+
if !config.Internal {
233+
return nil
234+
}
235+
236+
// Determine the base URL and secret for chatbot service
237+
baseURL := config.ChatbotBaseURL
238+
secret := config.ChatbotSecret
239+
240+
// Create base client for chatbot
241+
c, err := createClient(baseURL, config, secret)
242+
if err != nil {
243+
return err
244+
}
245+
246+
chatbotClient := &client.ChatbotService{Client: c}
247+
248+
// Create the chatbot toolset
249+
chatbot := toolsets.NewToolset("chatbot", "Harness Documentation Bot tools").
250+
AddReadTools(
251+
toolsets.NewServerTool(AskChatbotTool(config, chatbotClient)),
252+
)
253+
254+
// Add toolset to the group
255+
tsg.AddToolset(chatbot)
256+
return nil
257+
}
258+
224259
// registerLogs registers the logs toolset
225260
func registerLogs(config *config.Config, tsg *toolsets.ToolsetGroup) error {
226261
// Determine the base URL and secret for logs

0 commit comments

Comments
 (0)