github/github-mcp-server

MCP server

GitHub's official MCP Server

githubGo, TypeScript, JavaScript34,348 starsScanned Oct. 22nd, 2026at b98d115View source

Trust score

33/100Failing
Likely 9–57 · medium confidence

Scanned Oct. 22nd, 2026 at b98d115 against 26 rules · How scoring works

For developersOpen rawGET /c/github/github-mcp-server/report.json
At a glance

At a glance

Contains
1 MCP server · 116 tools
Its tools can
Delete or overwrite data 7Use secrets and credentials 2Change settings 3Returns untrusted content 28Spend money or metered resources 4Post or message on your behalf 2Create or change data 51Read data 60
Issues flagged
4 high5 medium9 of 26 rule checks, severity as assessed for this repo
Where issues are
  • Repository-wide6 of 15
  • mcp-server (MCP server)3 of 11
Trust signals
Verified publisherSigned build10 known CVEs in dependenciesActively maintained

What to fix first

9 issues, most severe first. Each one points to the exact code, with the recommended fix and a prompt you can paste into your coding agent.

  1. 1HighDependency Risk & Supply Chain
    Repository-wide · ATR-C0007
    Trivy identified five CVEs affecting golang.org/x/net v0.59.0, but the only direct imports found in the codebase are the html and httpguts packages, not the HTTP/2 implementation; the advisories establish vulnerable dependency presence rather than a demonstrated HTTP/2 exploitation path reachable from this code. A more concrete supply-chain risk is the release workflow (.github/workflows/registry-releaser.yml), which clones and builds an unversioned/unpinned publisher repository and executes it with OIDC publishing authority, meaning any compromise of that external source could execute arbitrary code with release-publishing credentials. Mutable version tags on actions/cache and actions/setup-node in build-ui/action.yml are a related integrity weakness; the absence of a Dependabot cooldown is a hardening gap rather than demonstrated compromise.
    golang.org/x/net v0.59.0
    golang.org/x/oauth2 v0.37.0
    Show 2 more locations
    module github.com/github/github-mcp-server/v2
    
    go 1.27.3
        - name: Cache UI artifacts
          id: cache-ui
          uses: actions/cache@v6
          with:
            path: |
              pkg/github/ui_dist/get-me.html
              pkg/github/ui_dist/issue-write.html
              pkg/github/ui_dist/pr-write.html
              pkg/github/ui_dist/pr-edit.html
            key: ui-dist-v3-${{ hashFiles(...) }}
            enableCrossOsArchive: true
    
        - name: Set up Node.js
          if: steps.cache-ui.outputs.cache-hit != 'true'
    …

    +6 more locations in the finding JSON

    Recommended fix

    Bump golang.org/x/net to >=0.60.0; pin actions/cache and actions/setup-node to immutable commit SHAs; add dependabot cooldown entries for gomod/npm/docker ecosystems; and pin the registry-releaser workflow's cloned publisher source to a fixed, verified commit or tag rather than building and executing it unpinned with OIDC publish authority.

  2. 2HighTool Output Sanitization & Injection Containment
    Repository-wide · ATR-C0013
    Third-party/user-authored content returned by tools (issue bodies, PR bodies, comments, discussions, etc.) is passed through sanitize.Content before being placed into tool results, but sanitize.Content only filters invisible/zero-width/bidi Unicode characters -- it does not strip HTML or code-fence metadata; only sanitize.PlainText, used for title-type fields, performs that additional filtering. An all-ASCII issue body containing a fabricated role/instruction marker passes through sanitize.Content unchanged. The IFC untrusted-content labeling mechanism provides a real mitigation but is feature-gated and is omitted when a visibility lookup fails, so it does not provide a mandatory, universally-applied containment policy. The JSON structural boundary around tool results is a real but partial mitigation that does not, by itself, neutralize crafted instruction-like text from being included verbatim in model-visible output, though this does not demonstrate that a downstream model actually executes an injected instruction.
    func convertToMinimalIssue(issue *github.Issue) MinimalIssue {
    	m := MinimalIssue{
    		Number:            issue.GetNumber(),
    		Title:             sanitize.PlainText(issue.GetTitle()),
    		Body:              sanitize.Content(issue.GetBody()),
    		State:             issue.GetState(),
    Show 2 more locations
    func Content(input string) string {
    	return FilterInvisibleCharacters(input)
    }
    		}
    	}
    	return input
    }
    
    // filterInvisibleFrom resumes FilterInvisibleCharacters at start, the first byte
    // that could need filtering. It buffers output only once a rune actually
    // changes, so input that turns out to be clean is still returned as-is.
    func filterInvisibleFrom(input string, start int) string {
    	var (

    +2 more locations in the finding JSON

    Recommended fix

    Apply instruction/role-marker-aware filtering, not only invisible-character stripping, to all third-party content routed through sanitize.Content, and make IFC untrusted-content labeling mandatory (fail-closed) rather than omitted on visibility-lookup failure, so externally authored text cannot masquerade as system/tool-level instructions.

  3. 3HighBinding to 0.0.0.0 Without Auth/TLS
    mcp-server (MCP server) · ATR-M0004
    The HTTP server binds to all interfaces by default when --listen-host is unset (resolveListenAddress returns ':8082'), and the default self-hosted deployment serves this listener via plaintext ListenAndServe with no TLS termination enforced in-process or required by configuration. Every request is gated by mandatory bearer-token authentication (ExtractUserToken middleware returns 401 with a WWW-Authenticate challenge when the Authorization header is missing), so an unauthenticated caller cannot invoke any tool merely by reaching the port. However, this check does not protect the bearer credential itself in transit: a caller connecting to this default, all-interface, non-TLS listener from a remote network position can have their Authorization header intercepted on-path, since no TLS or loopback restriction is enforced by default for the self-hosted binary. The documented hosted endpoint (https://api.githubcopilot.com/mcp/) is a separate, TLS-terminated deployment and does not protect operators running the default self-hosted configuration directly.
    func resolveListenAddress(host string, port int) string {
    	if host == "" {
    		return fmt.Sprintf(":%d", port)
    	}
    	return net.JoinHostPort(host, strconv.Itoa(port))
    }
    Show 2 more locations
    	addr := resolveListenAddress(cfg.ListenHost, cfg.Port)
    	httpSvr := http.Server{
    		Addr:              addr,
    		Handler:           r,
    		ReadHeaderTimeout: 60 * time.Second,
    	}
    	logger.Info("HTTP server listening", "addr", addr)
    	if err := httpSvr.ListenAndServe(); err != nil && err != http.ErrServerClosed {
    		return fmt.Errorf("HTTP server error: %w", err)
    	}

    +2 more locations in the finding JSON

    Recommended fix

    Default --listen-host to 127.0.0.1 so the server does not bind to all interfaces without an explicit operator opt-in, and/or require (not merely document) that the process sit behind a TLS-terminating reverse proxy before accepting bearer-token-bearing requests; refuse to start in all-interface mode without TLS unless explicitly overridden.

  4. 4HighTransport Security: TLS Everywhere (Remote)
    mcp-server (MCP server) · ATR-M0009
    RunHTTPServer binds to all interfaces by default via an address derived from an unset --listen-host and serves the connection via plaintext ListenAndServe -- there is no TLS/loopback requirement enforced, and no TLS-terminating proxy is required by the executable itself. Outbound client traffic to the GitHub API does enforce HTTPS, but that outbound enforcement does not secure the inbound MCP transport that this rule evaluates. The documented hosted endpoint (https://api.githubcopilot.com/mcp/) is a separately TLS-terminated deployment and does not establish TLS protection for this executable's own default, self-hosted startup path; a caller connecting directly to the default HTTP listener transmits its bearer Authorization header in cleartext, which an on-path attacker could intercept.
    			}
    
    			return ghhttp.RunHTTPServer(httpConfig)
    		},
    Show 2 more locations
    	addr := resolveListenAddress(cfg.ListenHost, cfg.Port)
    	httpSvr := http.Server{
    		Addr:              addr,
    		Handler:           r,
    		ReadHeaderTimeout: 60 * time.Second,
    	}
    	logger.Info("HTTP server listening", "addr", addr)
    	if err := httpSvr.ListenAndServe(); err != nil && err != http.ErrServerClosed {
    		return fmt.Errorf("HTTP server error: %w", err)
    	}

    +2 more locations in the finding JSON

    Recommended fix

    Require TLS for the HTTP listener by default (or refuse to serve bearer-token-protected routes over plaintext without an explicit operator override), and/or make a TLS-terminating reverse proxy mandatory rather than merely documented for any non-loopback binding.

  5. 5MediumDoS Protections: Timeouts, Concurrency, Streaming Limits
    Repository-wide · ATR-C0004
    The HTTP server sets ReadHeaderTimeout (60s), which bounds only the time to receive request headers. The request body is read via io.ReadAll in the handler path with no accompanying read deadline or per-request timeout, so a client that completes headers and then sends a body slowly, staying under the configured max-body-size, can hold the connection and its associated goroutine open indefinitely. Neither the max-body-size limit nor the batch-operation semaphores bound this slow-body read path, and no request-level or body-read deadline is configured distinct from the server's long-lived streaming response policy.
    	r := newHTTPRouter(
    		func(r chi.Router) {
    			// Register Middleware First, needs to be before route registration
    			handler.RegisterMiddleware(r)
    			// Register MCP server routes
    			handler.RegisterRoutes(r)
    		},
    		oauthHandler.RegisterRoutes,
    	)
    	logger.Info("MCP endpoints registered", "baseURL", cfg.BaseURL)
    	logger.Info("OAuth protected resource endpoints registered", "baseURL", cfg.BaseURL)
    
    	addr := resolveListenAddress(cfg.ListenHost, cfg.Port)
    	httpSvr := http.Server{
    …
    Show 2 more locations
    func (h *Handler) RegisterMiddleware(r chi.Router) {
    	r.Use(
    		// Must run first: bounds the body before anything downstream reads it.
    		middleware.WithMaxBodySize(h.maxRequestBodyBytes()),
    		middleware.ExtractUserToken(h.oauthCfg),
    		middleware.WithRequestConfig,
    		middleware.WithMCPParse(),
    		middleware.WithPATScopes(h.logger, h.scopeFetcher),
    	)
    			// Only parse POST requests (MCP uses JSON-RPC over POST)
    			if r.Method != http.MethodPost {
    				next.ServeHTTP(w, r)
    				return
    			}
    
    			// Read the request body
    			body, err := io.ReadAll(r.Body)
    			if err != nil {
    				if isMaxBytesError(err) {
    					writeRequestTooLarge(w)
    					return
    				}

    +1 more location in the finding JSON

    Recommended fix

    Add an explicit read deadline (e.g. a timeout handler, SetReadDeadline, or a per-request context deadline applied before io.ReadAll) for request bodies, separate from the timeout used for long-lived streaming responses, so slow-body connections are closed after a bounded interval.

Show the other 4 issues
  1. 6MediumLogging Hygiene (No Secrets/PII/Tokens) & Audit Trail
    Repository-wide · ATR-C0005
    When command/transport logging is enabled (opt-in), the IOLogger logs every incoming and outgoing JSON-RPC message at Info level, including raw tool inputs and results, without redaction or scrubbing. This can include private tool inputs/results and potentially PII or secrets present in repository content. The log file is created with mode 0600 and the feature is opt-in, which reduces exposure, but the logging still writes unscrubbed payloads at a non-debug level, which does not meet the bar of ensuring sensitive content is scrubbed even when verbose logging is enabled. Scope-name logging on the auth path remains clean, recording only scope names rather than secrets.
    		in = os.Stdin
    		out = os.Stdout
    
    		if cfg.EnableCommandLogging {
    			loggedIO := mcplog.NewIOLogger(in, out, logger)
    			in, out = loggedIO, loggedIO
    		}
    
    		// enable GitHub errors in the context
    		ctx := errors.ContextWithGitHubErrors(ctx)
    		errC <- ghServer.Run(ctx, &mcp.IOTransport{Reader: in, Writer: out})
    	}()
    Show 2 more locations
    // Read reads data from the underlying io.Reader and logs it.
    func (l *IOLogger) Read(p []byte) (n int, err error) {
    	if l.reader == nil {
    		return 0, io.EOF
    	}
    	n, err = l.reader.Read(p)
    	if n > 0 {
    		l.logger.Info("[stdin]: received bytes", "count", n, "data", string(p[:n]))
    	}
    	return n, err
    }
    
    // Write writes data to the underlying io.Writer and logs it.
    func (l *IOLogger) Write(p []byte) (n int, err error) {
    …
    	if cfg.LogFilePath != "" {
    		file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
    		if err != nil {
    			return fmt.Errorf("failed to open log file: %w", err)
    		}
    		logOutput = file
    		slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelDebug})
    	} else {
    		logOutput = os.Stderr
    		slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo})
    	}
    Recommended fix

    Redact or scrub tool inputs/results before writing them to the IOLogger, or gate raw payload logging to an explicit debug level with documented scrubbing, rather than logging raw JSON-RPC bytes at Info level.

  2. 7MediumContainer/Runtime Hardening
    Repository-wide · ATR-C0006
    The final runtime stage of the Dockerfile is FROM gcr.io/distroless/base-debian12 with no USER directive and no securityContext hardening, so the ENTRYPOINT process runs as root inside the container. If the server process were compromised via any future vulnerability, it would execute as root within the container, maximizing blast radius for container-breakout-adjacent primitives. The distroless base ships no shell/package manager, narrowing but not eliminating impact.
    # Make a stage to run the app
    FROM gcr.io/distroless/base-debian12@sha256:fabbf1c0c357a3d42550111351daed089b20a2c954df13ee2fcff60602515e84
    
    LABEL io.modelcontextprotocol.server.name="io.github.github/github-mcp-server"
    
    WORKDIR /server
    COPY --from=build /bin/github-mcp-server .
    EXPOSE 8082
    ENTRYPOINT ["/server/github-mcp-server"]
    CMD ["stdio"]
    Show 1 more location
    FROM node:26-alpine@sha256:e88a35be04478413b7c71c455cd9865de9b9360e1f43456be5951032d7ac1a66 AS ui-build
    WORKDIR /app
    COPY ui/package*.json ./ui/
    RUN cd ui && npm ci
    COPY ui/ ./ui/
    # Create output directory and build - vite outputs directly to pkg/github/ui_dist/
    RUN mkdir -p ./pkg/github/ui_dist && \
        cd ui && npm run build
    
    FROM golang:1.27.3-alpine@sha256:f2c872bcee949b7be9c490efd02b48a8a2fbf947cdf533f1c58525ec9d8170fd AS build
    Recommended fix

    Use gcr.io/distroless/base-debian12:nonroot (or add an explicit non-root USER/numeric UID) as the final base image so the ENTRYPOINT does not run as root; add a HEALTHCHECK and run with a read-only root filesystem at deployment time.

  3. 8MediumDenial of Wallet / Cost Controls
    Repository-wide · ATR-C0012
    The server exposes actions_run_trigger, which allows a caller with the requisite repository credential to dispatch or rerun GitHub Actions workflows repeatedly, consuming billable runner time. The handler validates parameters and requires appropriate repo scope before allowing the call, but there is no per-user or per-task dispatch quota or spending check layered on top of that access control. GitHub's API rate limits bound request throughput, not the monetary cost or duration of workflows that are launched, so they do not function as a cost control for this capability. This is a missing cost-control layer on an intentionally exposed capability, not an authentication or authorization bypass.
    		// Actions tools
    		ActionsList(t),
    		ActionsGet(t),
    		ActionsRunTrigger(t),
    		ActionsGetJobLogs(t),
    Show 2 more locations
    		ToolsetMetadataActions,
    		mcp.Tool{
    			Name:         "actions_run_trigger",
    			OutputSchema: actionsRunTriggerOutputSchema(),
    			Description:  t("TOOL_ACTIONS_RUN_TRIGGER_DESCRIPTION", "Trigger GitHub Actions workflow operations, including running, re-running, cancelling workflow runs, and deleting workflow run logs."),
    			Annotations: &mcp.ToolAnnotations{
    		scopes.RequireAll(scopes.Repo),
    		func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args ActionsRunTriggerInput) (*mcp.CallToolResult, *ActionsRunTriggerOutput, error) {
    			owner, repo, method := args.Owner, args.Repo, args.Method
    			workflowID, ref, runID, inputs := args.WorkflowID, args.Ref, args.RunID, args.Inputs
    
    			// Validate required parameters based on action type
    			if method == actionsMethodRunWorkflow {
    				if workflowID == "" {
    					return utils.NewToolResultError("workflow_id is required for run_workflow action"), nil, nil
    				}
    				if ref == "" {
    					return utils.NewToolResultError("ref is required for run_workflow action"), nil, nil
    				}
    			} else if runID == 0 {
    …

    +3 more locations in the finding JSON

    Recommended fix

    Add a per-user or per-task quota/rate limit on actions_run_trigger dispatch/rerun calls, and consider requiring explicit confirmation or a cost-awareness flag before dispatching workflows, to prevent runaway billable CI usage.

  4. 9MediumCORS / Origin / Host Validation (DNS Rebinding Defense)
    mcp-server (MCP server) · ATR-M0003
    SetCorsHeaders unconditionally sets 'Access-Control-Allow-Origin: *' with no Origin allowlist or validation anywhere in the request path, and the HTTP server binds to all interfaces by default when --listen-host is unset -- together these independently satisfy the rule's missing-Origin-validation-plus-all-interface-binding impact condition. Authentication is enforced via an explicit Authorization bearer header rather than cookies or other ambient credentials, which a cross-origin page cannot cause a browser to attach automatically; this mitigates classic ambient-credential CSRF/DNS-rebinding exploitation, since a rebound-origin page cannot forge the victim's bearer token into its requests without already possessing it. This mitigation reduces exploitability but does not establish a demonstrated browser-token-theft or DNS-rebinding exploit chain -- no explicit Origin or Host allowlist exists to close the gap this rule evaluates.
    // SetCorsHeaders is middleware that sets CORS headers to allow browser-based
    // MCP clients to connect from any origin. This is safe because the server
    // authenticates via bearer tokens (not cookies), so cross-origin requests
    // cannot exploit ambient credentials.
    func SetCorsHeaders(h http.Handler) http.Handler {
    	fixedAllowHeaders := strings.Join(corsAllowedRequestHeaders, ", ")
    
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		w.Header().Set("Access-Control-Allow-Origin", "*")
    		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
    		w.Header().Set("Access-Control-Max-Age", "86400")
    		w.Header().Add("Access-Control-Expose-Headers", "Mcp-Session-Id, WWW-Authenticate")
    		w.Header().Set("Access-Control-Allow-Headers", fixedAllowHeaders)
    Show 2 more locations
    func resolveListenAddress(host string, port int) string {
    	if host == "" {
    		return fmt.Sprintf(":%d", port)
    	}
    	return net.JoinHostPort(host, strconv.Itoa(port))
    }
    func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    	inv, err := h.inventoryFactoryFunc(r)
    	...
    	ghServer, err := h.githubMcpServerFactory(r, h.deps, invToUse, &github.MCPServerConfig{ ... })
    Recommended fix

    Add an explicit Origin allowlist (or at minimum validate the Host header matches the expected deployment host) in front of the MCP routes, in addition to the existing bearer-token requirement, to provide defense-in-depth against DNS rebinding for any client that does cache/attach tokens automatically.

What’s inside

BlueRock found 1 component in this repository. Each is checked against the rules for its type.

MCP servermcp-server.38Failing2 high1 medium

Its tools can

Delete or overwrite data 7Use secrets and credentials 2Change settings 3Returns untrusted content 28Spend money or metered resources 4Post or message on your behalf 2Create or change data 51Read data 60

What stood out

  • Direct dependency golang.org/x/net v0.59.0 carries an unpatched HIGH-severity CVE (CVE-2026-78669, HTTP/2 DoS) per Trivy -- fix available in v0.60.0.
  • Final Docker runtime stage (gcr.io/distroless/base-debian12) has no USER directive and runs as root (confirmed by Trivy DS-0002 and Semgrep dockerfile.security.missing-user); narrower blast radius due to distroless having no shell, but still a hardening gap.
  • HTTP transport binds to all interfaces by default and sets Access-Control-Allow-Origin:* with no Origin/Host allowlist (ATR-M0003); mitigated in practice by mandatory bearer-token auth, but no defense-in-depth Origin check exists.
  • Supply-chain hygiene gaps: mutable (non-SHA) action tags in .github/actions/build-ui/action.yml, and missing Dependabot cooldown for gomod/npm/docker ecosystems.
  • mcpcurl (cmd/mcpcurl) is a developer CLI that intentionally executes operator-supplied shell commands (Semgrep dangerous-exec-command); it is a local test/dev tool, not part of the deployed MCP server's attack surface.

MCP server rules 3 of 11 flagged

7 rules passed · 1 did not applyShow
PassedOAuth 2.1 + PKCE Correctness (MCP Authorization)ATR-M0005Details

Why it got this result

Remote (HTTP) transport: the server acts strictly as an OAuth 2.0 Protected Resource (RFC 9728) and never implements token issuance itself -- it serves /.well-known/oauth-protected-resource metadata pointing clients at GitHub's own authorization server, which is the MCP-spec-compliant delegation pattern. Local (stdio) transport: internal/oauth/flow.go implements authorization-code plus PKCE with S256 challenge, a cryptographically random state parameter validated on callback (a 'state mismatch' check), and a local callback listener; native CLI runs bind this callback listener to loopback, while the Docker-container code path explicitly binds it to all interfaces when running in a container, accompanied by an explicit warning instructing operators to publish the port on loopback only. State validation and PKCE verification are enforced regardless of which interface the callback listener binds to, so the container-mode binding does not itself defeat the CSRF/code-interception protections those checks provide.

Why it matters

Misconfig enables token theft and confused-deputy attacks.

Evidence

func (m *Manager) beginPKCE(prompter Prompter) (*flowPlan, error) {
	state, err := randomState()
	...
	verifier := oauth2.GenerateVerifier()
	...
	authURL := oc.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))
	run := func(ctx context.Context) (*oauth2.Token, error) {
		code, err := cs.wait(ctx)
		...
		tok, err := oc.Exchange(ctx, code, oauth2.VerifierOption(verifier))
		...
Show 2 more locations
		if q.Get("state") != expectedState {
			cs.report(callbackResult{err: fmt.Errorf("state mismatch (possible CSRF)")})
			renderError(w, "state mismatch")
			return
		}
func (h *AuthHandler) metadataHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		...
		metadata := &oauthex.ProtectedResourceMetadata{
			Resource:               resourceURL,
			AuthorizationServers:   []string{authorizationServerURL},
			...
PassedNo Authentication or Authorization Checks (General)ATR-M0010Details

Why it got this result

The HTTP listener requires an Authorization header on every request before tool dispatch: ExtractUserToken middleware parses and validates the header's format/prefix and denies (401 plus WWW-Authenticate challenge) any request with a missing or malformed Authorization header, fail-closed by default; it runs after the request body-size limiting middleware rather than first in the chain. This middleware validates token format/prefix rather than verifying the credential's secret value itself -- the caller's own token is threaded through context and the real authorization/credential check happens when that token is used to call the GitHub API, so every tool action's actual authorization is enforced by GitHub against the caller's own scopes and secret, not by a locally implemented secret-comparison step. There is no shared or ambient service-account identity applied across callers; each request's own credential is what GitHub ultimately verifies.

Why it matters

Prevents unauthorized tool use and data access.

Evidence

func (h *Handler) RegisterMiddleware(r chi.Router) {
	r.Use(
		middleware.WithMaxBodySize(h.maxRequestBodyBytes()),
		middleware.ExtractUserToken(h.oauthCfg),
		middleware.WithRequestConfig,
		middleware.WithMCPParse(),
		middleware.WithPATScopes(h.logger, h.scopeFetcher),
	)
	if h.config.ScopeChallenge {
		r.Use(middleware.WithScopeChallenge(h.oauthCfg, h.scopeFetcher))
	}
}
Show 1 more location
			tokenType, token, err := utils.ParseAuthorizationHeader(r)
			if err != nil {
				if errors.Is(err, utils.ErrMissingAuthorizationHeader) {
					sendAuthChallenge(w, r, oauthCfg)
					return
				}
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
PassedTool Definition Integrity & Versioning (ETDI-style)ATR-M0001Details

Why it got this result

All 116+ MCP tools are statically compiled Go code registered via AllTools() in pkg/github/tools.go; there is no runtime fetch or hot-reload of tool manifests from a URL or external file. Releases are built and published via CI workflows carrying signed/attested build provenance, and a dedicated MCP Server Diff workflow runs on every PR to flag unexpected tool schema changes. However, pkg/translations/translations.go does read a github-mcp-server-config.json file from the current working directory and supports environment-variable overrides for tool description text, so not every piece of tool metadata is strictly immutable at runtime, though no attacker-controlled path to reach or modify that local config/environment at runtime was established. The diff workflow's presence demonstrates comparison reporting on schema changes; it does not by itself demonstrate that human approval or branch protection is mandatorily enforced before a changed definition can ship.

Why it matters

Prevents rug-pull tool poisoning.

Evidence

func AllTools(t translations.TranslationHelperFunc, opts ...ToolOption) []inventory.ServerTool {
	cfg := newToolConfig(opts)
	return withCSVOutput([]inventory.ServerTool{
		// Context tools
		GetMe(t),
		GetTeams(t),
		GetTeamMembers(t),

		// Repository tools
		SearchRepositories(t),
		GetFileContents(t),
		...
Show 1 more location
name: MCP Server Diff

on:
  pull_request:
  push:
    branches: [main]
    tags: ['v*']

permissions:
  contents: read

jobs:
  mcp-diff:
    runs-on: ubuntu-latest
…
PassedUnrestricted Tool Permissions (Overprivileged Tools)ATR-M0002Details

Why it got this result

Tools are scoped using GitHub's own OAuth-scope model: pkg/scopes/scopes.go defines RequireAll/PublicRead/DynamicChallenge helpers that gate each tool's visibility/challenge behavior by the OAuth scopes it needs. pkg/http/handler.go's PATScopeFilter narrows the visible tool inventory to the caller's actual PAT scopes, but this filtering is advisory defense-in-depth rather than a universal, fail-closed control: it skips filtering for non-classic tokens and returns the inventory unchanged when a scope-fetch call errors. The control that actually bounds privilege regardless of this filtering is that every tool call is ultimately executed against GitHub's REST/GraphQL API using the caller's own token, so GitHub's server-side permission system, not the scope-filter narrowing, is the final enforcement boundary. Read-only mode (--read-only / X-MCP-Readonly) still enforces an upper bound on tool visibility that per-request headers cannot widen.

Why it matters

Limits blast radius and privilege escalation.

Evidence

func RequireAll(required ...Scope) inventory.ScopeAccess {
	required = append([]Scope(nil), required...)
	requiredScopes := scopeStrings(required)
	return inventory.ScopeAccess{
		Scopes: append([]string(nil), requiredScopes...),
		Visible: func(activeScopes []string) bool {
			return HasAll(activeScopes, required...)
		},
		Challenge: func(_ map[string]any, activeScopes []string) []string {
			if HasAll(activeScopes, required...) {
				return nil
			}
			return append([]string(nil), requiredScopes...)
		},
…
Show 1 more location
func PATScopeFilter(b *inventory.Builder, r *http.Request, fetcher scopes.FetcherInterface) *inventory.Builder {
	ctx := r.Context()
	tokenInfo, ok := ghcontext.GetTokenInfo(ctx)
	if !ok || tokenInfo == nil {
		return b
	}
	if tokenInfo.TokenType == utils.TokenTypePersonalAccessToken {
		existingScopes, ok := ghcontext.GetTokenScopes(ctx)
		if ok {
			return b.WithFilter(github.CreateToolScopeFilter(existingScopes))
		}
		...
PassedCORS Misuse: Wildcard with CredentialsATR-M0008Details

Why it got this result

SetCorsHeaders sets 'Access-Control-Allow-Origin: *' but never sets 'Access-Control-Allow-Credentials: true' anywhere in the codebase. Authentication uses an explicit Authorization bearer header rather than cookies, so the dangerous wildcard-origin-plus-credentials combination this rule targets does not exist.

Why it matters

Prevents credentialed cross-origin abuse.

Evidence

func SetCorsHeaders(h http.Handler) http.Handler {
	fixedAllowHeaders := strings.Join(corsAllowedRequestHeaders, ", ")
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Max-Age", "86400")
		w.Header().Add("Access-Control-Expose-Headers", "Mcp-Session-Id, WWW-Authenticate")
		w.Header().Set("Access-Control-Allow-Headers", fixedAllowHeaders)
		...
Show 1 more location
assert.Empty(t, rr.Header().Get("Access-Control-Allow-Credentials"))
PassedTool Name Collision / SpoofingATR-M0006Details

Why it got this result

Enumerated all 116+ registered tool names (e.g. get_file_contents, list_issues, search_code, create_pull_request). None are homographs of, or identical to, a specifically-named tool from another widely-deployed MCP server or client built-in. Names are generic GitHub-domain verbs, which the rule's own negative indicator treats as not itself a positive indicator absent a named collision.

Why it matters

Prevents malicious tool shadowing.

Evidence

func AllTools(t translations.TranslationHelperFunc, opts ...ToolOption) []inventory.ServerTool {
	cfg := newToolConfig(opts)
		SearchRepositories(t), GetFileContents(t), ListCommits(t), SearchCode(t), ...
PassedPrompt/Metadata Injection in Tool DescriptionsATR-M0007Details

Why it got this result

Searched all tool-description strings in pkg/github/*.go for zero-width/invisible Unicode characters and for common injection phrasing -- no matches found. Tool descriptions are short, functional, English-language strings describing the GitHub operation performed. Separately, the server actively sanitizes untrusted content returned from the GitHub API before it reaches the model, which is a related but distinct control from tool-description integrity.

Why it matters

Prevents covert model manipulation.

Evidence

func Sanitize(input string) string {
	filtered := FilterCodeFenceMetadata(FilterInvisibleCharacters(input))
	normalized := FilterHTMLTags(filtered)
	if normalized == filtered {
		return normalized
	}
	return FilterCodeFenceMetadata(FilterInvisibleCharacters(normalized))
}
Does not applyOAuth Proxy Per-Client Consent (Confused Deputy Defense)ATR-M0011Details

Why it got this result

This server is not an OAuth proxy. The HTTP transport only publishes OAuth Protected Resource Metadata pointing MCP clients directly at GitHub's own authorization server -- it never issues, proxies, or mints bridge authorization codes on a client's behalf, and exposes no dynamic MCP-client registration endpoint. The stdio flow is a direct, single-tenant OAuth client rather than a multi-tenant proxy forwarding to a distinct upstream IdP for dynamically registered third-party clients.

Why it matters

Shared upstream client + dynamic MCP-client registration + no per-client consent enables attacker-registered MCP clients to receive bridge codes carrying victim upstream tokens — the documented MCP Confused Deputy attack.

Evidence

func (h *AuthHandler) metadataHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		...
		metadata := &oauthex.ProtectedResourceMetadata{
			Resource:               resourceURL,
			AuthorizationServers:   []string{authorizationServerURL},
			ResourceName:           "GitHub MCP Server",
			...
		auth.ProtectedResourceMetadataHandler(metadata).ServeHTTP(w, r)
	})
}
Show its 116 tools
ToolIt can
actions_get
Read dataReturns untrusted content
actions_list
Read dataReturns untrusted content
actions_run_trigger
Create or change dataSpend money or metered resourcesDelete or overwrite data
add_comment_to_pending_review
Create or change data
add_issue_comment
Create or change data
add_issue_comment_reaction
Create or change data
add_issue_reaction
Create or change data
add_pull_request_review_comment
Create or change data
add_pull_request_review_comment_reaction
Create or change data
add_reply_to_pull_request_comment
Create or change data
assign_copilot_to_issue
Create or change dataSpend money or metered resources
assign_copilot_to_issue_with_intent
Create or change dataSpend money or metered resources
create_branch
Create or change data
create_gist
Create or change data
create_issue
Create or change data
create_or_update_file
Create or change data
create_pull_request
Create or change data
create_pull_request_review
Create or change data
create_repository
Create or change data
create_repository_ruleset
Change settings
custom_properties_read
Read data
custom_properties_write
Change settings
delete_file
Create or change dataDelete or overwrite data
delete_pending_pull_request_review
Create or change dataDelete or overwrite data
delete_repository
Delete or overwrite dataChange settings
discussion_comment_write
Create or change data
dismiss_notification
Create or change data
find_duplicate
Read dataReturns untrusted content
fork_repository
Create or change data
get_code_quality_finding
Read data
get_code_scanning_alert
Read data
get_commit
Read dataReturns untrusted content
get_dependabot_alert
Read data
get_discussion
Read dataReturns untrusted content
get_discussion_comments
Read dataReturns untrusted content
get_file_blame
Read dataReturns untrusted content
get_file_contents
Read dataReturns untrusted content
get_gist
Read dataReturns untrusted content
get_global_security_advisory
Read data
get_job_logs
Read dataReturns untrusted content
get_label
Read data
get_latest_release
Read dataReturns untrusted content
get_me
Read data
get_notification_details
Read dataReturns untrusted content
get_release_by_tag
Read dataReturns untrusted content
get_repository_tree
Read data
get_secret_scanning_alert
Read dataUse secrets and credentials
get_tag
Read data
get_team_members
Read data
get_teams
Read data
issue_dependency_read
Read data
issue_dependency_write
Create or change data
issue_read
Read dataReturns untrusted content
issue_write
Create or change data
label_write
Create or change dataDelete or overwrite data
list_branches
Read data
list_code_scanning_alerts
Read data
list_commits
Read dataReturns untrusted content
list_dependabot_alerts
Read data
list_discussion_categories
Read data
list_discussions
Read dataReturns untrusted content
list_gists
Read dataReturns untrusted content
list_global_security_advisories
Read data
list_issue_fields
Read data
list_issue_types
Read data
list_issues
Read dataReturns untrusted content
list_label
Read data
list_notifications
Read data
list_org_repository_security_advisories
Read data
list_pull_requests
Read dataReturns untrusted content
list_releases
Read dataReturns untrusted content
list_repository_collaborators
Read data
list_repository_security_advisories
Read data
list_secret_scanning_alerts
Read dataUse secrets and credentials
list_starred_repositories
Read data
list_tags
Read data
manage_notification_subscription
Create or change data
manage_repository_notification_subscription
Create or change data
mark_all_notifications_read
Create or change data
merge_pull_request
Create or change data
projects_get
Read dataReturns untrusted content
projects_list
Read dataReturns untrusted content
projects_write
Create or change dataDelete or overwrite data
pull_request_read
Read dataReturns untrusted content
pull_request_review_write
Create or change dataDelete or overwrite data
push_files
Create or change data
remove_issue_comment_reaction
Create or change data
remove_issue_reaction
Create or change data
remove_pull_request_review_comment_reaction
Create or change data
repository_ruleset_read
Read data
request_copilot_review
Create or change dataSpend money or metered resources
request_pull_request_reviewers
Create or change data
resolve_review_thread
Create or change data
search_code
Read dataReturns untrusted content
search_commits
Read dataReturns untrusted content
search_issues
Read dataReturns untrusted content
search_orgs
Read data
search_pull_requests
Read dataReturns untrusted content
search_repositories
Read dataReturns untrusted content
search_users
Read data
set_issue_fields
Create or change data
star_repository
Post or message on your behalf
sub_issue_write
Create or change data
submit_pending_pull_request_review
Create or change data
ui_get
Read data
unresolve_review_thread
Create or change data
unstar_repository
Post or message on your behalf
update_gist
Create or change data
update_issue_assignees
Create or change data
update_issue_comment
Create or change data
update_issue_labels
Create or change data
update_issue_state
Create or change data
update_issue_type
Create or change data
update_pull_request
Create or change data
update_pull_request_branch
Create or change data
update_pull_request_draft_state
Create or change data

Before you put it to work

  1. Bump golang.org/x/net to >=0.60.0 and re-run go mod tidy to resolve CVE-2026-78669 and related CVEs.
  2. Add a non-root USER directive to the final Docker stage (e.g. gcr.io/distroless/base-debian12:nonroot).
  3. Add an Origin/Host allowlist (or strict same-site check) to the HTTP transport as defense-in-depth alongside the existing bearer-token requirement.
  4. Default --listen-host to 127.0.0.1 for local/dev usage and document that production deployments must sit behind a TLS-terminating proxy.
  5. Pin all GitHub Actions to full commit SHAs and add Dependabot cooldown blocks for gomod/npm/docker ecosystems.
  6. Set explicit Content-Type on all HTTP error responses that echo request-derived strings, and refactor registry-releaser.yml to use env-indirection for github.ref_type/github.ref_name consistent with the rest of the repo.

Repository-wide rules

Checked once for the whole repository: dependencies, secrets, inputs, networking, CI and more. 6 of 15 flagged.

9 rules passedShow
PassedRuntime Command Injection via Process ExecutionATR-C0003Details

Why it got this result

All exec.Command call sites were enumerated: openBrowser() invokes a fixed platform launcher with an internally-constructed OAuth URL as a single argv element, with the URL produced by the server's own OAuth flow code rather than a remote attacker, and argv-vector execution prevents shell metacharacter injection regardless. The mcpcurl developer CLI builds a command from an operator-supplied string but is a standalone developer/test tool, not the shipped MCP server runtime. No exec/shell invocation was found reachable from the actual server runtime's tool-call handling path.

Why it matters

Unsanitized command construction in shipped runtime code can give a remote attacker arbitrary code execution with the application's privileges.

Evidence

func openBrowser(url string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "linux":
		if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" {
			return errNoDisplay
		}
		cmd = exec.Command("xdg-open", url)
	case "darwin":
		cmd = exec.Command("open", url)
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
Show 2 more locations
func executeServerCommand(cmdStr, jsonRequest string) (string, error) {
	cmdParts := strings.Fields(cmdStr)
	if len(cmdParts) == 0 {
		return "", fmt.Errorf("empty command")
	}

	cmd := exec.Command(cmdParts[0], cmdParts[1:]...) //nolint:gosec //mcpcurl is a test command that needs to execute arbitrary shell commands

+3 more locations in the finding JSON

PassedWeaponization: Malicious-Purpose Artifact (Offensive Capability by Design)ATR-R0001Details

Why it got this result

This repository is the official github/github-mcp-server: its declared purpose is to let an AI agent interact with GitHub repositories, issues, PRs, CI/CD, and code analysis via the GitHub API. No bundled script, tool description, or instruction body directs generation of malware, exploits, credential theft, or AV/EDR evasion, and no capability evades oversight/attribution. This is unambiguously a defensive/productivity developer tool, not offensive tooling.

Why it matters

An artifact whose purpose is to weaponize the agent -- to produce malware, steal credentials, evade defenses, or run attacks at scale -- is dangerous regardless of how securely it is built; posture controls (auth, input validation, least privilege) cannot make a weapon safe, which is why a confirmed weaponization finding caps the trust verdict to `blocked` rather than being averaged against a good posture score. Catching malicious PURPOSE is a distinct safety control, and it must be conservative because offensive capability is dual-use: legitimate pentest, CTF, defensive, and research work must abstain (UNKNOWN), never block -- unless the artifact itself hides from the people who install, run or audit it.

Evidence

# GitHub MCP Server

The GitHub MCP Server connects AI tools directly to GitHub's platform. This gives AI agents, assistants, and chatbots the ability to read repositories and code files, manage issues and PRs, analyze code, and automate workflows. All through natural language interactions.

### Use Cases

- Repository Management: Browse and query code, search files, analyze commits, and understand project structure across any repository you have access to.
- Issue & PR Automation: Create, update, and manage issues and pull requests. Let AI help triage bugs, review code changes, and maintain project boards.
- CI/CD & Workflow Intelligence: Monitor GitHub Actions workflow runs, analyze build failures, manage releases, and get insights into your development pipeline.
- Code Analysis: Examine security findings, review Dependabot alerts, understand code patterns, and get comprehensive insights into your codebase.
- Team Collaboration: Access discussions, manage notifications, analyze team activity, and streamline processes for your team.
PassedUnrestricted Network Fetch / SSRFATR-C0001Details

Why it got this result

The repo exposes no tool that fetches an arbitrary attacker-supplied URL/host. The only outbound HTTP fetch of a dynamic URL is downloadLogContent(), which fetches a short-lived download URL returned by an authenticated GitHub API call, not a user-supplied host. The raw-content client builds URLs by joining a fixed, server-configured base host with owner/repo/ref/path, explicitly rejecting path-traversal segments. All other GitHub API calls go through a fixed-base-URL SDK client. No generic fetch-URL style tool exists.

Why it matters

Enables internal pivoting and data exfiltration.

Evidence

func downloadLogContent(ctx context.Context, logURL string, tailLines int, maxLines int) (string, int, *http.Response, error) {
	prof := profiler.New(nil, profiler.IsProfilingEnabled())
	finish := prof.Start(ctx, "log_buffer_processing")

	httpResp, err := http.Get(logURL) //nolint:gosec
	if err != nil {
		return "", 0, httpResp, fmt.Errorf("failed to download logs: %w", err)
	}
	defer func() { _ = httpResp.Body.Close() }()
Show 2 more locations
func (c *Client) refURL(owner, repo, ref, path string) (string, error) {
	if ref == "" {
		ref = "HEAD"
	}
	if err := rejectPathTraversal(owner, repo, ref, path); err != nil {
		return "", err
	}
	return c.url.JoinPath(owner, repo, ref, path).String(), nil
}
PassedStatic SecretsATR-C0002Details

Why it got this result

Gitleaks (full git history scan) returned 0 findings. Manual grep for API_KEY/SECRET/PASSWORD/TOKEN literal assignments across Go/YAML/JSON/env files found no committed secret values. The only credential-adjacent code declares empty OAuth client ID/secret package vars populated only at build time via ldflags from build secrets mounted during the Docker build, and these are documented as public OAuth client identifiers kept out of source.

Why it matters

Plaintext secrets are easily exfiltrated.

Evidence

Code not shown: this line may hold a secret.

Show 1 more location

Code not shown: this line may hold a secret.

PassedMissing or Inadequate Input ValidationATR-C0008Details

Why it got this result

Tool parameters are declared with JSON Schema and fetched via typed helper functions that enforce type conversion and return errors on mismatch rather than silently coercing. Pagination and other numeric params go through dedicated schema builders with declared min/max constraints. The MCP SDK performs schema validation of incoming tool-call arguments against each tool's declared InputSchema before the handler runs.

Why it matters

Frontline control to prevent multiple classes of vulns.

Evidence

// OptionalParam is a helper function that can be used to fetch a requested parameter from the request.
func OptionalParam[T any](args map[string]any, p string) (T, error) {
PassedInsecure DeserializationATR-C0009Details

Why it got this result

The codebase uses standard encoding/json, which can invoke custom UnmarshalJSON methods during decoding; the custom UnmarshalJSON implementations present in this codebase were inspected and only perform safe, bounded field decoding, with no code-executing or constructor-invoking behavior. Batch mutation handling does build a struct type dynamically via reflect.StructOf, but the fields of that struct have fixed, statically-determined types -- the dynamic construction selects among known result shapes rather than resolving an attacker-supplied type name into an arbitrary executable constructor. No pickle-equivalent, gob, or yaml.Unmarshal-into-interface{} pattern capable of executing code on load exists in the codebase.

Why it matters

Can lead to arbitrary code execution.

Evidence

if err := json.Unmarshal(encoded, schema); err != nil {
    return nil, fmt.Errorf(...)
}
PassedPath Traversal & Symlink Bypass (Filesystem Tools)ATR-C0011Details

Why it got this result

No MCP tool handler accepts a caller-supplied path and resolves it against the local filesystem; GitHub-facing file tools operate through the GitHub API against a remote repository, not local disk. The raw-content client explicitly rejects path-traversal segments, including recursively percent-decoded '..', before constructing its URL. Local-filesystem reads from a variable path exist only for operator-controlled purposes: a test-only snapshot helper, private-key loading for GitHub App authentication, CLI documentation-generation commands, and configured log file writes -- none of these accept a network/request-controlled path; they are populated from local CLI flags or operator configuration, not from MCP tool-call arguments.

Why it matters

Traversal exposes arbitrary host files; can lead to code execution.

Evidence

func rejectSegment(segment string) error {
	if segment == "" {
		return nil
	}
	if segment == ".." {
		return errPathTraversal
	}
	decoded, err := url.PathUnescape(segment)
	if err != nil || decoded == segment {
		return nil
	}
	for subsegment := range strings.SplitSeq(decoded, "/") {
		if err := rejectSegment(subsegment); err != nil {
			return err
…
Show 1 more location
PassedCI/CD Workflow Script InjectionATR-C0014Details

Why it got this result

The registry-releaser workflow interpolates github.ref_type and github.ref_name directly into shell run blocks, but it only triggers on push of 'v*' tags or manual workflow_dispatch, both of which require the actor to already hold repository push/write privileges; no protected-environment boundary or lower-privilege trigger exists that would let a less-privileged actor reach this workflow, and the actor pushing the tag can already supply the checked-out executable code that the same job runs. This does not establish that every repository writer necessarily holds all release or secrets privileges beyond what this specific workflow exposes. Other workflows use the safe env-indirection pattern: issue-labeler.yml and docker-publish.yml assign untrusted values to env vars and reference them as quoted shell variables rather than interpolating them directly, and the license-check workflow's PR-number interpolation is a GitHub-enforced numeric value. No issue_comment or pull_request_target workflow interpolates untrusted event content into a shell run block.

Why it matters

CI/CD script injection can execute attacker-controlled commands with workflow tokens, repository secrets, trusted-runner access, artifact or cache authority, and release or deployment privileges.

Evidence

on:
  push:
    tags: ["v*"]  # Triggers on version tags like v1.0.0
  workflow_dispatch:  # Allow manual triggering
...
      - name: Wait for Docker image
        run: |
          if [[ "${{ github.ref_type }}" == "tag" ]]; then
            TAG="${{ github.ref_name }}"
          else
Show 2 more locations
on:
  issues:
    types: [reopened, opened]
jobs:
  label_issues:
    permissions:
      issues: write
    steps:
      - name: Add AI review label to issue
        run: gh issue edit "$NUMBER" --add-label "$LABELS"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GH_REPO: ${{ github.repository }}
          NUMBER: ${{ github.event.issue.number }}
…
      - name: Wait for Docker image
        run: |
          if [[ "${{ github.ref_type }}" == "tag" ]]; then
            TAG="${{ github.ref_name }}"
          else
            TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)
          fi
          IMAGE="ghcr.io/github/github-mcp-server:$TAG"

+1 more location in the finding JSON

PassedLack of Output Encoding/SanitizationATR-C0010Details

Why it got this result

The two non-test direct ResponseWriter.Write call sites were reviewed: one sets an explicit JSON content type before writing, avoiding content-type sniffing/HTML-rendering risk, and the other's error response always begins with a fixed, non-attacker-controlled prefix, which prevents content-sniffing from ever classifying the response as HTML even though no explicit Content-Type is set. Separately, sanitize.Content, used for third-party GitHub content such as issue/PR bodies, only filters invisible/zero-width/bidi Unicode characters -- it does not strip HTML tags or code-fence metadata; sanitize.PlainText is the function that additionally applies HTML stripping and code-fence filtering, used for title-type fields. The UI renders content via React text interpolation rather than raw HTML insertion, providing an additional safe output context.

Why it matters

Prevents UI-level injection and operator compromise.

Evidence

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	inv, err := h.inventoryFactoryFunc(r)
	if err != nil {
		if errors.Is(err, inventory.ErrUnknownTools) {
			w.WriteHeader(http.StatusBadRequest)
			if _, writeErr := w.Write([]byte(err.Error())); writeErr != nil {
				h.logger.Error("failed to write response", "error", writeErr)
			}
			return
		}
Show 1 more location
// ErrUnknownTools is returned when tools specified via WithTools() are not recognized.
ErrUnknownTools = errors.New("unknown tools specified in WithTools")
From build to runtime

You know what it is. Now see what it does.

A scan helps you understand what you’re building with. When your agent runs, BlueRock shows you what actually happens across tools and systems. Follow actions in context, understand behavior as it unfolds and add controls when you need them.

BUILDRegistry + scan
RUNSee actual execution
OPERATEObserve, control, optimize

Results reflect the code at the commit and date shown.