<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Developer on Grafana Labs</title><link>https://grafana.com/docs/grafana/v13.1/developer-resources/mcp/developer/</link><description>Recent content in Developer on Grafana Labs</description><generator>Hugo -- gohugo.io</generator><language>en</language><atom:link href="/docs/grafana/v13.1/developer-resources/mcp/developer/index.xml" rel="self" type="application/rss+xml"/><item><title>Go SDK (programmatic use)</title><link>https://grafana.com/docs/grafana/v13.1/developer-resources/mcp/developer/go-sdk/</link><pubDate>Wed, 29 Jul 2026 17:47:59 +0200</pubDate><guid>https://grafana.com/docs/grafana/v13.1/developer-resources/mcp/developer/go-sdk/</guid><content><![CDATA[&lt;h1 id=&#34;go-sdk-programmatic-use&#34;&gt;Go SDK (programmatic use)&lt;/h1&gt;
&lt;p&gt;You can use the Grafana MCP server as a Go library to build custom MCP server contexts, for example with custom TLS or debug settings. The package is available on &lt;a href=&#34;https://pkg.go.dev/github.com/grafana/mcp-grafana&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;pkg.go.dev&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;what-youll-achieve&#34;&gt;What you&amp;rsquo;ll achieve&lt;/h2&gt;
&lt;p&gt;You integrate the server into your own Go program, using &lt;code&gt;ComposedStdioContextFunc&lt;/code&gt; and &lt;code&gt;GrafanaConfig&lt;/code&gt; (and optionally &lt;code&gt;TLSConfig&lt;/code&gt;) so the server runs with your chosen configuration.&lt;/p&gt;
&lt;h2 id=&#34;before-you-begin&#34;&gt;Before you begin&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A Go toolchain and the module in your project: &lt;code&gt;go get github.com/grafana/mcp-grafana&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;compose-a-stdio-context-with-config&#34;&gt;Compose a stdio context with config&lt;/h2&gt;
&lt;p&gt;Create a &lt;code&gt;GrafanaConfig&lt;/code&gt; with your Grafana URL, credentials (or token), and options such as &lt;code&gt;Debug&lt;/code&gt;. For TLS to Grafana, set &lt;code&gt;TLSConfig&lt;/code&gt; with &lt;code&gt;CertFile&lt;/code&gt;, &lt;code&gt;KeyFile&lt;/code&gt;, and &lt;code&gt;CAFile&lt;/code&gt;. Then build the context function with &lt;code&gt;mcpgrafana.ComposedStdioContextFunc(grafanaConfig)&lt;/code&gt; and pass it to your MCP server setup so the server uses this context when handling requests.&lt;/p&gt;
&lt;p&gt;Example (structure only):&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Go&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-go&#34;&gt;grafanaConfig := mcpgrafana.GrafanaConfig{
    Debug: true,
    TLSConfig: &amp;amp;mcpgrafana.TLSConfig{
        CertFile: &amp;#34;/path/to/client.crt&amp;#34;,
        KeyFile:  &amp;#34;/path/to/client.key&amp;#34;,
        CAFile:   &amp;#34;/path/to/ca.crt&amp;#34;,
    },
}
contextFunc := mcpgrafana.ComposedStdioContextFunc(grafanaConfig)&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Refer to the &lt;a href=&#34;https://pkg.go.dev/github.com/grafana/mcp-grafana&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;package documentation&lt;/a&gt; for the exact types and constructors.&lt;/p&gt;
&lt;h2 id=&#34;next-steps&#34;&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;../observability-metrics-and-tracing/&#34;&gt;Observability (metrics and tracing)&lt;/a&gt; for Prometheus and OTLP.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;../../configure/client-tls-grafana-connection/&#34;&gt;Client TLS (Grafana connection)&lt;/a&gt; for TLS flag reference.&lt;/li&gt;
&lt;/ul&gt;
]]></content><description>&lt;h1 id="go-sdk-programmatic-use">Go SDK (programmatic use)&lt;/h1>
&lt;p>You can use the Grafana MCP server as a Go library to build custom MCP server contexts, for example with custom TLS or debug settings. The package is available on &lt;a href="https://pkg.go.dev/github.com/grafana/mcp-grafana" target="_blank" rel="noopener noreferrer">pkg.go.dev&lt;/a>.&lt;/p></description></item><item><title>Observability (metrics, tracing, and logs)</title><link>https://grafana.com/docs/grafana/v13.1/developer-resources/mcp/developer/observability-metrics-and-tracing/</link><pubDate>Thu, 01 Oct 2026 13:22:11 +0000</pubDate><guid>https://grafana.com/docs/grafana/v13.1/developer-resources/mcp/developer/observability-metrics-and-tracing/</guid><content><![CDATA[&lt;h1 id=&#34;observability-metrics-tracing-and-logs&#34;&gt;Observability (metrics, tracing, and logs)&lt;/h1&gt;
&lt;p&gt;The MCP server can expose &lt;strong&gt;Prometheus metrics&lt;/strong&gt; and supports &lt;strong&gt;&lt;a href=&#34;https://opentelemetry.io/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;OpenTelemetry&lt;/a&gt;&lt;/strong&gt; distributed tracing and log export, following the &lt;a href=&#34;https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;OTel MCP semantic conventions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Metrics require the &lt;strong&gt;SSE&lt;/strong&gt; or &lt;strong&gt;streamable-http&lt;/strong&gt; transport. Tracing and log export use standard &lt;code&gt;OTEL_*&lt;/code&gt; environment variables and work with any transport, independently of &lt;code&gt;--metrics&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: mcp-grafana currently only supports the OTLP/gRPC transport for both traces and logs. &lt;code&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;/code&gt; (and its &lt;code&gt;_TRACES_PROTOCOL&lt;/code&gt; / &lt;code&gt;_LOGS_PROTOCOL&lt;/code&gt; variants) are not honored — gRPC is used regardless.&lt;/p&gt;
&lt;h2 id=&#34;what-youll-achieve&#34;&gt;What you&amp;rsquo;ll achieve&lt;/h2&gt;
&lt;p&gt;You can scrape MCP operation metrics (HTTP transports only) and export traces and logs to Tempo, Loki, or Grafana Cloud under any transport, including stdio.&lt;/p&gt;
&lt;h2 id=&#34;before-you-begin&#34;&gt;Before you begin&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The server running with &lt;strong&gt;SSE&lt;/strong&gt; or &lt;strong&gt;streamable-http&lt;/strong&gt; (metrics are not available with stdio).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;enable-prometheus-metrics&#34;&gt;Enable Prometheus metrics&lt;/h2&gt;
&lt;p&gt;When using SSE or streamable HTTP transports, enable Prometheus metrics with &lt;code&gt;--metrics&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;# Metrics on the main server at /metrics
./mcp-grafana -t streamable-http --metrics&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;# Metrics on a separate listen address
./mcp-grafana -t streamable-http --metrics --metrics-address :9090&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Available metrics:&lt;/strong&gt;&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;button-div&#34;&gt;
      &lt;button class=&#34;expand-table-btn&#34;&gt;Expand table&lt;/button&gt;
    &lt;/div&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Metric&lt;/th&gt;
              &lt;th&gt;Type&lt;/th&gt;
              &lt;th&gt;Description&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_server_operation_duration_seconds&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Histogram&lt;/td&gt;
              &lt;td&gt;MCP operation duration (labels: &lt;code&gt;mcp_method_name&lt;/code&gt;, &lt;code&gt;gen_ai_tool_name&lt;/code&gt;, &lt;code&gt;error_type&lt;/code&gt;, &lt;code&gt;network_transport&lt;/code&gt;, &lt;code&gt;mcp_protocol_version&lt;/code&gt;, and — for &lt;code&gt;tools/call&lt;/code&gt; on selected tools — &lt;code&gt;mcp_tool_operation&lt;/code&gt;, &lt;code&gt;mcp_tool_resource_type&lt;/code&gt;, &lt;code&gt;mcp_tool_phase&lt;/code&gt;)&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_server_session_duration_seconds&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Histogram&lt;/td&gt;
              &lt;td&gt;MCP client session duration (labels: &lt;code&gt;network_transport&lt;/code&gt;, &lt;code&gt;mcp_protocol_version&lt;/code&gt;)&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;http_server_request_duration_seconds&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Histogram&lt;/td&gt;
              &lt;td&gt;HTTP server request duration (from otelhttp)&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Metrics are only available when using SSE or streamable HTTP transports. They are &lt;strong&gt;not&lt;/strong&gt; available with stdio transport.&lt;/p&gt;
