ARIADNE
mcp server

The read path an outside agent would take

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.

the handshake

What the server offers this instance

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 part that cost an afternoon

The stall that happens before the handshake

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 second one

Trimmed responses, and the field that goes missing

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.

fieldagent context kitmcp servereffect
urnpresentpresentsafe to key on
typepresentdroppeda filter on it matches nothing, silently
properties.namepresentpresentsafe
facetspresenttrimmednever 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.

why both transports exist

Fast in process, honest over stdio, checked against each other

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