Highlights
The business glossary is browsable as a tree. Until now the client could look up an individual glossary term or search terms by name, but it could not create a glossary node, list the top of the glossary, walk a node's children, or place a term in its branch. This release adds glossary node creation and full hierarchy enumeration to the client library.
Library-only release. No new MCP tools, no changes to any existing tool, and no behaviour change for existing term operations — get, create-with-parent, delete, and description update are untouched. If you use mcp-datahub as a server, nothing about your setup changes.
What's new
Glossary node create and hierarchy enumeration (#199, #200)
// Create
CreateGlossaryNode(ctx, name, definition, parentNode string) (string, error)
// Enumerate
GetRootGlossaryNodes(ctx, start, count int) ([]types.GlossaryNode, int, error)
GetRootGlossaryTerms(ctx, start, count int) ([]types.GlossaryTerm, int, error)
GetGlossaryNodeChildren(ctx, nodeURN string, start, count int) (*types.GlossaryChildren, error)
GetGlossaryParentChain(ctx, urn string) ([]types.GlossaryNode, error)Two new domain types in pkg/types:
// A directory in the glossary.
type GlossaryNode struct {
URN, Name, Description, ParentNode string
TermsCount, NodesCount int // DataHub's own childrenCount tally
}
// A page of what sits directly under a node. DataHub returns child nodes and
// child terms as one mixed result set, so Start/Count/Total describe the
// combined page rather than either slice on its own.
type GlossaryChildren struct {
Nodes []GlossaryNode
Terms []GlossaryTerm
Start, Count, Total int
}Root and children enumeration are paged via start/count and surface total, so a caller can walk a large glossary. count falls back to DefaultLimit when non-positive and is clamped to MaxLimit.
Walking the tree
// Top of the glossary.
nodes, total, err := c.GetRootGlossaryNodes(ctx, 0, 50)
// One level down. Nodes and terms arrive together.
children, err := c.GetGlossaryNodeChildren(ctx, nodes[0].URN, 0, 50)
for _, n := range children.Nodes { /* sub-directories */ }
for _, t := range children.Terms { /* terms in this directory */ }
// Where does a term live? Direct parent first, up to the root.
chain, err := c.GetGlossaryParentChain(ctx, "urn:li:glossaryTerm:arr")Each node in a parent chain carries its own ParentNode (the next link up), so the branch can be rebuilt without a second round trip.
Behaviour worth knowing
Both of these were found by running against a live DataHub, not inferred from the schema.
Children lag writes. A node's children are served from DataHub's graph index, which is populated asynchronously through its MCL consumers. A term or node created moments earlier will not appear in GetGlossaryNodeChildren immediately. If you are building an interactive glossary browser, expect to poll or refresh after a create rather than reading back straight away.
GetGlossaryParentChain reads the entity itself and is immediately consistent, so prefer it when confirming a just-written parent.
Unknown nodes return ErrNotFound. DataHub answers a lookup of a missing glossary node with an empty stub rather than an error, which is indistinguishable from a node that simply has no children. The client selects exists and converts that case to ErrNotFound. It is decoded through a pointer, so a DataHub version that omits the field is not misread as "absent" — only a definitive false counts.
Non-glossary URNs are rejected. GetGlossaryParentChain and GetGlossaryNodeChildren return ErrInvalidURN for a URN that is not a glossary term / node, rather than silently returning an empty result that reads as "this is at the root".
How children are enumerated
DataHub's RelationshipsInput.types is a free-form [String!]!, so the GraphQL schema does not name the relationship used for glossary parentage — it cannot be read out of entity.graphql. It was determined empirically against a live DataHub v1.6.0 instance rather than guessed. A node's children are the INCOMING side of the IsPartOf relationship on the parent:
glossaryNode(urn: $urn) {
children: relationships(input: {types: ["IsPartOf"], direction: INCOMING, start: 0, count: 100}) {
total start count
relationships { entity { urn type ... } }
}
}Verified against a real tree (root → child node → grandchild term, plus a child term): the edge returns both kinds of children, pages correctly on start/count, and its total matches the node's own childrenCount (termsCount + nodesCount). This also matches how DataHub's own UI fetches node children, so the relationship name is not an artifact of one deployment.
Compatibility
No change to the minimum supported DataHub version. Every mechanism used here is present in entity.graphql at the v1.3.0 tag — the project's documented floor — verified against the upstream repository:
| Mechanism | Present at v1.3.0 |
|---|---|
createGlossaryNode |
entity.graphql:763 |
getRootGlossaryTerms / getRootGlossaryNodes |
:180 / :187 |
GlossaryNode.childrenCount |
:2693 |
ParentNodesResult |
:2718 |
All four new queries are registered in TestGraphQLQueriesMatchSchema and validated against the pinned schema (v1.5.0.1) on every make schema-check.
This release is fully backward compatible: additive API only, no signature changes, no behaviour changes to existing calls.
Fixed
The integration test suite no longer compiled. getAspect, readGlobalTags, readGlossaryTerms, and readInstitutionalMemory had each gained an entityType parameter that write_integration_test.go was never updated for, so make test-integration failed at build time. Fixed — the full integration suite builds and passes against DataHub v1.6.0 again. This affects contributors only; no shipped behaviour changed.
Verification
make verifyclean — lint 0 issues across 43 linters, coverage 93.0% (client package 95.1%), schema-check, gosec, govulncheck, build-check- Patch coverage 182/182 changed lines = 100%
make test-integrationgreen against a live DataHub v1.6.0, including a newTestIntegrationGlossaryHierarchythat builds a glossary tree, enumerates it every way the API supports, cross-checkschildrenCount, and cleans up after itself- Unit coverage via GraphQL httptest mocks: field mapping, the mixed node/term split, relationship input, paging clamps,
existshandling in both directions, parent-chain ordering and linkage, and URN rejection
Note on versioning
There is no v1.14.0 release. A tag was cut against a stale commit during a merge race and contained no changes over v1.13.0; the release was removed and this one cut from the correct commit. Nothing was lost — v1.14.0 was byte-identical to v1.13.0. Upgrade from v1.13.0 straight to v1.15.0.
Installation
Claude Desktop (macOS/Windows)
Download the .mcpb bundle for your platform and double-click to install:
- macOS Apple Silicon (M1/M2/M3/M4):
mcp-datahub_1.15.0_darwin_arm64.mcpb - macOS Intel:
mcp-datahub_1.15.0_darwin_amd64.mcpb - Windows:
mcp-datahub_1.15.0_windows_amd64.mcpb
Homebrew (macOS)
brew install txn2/tap/mcp-datahubClaude Code CLI
claude mcp add datahub \
-e DATAHUB_URL=https://your-datahub.example.com/api/graphql \
-e DATAHUB_TOKEN=your-token \
-- mcp-datahubGo library
go get github.com/txn2/mcp-datahub@v1.15.0Docker
docker pull ghcr.io/txn2/mcp-datahub:v1.15.0Verification of artifacts
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-datahub_1.15.0_linux_amd64.tar.gz.sigstore.json \
mcp-datahub_1.15.0_linux_amd64.tar.gzChangelog
Full Changelog: v1.13.0...v1.15.0