From 638fbff1b8067825cfa6cfa8bd368762981223d5 Mon Sep 17 00:00:00 2001 From: Yeshwanth Gunasekaran Date: Mon, 17 Nov 2025 14:24:02 +0000 Subject: [PATCH 1/5] Improvements to Rag and design prompt, remove knowledge queries --- devops-mcp-server/prompts/cicd.go | 9 +- ...esign_prompt.txt => cicd_design_prompt.md} | 19 +--- devops-mcp-server/prompts/deploy.go | 7 +- .../{deploy_prompt.txt => deploy_prompt.md} | 0 .../rag}/devops-rag.db | Bin devops-mcp-server/rag/rag.go | 88 +++++++----------- devops-mcp-server/rag/rag_test.go | 27 ------ devops-mcp-server/rag/testing/rag_test.go | 49 ---------- devops-mcp-server/version.txt | 2 +- local-rag/main.go | 7 +- 10 files changed, 49 insertions(+), 159 deletions(-) rename devops-mcp-server/prompts/{cicd_design_prompt.txt => cicd_design_prompt.md} (77%) rename devops-mcp-server/prompts/{deploy_prompt.txt => deploy_prompt.md} (100%) rename {local-rag => devops-mcp-server/rag}/devops-rag.db (100%) delete mode 100644 devops-mcp-server/rag/rag_test.go delete mode 100644 devops-mcp-server/rag/testing/rag_test.go diff --git a/devops-mcp-server/prompts/cicd.go b/devops-mcp-server/prompts/cicd.go index 414c210..def2cb3 100644 --- a/devops-mcp-server/prompts/cicd.go +++ b/devops-mcp-server/prompts/cicd.go @@ -32,7 +32,7 @@ func DesignPrompt(ctx context.Context, server *mcp.Server) { Description: "Helps design and implement GCP CI/CD pipelines.", Messages: []*mcp.PromptMessage{ { - Role: "user", + Role: "user", Content: &mcp.TextContent{ Text: fmt.Sprintf(promptCICDText, req.Params.Arguments["query"]), }, @@ -43,16 +43,15 @@ func DesignPrompt(ctx context.Context, server *mcp.Server) { // Create a server with a single prompt. prompt := &mcp.Prompt{ - Name: "devops:design", + Name: "devops:design", Title: "Design and implement a Google Cloud based CI/CD pipeline.", Arguments: []*mcp.PromptArgument{ { Name: "query", - Description: "CICD pipeline description, as explained by te user", + Description: "CICD pipeline description", Required: true, }, }, } - server.AddPrompt(prompt,promptHandler) + server.AddPrompt(prompt, promptHandler) } - diff --git a/devops-mcp-server/prompts/cicd_design_prompt.txt b/devops-mcp-server/prompts/cicd_design_prompt.md similarity index 77% rename from devops-mcp-server/prompts/cicd_design_prompt.txt rename to devops-mcp-server/prompts/cicd_design_prompt.md index d0e5993..c644119 100644 --- a/devops-mcp-server/prompts/cicd_design_prompt.txt +++ b/devops-mcp-server/prompts/cicd_design_prompt.md @@ -4,10 +4,9 @@ You are a comprehensive Google Cloud DevOps Assistant. Your primary function is First, analyze the user's request to determine the primary intent. -* If the intent is a high-level goal like **"build a pipeline," "design an architecture,"** or **"migrate my Jenkins pipeline,"** you must follow the two-stage **Workflow A: Design & Implement**. -* If the intent is a direct, concrete command like **"create an artifact registry repo," "deploy to prod,"** or **"run the main-branch trigger,"** you must follow **Workflow B: Direct Action**. +* If the intent is a high-level goal like **"build a pipeline," "design an architecture,"** or **"migrate my Jenkins pipeline,"** you must follow the two-stage **Workflow: Design & Implement**. -## Workflow A: Design & Implement +## Workflow: Design & Implement This workflow is for high-level, architectural tasks. It consists of a design phase followed by an implementation phase. @@ -29,20 +28,10 @@ Once the user has approved the YAML plan, your sole purpose is to execute it by 1. **Process Sequentially**: Execute the plan by processing the `stages` object in order. 2. **Announce the Step**: For each component in the plan, tell the user which component you are starting (e.g., "Starting step: 'Build and Test'"). -3. **Consult Knowledge Base**: Use the `query_knowledge` tool to find out how to implement the component based on its `type` and `name`. -4. **Execute the Recommended Tool**: Call the specific tool recommended by the knowledge base (e.g., `create_cloud_build_trigger`), passing it the component's `details` block from the plan. -5. **Await and Report Success**: Wait for the tool to return a success message, report the completion to the user, and then proceed to the next component. +3. **Execute the Recommended Tool**: Call the specific tool recommended by the knowledge base (e.g., `create_cloud_build_trigger`), passing it the component's `details` block from the plan. +4. **Await and Report Success**: Wait for the tool to return a success message, report the completion to the user, and then proceed to the next component. -## Workflow B: Direct Action - -This workflow is for executing single, direct commands. - -1. **Identify the Intent**: Determine the single action the user wants to perform (e.g., `create_artifact_registry_repo`). -2. **Gather Parameters**: Analyze the request to find all necessary parameters (e.g., `repo_name: "my-app-images"`). -3. **Clarify if Needed**: If any mandatory parameters are missing, you MUST ask the user for them before proceeding. Do not guess or make assumptions. -4. **Execute**: Call the single, correct tool to perform the action. - ## Universal Protocols & Constraints diff --git a/devops-mcp-server/prompts/deploy.go b/devops-mcp-server/prompts/deploy.go index 9142100..c82c4b7 100644 --- a/devops-mcp-server/prompts/deploy.go +++ b/devops-mcp-server/prompts/deploy.go @@ -32,7 +32,7 @@ func DeployPrompt(ctx context.Context, server *mcp.Server) { Description: "Helps deploy applications to GCP.", Messages: []*mcp.PromptMessage{ { - Role: "user", + Role: "user", Content: &mcp.TextContent{ Text: fmt.Sprintf(promptDeployText, req.Params.Arguments["query"]), }, @@ -43,7 +43,7 @@ func DeployPrompt(ctx context.Context, server *mcp.Server) { // Create a server with a single prompt. prompt := &mcp.Prompt{ - Name: "devops:deploy", + Name: "devops:deploy", Title: "Deploy an application to GCP.", Arguments: []*mcp.PromptArgument{ { @@ -53,6 +53,5 @@ func DeployPrompt(ctx context.Context, server *mcp.Server) { }, }, } - server.AddPrompt(prompt,promptHandler) + server.AddPrompt(prompt, promptHandler) } - diff --git a/devops-mcp-server/prompts/deploy_prompt.txt b/devops-mcp-server/prompts/deploy_prompt.md similarity index 100% rename from devops-mcp-server/prompts/deploy_prompt.txt rename to devops-mcp-server/prompts/deploy_prompt.md diff --git a/local-rag/devops-rag.db b/devops-mcp-server/rag/devops-rag.db similarity index 100% rename from local-rag/devops-rag.db rename to devops-mcp-server/rag/devops-rag.db diff --git a/devops-mcp-server/rag/rag.go b/devops-mcp-server/rag/rag.go index 24f276d..ec9e238 100644 --- a/devops-mcp-server/rag/rag.go +++ b/devops-mcp-server/rag/rag.go @@ -15,11 +15,13 @@ package rag import ( + "bytes" "context" "devops-mcp-server/auth" + _ "embed" + "encoding/json" "fmt" "log" - "os" "sync" chromem "github.com/philippgille/chromem-go" @@ -27,40 +29,33 @@ import ( var initOnce sync.Once -// ListResult defines a generic struct to wrap a list of items. -type ListResult[T any] struct { - Items []T `json:"items"` -} +//go:embed devops-rag.db +var embeddedDB []byte type RagData struct { - DB *chromem.DB - Pattern *chromem.Collection - Knowledge *chromem.Collection + DB *chromem.DB + Pattern *chromem.Collection +} + +// Only expose what the LLM needs to read. +type Result struct { + Content string `json:"content"` + Metadata map[string]string `json:"metadata,omitempty"` // Source info + Similarity float32 `json:"relevance_score"` // Helps LLM weigh confidence } var RagDB RagData // loadRAG performs the one-time initialization. func loadRAG(ctx context.Context) error { - dbFile := os.Getenv("RAG_DB_PATH") RagDB = RagData{DB: chromem.NewDB()} - if len(dbFile) < 1 { - // RETURN AN ERROR - return fmt.Errorf("Env variable RAG_DB_PATH is not set for RAG data file") - } - - //check if file exists - if _, err := os.Stat(dbFile); os.IsNotExist(err) { - // RETURN AN ERROR - return fmt.Errorf("RAG_DB_PATH file does not exist, skipping import: %v", dbFile) - } - - err := RagDB.DB.ImportFromFile(dbFile, "") + reader := bytes.NewReader(embeddedDB) + err := RagDB.DB.ImportFromReader(reader, "") if err != nil { - log.Printf("Unable to import from the RAG DB file:%s - %v", dbFile, err) - // This seems non-fatal based on your log, so we continue. + log.Printf("Unable to import from the RAG DB file: %v", err) + return err } - log.Printf("IMPORTED from the RAG DB file:%s - %v", dbFile, len(RagDB.DB.ListCollections())) + log.Printf("IMPORTED from the RAG DB collections: %v", len(RagDB.DB.ListCollections())) creds, err := auth.GetAuthToken(ctx) if err != nil { @@ -81,13 +76,6 @@ func loadRAG(ctx context.Context) error { } log.Printf("LOADED collection pattern: %v", RagDB.Pattern.Count()) - RagDB.Knowledge, err = RagDB.DB.GetOrCreateCollection("knowledge", nil, vertexEmbeddingFunc) - if err != nil { - // RETURN AN ERROR - return fmt.Errorf("Unable to get collection knowledge: %w", err) - } - log.Printf("LOADED collection knowledge: %v", RagDB.Knowledge.Count()) - log.Print("RAG Init Completed!") return nil // Success } @@ -104,34 +92,24 @@ func GetRAG(ctx context.Context) (RagData, error) { return RagDB, initErr } -func QueryKnowledge(ctx context.Context, query string) (*ListResult[chromem.Result], error) { - ragDB, err := GetRAG(ctx) - if err != nil { - // Initialization failed, return the error. - return nil, fmt.Errorf("RAG system not initialized: %w", err) - } - if ragDB.Knowledge == nil { - return nil, fmt.Errorf("RagDB does not contain Knowledge collection, among %d collections", len(RagDB.DB.ListCollections())) - } - result, err := RagDB.Knowledge.Query(ctx, query, 3, nil, nil) +func (r *RagData) QueryPattern(ctx context.Context, query string) (string, error) { + results, err := r.Pattern.Query(ctx, query, 2, nil, nil) if err != nil { log.Fatalf("Unable to Query collection pattern: %v", err) } - return &ListResult[chromem.Result]{Items: result}, nil -} - -func QueryPattern(ctx context.Context, query string) (*ListResult[chromem.Result], error) { - ragDB, err := GetRAG(ctx) - if err != nil { - // Initialization failed, return the error. - return nil, fmt.Errorf("RAG system not initialized: %w", err) - } - if ragDB.Pattern == nil { - return nil, fmt.Errorf("RagDB does not contain Pattern collection, among %d collections", len(RagDB.DB.ListCollections())) + cleanResults := make([]Result, len(results)) + for i, r := range results { + cleanResults[i] = Result{ + Content: r.Content, + Metadata: r.Metadata, + Similarity: r.Similarity, + } } - result, err := RagDB.Pattern.Query(ctx, query, 2, nil, nil) + + // Marshal to JSON + jsonData, err := json.Marshal(cleanResults) if err != nil { - log.Fatalf("Unable to Query collection pattern: %v", err) + return "", fmt.Errorf("failed to marshal results: %w", err) } - return &ListResult[chromem.Result]{Items: result}, nil + return string(jsonData), nil } diff --git a/devops-mcp-server/rag/rag_test.go b/devops-mcp-server/rag/rag_test.go deleted file mode 100644 index 490b5e6..0000000 --- a/devops-mcp-server/rag/rag_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package rag - -import ( - "os" - "testing" -) - -func TestRAGInit(t *testing.T) { - t.Skip("Skipping test due to missing RAG_DB_PATH env var") - if RagDB.DB == nil { - t.Fatalf("init() failed to load: %v", os.Getenv("RAG_DB_PATH")) - } -} diff --git a/devops-mcp-server/rag/testing/rag_test.go b/devops-mcp-server/rag/testing/rag_test.go deleted file mode 100644 index fff2058..0000000 --- a/devops-mcp-server/rag/testing/rag_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Cloud Build end-to-end tests -package testing - -import ( - "context" - "devops-mcp-server/auth" - "devops-mcp-server/rag" - "log" - "testing" -) - -func TestRAGQuery(t *testing.T) { - t.Skip("Skipping test due to missing RAG_DB_PATH env var") - ctx := context.Background() - creds, err := auth.GetAuthToken(ctx) - if creds.Token == "" || creds.ProjectId == "" || err != nil { - t.Skipf("Skipping test! Google Cloud account not found %v : %v", creds, err) - } - - patternResult, err := rag.QueryKnowledge(ctx, "Simple pipeline that deploys to Cloud Run") - if err != nil { - log.Fatalf("Unable to Query collection pattern: %v", err) - } - if (patternResult == nil) || (len(patternResult.Items) < 2) || (patternResult.Items[0].Content == "") { - log.Fatalf("Failed to find knowledge: %v", patternResult) - } - - knowledgeResult, err := rag.QueryKnowledge(ctx, "Package a Python application") - if err != nil { - log.Fatalf("Unable to Query collection knowledge: %v", err) - } - if (knowledgeResult == nil) || (len(knowledgeResult.Items) < 3) || (knowledgeResult.Items[0].Content == "") { - log.Fatalf("Failed to find knowledge: %v", knowledgeResult) - } -} diff --git a/devops-mcp-server/version.txt b/devops-mcp-server/version.txt index 6c6aa7c..6da28dd 100644 --- a/devops-mcp-server/version.txt +++ b/devops-mcp-server/version.txt @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.1.1 \ No newline at end of file diff --git a/local-rag/main.go b/local-rag/main.go index dd51173..51a2b73 100644 --- a/local-rag/main.go +++ b/local-rag/main.go @@ -124,12 +124,13 @@ var KNOWLEDGE_RAG_SOURCES = []Source{ func processSource(source Source, tmpDir string) { sourceType := source.Type - if sourceType == "webpage" { + switch sourceType { + case "webpage": err := downloadWebsites(&source, tmpDir) if err != nil { log.Printf("Error downloading websites from source %s: %v", source.Name, err) } - } else if sourceType == "git_repo" { + case "git_repo": for _, url := range source.URLs { repoDir := filepath.Join(tmpDir, source.Dir) err := fetchRepository(url, repoDir) @@ -137,7 +138,7 @@ func processSource(source Source, tmpDir string) { log.Printf("Error downloading git repo %s: %v", url, err) } } - } else { + default: log.Printf("RAG Source type [%s] is not supported", sourceType) } } From ef7cc3c80b4bfa7af76c00a5e4a9a1378752b872 Mon Sep 17 00:00:00 2001 From: Yeshwanth Gunasekaran Date: Mon, 17 Nov 2025 14:29:07 +0000 Subject: [PATCH 2/5] Result refactor --- devops-mcp-server/rag/rag.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/devops-mcp-server/rag/rag.go b/devops-mcp-server/rag/rag.go index ec9e238..f602a57 100644 --- a/devops-mcp-server/rag/rag.go +++ b/devops-mcp-server/rag/rag.go @@ -33,8 +33,9 @@ var initOnce sync.Once var embeddedDB []byte type RagData struct { - DB *chromem.DB - Pattern *chromem.Collection + DB *chromem.DB + Pattern *chromem.Collection + Knowledge *chromem.Collection } // Only expose what the LLM needs to read. @@ -68,10 +69,13 @@ func loadRAG(ctx context.Context) error { creds.Token, creds.ProjectId, chromem.EmbeddingModelVertexEnglishV4) - + RagDB.Knowledge, err = RagDB.DB.GetOrCreateCollection("knowledge", nil, vertexEmbeddingFunc) + if err != nil { + return fmt.Errorf("Unable to get collection knowledge: %w", err) + } + log.Printf("LOADED collection knowledge: %v", RagDB.Pattern.Count()) RagDB.Pattern, err = RagDB.DB.GetOrCreateCollection("pattern", nil, vertexEmbeddingFunc) if err != nil { - // RETURN AN ERROR return fmt.Errorf("Unable to get collection pattern: %w", err) } log.Printf("LOADED collection pattern: %v", RagDB.Pattern.Count()) @@ -113,3 +117,25 @@ func (r *RagData) QueryPattern(ctx context.Context, query string) (string, error } return string(jsonData), nil } + +func (r *RagData) Queryknowledge(ctx context.Context, query string) (string, error) { + results, err := r.Knowledge.Query(ctx, query, 2, nil, nil) + if err != nil { + log.Fatalf("Unable to Query collection knowledge: %v", err) + } + cleanResults := make([]Result, len(results)) + for i, r := range results { + cleanResults[i] = Result{ + Content: r.Content, + Metadata: r.Metadata, + Similarity: r.Similarity, + } + } + + // Marshal to JSON + jsonData, err := json.Marshal(cleanResults) + if err != nil { + return "", fmt.Errorf("failed to marshal results: %w", err) + } + return string(jsonData), nil +} From 513993fdd0179b5b87bccc0c76476a47630b7086 Mon Sep 17 00:00:00 2001 From: Yeshwanth Gunasekaran Date: Mon, 17 Nov 2025 15:19:59 +0000 Subject: [PATCH 3/5] Fix imports --- devops-mcp-server/prompts/cicd.go | 2 +- devops-mcp-server/prompts/deploy.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/devops-mcp-server/prompts/cicd.go b/devops-mcp-server/prompts/cicd.go index def2cb3..0df0d88 100644 --- a/devops-mcp-server/prompts/cicd.go +++ b/devops-mcp-server/prompts/cicd.go @@ -22,7 +22,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -//go:embed cicd_design_prompt.txt +//go:embed cicd_design_prompt.md var promptCICDText string // Helps design and implement GCP CI/CD pipelines. diff --git a/devops-mcp-server/prompts/deploy.go b/devops-mcp-server/prompts/deploy.go index c82c4b7..862f70d 100644 --- a/devops-mcp-server/prompts/deploy.go +++ b/devops-mcp-server/prompts/deploy.go @@ -22,7 +22,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -//go:embed deploy_prompt.txt +//go:embed deploy_prompt.md var promptDeployText string // Helps deploy applications to GCP. From 3595fb67cd6487515dac475d01169ecb6654628c Mon Sep 17 00:00:00 2001 From: Yeshwanth Gunasekaran Date: Mon, 17 Nov 2025 15:24:16 +0000 Subject: [PATCH 4/5] Add logging to seperate file --- devops-mcp-server/main.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/devops-mcp-server/main.go b/devops-mcp-server/main.go index a4cc1e5..91e04b7 100644 --- a/devops-mcp-server/main.go +++ b/devops-mcp-server/main.go @@ -72,4 +72,16 @@ func main() { log.Printf("Server failed: %v", err) } } + + setupLogging() +} + +func setupLogging() { + f, err := os.OpenFile("/tmp/mcp-server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + // Fallback if file fails + log.SetOutput(os.Stderr) + return + } + log.SetOutput(f) } From a467ac8de23a14d35c0dddd62e533d4cbeb37c03 Mon Sep 17 00:00:00 2001 From: Yeshwanth Gunasekaran Date: Mon, 17 Nov 2025 15:26:46 +0000 Subject: [PATCH 5/5] Fix log name --- devops-mcp-server/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devops-mcp-server/main.go b/devops-mcp-server/main.go index 91e04b7..8052bb9 100644 --- a/devops-mcp-server/main.go +++ b/devops-mcp-server/main.go @@ -30,6 +30,7 @@ import ( var ( httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout. e.g. localhost:8080") pprofAddr = flag.String("pprof", "", "if set, host the pprof debugging server at this address") + logFile = "/tmp/devops-mcp-server.log" ) func main() { @@ -77,7 +78,7 @@ func main() { } func setupLogging() { - f, err := os.OpenFile("/tmp/mcp-server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + f, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { // Fallback if file fails log.SetOutput(os.Stderr)