&lt;h3 id=&#34;tool-call-dimension-labels&#34;&gt;Tool-call dimension labels&lt;/h3&gt;
&lt;p&gt;For &lt;code&gt;tools/call&lt;/code&gt;, &lt;code&gt;mcp_server_operation_duration_seconds&lt;/code&gt; can carry up to three extra low-cardinality labels so durations are sliceable by what the call was doing:&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;button-div&#34;&gt;
      &lt;button class=&#34;expand-table-btn&#34;&gt;Expand table&lt;/button&gt;
    &lt;/div&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Label&lt;/th&gt;
              &lt;th&gt;Source&lt;/th&gt;
              &lt;th&gt;Notes&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_tool_operation&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;the tool&amp;rsquo;s &lt;code&gt;operation&lt;/code&gt; argument&lt;/td&gt;
              &lt;td&gt;Multiplexer tools only (e.g. &lt;code&gt;alerting_rules_write&lt;/code&gt;); one of the tool&amp;rsquo;s declared operations, else &lt;code&gt;other&lt;/code&gt;.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_tool_resource_type&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;the tool&amp;rsquo;s &lt;code&gt;type&lt;/code&gt; argument&lt;/td&gt;
              &lt;td&gt;e.g. the datasource plugin type on &lt;code&gt;create_datasource&lt;/code&gt;; a plugin type the server ships a schema for, else &lt;code&gt;other&lt;/code&gt;.&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_tool_phase&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;the tool&amp;rsquo;s result &lt;code&gt;_meta&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Phase of a multi-call flow (e.g. &lt;code&gt;create_datasource&lt;/code&gt; schema guidance vs. actual creation).&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;p&gt;Arguments are raw client input, so the two argument-derived labels are allowlisted by tool &lt;strong&gt;and&lt;/strong&gt; by value: unlisted tools emit neither, and unlisted values collapse into one &lt;code&gt;other&lt;/code&gt; bucket, giving each label a fixed maximum series count. Tool validation alone would not bound them — a rejected &lt;code&gt;operation&lt;/code&gt; is still instrumented, and &lt;code&gt;create_datasource&lt;/code&gt;&amp;rsquo;s &lt;code&gt;type&lt;/code&gt; is free text that succeeds for unknown values. &lt;code&gt;mcp_tool_phase&lt;/code&gt; is exempt: the tool sets it on its own result, and tools must keep that value set small.&lt;/p&gt;
