mcp

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 16 Imported by: 0

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

func Command(reg *engine.Registry, args []string) int

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 NewAuditLog

func NewAuditLog() *AuditLog

NewAuditLog returns an empty audit log.

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

func WithClock(fn func() time.Time) Option

WithClock injects the time source used for audit timestamps, for deterministic tests. Analysis itself never reads this — only the audit trail does.

func WithMutations

func WithMutations(on bool) Option

WithMutations enables state-changing tool behaviour (scan persistence). Off by default: an agent gets a read-only surface unless the operator opts in.

func WithStore

func WithStore(s *store.Store) Option

WithStore attaches a scan store, enabling the inventory-backed tools (get_findings, query_inventory) and scan persistence.

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.

func BuildPlan

func BuildPlan(target string, findings []engine.Finding) RemediationPlan

BuildPlan turns findings into a prioritized plan: most-severe first, then by rule id for stability. Priority numbers are assigned after sorting so they read 1..N top-to-bottom.

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 New

func New(reg *engine.Registry, opts ...Option) *Server

New builds an MCP server exposing the given module registry.

func (*Server) Audit

func (s *Server) Audit() *AuditLog

Audit exposes the audit log so an operator (or a test) can inspect what mutating calls were attempted.

func (*Server) HTTPHandler

func (s *Server) HTTPHandler() http.Handler

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

func (s *Server) ServeStdio(ctx context.Context, in io.Reader, out io.Writer) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL