Documentation
¶
Overview ¶
Package mcp implements a Model Context Protocol server that exposes the docker-security engine to AI agents. MCP is JSON-RPC 2.0; this package speaks the protocol directly over stdio (newline-delimited messages) and HTTP, with no SDK dependency — the wire format is small and implementing it ourselves keeps the zero-dependency posture.
The design is read-first. Every tool that only inspects (scan_target, get_findings, explain_finding, query_inventory, suggest_remediation, list_modules) is always available. The single mutating capability — persisting a scan into the store — is off by default, gated behind WithMutations, and every attempt (allowed or denied) is written to an audit log. An agent can reason about security posture freely; it cannot quietly change state.
The server is generic over engine.Registry: it exposes whatever modules the caller registered. It never imports capability modules, so the same server serves a one-module or a twenty-module engine unchanged.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Command ¶
Command implements `dsecrat mcp`: start the MCP server so an AI agent can drive the engine. It defaults to stdio (how agent hosts launch tool servers) and can serve HTTP with --http. It takes the module registry from the caller so this package never imports the module aggregator — the master wires it (see NOTES.md):
case "mcp":
return mcp.Command(modules.Default(), rest)
Types ¶
type AuditEntry ¶
type AuditEntry struct {
Time time.Time `json:"time"`
Tool string `json:"tool"`
Allowed bool `json:"allowed"`
ArgsDigest string `json:"args_digest"`
Note string `json:"note,omitempty"`
}
AuditEntry is one recorded mutating-tool attempt.
type AuditLog ¶
type AuditLog struct {
// contains filtered or unexported fields
}
AuditLog is a thread-safe, in-memory record of mutating-tool attempts.
func (*AuditLog) Entries ¶
func (l *AuditLog) Entries() []AuditEntry
Entries returns a copy of the audit trail in chronological order.
type Explanation ¶
type Explanation struct {
RuleID string `json:"rule_id"`
Module string `json:"module"`
Severity string `json:"severity"`
SeverityRank int `json:"severity_rank"` // 0..5, higher = worse
Category string `json:"category"`
Title string `json:"title"`
Resource string `json:"resource,omitempty"`
WhyItMatters string `json:"why_it_matters"`
Detail string `json:"detail,omitempty"`
Remediation []string `json:"remediation"`
HasRemediation bool `json:"has_remediation"`
Effort string `json:"effort"`
Frameworks []string `json:"frameworks,omitempty"` // extracted ATT&CK/CIS/NIST refs
References []string `json:"references,omitempty"`
Confidence string `json:"confidence"`
}
Explanation is the machine-and-human view of a single finding.
func Explain ¶
func Explain(f engine.Finding) Explanation
Explain projects a finding into a structured explanation. It is deterministic and self-contained: no lookups outside the finding and the static rule-class table.
type Option ¶
type Option func(*Server)
Option configures a Server.
func WithClock ¶
WithClock injects the time source used for audit timestamps, for deterministic tests. Analysis itself never reads this — only the audit trail does.
func WithMutations ¶
WithMutations enables state-changing tool behaviour (scan persistence). Off by default: an agent gets a read-only surface unless the operator opts in.
type PlanAction ¶
type PlanAction struct {
Priority int `json:"priority"`
Severity string `json:"severity"`
RuleID string `json:"rule_id"`
Module string `json:"module,omitempty"`
Resource string `json:"resource,omitempty"`
Title string `json:"title"`
Steps []string `json:"steps"`
Rationale string `json:"rationale"`
Effort string `json:"effort"`
References []string `json:"references,omitempty"`
}
PlanAction is one prioritized step in a remediation plan.
type RemediationPlan ¶
type RemediationPlan struct {
Target string `json:"target"`
Total int `json:"total"`
Counts map[string]int `json:"counts"`
Actions []PlanAction `json:"actions"`
Summary string `json:"summary"`
}
RemediationPlan is a prioritized, explained action plan an agent or human can execute top-to-bottom. It is the deterministic data behind the "security copilot": a model may reword or regroup it, but the ordering and content are reproducible from the findings alone.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is an MCP endpoint over the engine. Construct with New and drive it via ServeStdio or HTTPHandler.
Example ¶
ExampleServer shows an AI agent driving the platform over MCP: it lists tools, runs a blast-radius inventory query, gets a machine-readable explanation, and asks for a prioritized remediation plan — all deterministic, no model present.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/Ratnadeepdeyroy/docker-security/internal/engine"
"github.com/Ratnadeepdeyroy/docker-security/internal/store"
)
// ExampleServer shows an AI agent driving the platform over MCP: it lists tools,
// runs a blast-radius inventory query, gets a machine-readable explanation, and
// asks for a prioritized remediation plan — all deterministic, no model present.
func main() {
// A store seeded with one scanned image carrying a vulnerable component.
st := store.NewMemory()
st.Put(&store.Scan{
Image: "acme/api:1.2.3",
RecordedAt: fixedClock(),
Labels: map[string]string{"owner": "team-platform"},
Report: &engine.Report{Target: "acme/api:1.2.3", Findings: []engine.Finding{
{RuleID: "DS-RAT-VULN-042", Module: "vuln", Severity: engine.SeverityCritical, Title: "openssl CVE-2022-3602",
Remediation: "Upgrade openssl to 3.0.7"},
}},
Components: []store.Component{{Name: "openssl", Version: "3.0.1"}},
})
reg := engine.NewRegistry()
reg.Register(fakeModule{})
srv := New(reg, WithStore(st), WithClock(fixedClock))
send := func(method string, params any) *response {
p, _ := json.Marshal(params)
req, _ := json.Marshal(request{JSONRPC: "2.0", ID: json.RawMessage(`1`), Method: method, Params: p})
out, _ := srv.handleMessage(context.Background(), req)
var r response
json.Unmarshal(out, &r)
return &r
}
toolText := func(r *response) map[string]any {
txt := r.Result.(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string)
var m map[string]any
json.Unmarshal([]byte(txt), &m)
return m
}
// 1. Discover tools.
tools := send("tools/list", nil).Result.(map[string]any)["tools"].([]any)
fmt.Printf("tools available: %d\n", len(tools))
// 2. Blast radius: which images ship openssl?
inv := toolText(send("tools/call", map[string]any{"name": "query_inventory", "arguments": map[string]any{"name": "openssl"}}))
m := inv["matches"].([]any)[0].(map[string]any)
fmt.Printf("blast radius: %v image(s); first = %s owner=%s\n", inv["count"], m["image"], m["owner"])
// 3. Explain a finding for the agent.
ex := toolText(send("tools/call", map[string]any{"name": "explain_finding", "arguments": map[string]any{
"rule_id": "DS-RAT-VULN-042", "severity": "critical", "remediation": "Upgrade openssl to 3.0.7"}}))
fmt.Printf("explain: category=%s effort=%s\n", ex["category"], ex["effort"])
// 4. Prioritized remediation plan (the security copilot).
plan := toolText(send("tools/call", map[string]any{"name": "suggest_remediation", "arguments": map[string]any{"scan_id": firstScanID(st)}}))
fmt.Printf("plan: %v action(s); #1 = %s\n", plan["total"], plan["actions"].([]any)[0].(map[string]any)["severity"])
}
// firstScanID returns the id of the single seeded scan.
func firstScanID(st *store.Store) string { return st.Scans()[0].ID }
Output: tools available: 6 blast radius: 1 image(s); first = acme/api:1.2.3 owner=team-platform explain: category=known-vulnerability effort=medium plan: 1 action(s); #1 = CRITICAL
func (*Server) Audit ¶
Audit exposes the audit log so an operator (or a test) can inspect what mutating calls were attempted.
func (*Server) HTTPHandler ¶
HTTPHandler returns an http.Handler that accepts one JSON-RPC request per POST and returns its response. The master can mount this at /mcp on the existing server mux. A notification (no id) yields 204 No Content.
func (*Server) ServeStdio ¶
ServeStdio runs the server over a newline-delimited JSON-RPC stream until in is exhausted or ctx is cancelled. Each inbound line is one request; each response is written as one line. Notifications produce no output.
type Tool ¶
type Tool struct {
Name string
Description string
InputSchema map[string]any
Mutating bool
Handler func(ctx context.Context, s *Server, args json.RawMessage) (any, error)
}
Tool is one MCP tool: its advertised schema plus a handler. Mutating tools are gated and audited by the caller (callToolResult); the handler itself just does the work.