&lt;p&gt;The high-cardinality &lt;strong&gt;target&lt;/strong&gt; of a call (the datasource &lt;code&gt;uid&lt;/code&gt;, else &lt;code&gt;name&lt;/code&gt;) is never a metric label — it is span-only as &lt;code&gt;mcp.tool.target&lt;/code&gt;, and empty for calls that name no entity (see below).&lt;/p&gt;
&lt;h3 id=&#34;metric-name-discovery&#34;&gt;Metric-name discovery&lt;/h3&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;button-div&#34;&gt;
      &lt;button class=&#34;expand-table-btn&#34;&gt;Expand table&lt;/button&gt;
    &lt;/div&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Metric&lt;/th&gt;
              &lt;th&gt;Type&lt;/th&gt;
              &lt;th&gt;Description&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_metric_names_response_size_bytes&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Native histogram&lt;/td&gt;
              &lt;td&gt;Bytes consumed from complete successful discovery HTTP responses, after HTTP decompression&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_metric_names_count&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Native histogram&lt;/td&gt;
              &lt;td&gt;Number of metric names decoded successfully before local filtering and pagination&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;p&gt;These metrics help us understand the responses we get from prometheus-type backends when discovering available metric names. The metrics only measure the sizes of successful responses. The bounded &lt;code&gt;backend&lt;/code&gt; label distinguishes &lt;code&gt;prometheus&lt;/code&gt; (including compatible backends) from &lt;code&gt;cloud_monitoring&lt;/code&gt;. Unlike the prometheus backend, Cloud Monitoring measures the full descriptor response, which contain more than just names, so comparing response byte sizes between the two backends is not meaningful.&lt;/p&gt;
