mcp

package
v0.121.0 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package mcp exposes Draugr to AI coding agents over the Model Context Protocol.

The reason this exists is narrower than "agents are popular". An agent asked to check a change for security problems will do it one way or another: if Draugr isn't callable it improvises, shells out to whatever scanner it can find, picks its own scope, and reads raw tool output in its own context window. That improvised answer has no recorded scope, no organizational risk context, and no relationship to what CI will decide. Being callable is what makes the agent's answer and the pipeline's answer the same answer.

Two design rules follow from that:

  • **Read-only by default.** Scanning clones repositories, executes external tools and reaches the network. An agent triggering that unprompted is a bad surprise, so `scan` is registered only when the operator opts in. Everything else is safe to call freely.
  • **Return decisions, not data.** A tool that hands back raw scanner output has moved the problem into the agent's context window rather than solving it. These tools return prioritized, deduplicated, normalized results, the same thing a person sees.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ListSurveyorsTool added in v0.98.0

func ListSurveyorsTool(reg *surveyor.Registry) mcp.ToolHandlerFor[struct{}, ListSurveyorsOutput]

ListSurveyorsTool names the surveyors this build has.

From the registry rather than a list written here, so a surveyor added to Draugr is reachable without anyone remembering to mention it in two places.

func NewServer

func NewServer(opts Options) (*mcp.Server, error)

NewServer builds the MCP server. Tools are registered here rather than discovered so the exposed surface is a deliberate, reviewable list.

func SurveyTool added in v0.98.0

SurveyTool discovers a surface and returns a descriptor for it.

It returns the descriptor rather than writing one. A tool that writes a file is a tool that has to ask first, and merging into an existing descriptor carries decisions. Which exposure wins, what a narrower scope means. That belong with the person who owns the file. Handing back YAML lets an assistant show it, validate it with validate_saga, and let a human decide where it goes.

Writing a descriptor by hand from get_saga_schema is the alternative, and it is guesswork about a live system: which namespaces exist, which images are actually running, at which digest.

Types

type CheckToolsInput added in v0.39.0

type CheckToolsInput struct {
	Path string `json:"path,omitempty" jsonschema:"path to a Saga descriptor; without it, every tool Draugr knows about is checked"`
}

CheckToolsInput optionally narrows the check to what one descriptor actually needs.

type CheckToolsOutput added in v0.39.0

type CheckToolsOutput struct {
	Ready   bool         `json:"ready" jsonschema:"true when nothing required is missing"`
	Tools   []ToolStatus `json:"tools"`
	Missing []string     `json:"missing,omitempty" jsonschema:"required tools that are absent"`
	// Remedy is the single command that installs everything missing.
	Remedy string `json:"remedy,omitempty"`
	Note   string `json:"note"`
}

CheckToolsOutput is the answer to "can Draugr actually scan here?".

func CheckToolsTool added in v0.39.0

CheckToolsTool reports which external scanners are present.

This exists because of what Draugr does when one is missing: the control can't run, and a scan that can't run is not a pass. An assistant that hits that needs to say what's wrong and what fixes it. Which is a question about the machine, and answering it is free.

It deliberately stops there. Draugr will not install anything on a user's behalf over MCP: that's a write to their machine, and their client already has a permission model for running commands which is stronger than anything this server could offer. Report the command; let the person approve it where they already approve such things.

type Control

type Control struct {
	Name            string   `json:"name" jsonschema:"the control's name, as used under config.controllers in a Saga"`
	Scope           string   `json:"scope" jsonschema:"whether the control runs per component or once for the project"`
	Purpose         string   `json:"purpose" jsonschema:"what the control checks for"`
	DefaultScanners []string `json:"defaultScanners" jsonschema:"scanners that run when the control is enabled"`
	OptInScanners   []string `` /* 134-byte string literal not displayed */
	// ScannerOptions is what each of this control's scanners accepts in its Saga block, keyed by
	// scanner name. An entry present but empty means that scanner accepts no options; anything
	// else written under its block is rejected before the scan runs.
	ScannerOptions map[string][]plugin.Option `` /* 151-byte string literal not displayed */
}

