<#1022 Add `osctrl-mcp`: read-only MCP server for ...
# osctrl
g
#1022 Add `osctrl-mcp`: read-only MCP server for fleet inspection Pull request opened by javuto Add a read-only MCP server for osctrl Adds
osctrl-mcp
, a Model Context Protocol server that exposes osctrl's read surface over stdio, so an MCP client (Claude Code, Claude Desktop, or any other) can inspect a fleet — environments, nodes, the osquery schema, and query results. Builds on the
pkg/apiclient
extraction:
*apiclient.OsctrlAPI
satisfies the MCP server's
Backend
interface as-is. What's new •
pkg/mcp
— nine read-only tools behind a narrow
Backend
interface:
list_environments
,
fleet_stats
,
search_nodes
,
get_node
,
list_osquery_tables
,
get_table_schema
,
list_queries
,
list_saved_queries
,
get_query_results
. A compile-time assertion pins the interface, so a signature drift in
pkg/apiclient
breaks in the package rather than at the call site. •
cmd/mcp
— the stdio binary.
make mcp
→
bin/osctrl-mcp
. •
pkg/apiclient
— three endpoints the client didn't cover:
GetStats
,
GetOsqueryTables
, and
GetQueryResults
.
cmd/cli
had been reaching the first and last through inline
GetGeneric
calls; they're typed methods now. •
docs/mcp.md
— build, service-user scoping, client config, tool reference. • Dependency —
<http://github.com/modelcontextprotocol/go-sdk|github.com/modelcontextprotocol/go-sdk>
v1.7.0 (MCP spec 2026-07-28). Authorization The server has no authorization logic of its own. It authenticates to osctrl-api with a Bearer token, so that token's existing per-environment RBAC is what bounds the agent — a token that can't see an environment gets the same empty results the operator would. The docs recommend a dedicated service user scoped to read. The environment projection is a security boundary
environments.TLSEnvironment
carries
Secret
,
EnrollSecretPath
, and
Certificate
. Returning it raw would put live enrollment secrets into a model's context, and from there into whatever transcript store the client keeps.
EnvironmentSummary
exposes four fields, and a test asserts the serialized payload contains no secret material.
search_nodes
caps results (50 default, 500 max) and reports
matched
/
returned
/
truncated
, so a capped list doesn't read to the model as a complete one. Two things the tool text tells the model Distributed queries are asynchronous. Results accumulate as nodes check in, so an empty first page means "not yet", not "no matches".
get_query_results
returns explicit guidance on a zero-row response, and the server instructions tell the model to compare
total_items
rather than trust the first page. Fleet data is untrusted. Hostnames, process names, and result rows come from monitored endpoints — exactly the machines an attacker might control. Tool descriptions and the server instructions state that this content is data, never instructions. Verification Seven tests run over the SDK's in-memory transport, exercising the real MCP handshake, schema generation, and dispatch rather than calling handlers directly. Also confirmed the generated JSON schemas mark the right fields required, and drove the built binary over actual stdio against a stub API — including that the stub's deliberately-planted enrollment secret does not appear in the response.
go build ./...
,
go vet ./...
clean; all 45 packages pass. Limitations • Read-only. Write tools (
run_query
,
tag_nodes
) are planned behind an explicit opt-in. • stdio only. A hosted transport mounted inside osctrl-api is the next step. • Tool calls aren't recorded in osctrl's audit log yet. jmpsec/osctrl