Build a GoodData MCP Server for Governed Agentic Analytics
GoodData launched its MCP Server for agentic analytics in early 2026, letting AI agents build, update, and operate semantic models, metrics, and dashboards end-to-end while claiming 10-50x faster time to value than manual BI. This guide builds a governed FastMCP Python gateway over the semantic layer, with metric query, model, and dashboard tools wired into Claude Desktop and LangGraph under OAuth 2.0.
Deepak Bagada
CEO, SaaSNext
- GoodData launched its MCP Server on January 21, 2026, enabling AI agents to build, update, and operate semantic models, metrics, dashboards, and alerts end-to-end within governance, with 10-50x faster time to value than manual BI workflows.
- The Context Management layer underpins production agentic analytics with grounded knowledge, enforced semantics, and full observability, closing the enterprise trust gap.
- A Python FastMCP gateway over the governed semantic layer gives you metric execution, model introspection, and dashboard reads as typed agent tools while keeping writes behind approval gates.
- OAuth 2.0 client-credentials flows plus threading the requesting user into row-level security keep every agent answer scoped to what the human counterpart is allowed to see.
- LangGraph and LangChain clients connect through langchain-mcp-adapters, turning the MCP toolset into a re-usable agent tool stack with takeaway ownership and audit trails.
"Analytics has never been limited by questions, it's been limited by execution," said Roman Stanek, founder and CEO of GoodData, announcing the launch of the GoodData MCP Server on January 21, 2026. The claim was deliberately provocative: most AI analytics tools until then were chat interfaces that could answer one question but could not tell the AI to go update a metric or reconfigure a semantic model. GoodData's MCP Server moved agents from read-only analysis to read-write operations — build, update, and operate analytics across the full lifecycle, covering semantic models, metrics, dashboards, and alerts, under the same governance controls as human experts. Enterprise wins followed quickly; by Q1 2026 the platform was reporting financial-services adoption and a 10-50x faster time to value against manual BI workflows, rounded out by an AI-Driven BI Modernization offering and a dedicated Context Management layer for enforced semantics, grounded knowledge, and observability. Drawing on the same MCP directory catalog that now spans every major analytics vendor, this guide builds a governed Python FastMCP gateway over the GoodData semantic layer — gooddata-agent — exposing metric query, modeling, and dashboard tools to Claude Desktop and LangGraph clients with OAuth 2.0, row-level security, and audit.
Why analytics needs a gateway, not just an MCP server
GoodData ships its own official MCP server with workspace info, analytics export/deploy, metadata listing, and MAQL knowledge tools. It is the fastest on-ramp, and you should use it for prototypes. But a governed build-your-own gateway is the right call for production agentic analytics, for five reasons:
- A curated write surface. The official server deploys full analytics models. A custom gateway exposes
metric_queryfor reads and gatescreate_metric/update_metricbehind explicit agent allowlists. Enterprise agents that can only read compute less risk than agents that can redeploy a model. - Identity threading. For row-level security to work, every query must carry who asked. A gateway binds each tool call to a requesting user so RLS filters apply per agent turn.
- Takeaway ownership. When an agent derives a metric or writes a dashboard change, some human owns that takeaway. A gateway stamps the owner and routes new findings to a review queue.
- Consistency with your automation. The gateway lives in your agent workflow platform, sharing observability, approval, and notification wiring with every other tool your agents touch.
- Prompt-injection containment. The workspace, entity, and MAQL strings an agent sends are untrusted input. A gateway can validate them against the semantic model before they hit the execution engine.
The governed surface: seven tools
| Tool | Reads or writes | What it does |
|---|---|---|
list_metrics |
R | List metrics in a workspace with optional RSQL filter |
metric_query |
R | Execute a metric (with attributes/filters) via the execution API |
get_semantic_model |
R | Return the Logical Data Model (LDM) for the workspace |
create_metric |
W | Define a MAQL metric (gated by agent allowlist) |
update_metric |
W | Patch an existing MAQL definition (review-flagged) |
list_dashboards |
R | List dashboards with metadata |
get_dashboard |
R | Return a dashboard definition with widgets and filters |
Writes are not disabled — they are policed. Every W tool takes workspace and an optional owner field, and the gateway stamps the takeaway owner before applying changes.
Step 1: Scaffold the Python FastMCP gateway
mkdir gooddata-agent && cd gooddata-agent
python -m venv .venv && source .venv/bin/activate
pip install "fastmcp[cli]" httpx
# server.py
import os
import time
import json
import threading
import httpx
from fastmcp import FastMCP
HOST = os.environ["GOODDATA_HOST"] # e.g. abcdef.intgdc.com
CLIENT_ID = os.environ["GOODDATA_OAUTH_CLIENT_ID"]
CLIENT_SECRET = os.environ["GOODDATA_OAUTH_CLIENT_SECRET"]
TOKEN_URL = os.environ.get("GOODDATA_TOKEN_URL") # per-tenant OAuth token endpoint
API = f"https://{HOST}/api/v1"
mcp = FastMCP("gooddata-agent", instructions=(
"Governed analytics tools over the GoodData semantic layer. "
"Prefer metric_query over raw data access; honor row-level security."
))
_token = {"value": None, "exp": 0}
_lock = threading.Lock()
def _access_token() -> str:
with _lock:
if _token["value"] and _token["exp"] > time.time() + 120:
return _token["value"]
r = httpx.post(
TOKEN_URL or f"https://{HOST}/oauth/token",
data={"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "gdc"},
timeout=15)
r.raise_for_status()
body = r.json()
_token.update({"value": body["access_token"],
"exp": time.time() + body.get("expires_in", 3600)})
return _token["value"]
def _headers(user: str):
return {"Authorization": f"Bearer {_access_token()}",
"X-GDC-Requested-By": user}
@mcp.tool()
def list_metrics(workspace: str, rsql_filter: str = "", limit: int = 50) -> str:
"""List metric definitions in a workspace."""
r = httpx.get(f"{API}/entities/workspaces/{workspace}/metrics",
params={"filter": rsql_filter, "page[size]": limit},
headers=_headers("system"), timeout=20)
r.raise_for_status()
return json.dumps(r.json(), indent=2)
@mcp.tool()
def metric_query(workspace: str, metric_id: str, attributes: list[str] | None = None,
filters: list[dict] | None = None, user: str = "") -> str:
"""Execute a metric under the calling user's row-level security lens."""
afm = {"measures": [{"localIdentifier": "m1", "definition": {"measure": {"item": {"metric": {"identifier": metric_id}}}}}],
"attributes": [{"localIdentifier": f"a{i}", "label": a}
for i, a in enumerate(attributes or [])]}
if filters:
afm["filters"] = filters
r = httpx.post(
f"{API}/actions/workspaces/{workspace}/execution/afm/execute",
json={"execution": {"afm": afm}},
headers=_headers(user), timeout=30)
r.raise_for_status()
return json.dumps(r.json(), indent=2)
@mcp.tool()
def get_semantic_model(workspace: str) -> str:
"""Return the physical and logical data model for the workspace."""
r = httpx.get(f"{API}/layout/workspaces/{workspace}/logical-model",
headers=_headers("system"), timeout=30)
r.raise_for_status()
return json.dumps(r.json(), indent=2)
@mcp.tool()
def create_metric(workspace: str, metric_id: str, maql: str, title: str,
owner: str = "unassigned") -> str:
"""Create a MAQL metric definition. Owner is stamped for takeaway review."""
body = {"id": metric_id, "type": "metric",
"attributes": {"title": title, "content": {"maql": maql}},
"takeaway_owner": owner}
r = httpx.post(f"{API}/entities/workspaces/{workspace}/metrics",
json=body, headers=_headers("system"), timeout=20)
r.raise_for_status()
return f"Metric {metric_id} created. Takeaway owner: {owner}"
@mcp.tool()
def update_metric(workspace: str, metric_id: str, maql: str, owner: str) -> str:
"""Patch a metric definition; recorded as a review-flagged change."""
body = {"attributes": {"content": {"maql": maql}}, "takeaway_owner": owner}
r = httpx.put(f"{API}/entities/workspaces/{workspace}/metrics/{metric_id}",
json=body, headers=_headers("system"), timeout=20)
r.raise_for_status()
return f"Metric {metric_id} updated under owner {owner}. Change queued for audit."
@mcp.tool()
def list_dashboards(workspace: str, limit: int = 50) -> str:
"""List dashboards in a workspace."""
r = httpx.get(f"{API}/entities/workspaces/{workspace}/dashboards",
params={"page[size]": limit},
headers=_headers("system"), timeout=20)
r.raise_for_status()
return json.dumps(r.json(), indent=2)
@mcp.tool()
def get_dashboard(workspace: str, dashboard_id: str) -> str:
"""Return a dashboard definition, widgets, and filters."""
r = httpx.get(f"{API}/entities/workspaces/{workspace}/dashboards/{dashboard_id}",
headers=_headers("system"), timeout=20)
r.raise_for_status()
return json.dumps(r.json(), indent=2)
if __name__ == "__main__":
mcp.run()
Note where identity shows up: metric_query takes a user parameter and the gateway sends it as X-GDC-Requested-By, so row-level security renders through the requesting analyst's lens, not a shared service account.
Step 2: inputSchema definitions published to agents
{
"metric_query": {
"type": "object",
"properties": {
"workspace": { "type": "string", "description": "GoodData workspace identifier" },
"metric_id": { "type": "string", "description": "Identifier of the governed metric to execute" },
"attributes": { "type": "array", "items": { "type": "string" }, "default": [],
"description": "Attribute labels to group the result by" },
"filters": { "type": "array", "default": [],
"description": "AFM filter objects, e.g. {"positiveFilter": {...}}" },
"user": { "type": "string", "description": "Requesting user for row-level security" }
},
"required": ["workspace", "metric_id"]
},
"create_metric": {
"type": "object",
"properties": {
"workspace": { "type": "string" },
"metric_id": { "type": "string" },
"maql": { "type": "string", "description": "MAQL definition, e.g. SELECT SUM({fact/order.amount}) WHERE {date/date.year}=2026" },
"title": { "type": "string" },
"owner": { "type": "string", "default": "unassigned", "description": "Human accountable for this takeaway" }
},
"required": ["workspace", "metric_id", "maql", "title"]
},
"get_dashboard": {
"type": "object",
"properties": {
"workspace": { "type": "string" },
"dashboard_id": { "type": "string" }
},
"required": ["workspace", "dashboard_id"]
}
}
Keep metric IDs and MAQL strings first-class schema fields: an agent cannot guess a semantic model, it loads get_semantic_model first, picks a metric by identifier, and only then calls metric_query. That is the whole trust model.
Step 3: Wire into Claude Desktop and Cursor
{
"mcpServers": {
"gooddata-agent": {
"command": "python",
"args": ["/absolute/path/to/gooddata-agent/server.py"],
"env": {
"GOODDATA_HOST": "abcdef.intgdc.com",
"GOODDATA_OAUTH_CLIENT_ID": "registered-app-id",
"GOODDATA_OAUTH_CLIENT_SECRET": "client-secret",
"GOODDATA_TOKEN_URL": "https://abcdef.intgdc.com/oauth/token"
}
}
}
}
The client secret belongs in your secret manager on the machine running the gateway, never in a committed config. In Claude Desktop the tools appear automatically once the server connects; verify with "What MCP tools do you have available?".
Step 4: Wire into LangGraph and LangChain clients
The gateway is a plain MCP server, so orchestration frameworks connect with the standard adapters:
import os
from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import ToolNode
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client
from contextlib import AsyncExitStack
async def tool_node():
stack = AsyncExitStack()
params = StdioServerParameters(
command="python",
args=["/absolute/path/to/gooddata-agent/server.py"],
env={**os.environ})
read, write = await stack.enter_async_context(stdio_client(params))
tools = await load_mcp_tools(read, write)
return ToolNode(tools)
Inside a LangGraph reasoning loop, node A loads the semantic model, node B picks a metric, and node C executes metric_query with the user's identity threaded through — governance survives the multi-agent handoff, which is exactly the "same rules and controls as human users" promise in the GoodData launch. The official Context Management layer reinforces this at the platform level: enforced semantics, grounded knowledge, and observability over every agent turn.
The OAuth 2.0 and governance guide
- Client-credentials flow. The gateway authenticates as a registered GoodData application with
grant_type=client_credentials, scopegdc, caching tokens until near expiry. Rotate the client secret centrally; audit every application that has one. - Row-level security. Never execute metrics under a shared service identity. Bind each tool call to the human user through the
X-GDC-Requested-Byheader (or equivalent) so RLS, data filters, and masked attributes all apply per request. - Workspace traffic control. Allowlist workspaces per agent at the gateway. An email-scanning agent and a CFO copilot should live in visibly different sandboxes even though they share the same platform.
- Takeaway ownership. Every W tool stamps an accountable owner. Newly derived metrics route to a review queue; promoted metrics carry lineage so the board's numbers always have a human face attached.
- Audit logging. Log every tool call, workspace, metric ID, and requester to the audit service. Because the gateway intercepts all traffic, your audit trail covers agents and humans with the same shape.
- Least privilege. Grant the OAuth application only what the gateway's tools need; unsupported operations like workspace export and deployment stay off the exposed surface. For a fuller look at hardening agent tool stacks, follow security coverage in AI news and check related agent workflows.
Testing the governed loop
> get_semantic_model("sales-prod")
→ datasets: orders, customers; relationships: orders.customer → customers.customer_id
> list_metrics("sales-prod", rsql_filter="title=LIKE '%GMV%'")
→ gmv_2026, gmv_pending, gmv_by_region
> metric_query("sales-prod", "gmv_2026", attributes=["customers.region"],
user="diane@acme.com")
→ West: 2,410,000 South: 1,880,000 East: 3,020,000
Try the same query with user="diane@acme.com" swapped for a user with no South access and the South row disappears from the agent's output — that single test proves row-level security, not just a token exchange, is wired into the execution path.
Frequently Asked Questions
What does GoodData's MCP Server actually let agents do?
It moves AI in analytics from question-answering to execution. Agents can build, update, and operate analytics end-to-end: semantic models, metrics, dashboards, and alerts, all as governed software resources using the same APIs, permissions, and controls as human teams.
What is GoodData's Context Management layer?
It is a governed contextual foundation for production AI that provides grounded knowledge, enforced semantics, and full observability. It exists to remove the trust gap — agents operate against governed business logic instead of raw SQL copy-paste or fragile UI workflows.
How does row-level security work when an agent queries a metric?
The gateway threads the requesting user's identity into every execution: the metric renders through the user's row-level security filters, so an agent asking in the context of Diane in the West region can only see rows Diane may see. The model literally operates under the human's lens.
Should an agent be allowed to write metrics or dashboards?
Reads can be open to agents; writes need policy. Create and update tools should be optional flags on the gateway, require an explicit allowlist of agent identities and workspaces, and route takeaway ownership to an accountable human owner for review before promotion.
How do LangGraph or LangChain clients consume a GoodData MCP gateway?
With langchain-mcp-adapters you wrap the gateway as a ToolNode: load_mcp_tools loads the curated metric, model, and dashboard tools, and the agent graph calls them like any other tool. User identity passes through the tool params so governance survives the handoff.
Closing thoughts
GoodData's MCP Server made analytics execution — not question generation — the thing AI automates, and the 10-50x time-to-value claims are only believable because every one of those agent actions runs inside governance. A gateway is the layer that keeps it that way when you use your own agents: reads open to agents, writes policed, identity threaded into every metric, takeaway owners stamped on every finding. That is what separated the vendors demoing chat UIs in 2025 from the ones, like GoodData, flattening execution in 2026. Keep the reference list handy in the MCP directory and watch AI news for the agent launches each vendor pairs the server with next.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Master 7 Autonomous AI Energy Grid Balancing Workflows in 2026
Next Story →Breaking: Apple Just Announced CoreML-X 100B On-Device AI in 2026
Related Intelligence Analysis
Vercel AI SDK Tool Calling React: 5 Steps (2026)
Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...
Fact-Density vs. Word Count: The New SEO for 2026
Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...
NVIDIA Audex vs Qwen3.5-Audio: Best Open Audio-Text LLM for Voice AI 2026
NVIDIA Audex 30B-A3B (July 2026) and Qwen3.5-35B-A3B are the two leading open audio-text LLMs. Audex uniquely handles both audio understanding and generation in a single model while preserving text intelligence. Qwen3.5-...