Ariadne began by talking to GraphQL directly. That was the right way to prove the graph existed and the wrong door for an agent to use. The official MCP server is now a real transport: spawned as a child process, spoken to in JSON-RPC over stdio, tools discovered at the handshake rather than assumed.
Nothing here is a list written by us. The server is asked what it has and it answers. That matters because the answer changes with deployment: this open source instance keeps the mutation, user and data quality groups switched off, so a tool name copied from a richer deployment would work in one place and fail in another.
python tools/context.py --via mcp
transport mcp, gms http://localhost:8080
server datahub 3.4.5
tools 6 discovered: get_dataset_queries, get_entities, get_lineage,
get_lineage_paths_between, list_schema_fields, search
search workforce_features: 5 entities
urn:li:dataset:(urn:li:dataPlatform:dbt,warehouse.analytics_marts.workforce_features,PROD)
urn:li:dataset:(urn:li:dataPlatform:postgres,warehouse.analytics_marts.workforce_features,PROD)
urn:li:dataset:(urn:li:dataPlatform:mlflow,warehouse.analytics_marts.workforce_features,PROD)
urn:li:dataset:(urn:li:dataPlatform:dbt,warehouse.analytics_marts.income_features,PROD)
urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:postgres,warehouse.analytics_marts.workforce_features,PROD),age)
Three of those five are the same table. dbt holds the governance tags, postgres holds the physical table, mlflow holds the training frame, and they are siblings. Which one a walk starts from changes the answer, which is the first thing on the column walk in the demo.
Asking for something the transport does not have fails at the point of asking:
python -c "from context import open_context; open_context('kit').call('add_tags', {})"
kit does not offer 'add_tags'. It offers: get_lineage, list_schema_fields, search
The first MCP session never returned. No error, no timeout, no output. The server
posts a usage ping to an external host before it answers initialize, and
on a box that cannot reach that host the send retries. Because it happens before the
handshake, an MCP client has nothing to report: the protocol has not started yet.
printf '%s\n' "$INIT" | mcp-server-datahub --transport stdio
2026-07-28 14:19:03 | INFO | mcp_server_datahub:register_all_tools - Registering MCP tools (is_oss=True)
WARNING:urllib3.connectionpool:Retrying (Retry(total=3, ...)) after connection broken by
'ConnectTimeoutError(<HTTPSConnection(host='track.datahubproject.io', port=443)>,
'Connection to track.datahubproject.io timed out. (connect timeout=10)')': /mp/engage
WARNING:urllib3.connectionpool:Retrying (Retry(total=2, ...))
WARNING:urllib3.connectionpool:Retrying (Retry(total=1, ...))
WARNING:urllib3.connectionpool:Retrying (Retry(total=0, ...))
Four retries at a ten second connect timeout each. The fix is one environment
variable, DATAHUB_TELEMETRY_ENABLED=false, and it is set in
tools/context.py with the reason written next to it so nobody removes it
as tidying.
The MCP server trims responses so an answer fits inside a model's context window.
That is sensible and it has a consequence: fields present through an in process call
can be absent over MCP. Entity type on search results is one of them.
| field | agent context kit | mcp server | effect |
|---|---|---|---|
urn | present | present | safe to key on |
type | present | dropped | a filter on it matches nothing, silently |
properties.name | present | present | safe |
facets | present | trimmed | never used for a decision here |
So entity type is derived from the urn instead. urn:li:dataset: is a
dataset whatever the payload says, because the urn is the identifier and cannot be
trimmed away. This is exactly the failure this whole project is about, met from the
other side: a check that returns nothing and reads as good news.
Two read paths are only worth having if they agree. A fast path that quietly returns a shorter answer is worse than no fast path, because every check built on it reports all clear on a smaller graph. So the same walks run down both and the results are compared on the parts a decision is made from: which nodes, how far away, and which column carried the edge.
python tools/agree.py
== do the two DataHub surfaces agree ==
kit datahub-agent-context, in process
mcp datahub 3.4.5 over stdio, 6 tools
agree 7 nodes upstream of workforce_features on public_coverage_flag
agree 7 nodes downstream of workforce_features
agree 5 nodes upstream of dim_person on disability_code
3/3 walks identical through both
It deliberately does not compare raw payloads. Those differ by design, and a check that fails on nothing is not a check.
the agents built on top of this