<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** #43544. Moves `fleet-mcp` from `tools/fleet-mcp/` to `cmd/fleet-mcp/`. It is becoming a production server used by customers, so it now lives under `cmd/` alongside the other Fleet binaries. Per the module strategy chosen for this move, it **remains a standalone Go module** (keeps its own `go.mod`/`go.sum` and isolated deps such as `mark3labs/mcp-go`, `logrus`, `gorilla/websocket`, `godotenv`) — the root `github.com/fleetdm/fleet/v4` module is unchanged. ### What changed - `git mv tools/fleet-mcp/ → cmd/fleet-mcp/` (history preserved as renames). - Updated all path references: - Root `Makefile` `update-go` module list. - `.github/workflows/test-fleet-mcp.yml` — trigger paths, `go-version-file`, `working-directory`. - `.github/dependabot.yml` — gomod directory. - `cmd/fleet-mcp/render.yaml` — `rootDir`. - `cmd/fleet-mcp/README.md`, `Makefile`, `schema.go` — path comments/links. - `articles/fleet-mcp.md` — README link. - Removed the `fleet-mcp/` row from `tools/README.md`. ### Follow-up (not in this PR) - The Render service's Blueprint file path must be updated from `tools/fleet-mcp/render.yaml` to `cmd/fleet-mcp/render.yaml` in the Render dashboard. ## Testing - `go build .` in `cmd/fleet-mcp` — OK - `go test -race -count=1 ./...` — `ok fleet-mcp` - [x] QA'd all new/changed functionality manually
58 lines
3.5 KiB
Go
58 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"github.com/mark3labs/mcp-go/server"
|
|
)
|
|
|
|
const defaultEndpointsPerPage = 50
|
|
|
|
// fleetMCPInstructions is the server-level system prompt advertised to MCP
|
|
// clients (Claude Desktop, Cursor, etc.) via the `initialize` response. It
|
|
// mandates the schema-first workflow that prevents the most common class of
|
|
// silent-zero-row bug (assuming column types when writing osquery SQL) and
|
|
// directs the client to confirm write operations with the operator first.
|
|
//
|
|
// NOTE: these are advisory instructions to a cooperating LLM client, not a
|
|
// server-enforced control. A prompt-injected or non-cooperating client (or any
|
|
// caller driving the tools over raw JSON-RPC) can ignore them — the real bounds
|
|
// on writes are the FLEET_API_KEY's Fleet role (an observer token => Fleet 403s
|
|
// the writes) and agent-side osquery `--disable_tables`.
|
|
const fleetMCPInstructions = `Fleet MCP — host management and live osquery on managed devices.
|
|
|
|
CRITICAL WORKFLOW for any tool that takes a 'sql' argument (run_live_query):
|
|
|
|
1. BEFORE writing SQL, call get_osquery_schema(platform=<target>) to fetch the curated table list for that platform.
|
|
2. For any table you reference, verify column NAMES and TYPES against the schema response. If a needed table is not in the curated list, call get_osquery_schema(tables="table1,table2") for full canonical coverage.
|
|
3. Pay attention to column TYPE in the schema response. Many osquery columns are 'text' even when their values look numeric (e.g. windows_update_history.result_code is text with values like 'Succeeded' / 'Failed', NOT integer codes). Comparing a text column against an unquoted integer literal silently returns zero rows.
|
|
4. prepare_live_query already returns the schema for the inferred platform — use it as a single 'preview targets + schema' call, then pass the same filter args to run_live_query.
|
|
|
|
Schema freshness: the in-memory schema is refreshed periodically from https://raw.githubusercontent.com/fleetdm/fleet/main/schema/osquery_fleet_schema.json (the JSON behind https://fleetdm.com/tables). If you suspect a schema mismatch — e.g. fleet docs show a column the response is missing — call refresh_osquery_schema and try again.
|
|
|
|
Team (Fleet) scoping: when the user names a team in the conversation (e.g. "Workstations", "Servers"), pass it as the 'fleet' argument to run_live_query to restrict the targeted hosts to that team. Only omit 'fleet' when the user explicitly wants all teams.
|
|
|
|
CONFIRM BEFORE RUNNING run_live_query: show the operator the exact SQL and the resolved target scope (the host_ids / label / 'fleet', or "all hosts" if unscoped), then wait for explicit confirmation. Never auto-approve.
|
|
|
|
Skipping step 1 produces queries that parse and run but return wrong or empty results. Always verify before emitting SQL.`
|
|
|
|
// SetupMCPServer creates and configures the MCP server with all available tools.
|
|
// Tool registrations are split by domain across mcp_tools_*.go files.
|
|
func SetupMCPServer(config *Config, fleetClient *FleetClient) *server.MCPServer {
|
|
s := server.NewMCPServer(
|
|
"fleet-mcp", "1.0.0",
|
|
server.WithLogging(),
|
|
server.WithInstructions(fleetMCPInstructions),
|
|
)
|
|
|
|
// Kick off background refresh of the osquery schema from the canonical
|
|
// fleetdm/fleet source. Reads the embedded snapshot synchronously at
|
|
// init() so this is purely best-effort freshness.
|
|
StartSchemaRefresh(0)
|
|
|
|
registerHostTools(s, fleetClient)
|
|
registerQueryTools(s, fleetClient)
|
|
registerPolicyTools(s, fleetClient)
|
|
registerInventoryTools(s, fleetClient)
|
|
|
|
return s
|
|
}
|