&lt;p&gt;For example, the p95 response size over the last hour is:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;promql&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-promql&#34;&gt;histogram_quantile(0.95, sum by (backend) (rate(mcp_metric_names_response_size_bytes[1h])))&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;These metrics use Native Histograms, so the scraper must also support native histograms.&lt;/p&gt;
&lt;h3 id=&#34;loki-cost-guardrail-metrics&#34;&gt;Loki cost guardrail metrics&lt;/h3&gt;
&lt;p&gt;When the &lt;a href=&#34;../../configure/command-line-flags/&#34;&gt;Loki query cost guardrail&lt;/a&gt; is enabled (&lt;code&gt;--loki-guardrail-mode&lt;/code&gt; is &lt;code&gt;shadow&lt;/code&gt; or &lt;code&gt;enforce&lt;/code&gt;), every &lt;code&gt;query_loki_logs&lt;/code&gt; call it evaluates increments exactly one of four counters:&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;button-div&#34;&gt;
      &lt;button class=&#34;expand-table-btn&#34;&gt;Expand table&lt;/button&gt;
    &lt;/div&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Metric&lt;/th&gt;
              &lt;th&gt;Type&lt;/th&gt;
              &lt;th&gt;Incremented when&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_loki_guardrail_admitted_total&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Counter&lt;/td&gt;
              &lt;td&gt;The query passed every enabled check&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_loki_guardrail_would_block_total&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Counter&lt;/td&gt;
              &lt;td&gt;The query failed a check in &lt;code&gt;shadow&lt;/code&gt; mode and ran anyway&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_loki_guardrail_blocked_total&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Counter&lt;/td&gt;
              &lt;td&gt;The query failed a check in &lt;code&gt;enforce&lt;/code&gt; mode and was rejected&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;mcp_loki_guardrail_fail_open_total&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Counter&lt;/td&gt;
              &lt;td&gt;The guardrail could not reach a verdict and admitted the query&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;p&gt;The four partition the guarded population, so their sum is the number of calls the guardrail evaluated, and each is a query count rather than a check count.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Labels:&lt;/strong&gt;&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;button-div&#34;&gt;
      &lt;button class=&#34;expand-table-btn&#34;&gt;Expand table&lt;/button&gt;
    &lt;/div&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Label&lt;/th&gt;
              &lt;th&gt;On&lt;/th&gt;
              &lt;th&gt;Values&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;reason&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;&lt;code&gt;would_block&lt;/code&gt;, &lt;code&gt;blocked&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;&lt;code&gt;selector&lt;/code&gt; (no selective label matcher), &lt;code&gt;range&lt;/code&gt; (effective time range over the cap), &lt;code&gt;bytes&lt;/code&gt; (index/stats estimate over the budget)&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;cause&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;&lt;code&gt;fail_open&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;&lt;code&gt;unparseable&lt;/code&gt; (no stream selector the scanner recognises), &lt;code&gt;estimate_failed&lt;/code&gt; (index/stats unavailable)&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;code&gt;backend&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;all four&lt;/td&gt;
              &lt;td&gt;&lt;code&gt;loki&lt;/code&gt;, &lt;code&gt;victorialogs&lt;/code&gt;, &lt;code&gt;unknown&lt;/code&gt;&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;p&gt;A query can trip more than one check. It is counted &lt;strong&gt;once&lt;/strong&gt;, labelled with the check that ran first — &lt;code&gt;selector&lt;/code&gt;, then &lt;code&gt;range&lt;/code&gt;, then &lt;code&gt;bytes&lt;/code&gt;. That ordering is deliberate: the selectivity check is unconditional, so a query attributed to &lt;code&gt;selector&lt;/code&gt; would not be admitted by raising &lt;code&gt;--loki-guardrail-max-range&lt;/code&gt; or &lt;code&gt;--loki-guardrail-max-bytes&lt;/code&gt;. When promoting from &lt;code&gt;shadow&lt;/code&gt; to &lt;code&gt;enforce&lt;/code&gt;, read &lt;code&gt;sum(rate(mcp_loki_guardrail_would_block_total[5m]))&lt;/code&gt; for the size of the affected population and &lt;code&gt;sum by (reason) (...)&lt;/code&gt; for whether tuning the bounds would shrink it.&lt;/p&gt;
