Skip to content

Add DataHub MCP recall context - #341

Open
King-Dylan wants to merge 8 commits into
mem9-ai:mainfrom
King-Dylan:codex/datahub-mcp-context
Open

Add DataHub MCP recall context#341
King-Dylan wants to merge 8 commits into
mem9-ai:mainfrom
King-Dylan:codex/datahub-mcp-context

Conversation

@King-Dylan

@King-Dylan King-Dylan commented Jun 1, 2026

Copy link
Copy Markdown

Summary

  • Add a read-only DataHub MCP external context provider for mem9 recall.
  • Attach optional external_context to memory list/search responses for data-asset questions.
  • Orchestrate DataHub MCP search, get_entities, and one-hop upstream/downstream get_lineage calls.
  • Add configuration, docs, and OpenAPI coverage for enabling or suppressing DataHub context retrieval.
  • Harden MCP/JSON/SSE normalization for structuredContent, search result aliases, entity wrappers, lineage variants, repeated query params, UTF-8 snippets, and graceful fallback behavior.
  • Preserve query casing for named assets like Executive Revenue and prefer dataset URNs when selecting the lineage root so recall consistently returns the right lineage target.

Purpose

This PR is the mem9-side DataHub integration path only. mem9 remains the source of truth for user and agent memory, while DataHub remains the source of truth for data assets, metadata, ownership, quality, and lineage. At recall time, agents can combine mem9 long-term memory with governed DataHub metadata context without copying DataHub into mem9 or writing mem9 data back to DataHub yet.

Refs #340

Demo Boundary

The standalone demo has been extracted into a separate repository:

This PR intentionally keeps only the integration and recall behavior inside mem9.

Next Step

Follow-up work should add an optional mem9 -> DataHub publish path for curated data-asset observations. That path should classify and reconcile mem9 memories that clearly refer to DataHub entities, map them to DataHub URNs, and publish selected metadata-grade signals such as dashboard or data issue observations, freshness or quality annotations, owner hints, glossary notes, or custom DataHub aspects. Raw personal memory and unrelated agent memory should stay in mem9.

Validation

  • python3 -m json.tool docs/api/openapi.json >/dev/null
  • cd server && go test -count=1 -run 'TestDataHubMCP|TestShouldQueryDataHubContext|TestFormatDataHubSearchQuery|TestNormalizeDataHubTextContent|TestNormalizeDataHubLineageToolResult|TestExtractSSEData|TestCollectDataHubURNs|TestTrimExternalContextItems|TestCompactSnippet|TestListMemories_IncludesDataHubExternalContextForDataQuestions|TestListMemories_ExternalContextFalseOverrideWins|TestListMemories_ExternalContextRepeatedFalseOverrideWins|TestLoad_DataHubMCP' ./internal/service ./internal/handler ./internal/config
  • cd server && go test -race -count=1 ./internal/service/...
  • git diff --check
  • Live EC2 verification against the real DataHub MCP endpoint confirmed recall now returns Executive Revenue, mart.revenue, and both upstream and downstream lineage summaries.

…text

# Conflicts:
#	docs/api/openapi.json
#	server/cmd/mnemo-server/main.go
#	server/internal/handler/handler.go
#	server/internal/handler/memory.go
@King-Dylan
King-Dylan marked this pull request as ready for review June 1, 2026 18:27
@King-Dylan King-Dylan changed the title [codex] Add DataHub MCP recall context Add DataHub MCP recall context Jun 1, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7edcc9e8c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +355 to +356
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("MCP %s returned HTTP %d: %s", method, resp.StatusCode, compactSnippet(string(responseBody), 300))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset stale MCP sessions on 404

When a stateful MCP server restarts or expires a session, requests carrying the old Mcp-Session-Id return HTTP 404; the Streamable HTTP spec requires the client to start a new initialize without that session. This path only returns an error and leaves initialized/sessionID unchanged, so every later recall keeps sending the stale session ID and DataHub context remains broken until the mem9 process is restarted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@King-Dylan

From the DataHub side - I don't think this 404 path is actually reachable against DataHub's MCP server. It runs stateless (stateless_http=True), so there's no server-side session to expire or 404 on; a stale Mcp-Session-Id with a valid token just proceeds. So the re-initialize-on-404 handling likely isn't needed.

The failure that does happen is auth-related, and it differs by deployment: self-hosted has no HTTP auth layer and surfaces a bad/expired token as a JSON-RPC error inside a 200 body (which post() already catches), while DataHub Cloud's managed endpoint rejects at the HTTP layer with 401 (or 400 when OAuth2 isn't enabled). Might be worth special-casing 401/400 so a Cloud token problem gives a clear signal — but the stateless 404 case here shouldn't bite.

@lakshay-nasa lakshay-nasa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. Read through the DataHub integration and dropped a few notes inline — most are minor, one's worth a closer look (how curated names/descriptions get resolved on the DataHub side).

}

func dataHubEntityTitle(entity map[string]any) string {
return firstString(entity, "name", "displayName", "qualifiedName", "properties.name", "properties.displayName", "properties.qualifiedName", "urn")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading through the normalization here - one thing worth knowing from the DataHub side: the human-set display values live under editableProperties, and DataHub expects clients to prefer those over the ingested properties.* ones (resolution order is editablePropertiesproperties → top-level). This title waterfall checks properties.name but not editableProperties.name, and just above in dataHubEntityItem the description checks editableProperties.description last rather than first.

The effect is that for assets a team has curated in DataHub, you'd surface the raw ingested name/description instead of the display values they actually set - which tends to be exactly the well-managed datasets people search for. Putting editableProperties.* first for both should cover it; it falls through harmlessly on types that don't have it (dashboards/charts only carry editableProperties.description, not a name).

Since the tests mock the MCP responses, this reads green today - a fixture with editableProperties populated would surface it.

ID: firstString(entity, "urn"),
Title: title,
Snippet: compactSnippet(snippet, 800),
URL: firstString(entity, "url", "properties.externalUrl"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing: url only gets populated on DataHub Cloud – self-hosted/OSS doesn't inject it, so this will be empty there. Probably worth treating as optional in whatever renders external_context. (properties.externalUrl as a fallback is a nice touch — it's sometimes set, though often not either.)

"max_results": lineageLimit,
"offset": 0,
})
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: a get_lineage failure gets swallowed here with no log. The best-effort behavior is right — better to return partial context than fail recall — but a debug-level log would make a systematically broken lineage path noticeable instead of silently empty.

Comment on lines +355 to +356
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("MCP %s returned HTTP %d: %s", method, resp.StatusCode, compactSnippet(string(responseBody), 300))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@King-Dylan

From the DataHub side - I don't think this 404 path is actually reachable against DataHub's MCP server. It runs stateless (stateless_http=True), so there's no server-side session to expire or 404 on; a stale Mcp-Session-Id with a valid token just proceeds. So the re-initialize-on-404 handling likely isn't needed.

The failure that does happen is auth-related, and it differs by deployment: self-hosted has no HTTP auth layer and surfaces a bad/expired token as a JSON-RPC error inside a 200 body (which post() already catches), while DataHub Cloud's managed endpoint rejects at the HTTP layer with 401 (or 400 when OAuth2 isn't enabled). Might be worth special-casing 401/400 so a Cloud token problem gives a clear signal — but the stateless 404 case here shouldn't bite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants