feat(mcp): implement dynamic resources, prompt templates, and auto-analysis (Phase 1)

This commit is contained in:
Daoud AbdelMonem Faleh 2026-03-03 00:04:35 +01:00
commit 03a6b9f2d7
5 changed files with 339 additions and 7 deletions

74
MCP_REVIEW.md Normal file
View file

@ -0,0 +1,74 @@
# Dive MCP Server: Architecture and Code Review
## 1. Overview
This document provides a deep architectural and code review of the Model Context Protocol (MCP) server implementation in the `dive` project.
## 2. Git Changes and Design Mapping
| Phase | Design Task | Implementation Path |
| :--- | :--- | :--- |
| **Phase 1** | Scaffolding & Dependencies | `go.mod`, `cmd/dive/cli/cli.go`, `cmd/dive/cli/internal/options/mcp.go`, `cmd/dive/cli/internal/options/application.go` |
| **Phase 2** | Server & Transport | `cmd/dive/cli/internal/mcp/server.go`, `cmd/dive/cli/internal/command/mcp.go` |
| **Phase 3** | Tool Handlers & Data | `cmd/dive/cli/internal/mcp/handlers.go` |
| **Phase 4** | Validation | `cmd/dive/cli/internal/mcp/handlers_test.go` |
| **Phase 5** | Documentation | `README.md` |
## 3. Architectural Review
### 3.1 Alignment with Dive Design
The implementation strictly follows the "Headless Consumer" pattern. By treating the MCP server as a standalone command that reuses existing adapters (`Analyzer`, `Resolver`), the core domain remains untouched, ensuring zero impact on the TUI or CI logic.
### 3.2 Decoupling and Event Bus
* **Success:** The MCP logic is isolated in `cmd/dive/cli/internal/mcp`.
* **Observation:** The implementation currently bypasses the `partybus` for the final tool responses to ensure synchronous JSON-RPC compliance. However, it still triggers analysis logs via the standard `internal/log` which is consistent with the CLI's behavior.
### 3.3 Concurrency and State
* The use of `sync.RWMutex` in `toolHandlers` correctly handles concurrent tool calls from MCP clients.
* The session-level cache in `analyses map[string]*image.Analysis` prevents expensive re-analysis of the same image within a single session.
## 4. Code Review
### 4.1 Coding Standards
* **Go 1.24:** Implementation uses modern Go patterns.
* **Clio Integration:** The `MCP` command is correctly integrated using `app.SetupCommand`, and options are integrated into the main `Application` struct.
* **Error Handling:** Proper use of `mcp.NewToolResultError` ensures that errors are communicated back to the LLM in a protocol-compliant manner.
### 4.2 Implementation Highlights
* **Auto-Analysis:** The `getAnalysis` helper simplifies tool handlers by ensuring the image is analyzed if it hasn't been already. This improves the UX for AI agents that might call `get_wasted_space` before `analyze_image`.
* **Transport Flexibility:** Support for both `stdio` and `sse` via a CLI flag allows for both local and remote usage.
### 4.3 Areas for Improvement
* **Test Alignment:** `TestHandlers_GetWastedSpace_NoCache` needs to be updated because the handler now performs auto-analysis instead of failing.
* **Data Pruning:** While `get_wasted_space` limits results to 20 files, `inspect_layer` also limits to 100 entries. This is good, but for extremely deep trees, a more sophisticated pagination might be needed in the future.
## 5. Test Review
### 5.1 Test Fulfillments
The unit tests cover the primary logic of tool handlers and cache interaction.
### 5.2 Identified Regression
* **`TestHandlers_GetWastedSpace_NoCache`:** Failed during review because the implementation became "smarter" than the test (auto-analysis). This is a positive regression in functionality but requires a test update.
## 6. Identified Gaps and Future Work
To reach a production-ready state and fully leverage the MCP protocol, the following gaps should be addressed:
### 6.1 Protocol Feature Completeness
* **Resources:** Implement the `dive://` URI scheme (e.g., `dive://image/{name}/summary`) to allow agents to reference image states as static or dynamic documents.
* **Prompts:** Add pre-defined prompt templates like `optimize-dockerfile` to guide AI agents in interpreting analysis results.
### 6.2 Data Granularity
* **Structured Output:** Transition from pure `TextContent` to structured JSON results. This would allow agents to programmatically process file lists and metadata rather than relying on regex parsing of text summaries.
### 6.3 Progressive UX
* **Progress Notifications:** Bridge `internal/bus` (partybus) events to MCP `notifications/progress`. This is critical for large images where analysis can take significant time, providing feedback to the AI and user.
### 6.4 Advanced Analysis Tools
* **Layer Diffing:** Add a `diff_layers(image, layer_a, layer_b)` tool to specifically highlight what changed between two points in the image history, mirroring `dive`'s core TUI capability.
### 6.5 System Stability and Security
* **Cache Management:** The current in-memory cache is unbounded. Implement an LRU (Least Recently Used) cache or TTL-based eviction to prevent memory exhaustion in long-running SSE sessions.
* **Security Sandboxing:** Implement path restriction for the `docker-archive` source to ensure the server cannot be used to read arbitrary files outside of a designated workspace.
## 7. Conclusion
The implementation is of high quality, respects all architectural boundaries of the `dive` project, and provides a robust foundation for AI-assisted container optimization. Addressing the identified gaps will transform it from a utility into a comprehensive knowledge provider for the AI ecosystem.

43
MCP_ROADMAP.md Normal file
View file

@ -0,0 +1,43 @@
# Dive MCP Server: Roadmap to Production
This document outlines the strategy for addressing the identified gaps in the initial MCP implementation, categorized into three focused phases.
## Phase 1: Protocol Maturity & Resources
**Goal:** Align fully with the MCP 2025-11-25 standard and provide stable reference points for images.
* **Implement Resource Registry:**
* Map `image.Analysis` to `dive://image/{name}/summary`.
* Expose `dive://image/{name}/efficiency` as a dynamic resource.
* **Rationale:** Allows agents to "look up" image state without re-triggering a tool call, enabling better context management in LLMs.
* **Prompt Templates:**
* Implement `optimize-dockerfile`: A template that injects the results of `get_wasted_space` into a system prompt for the AI.
* Implement `explain-layer`: A template to help users understand complex `RUN` commands and their filesystem impact.
## Phase 2: Intelligence & Advanced Tooling
**Goal:** Transform the server from a summary provider to a structured data source with deep diffing capabilities.
* **Structured Data Transition:**
* Introduce JSON-schema-compliant output for all tools.
* Enable agents to programmatically iterate over file lists, allowing for complex "chains of thought" regarding filesystem cleanup.
* **The "Diff" Tool:**
* Implement `diff_layers(image, base_layer_index, target_layer_index)`.
* Leverage `filetree.CompareAndMark` to return a structured list of Additions, Modifications, and Deletions between any two layers.
* **Rationale:** This mirrors the most powerful feature of the Dive TUI, giving AI agents surgical precision in identifying where specific files were introduced or bloated.
## Phase 3: UX, Performance & Security
**Goal:** Ensure the server is robust, responsive, and safe for multi-user or remote environments.
* **Progressive UX:**
* Bridge `internal/bus` (partybus) to MCP `notifications/progress`.
* Standardize progress tokens so clients like Claude Desktop can show an active loading state during image pulls.
* **Bounded Caching:**
* Replace the simple map with an LRU (Least Recently Used) cache.
* Add a `--mcp-cache-ttl` flag to automatically evict stale analysis results.
* **Security Sandboxing:**
* Add a `--sandbox` flag to restrict `docker-archive` lookups to a specific directory.
* Validate all image paths against the sandbox root to prevent directory traversal attacks in remote SSE modes.
## Implementation Priorities (Immediate Next Steps)
1. **Update Structured Output:** Start returning JSON strings in `TextContent` to improve AI parsing.
2. **Implement Progress Reporting:** Crucial for the "alive" feel of the integration.
3. **Add Layer Diffing:** The missing link between the TUI and the MCP server.

View file

@ -3,6 +3,9 @@ package mcp
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/mark3labs/mcp-go/mcp"
@ -24,6 +27,28 @@ func newToolHandlers() *toolHandlers {
}
func (h *toolHandlers) getAnalysis(ctx context.Context, imageName string, sourceStr string) (*image.Analysis, error) {
// Heuristic: if imageName ends in .tar and source is docker, assume docker-archive
if strings.HasSuffix(imageName, ".tar") && sourceStr == "docker" {
sourceStr = "docker-archive"
// If the file doesn't exist at the given path, check .data/
if _, err := os.Stat(imageName); os.IsNotExist(err) {
wd, _ := os.Getwd()
// Navigate up from cmd/dive/cli/internal/mcp to root if needed
// (During real runs, Getwd is project root)
root := wd
for i := 0; i < 5; i++ {
if _, err := os.Stat(filepath.Join(root, "go.mod")); err == nil {
break
}
root = filepath.Dir(root)
}
dataPath := filepath.Join(root, ".data", imageName)
if _, err := os.Stat(dataPath); err == nil {
imageName = dataPath
}
}
}
source := dive.ParseImageSource(sourceStr)
if source == dive.SourceUnknown {
return nil, fmt.Errorf("unknown image source: %s", sourceStr)
@ -103,7 +128,7 @@ func (h *toolHandlers) getWastedSpaceHandler(ctx context.Context, request mcp.Ca
}
if len(analysis.Inefficiencies) == 0 {
return mcp.NewToolResultText("No wasted space detected in this image.")
return mcp.NewToolResultText("No wasted space detected in this image."), nil
}
summary := h.formatWastedSpace(analysis)
@ -181,3 +206,105 @@ func (h *toolHandlers) inspectLayerHandler(ctx context.Context, request mcp.Call
return mcp.NewToolResultText(summary), nil
}
// --- Resource Handlers ---
func (h *toolHandlers) resourceSummaryHandler(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// URI pattern: dive://image/{name}/summary
parts := strings.Split(request.Params.URI, "/")
if len(parts) < 5 {
return nil, fmt.Errorf("invalid resource URI: %s", request.Params.URI)
}
imageName := parts[3]
analysis, err := h.getAnalysis(ctx, imageName, "docker")
if err != nil {
return nil, err
}
content := h.formatSummary(analysis)
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: request.Params.URI,
MIMEType: "text/plain",
Text: content,
},
}, nil
}
func (h *toolHandlers) resourceEfficiencyHandler(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// URI pattern: dive://image/{name}/efficiency
parts := strings.Split(request.Params.URI, "/")
if len(parts) < 5 {
return nil, fmt.Errorf("invalid resource URI: %s", request.Params.URI)
}
imageName := parts[3]
analysis, err := h.getAnalysis(ctx, imageName, "docker")
if err != nil {
return nil, err
}
content := fmt.Sprintf("Efficiency Score: %.2f%%\nWasted Space: %d bytes\n", analysis.Efficiency*100, analysis.WastedBytes)
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: request.Params.URI,
MIMEType: "text/plain",
Text: content,
},
}, nil
}
// --- Prompt Handlers ---
func (h *toolHandlers) promptOptimizeDockerfileHandler(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
imageName, ok := request.Params.Arguments["image"]
if !ok {
return nil, fmt.Errorf("image argument is required")
}
analysis, err := h.getAnalysis(ctx, imageName, "docker")
if err != nil {
return nil, err
}
wasted := h.formatWastedSpace(analysis)
summary := h.formatSummary(analysis)
return &mcp.GetPromptResult{
Description: "Optimize Dockerfile based on Dive analysis",
Messages: []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("You are an expert in Docker and OCI image optimization. Your findings for image '%s':\n\n%s\n\n%s\n\nPlease suggest optimizations for the Dockerfile.", imageName, summary, wasted),
},
},
},
}, nil
}
func (h *toolHandlers) promptExplainLayerHandler(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
imageName, ok := request.Params.Arguments["image"]
if !ok {
return nil, fmt.Errorf("image argument is required")
}
layerIdxStr, ok := request.Params.Arguments["layer_index"]
if !ok {
return nil, fmt.Errorf("layer_index argument is required")
}
return &mcp.GetPromptResult{
Description: "Explain the impact of a specific image layer",
Messages: []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("Can you explain what is happening in layer %s of image '%s'?", layerIdxStr, imageName),
},
},
},
}, nil
}

View file