Control is one control an agent could enable.

type ControlsOutput

type ControlsOutput struct {
	Controls []Control `json:"controls"`
	// Hint is where the agent should put what it learned. Returning capability without saying
	// how to use it just moves the guesswork somewhere else.
	Hint string `json:"hint"`
}

ControlsOutput is the list_controls result.

func ListControls

func ListControls(reg *engine.Registry) ControlsOutput

ListControls reports the controls this build registers.

type Counts

type Counts struct {
	Critical int `json:"critical"`
	High     int `json:"high"`
	Medium   int `json:"medium"`
	Low      int `json:"low"`
}

Counts tallies the whole report, not just what was returned. A narrowed list shouldn't make the rest of the backlog look like it vanished.

type DiffInput added in v0.98.0

type DiffInput struct {
	BasePath string `json:"basePath" jsonschema:"path to the results.sarif from the base revision"`
	HeadPath string `json:"headPath" jsonschema:"path to the results.sarif from the revision being proposed"`
	// FailOnNew mirrors the flag CI uses, so an assistant can ask the question the pipeline
	// will ask rather than a different one.
	FailOnNew string `` /* 261-byte string literal not displayed */
	Limit     int    `json:"limit,omitempty" jsonschema:"maximum new findings to return; defaults to 20"`
}

DiffInput compares two reports.

type DiffOutput added in v0.98.0

type DiffOutput struct {
	NewCount   int `json:"newCount" jsonschema:"findings present in head and absent from base"`
	FixedCount int `json:"fixedCount" jsonschema:"findings present in base and absent from head"`
	// New is what the change introduced, most urgent first, carrying the same remediation and
	// package detail a summary does.
	New []Finding `json:"new,omitempty"`
	// Fixed is named rather than only counted, because it is the half of a change worth saying
	// out loud and an assistant summarizing a diff has nothing else to praise.
	Fixed []Finding `json:"fixed,omitempty"`
	// WouldFail answers the question CI will ask, when failOnNew is given.
	WouldFail   bool   `json:"wouldFail,omitempty" jsonschema:"true when a new finding meets failOnNew; only meaningful when failOnNew was given"`
	GateApplied string `json:"gateApplied,omitempty" jsonschema:"the threshold wouldFail was computed against"`
	Note        string `json:"note,omitempty"`
}

DiffOutput says what a change introduced and what it resolved.

func DiffReportsTool added in v0.98.0

DiffReportsTool compares two scans and reports what the change introduced.

The question in a coding session is almost never "what is wrong with this repository". It is "did what I just wrote make it worse". A project with two hundred inherited findings answers the first question the same way before and after a change, which tells an assistant nothing about the change. This is the same comparison `draugr diff` makes, so the answer an assistant gives and the answer the pull request gate gives cannot differ.

type EmptyInput

type EmptyInput struct{}

EmptyInput is the argument type for tools that take none. The SDK derives a schema from the input type, and an empty struct is how you say "no arguments" rather than "any arguments".

type ExplainInput added in v0.98.0

type ExplainInput struct {
	RuleID string `json:"ruleId" jsonschema:"the rule to explain, in full or by the part that is unambiguous: 4.3.1 finds kube-bench/cis/4.3.1"`
	Path   string `json:"path" jsonschema:"path to a results.sarif produced by draugr scan"`
}

ExplainInput asks what a rule means and how to fix it.

type ExplainOutput added in v0.98.0

type ExplainOutput struct {
	RuleID      string   `json:"ruleId"`
	Description string   `json:"description,omitempty" jsonschema:"what the check is, in full rather than the truncated line a report shows"`
	Remediation string   `json:"remediation,omitempty" jsonschema:"how to fix it, as the scanner published it"`
	HelpURI     string   `json:"helpUri,omitempty"`
	FoundIn     []string `json:"foundIn,omitempty" jsonschema:"where this rule fired, capped"`
	Note        string   `json:"note,omitempty"`
}

ExplainOutput is what the scan already recorded about a rule.

func ExplainRuleTool added in v0.98.0

ExplainRuleTool answers what a finding means and what to change.

A rule id and a truncated line is enough to rank a finding and not enough to decide anything. The answer is already in the report: scanners publish remediation text and Draugr records it. Without this an assistant is left to fetch a help URI, a network round trip for text on disk, and for a benchmark a registration form in front of a PDF, which is not an answer at all.

type Finding

type Finding struct {
	Priority string  `json:"priority,omitempty" jsonschema:"P1 (act now) through P4 (lowest)"`
	Severity string  `json:"severity" jsonschema:"critical, high, medium or low"`
	Score    float64 `json:"score,omitempty" jsonschema:"CVSS-style numeric severity, when the scanner gave one"`
	RuleID   string  `json:"ruleId"`
	Scanner  string  `json:"scanner,omitempty"`
	Location string  `json:"location,omitempty" jsonschema:"file:line, image reference, or endpoint"`
	Message  string  `json:"message"`
	HelpURI  string  `json:"helpUri,omitempty" jsonschema:"where the rule is documented, for background beyond the remediation below"`

	// Remediation is the fix in the scanner's own words, not a summary of it.
	Remediation string `` /* 137-byte string literal not displayed */
	// Action classifies what kind of fix it is, so an assistant can tell work it can do from
	// work that belongs to whoever publishes the image or operates the cluster.
	Action string `` /* 217-byte string literal not displayed */
	// Package names what to upgrade and to what.
	Package      string `json:"package,omitempty" jsonschema:"the package this finding is in"`
	Version      string `json:"version,omitempty" jsonschema:"the version installed"`
	FixedVersion string `` /* 153-byte string literal not displayed */
	Ecosystem    string `` /* 139-byte string literal not displayed */
	PURL         string `json:"purl,omitempty" jsonschema:"package URL, portable across ecosystems"`
	// Component ties a finding to the part of the system it was found in, which is what its
	// priority was computed from.
	Component string `json:"component,omitempty" jsonschema:"the component in the descriptor this finding belongs to"`
	// Image and OSEndOfLife answer "why can I not just upgrade this".
	Image       string `json:"image,omitempty" jsonschema:"the image this finding is in, when it came from one"`
	OSEndOfLife bool   `` /* 134-byte string literal not displayed */
}

Finding is one prioritized result, flattened to what a reader needs to act.

type FixListInput added in v0.98.0

type FixListInput struct {
	Path  string `json:"path" jsonschema:"path to a results.sarif produced by draugr scan"`
	Limit int    `json:"limit,omitempty" jsonschema:"maximum actions to return; defaults to 20"`
}

FixListInput asks what to do about a report.

type FixListOutput added in v0.98.0

type FixListOutput struct {
	Actions []report.Action `json:"actions" jsonschema:"things to do, most urgent first; each says how many findings it clears"`
	Clears  int             `json:"clears" jsonschema:"findings these actions resolve between them"`
	Note    string          `json:"note,omitempty"`
}

FixListOutput is the work a report implies, rather than the findings in it.

func FixListTool added in v0.98.0

FixListTool answers "what should I do" with actions rather than findings.

One remediation usually clears many findings: eight vulnerabilities in one library are one upgrade, and every package inside an image somebody else publishes is one newer image. An assistant handed the findings has to work that out for itself, and will do it differently each time, so this uses the same grouping the terminal prints, and the two cannot disagree.

type ListSurveyorsOutput added in v0.98.0

type ListSurveyorsOutput struct {
	Surveyors []SurveyorInfo `json:"surveyors"`
	Hint      string         `json:"hint"`
}

ListSurveyorsOutput names what can be discovered.

type Options

type Options struct {
	// Scan says whether a client may start scans. The zero value is ScanOff.
	Scan ScanMode
	// Registry supplies the controllers and scanners. Required.
	Registry *engine.Registry
	// Root is the directory searched for Saga descriptors to expose as resources. Empty means
	// the working directory.
	Root string
	// Surveyors supplies the discovery plugins. Empty disables survey and list_surveyors, so a
	// caller embedding this server chooses whether it may reach a cluster or a forge at all.
	Surveyors *surveyor.Registry
}

Options configures the server's exposed surface.

type ScanInput

