Skip to content

Commit 6fd07c4

Browse files
yogesh-chauhanHarness
authored andcommitted
feat: [ML-1288]: implement module-specific prompt loading and registration system (#89)
* remove dead code * Merge remote-tracking branch 'origin' into yogesh/module_prompts * re-write * refactor: reorganize module prompt handling with new DefaultModulePrompts mixin * test: add prompt file loader tests and refactor file loading implementation * refactor: simplify prompt loading by removing unused files and methods * feat: add embedded filesystem support for prompt loading and update module prompts * add back ccm text * feat: [ML-1288]: implement module-specific prompt loading and registration system
1 parent 5411dc9 commit 6fd07c4

File tree

11 files changed

+319
-376
lines changed

11 files changed

+319
-376
lines changed

cmd/harness-mcp-server/main.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package main
33
import (
44
"context"
55
"fmt"
6-
"github.com/harness/harness-mcp/pkg/harness/prompts"
76
"io"
87
"log"
98
"log/slog"
@@ -15,6 +14,7 @@ import (
1514
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
1615
"github.com/harness/harness-mcp/pkg/harness"
1716
"github.com/harness/harness-mcp/pkg/harness/auth"
17+
"github.com/harness/harness-mcp/pkg/modules"
1818
"github.com/mark3labs/mcp-go/mcp"
1919
"github.com/mark3labs/mcp-go/server"
2020
"github.com/spf13/cobra"
@@ -374,8 +374,14 @@ func runStdioServer(ctx context.Context, config config.Config) error {
374374
// Register the tools with the server
375375
toolsets.RegisterTools(harnessServer)
376376

377-
// Set the guidelines prompts
378-
prompts.RegisterPrompts(harnessServer)
377+
// Create module registry
378+
moduleRegistry := modules.NewModuleRegistry(&config, toolsets)
379+
380+
// Register prompts from all enabled modules
381+
err = moduleRegistry.RegisterPrompts(harnessServer)
382+
if err != nil {
383+
return fmt.Errorf("failed to register module prompts: %w", err)
384+
}
379385

380386
// Create stdio server
381387
stdioServer := server.NewStdioServer(harnessServer)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/spf13/cobra v1.8.0
1717
github.com/spf13/viper v1.18.2
1818
golang.org/x/tools v0.19.0
19+
gopkg.in/yaml.v3 v3.0.1
1920
)
2021

2122
require (
@@ -54,5 +55,4 @@ require (
5455
google.golang.org/appengine v1.6.7 // indirect
5556
google.golang.org/protobuf v1.31.0 // indirect
5657
gopkg.in/ini.v1 v1.67.0 // indirect
57-
gopkg.in/yaml.v3 v3.0.1 // indirect
5858
)

pkg/harness/prompts/fetcher.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package prompts
2+
3+
import (
4+
"embed"
5+
)
6+
7+
//go:embed files/**/*.txt
8+
var PromptFiles embed.FS // Exported to be accessible from other packages
9+
10+
// GetModulePrompts retrieves prompts for a specific module from a given filesystem
11+
// GetModulePrompts retrieves prompts for a specific module from a given filesystem.
12+
// It loads common prompts from the "files/common" directory and mode-specific prompts
13+
// from the "files/internal" or "files/external" directory, depending on the value of `isInternal`.
14+
// It returns a slice of PromptFile containing the loaded prompts.
15+
func GetModulePrompts(fs embed.FS, module string, isInternal bool) ([]PromptFile, error) {
16+
// Use embedded filesystem instead of file system paths
17+
// Use the provided embedded filesystem
18+
fileLoader := NewEmbedFileLoader(fs)
19+
20+
var allPrompts []PromptFile
21+
22+
// Load common prompt for the module directly
23+
commonPath := "files/common/" + module + ".txt"
24+
commonPrompt, err := fileLoader.loadPromptFileFromEmbed(commonPath)
25+
if err == nil {
26+
allPrompts = append(allPrompts, commonPrompt)
27+
}
28+
29+
// Load mode-specific prompt for the module directly
30+
var modePath string
31+
if isInternal {
32+
modePath = "files/internal/" + module + ".txt"
33+
} else {
34+
modePath = "files/external/" + module + ".txt"
35+
}
36+
37+
modePrompt, err := fileLoader.loadPromptFileFromEmbed(modePath)
38+
if err == nil {
39+
allPrompts = append(allPrompts, modePrompt)
40+
}
41+
42+
return allPrompts, nil
43+
}

pkg/harness/prompts/file_loader.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package prompts
2+
3+
import (
4+
"embed"
5+
"fmt"
6+
"strings"
7+
8+
"gopkg.in/yaml.v3"
9+
)
10+
11+
// PromptMetadata represents the YAML frontmatter in prompt files
12+
type PromptMetadata struct {
13+
Name string `yaml:"name"`
14+
Description string `yaml:"description"`
15+
ResultDescription string `yaml:"result_description"`
16+
Module string `yaml:"module"`
17+
}
18+
19+
// PromptFile represents a loaded prompt file with metadata and content
20+
type PromptFile struct {
21+
Metadata PromptMetadata
22+
Content string
23+
FilePath string
24+
}
25+
26+
// FileLoader handles loading prompts from text files
27+
type FileLoader struct {
28+
embedFS embed.FS
29+
}
30+
31+
// NewEmbedFileLoader creates a new file loader with embedded filesystem
32+
func NewEmbedFileLoader(embedFS embed.FS) *FileLoader {
33+
return &FileLoader{
34+
embedFS: embedFS,
35+
}
36+
}
37+
38+
// loadPromptFileFromEmbed loads a single prompt file from embedded filesystem with YAML frontmatter
39+
func (fl *FileLoader) loadPromptFileFromEmbed(filePath string) (PromptFile, error) {
40+
data, err := fl.embedFS.ReadFile(filePath)
41+
if err != nil {
42+
return PromptFile{}, fmt.Errorf("failed to read embedded file: %w", err)
43+
}
44+
45+
content := string(data)
46+
lines := strings.Split(content, "\n")
47+
48+
var inFrontmatter bool
49+
var frontmatterLines []string
50+
var contentLines []string
51+
52+
for _, line := range lines {
53+
if line == "---" {
54+
if !inFrontmatter {
55+
inFrontmatter = true
56+
continue
57+
} else {
58+
inFrontmatter = false
59+
continue
60+
}
61+
}
62+
63+
if inFrontmatter {
64+
frontmatterLines = append(frontmatterLines, line)
65+
} else if !inFrontmatter && len(frontmatterLines) > 0 {
66+
contentLines = append(contentLines, line)
67+
}
68+
}
69+
70+
// Parse YAML frontmatter
71+
var metadata PromptMetadata
72+
if len(frontmatterLines) > 0 {
73+
yamlContent := strings.Join(frontmatterLines, "\n")
74+
if err := yaml.Unmarshal([]byte(yamlContent), &metadata); err != nil {
75+
return PromptFile{}, fmt.Errorf("failed to parse YAML frontmatter: %w", err)
76+
}
77+
}
78+
79+
// Join content lines
80+
fileContent := strings.Join(contentLines, "\n")
81+
fileContent = strings.TrimSpace(fileContent)
82+
83+
return PromptFile{
84+
Metadata: metadata,
85+
Content: fileContent,
86+
FilePath: filePath,
87+
}, nil
88+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
name: "ccm"
3+
description: "Ensure parameters are provided correctly and in the right format"
4+
result_description: "Input parameters validation"
5+
---
6+
7+
When calling get_ccm_overview, ensure you have: accountIdentifier, groupBy, startDate, and endDate.
8+
- If any are missing, ask the user for the specific value(s).
9+
- Always send startDate and endDate in the following format: 'MM/DD/YYYY' (e.g. '10/30/2025')
10+
- If no dates are supplied, default startDate to 60 days ago and endDate to now.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
name: "ccm"
3+
description: "Ensure parameters are provided correctly and in the right format"
4+
result_description: "Input parameters validation"
5+
---
6+
7+
When calling get_ccm_overview, ensure you have: accountIdentifier, groupBy, startDate, and endDate.
8+
- If any are missing, ask the user for the specific value(s).
9+
- Always send startDate and endDate in the following format: 'MM/DD/YYYY' (e.g. '10/30/2025')
10+
- If no dates are supplied, default startDate to 60 days ago and endDate to now.

pkg/harness/prompts/prompts.go

Lines changed: 0 additions & 24 deletions
This file was deleted.

pkg/harness/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ func NewServer(version string, opts ...server.ServerOption) *server.MCPServer {
1010
defaultOpts := []server.ServerOption{
1111
server.WithToolCapabilities(true),
1212
server.WithResourceCapabilities(true, true),
13+
server.WithPromptCapabilities(true),
1314
server.WithLogging(),
1415
}
1516
opts = append(defaultOpts, opts...)

pkg/modules/module.go

Lines changed: 2 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package modules
22

33
import (
4-
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
54
"github.com/harness/harness-mcp/pkg/toolsets"
65
)
76

@@ -28,77 +27,6 @@ type Module interface {
2827
IsDefault() bool
2928
}
3029

31-
// ModuleRegistry holds all available modules
32-
type ModuleRegistry struct {
33-
modules []Module
34-
config *config.Config
35-
tsg *toolsets.ToolsetGroup
36-
}
37-
38-
// NewModuleRegistry creates a new module registry with all available modules
39-
func NewModuleRegistry(config *config.Config, tsg *toolsets.ToolsetGroup) *ModuleRegistry {
40-
return &ModuleRegistry{
41-
modules: []Module{
42-
NewCoreModule(config, tsg),
43-
NewCIModule(config, tsg),
44-
NewCDModule(config, tsg),
45-
NewUnlicensedModule(config, tsg),
46-
NewCHAOSModule(config, tsg),
47-
NewSEIModule(config, tsg),
48-
NewSTOModule(config, tsg),
49-
NewSSCAModule(config, tsg),
50-
NewCODEModule(config, tsg),
51-
NewCCMModule(config, tsg),
52-
NewIDPModule(config, tsg),
53-
NewHARModule(config, tsg),
54-
NewDbOpsModule(config, tsg),
55-
},
56-
config: config,
57-
tsg: tsg,
58-
}
59-
}
60-
61-
// GetAllModules returns all available modules
62-
func (r *ModuleRegistry) GetAllModules() []Module {
63-
return r.modules
64-
}
65-
66-
// GetEnabledModules returns the list of enabled modules based on configuration
67-
func (r *ModuleRegistry) GetEnabledModules() []Module {
68-
// Create a map for quick lookup of enabled module IDs
69-
enabledModuleIDs := make(map[string]bool)
70-
var defaultModules []Module
71-
for _, module := range r.modules {
72-
if module.IsDefault() {
73-
defaultModules = append(defaultModules, module)
74-
enabledModuleIDs[module.ID()] = true
75-
}
76-
}
77-
78-
// If no specific modules are enabled, return all default modules
79-
if len(r.config.EnableModules) == 0 {
80-
return defaultModules
81-
}
82-
83-
for _, id := range r.config.EnableModules {
84-
enabledModuleIDs[id] = true
85-
}
86-
87-
// Check if "all" is enabled
88-
if enabledModuleIDs["all"] {
89-
return r.modules
90-
}
91-
92-
// Return only enabled modules
93-
var enabledModules []Module
94-
for _, module := range r.modules {
95-
if enabledModuleIDs[module.ID()] {
96-
enabledModules = append(enabledModules, module)
97-
}
98-
}
99-
return enabledModules
100-
}
101-
10230
// ModuleEnableToolsets is a helper function that safely enables toolsets
10331
// by only enabling toolsets that actually exist in the toolset group
10432
func ModuleEnableToolsets(m Module, tsg *toolsets.ToolsetGroup) error {
@@ -116,5 +44,7 @@ func ModuleEnableToolsets(m Module, tsg *toolsets.ToolsetGroup) error {
11644
if len(existingToolsets) == 0 {
11745
return nil
11846
}
47+
48+
// Enable the toolsets
11949
return tsg.EnableToolsets(existingToolsets)
12050
}

0 commit comments

Comments
 (0)