GitHub
09/04/2026, 7:49 AMosctrl-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/osctrlGitHub
09/04/2026, 8:06 AM