type ScanInput struct {
	Path        string `json:"path" jsonschema:"path to the Saga descriptor to scan"`
	MinPriority string `json:"minPriority,omitempty" jsonschema:"only return findings at this priority or above: p1, p2, p3 or p4"`
	Limit       int    `json:"limit,omitempty" jsonschema:"maximum findings to return; defaults to 20"`
}

ScanInput selects what to scan.

type ScanMode

type ScanMode string

ScanMode says whether, and on what terms, a client may start a scan.

const (
	// ScanOff doesn't register the tool at all. The default: an assistant can't set off work
	// like that because it was curious, and the read-only tools are where the value starts.
	ScanOff ScanMode = "off"
	// ScanAsk registers it and asks the user to approve each call, through the client. This is the
	// mode to want, permission granted for the scan in front of you rather than for every scan this
	// session. But it needs a client that implements elicitation, and many don't.
	ScanAsk ScanMode = "ask"
	// ScanAlways registers it and runs without asking. Right for a sandbox or CI, where there's
	// nobody to ask.
	ScanAlways ScanMode = "always"
)

The scan modes. A scan clones repositories, executes external tools and reaches the network, so the question isn't only "may it" but "who agrees to it, and when".

func ParseScanMode

func ParseScanMode(s string) (ScanMode, error)

ParseScanMode validates a mode name.

type ScanOutput

type ScanOutput struct {
	Verdict string `json:"verdict" jsonschema:"pass or fail, by the same policy the CI gate applies"`
	// Controls that actually ran, so the caller can see the scope rather than infer it.
	Controls []string `json:"controls" jsonschema:"the controls this scan ran; nothing outside them was examined"`
	// Uncovered names surfaces the descriptor declares that no enabled control looked at.
	Uncovered []string `json:"uncovered,omitempty" jsonschema:"surfaces this descriptor declares that no enabled control examined"`
	// Unexamined is the same sentence for everything no control covers at all.
	Unexamined string `json:"unexamined" jsonschema:"what a Draugr scan does not examine, whatever the verdict"`
	// Delivered names where the descriptor's publishers put the report, so a caller can point
	// the user at a file, or read it back later instead of scanning again.
	Delivered []string `json:"delivered,omitempty" jsonschema:"where this run's reports were delivered, from the descriptor's config.publishers"`
	SummarizeOutput
}

ScanOutput is the verdict plus the ranked findings behind it, and the boundary of what it describes.

Scope is not decoration. A verdict arriving on its own reads as the answer to whatever question prompted the scan, and the question is usually broader than the one Draugr answers: an assistant asked whether a repository is safe to ship, handed a PASS, has every reason to stop. Naming the controls that ran and the surfaces nothing looked at makes the floor visible as a floor.

type SchemaOutput

type SchemaOutput struct {
	Schema  map[string]any `json:"schema" jsonschema:"the JSON Schema for a Saga descriptor"`
	Version string         `json:"draugrVersion" jsonschema:"the Draugr build this schema came from"`
	Hint    string         `json:"hint"`
}

SchemaOutput carries the descriptor's JSON Schema.

Schema is a decoded object rather than raw bytes so a client receives the schema itself: MCP validates a tool's output against the schema derived from this type, and a []byte field derives as an array, which the schema document then fails to be.

func GetSchema

func GetSchema() (SchemaOutput, error)

GetSchema returns the schema embedded in this binary. The one that will actually be enforced, as opposed to whatever is published on the web for some other version.

type SummarizeInput

type SummarizeInput struct {
	Path string `json:"path" jsonschema:"path to a results.sarif or report.json produced by draugr scan"`
	// MinPriority narrows the list the way --min-priority does on the CLI.
	MinPriority string `json:"minPriority,omitempty" jsonschema:"only return findings at this priority or above: p1, p2, p3 or p4"`
	// Limit caps how many findings come back. Context is the scarce resource here.
	Limit int `json:"limit,omitempty" jsonschema:"maximum findings to return; defaults to 20"`
}

SummarizeInput points at a report already on disk.

type SummarizeOutput