&lt;p&gt;Watch &lt;code&gt;mcp_loki_guardrail_fail_open_total&lt;/code&gt; alongside them: a quiet &lt;code&gt;would_block&lt;/code&gt; rate means &amp;ldquo;nothing would be blocked&amp;rdquo; only if the fail-open rate is also low. A high &lt;code&gt;cause=&amp;quot;unparseable&amp;quot;&lt;/code&gt; rate on &lt;code&gt;backend=&amp;quot;loki&amp;quot;&lt;/code&gt; means the guardrail&amp;rsquo;s LogQL scanner does not recognise the query shapes in use, which calls for improving the scanner rather than tuning the bounds. On &lt;code&gt;backend=&amp;quot;victorialogs&amp;quot;&lt;/code&gt; a high &lt;code&gt;unparseable&lt;/code&gt; rate is expected — brace-less LogsQL is the normal shape there, and the byte-budget check never runs on that backend at all, which is why &lt;code&gt;backend&lt;/code&gt; is a label rather than being folded together.&lt;/p&gt;
&lt;p&gt;Neither the stream selector nor the LogQL is exported as a label: both are unbounded, and line filters can carry sensitive literals. The extracted selectors are logged at &lt;code&gt;WARN&lt;/code&gt; and the full query at &lt;code&gt;DEBUG&lt;/code&gt; instead.&lt;/p&gt;
&lt;h2 id=&#34;enable-opentelemetry-tracing&#34;&gt;Enable OpenTelemetry tracing&lt;/h2&gt;
&lt;p&gt;When &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; (or the signal-specific &lt;code&gt;OTEL_EXPORTER_OTLP_TRACES_ENDPOINT&lt;/code&gt;) is set, the server exports traces via OTLP/gRPC.&lt;/p&gt;
&lt;p&gt;Local example:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;# Send traces to a local Tempo instance
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_EXPORTER_OTLP_INSECURE=true \
./mcp-grafana -t streamable-http&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Grafana Cloud example:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;# Send traces to Grafana Cloud with authentication
OTEL_EXPORTER_OTLP_ENDPOINT=https://tempo-us-central1.grafana.net:443 \
OTEL_EXPORTER_OTLP_HEADERS=&amp;#34;Authorization=Basic ...&amp;#34; \
./mcp-grafana -t streamable-http&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Tool call spans follow naming like &lt;code&gt;tools/call &amp;lt;tool_name&amp;gt;&lt;/code&gt; and include attributes such as &lt;code&gt;gen_ai.tool.name&lt;/code&gt;, &lt;code&gt;mcp.method.name&lt;/code&gt;, and &lt;code&gt;mcp.session.id&lt;/code&gt;. The server supports W3C trace context propagation from the &lt;code&gt;_meta&lt;/code&gt; field of tool call requests.&lt;/p&gt;
&lt;h3 id=&#34;trace-context-propagation&#34;&gt;Trace context propagation&lt;/h3&gt;
&lt;p&gt;Under an HTTP transport (SSE or streamable-http) the server takes part in &lt;a href=&#34;https://www.w3.org/TR/trace-context/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;W3C trace context&lt;/a&gt; propagation on both sides, so a caller, mcp-grafana, and Grafana appear as one connected trace:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inbound&lt;/strong&gt;: a &lt;code&gt;traceparent&lt;/code&gt;/&lt;code&gt;tracestate&lt;/code&gt; header on the incoming request (from an MCP client or an upstream proxy) parents the server span, continuing the caller&amp;rsquo;s trace instead of starting a new one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbound&lt;/strong&gt;: requests to the Grafana API carry a &lt;code&gt;traceparent&lt;/code&gt; naming mcp-grafana&amp;rsquo;s own span, so Grafana&amp;rsquo;s spans hang off ours.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Propagation is always active — it does not require &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt;. With trace export off, no spans are recorded but an inbound trace context is still passed through to Grafana rather than dropped.&lt;/p&gt;
&lt;p&gt;The propagators used are configured with the standard &lt;a href=&#34;https://opentelemetry.io/docs/languages/sdk-configuration/general/#otel_propagators&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;OTEL_PROPAGATORS&lt;/code&gt;&lt;/a&gt; environment variable, defaulting to &lt;code&gt;tracecontext,baggage&lt;/code&gt;. Set it to interoperate with a non-W3C system (&lt;code&gt;b3&lt;/code&gt;, &lt;code&gt;b3multi&lt;/code&gt;, &lt;code&gt;jaeger&lt;/code&gt;, &lt;code&gt;xray&lt;/code&gt;, &lt;code&gt;ottrace&lt;/code&gt;, in any comma-separated combination) or to &lt;code&gt;none&lt;/code&gt; to disable propagation entirely.&lt;/p&gt;
&lt;p&gt;Tool call requests may also carry &lt;code&gt;traceparent&lt;/code&gt;/&lt;code&gt;tracestate&lt;/code&gt; in their MCP &lt;code&gt;_meta&lt;/code&gt; field; when present, that context parents the tool span. This is the only propagation channel available under stdio, where there is no HTTP request to read headers from.&lt;/p&gt;
&lt;p&gt;Listing &lt;code&gt;traceparent&lt;/code&gt; or &lt;code&gt;tracestate&lt;/code&gt; in &lt;code&gt;GRAFANA_FORWARD_HEADERS&lt;/code&gt; is not needed and no longer has an effect: forwarding the &lt;em&gt;caller&amp;rsquo;s&lt;/em&gt; &lt;code&gt;traceparent&lt;/code&gt; verbatim would parent Grafana&amp;rsquo;s spans onto the caller and cut mcp-grafana out of the middle of the trace, so a forwarded value never overrides the propagated one. Forwarded trace headers still apply when propagation is disabled with &lt;code&gt;OTEL_PROPAGATORS=none&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Tool-call spans also carry the &lt;a href=&#34;#tool-call-dimension-labels&#34;&gt;tool-call dimensions&lt;/a&gt; &lt;code&gt;mcp.tool.operation&lt;/code&gt;, &lt;code&gt;mcp.tool.resource_type&lt;/code&gt;, and &lt;code&gt;mcp.tool.phase&lt;/code&gt;, plus the span-only &lt;code&gt;mcp.tool.target&lt;/code&gt; (the datasource &lt;code&gt;uid&lt;/code&gt;, else &lt;code&gt;name&lt;/code&gt;) for grouping the spans that touch one entity. &lt;code&gt;mcp.tool.target&lt;/code&gt; is empty when a call names no entity — notably &lt;code&gt;create_datasource&lt;/code&gt;&amp;rsquo;s &lt;code&gt;phase=schema&lt;/code&gt; call, where the datasource does not exist yet — so stitching that call to the later &lt;code&gt;phase=created&lt;/code&gt; one relies on the shared trace (when the client propagates context via &lt;code&gt;_meta&lt;/code&gt;) or on &lt;code&gt;mcp.session.id&lt;/code&gt; plus &lt;code&gt;mcp_tool_resource_type&lt;/code&gt;, rather than on the target. Unlike the metric labels, these are attached for &lt;strong&gt;all&lt;/strong&gt; tools with their &lt;strong&gt;raw&lt;/strong&gt; values (no allowlist, no &lt;code&gt;other&lt;/code&gt; bucket), since traces are high-cardinality by design. They require an &lt;strong&gt;HTTP transport&lt;/strong&gt; (SSE or streamable-http, so a server span exists to enrich) &lt;strong&gt;and tracing enabled&lt;/strong&gt;, but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;--metrics&lt;/code&gt;; with stdio or tracing off, enrichment is a no-op.&lt;/p&gt;
&lt;h2 id=&#34;enable-opentelemetry-logs&#34;&gt;Enable OpenTelemetry logs&lt;/h2&gt;
&lt;p&gt;When &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; (or the signal-specific &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_ENDPOINT&lt;/code&gt;) is set, the server also exports structured logs via OTLP/gRPC in addition to the existing plain-text stderr output. Logs carry &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt; from the active span so they correlate with exported traces.&lt;/p&gt;
&lt;p&gt;Traces and logs resolve their endpoints independently, so the two signals can be enabled separately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setting only &lt;code&gt;OTEL_EXPORTER_OTLP_TRACES_ENDPOINT&lt;/code&gt; enables tracing &lt;strong&gt;without&lt;/strong&gt; log export.&lt;/li&gt;
&lt;li&gt;Setting only &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_ENDPOINT&lt;/code&gt; enables log export without tracing.&lt;/li&gt;
&lt;li&gt;Setting the generic &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; enables both.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;# Send logs and traces to a local OTel collector
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_EXPORTER_OTLP_INSECURE=true \
./mcp-grafana -t streamable-http&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Stderr logging continues unchanged; operators can pipe stderr to &lt;code&gt;/dev/null&lt;/code&gt; if they only want logs going to the OTel collector.&lt;/p&gt;
&lt;p&gt;Logs can be sent directly to any managed backend that accepts OTLP/gRPC — for example, Grafana Cloud — by pointing &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_ENDPOINT&lt;/code&gt; (or the generic &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt;) at the remote gRPC endpoint and supplying auth via &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_HEADERS&lt;/code&gt; (or &lt;code&gt;OTEL_EXPORTER_OTLP_HEADERS&lt;/code&gt;), mirroring the tracing example above. A local OTel collector is optional — useful for fan-out, batching, or multi-backend routing, but not required.&lt;/p&gt;
&lt;p&gt;The signal-specific variants &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_ENDPOINT&lt;/code&gt;, &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_HEADERS&lt;/code&gt;, &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_INSECURE&lt;/code&gt;, &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE&lt;/code&gt;, &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_TIMEOUT&lt;/code&gt;, and &lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_COMPRESSION&lt;/code&gt; are honored and override their generic &lt;code&gt;OTEL_EXPORTER_OTLP_*&lt;/code&gt; counterparts — see the &lt;a href=&#34;https://opentelemetry.io/docs/specs/otel/protocol/exporter/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;OTel exporter spec&lt;/a&gt; for the full list and precedence rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If the configured collector is unreachable, log records are buffered in memory (default queue: 2048) and the oldest records are dropped once the queue fills. The process continues without blocking the service. Configure a local OTel collector if you need lossless buffering during outages.&lt;/p&gt;
&lt;p&gt;Logs are also exported under the stdio transport, which makes it easy to centralize logs from local &lt;code&gt;mcp-grafana&lt;/code&gt; instances invoked by IDE clients.&lt;/p&gt;
&lt;h2 id=&#34;run-with-docker-metrics-tracing-and-logs&#34;&gt;Run with Docker (metrics, tracing, and logs)&lt;/h2&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;docker run --rm -p 8000:8000 \
  -e GRAFANA_URL=http://localhost:3000 \
  -e GRAFANA_SERVICE_ACCOUNT_TOKEN=&amp;lt;your token&amp;gt; \
  -e OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317 \
  -e OTEL_EXPORTER_OTLP_INSECURE=true \
  grafana/mcp-grafana \
  -t streamable-http --metrics&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;next-steps&#34;&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;../build-and-test/&#34;&gt;Build, test, and lint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;../../configure/transports-and-addresses/&#34;&gt;Transports and addresses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;../../configure/command-line-flags/&#34;&gt;Command-line flags&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
]]></content><description>&lt;h1 id="observability-metrics-tracing-and-logs">Observability (metrics, tracing, and logs)&lt;/h1>
&lt;p>The MCP server can expose &lt;strong>Prometheus metrics&lt;/strong> and supports &lt;strong>&lt;a href="https://opentelemetry.io/" target="_blank" rel="noopener noreferrer">OpenTelemetry&lt;/a>&lt;/strong> distributed tracing and log export, following the &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/" target="_blank" rel="noopener noreferrer">OTel MCP semantic conventions&lt;/a>.&lt;/p></description></item><item><title>Build, test, and lint</title><link>https://grafana.com/docs/grafana/v13.1/developer-resources/mcp/developer/build-and-test/</link><pubDate>Wed, 29 Jul 2026 17:47:59 +0200</pubDate><guid>https://grafana.com/docs/grafana/v13.1/developer-resources/mcp/developer/build-and-test/</guid><content><![CDATA[&lt;h1 id=&#34;build-test-and-lint&#34;&gt;Build, test, and lint&lt;/h1&gt;
&lt;p&gt;Contributions are welcome. Open an issue or pull request on &lt;a href=&#34;https://github.com/grafana/mcp-grafana&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;GitHub&lt;/a&gt;. This project is written in Go.&lt;/p&gt;
&lt;h2 id=&#34;what-youll-achieve&#34;&gt;What you&amp;rsquo;ll achieve&lt;/h2&gt;
&lt;p&gt;You can run the server from source, execute the test suites, and run linters locally.&lt;/p&gt;
&lt;h2 id=&#34;before-you-begin&#34;&gt;Before you begin&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://go.dev/doc/install&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Go&lt;/a&gt; installed for your platform.&lt;/li&gt;
&lt;li&gt;Optional: Docker for integration tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;run-the-server-locally&#34;&gt;Run the server locally&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;STDIO&lt;/strong&gt; (default for local development):&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make run&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;SSE:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;go run ./cmd/mcp-grafana --transport sse&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;build-and-run-the-docker-image&#34;&gt;Build and run the Docker image&lt;/h2&gt;
&lt;p&gt;Build the image:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make build-image&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Run in SSE mode (image default):&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;docker run -it --rm -p 8000:8000 mcp-grafana:latest&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Run in stdio mode:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;docker run -it --rm mcp-grafana:latest -t stdio&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;run-tests&#34;&gt;Run tests&lt;/h2&gt;
&lt;p&gt;There are three types of tests available: unit tests, integration tests, and cloud tests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; (no external dependencies required):&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make test-unit&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You can also run:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make test&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Integration tests&lt;/strong&gt; (requires Docker containers):&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make test-integration&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Cloud tests&lt;/strong&gt; (requires Grafana Cloud instance and credentials):&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make test-cloud&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Cloud tests are automatically configured in CI. For local development, you&amp;rsquo;ll need to set up your own Grafana Cloud instance and credentials.&lt;/p&gt;
&lt;p&gt;For the full integration suite, start the supporting services first (Grafana and its dependencies on port 3000):&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make run-test-services
make test-integration&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you add tools, add integration tests; existing tests are a good template.&lt;/p&gt;
&lt;h2 id=&#34;run-linters&#34;&gt;Run linters&lt;/h2&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make lint&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The repository includes a custom linter for unescaped commas in &lt;code&gt;jsonschema&lt;/code&gt; struct tags (&lt;code&gt;description&lt;/code&gt; fields must be escaped with &lt;code&gt;\\,&lt;/code&gt; to avoid truncation). Run only that linter:&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Bash&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;make lint-jsonschema&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Refer to the &lt;a href=&#34;https://github.com/grafana/mcp-grafana/blob/main/internal/linter/jsonschema/README.md&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;JSONSchema linter README&lt;/a&gt; in the repository.&lt;/p&gt;
&lt;h2 id=&#34;next-steps&#34;&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;../go-sdk/&#34;&gt;Go SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;../observability-metrics-and-tracing/&#34;&gt;Observability (metrics and tracing)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
]]></content><description>&lt;h1 id="build-test-and-lint">Build, test, and lint&lt;/h1>
&lt;p>Contributions are welcome. Open an issue or pull request on &lt;a href="https://github.com/grafana/mcp-grafana" target="_blank" rel="noopener noreferrer">GitHub&lt;/a>. This project is written in Go.&lt;/p>
&lt;h2 id="what-youll-achieve">What you&amp;rsquo;ll achieve&lt;/h2>
&lt;p>You can run the server from source, execute the test suites, and run linters locally.&lt;/p></description></item></channel></rss>