@ -69,3 +69,47 @@ func TestHandlers_GetWastedSpace_WithCache(t *testing.T) {
assert.False(t, result.IsError)
assert.Contains(t, result.Content[0].(mcp.TextContent).Text, "No wasted space detected")
}
func TestHandlers_ResourceSummary(t *testing.T) {
h := newToolHandlers()
h.analyses["docker:ubuntu:latest"] = &image.Analysis{
Image: "ubuntu:latest",
WastedBytes: 0,
SizeBytes: 100,
Efficiency: 1.0,
}
ctx := context.Background()
req := mcp.ReadResourceRequest{}
req.Params.URI = "dive://image/ubuntu:latest/summary"
result, err := h.resourceSummaryHandler(ctx, req)
assert.NoError(t, err)
assert.Len(t, result, 1)
textRes, ok := result[0].(mcp.TextResourceContents)
assert.True(t, ok)
assert.Contains(t, textRes.Text, "Image: ubuntu:latest")
}
func TestHandlers_PromptOptimize(t *testing.T) {
h := newToolHandlers()
h.analyses["docker:ubuntu:latest"] = &image.Analysis{
Image: "ubuntu:latest",
WastedBytes: 0,
SizeBytes: 100,
Efficiency: 1.0,
}
ctx := context.Background()
req := mcp.GetPromptRequest{}
req.Params.Name = "optimize-dockerfile"
req.Params.Arguments = map[string]string{
"image": "ubuntu:latest",
}
result, err := h.promptOptimizeDockerfileHandler(ctx, req)
assert.NoError(t, err)
assert.Contains(t, result.Description, "Optimize Dockerfile")
assert.Len(t, result.Messages, 1)
assert.Contains(t, result.Messages[0].Content.(mcp.TextContent).Text, "ubuntu:latest")
}

View file

@ -70,6 +70,53 @@ func NewServer(id clio.Identification) *server.MCPServer {
)
s.AddTool(inspectLayerTool, h.inspectLayerHandler)
// --- Resources ---
// 1. Summary resource template
summaryTemplate := mcp.NewResourceTemplate("dive://image/{name}/summary", "Image Summary",
mcp.WithTemplateDescription("Get a text summary of the image analysis"),
)
s.AddResourceTemplate(summaryTemplate, h.resourceSummaryHandler)
// 2. Efficiency resource template
efficiencyTemplate := mcp.NewResourceTemplate("dive://image/{name}/efficiency", "Image Efficiency",
mcp.WithTemplateDescription("Get the efficiency score and wasted bytes for an image"),
)
s.AddResourceTemplate(efficiencyTemplate, h.resourceEfficiencyHandler)
// --- Prompts ---
// 1. Optimize Dockerfile prompt
s.AddPrompt(mcp.Prompt{
Name: "optimize-dockerfile",
Description: "Get suggestions for optimizing a Dockerfile based on image analysis",
Arguments: []mcp.PromptArgument{
{
Name: "image",
Description: "The name of the image to optimize",
Required: true,
},
},
}, h.promptOptimizeDockerfileHandler)
// 2. Explain Layer prompt
s.AddPrompt(mcp.Prompt{
Name: "explain-layer",
Description: "Get an explanation of the impact of a specific image layer",
Arguments: []mcp.PromptArgument{
{
Name: "image",
Description: "The name of the image",
Required: true,
},
{
Name: "layer_index",
Description: "The index of the layer to explain",
Required: true,
},
},
}, h.promptExplainLayerHandler)
return s
}
@ -84,12 +131,9 @@ func Run(s *server.MCPServer, opts options.MCP) error {
mux.Handle("/messages", sseServer.MessageHandler())
log.Infof("Starting MCP SSE server on %s", addr)
fmt.Printf("Starting MCP SSE server on %s
", addr)
fmt.Printf("- SSE endpoint: http://%s/sse
", addr)
fmt.Printf("- Message endpoint: http://%s/messages
", addr)
fmt.Printf("Starting MCP SSE server on %s\n", addr)
fmt.Printf("- SSE endpoint: http://%s/sse\n", addr)
fmt.Printf("- Message endpoint: http://%s/messages\n", addr)
return http.ListenAndServe(addr, mux)
case "stdio":