type SummarizeOutput struct {
	Total int `json:"total" jsonschema:"findings the report judged, before any priority filter; excludes suppressed"`
	// Suppressed is reported rather than hidden: an assistant that cannot see a decision was
	// made cannot tell an accepted risk from one nobody has looked at.
	Suppressed int       `` /* 139-byte string literal not displayed */
	Returned   int       `json:"returned"`
	Counts     Counts    `json:"counts"`
	Findings   []Finding `json:"findings"`
	Note       string    `json:"note,omitempty"`
}

SummarizeOutput is the ranked answer to "what should I fix?".

func SummarizeReportTool

SummarizeReportTool reads a report from disk and returns it ranked.

type SurveyInput added in v0.98.0

type SurveyInput struct {
	// Surveys is a list because an application is rarely one surface. The repositories in an
	// organization and the images running in a namespace are the same application described twice,
	// and merging them into one descriptor is the point. Running them separately gives two
	// descriptors that each look complete.
	Surveys []SurveyRequest `json:"surveys" jsonschema:"the surveyors to run; results merge into one descriptor"`
	Name    string          `json:"name,omitempty" jsonschema:"project name for the descriptor"`
	Version string          `json:"version,omitempty" jsonschema:"release version for the descriptor; defaults to 0.0.0"`
}

SurveyInput asks one or more surveyors what is out there.

type SurveyOutput added in v0.98.0

type SurveyOutput struct {
	// Saga is the descriptor as YAML, for the caller to show, edit and write itself.
	Saga string `json:"saga" jsonschema:"the descriptor this survey produced, as YAML"`
	// Components names what was found, so a caller can say what it discovered without parsing.
	Components []string `json:"components,omitempty"`
	// Controls are the ones the discovered surface turned on.
	Controls []string `json:"controls,omitempty"`
	Note     string   `json:"note,omitempty"`
	// Warning carries what the survey could not reach. A survey that half worked and reads as
	// complete is the failure worth naming: the descriptor looks finished and is not.
	Warning string `json:"warning,omitempty"`
}

SurveyOutput is a descriptor to look at, not a file that appeared on disk.

type SurveyRequest added in v0.98.0

type SurveyRequest struct {
	Surveyor string `json:"surveyor" jsonschema:"which surveyor to run; list_surveyors names them"`
	Ref      string `` /* 136-byte string literal not displayed */
}

SurveyRequest is one surveyor and what to point it at.

type SurveyorInfo added in v0.98.0

type SurveyorInfo struct {
	Name     string   `json:"name"`
	Provides []string `json:"provides" jsonschema:"what it discovers: repositories, images, hosts"`
}

SurveyorInfo is one surveyor and what it finds.

type ToolStatus added in v0.39.0

type ToolStatus struct {
	Name      string `json:"name"`
	Installed bool   `json:"installed"`
	Version   string `json:"version,omitempty"`
	Path      string `json:"path,omitempty"`
	Category  string `json:"category,omitempty" jsonschema:"scanner (backs a control) or utility (supporting, e.g. git)"`
	Optional  bool   `json:"optional,omitempty" jsonschema:"absence of an optional tool is not a problem"`
	// Install is the command a person would run. Draugr will not run it for you.
	Install string `json:"install,omitempty"`
}

ToolStatus is one external tool Draugr would execute.

type ValidateInput

type ValidateInput struct {
	Path    string `json:"path,omitempty" jsonschema:"path to a Saga descriptor on disk"`
	Content string `json:"content,omitempty" jsonschema:"the descriptor's YAML content, for validating an edit before writing it"`
}

ValidateInput accepts a descriptor either by path or inline.

type ValidateOutput

type ValidateOutput struct {
	Valid bool `json:"valid"`
	// Error is why it isn't, in the same words the CLI would use.
	Error string `json:"error,omitempty"`
	// Components and Controls describe what the descriptor actually asks for, which is how an
	// agent checks that a descriptor says what it intended rather than merely parsing.
	Components []string `json:"components,omitempty"`
	Controls   []string `json:"controls,omitempty"`
}

ValidateOutput reports whether the descriptor is usable.

func ValidateSagaTool

ValidateSagaTool validates a descriptor from a path or inline content.

Jump to

Keyboard shortcuts

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