mirror of
https://github.com/wagoodman/dive
synced 2026-03-14 22:35:50 +01:00
feat(mcp): implement dynamic resources, prompt templates, and auto-analysis (Phase 1)
This commit is contained in:
parent
07c1bcf089
commit
03a6b9f2d7
5 changed files with 339 additions and 7 deletions
74
MCP_REVIEW.md
Normal file
74
MCP_REVIEW.md
Normal 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
43
MCP_ROADMAP.md
Normal 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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue