Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
22c024b
add: standard library imports
SatabrataPaul-GitAc Aug 20, 2025
18c4b04
add: static methods for atlan api requests
SatabrataPaul-GitAc Aug 20, 2025
ff5f808
export: get_custom_metadata_context tool
SatabrataPaul-GitAc Aug 20, 2025
56d4e6f
add: tool to fetch custom metadata context
SatabrataPaul-GitAc Aug 20, 2025
99b8d55
add: custom_metadata_context tool registration
SatabrataPaul-GitAc Aug 20, 2025
8652d58
remove: get_custom_metadata_context tool
SatabrataPaul-GitAc Aug 26, 2025
bf9ccb9
add: cache manager for perisisting custom metadata context between mu…
SatabrataPaul-GitAc Aug 26, 2025
8f0eaf4
add: utility function to fetch all custom metadata context from a tenant
SatabrataPaul-GitAc Aug 26, 2025
7eb389f
add: detect_custom_metadata_trigger tool
SatabrataPaul-GitAc Aug 26, 2025
d73074a
add: registration of detect_custom_metadata_from_query mcp tool
SatabrataPaul-GitAc Aug 26, 2025
8bfe222
add: pre-commit fixes and update pre-commit versions
SatabrataPaul-GitAc Aug 26, 2025
fcd70a5
add: support for custom_metadata filterds in search_assets tool
SatabrataPaul-GitAc Aug 26, 2025
150d08b
remove: custom metadata detector from query tool
SatabrataPaul-GitAc Sep 2, 2025
042babe
remove: detect_custom_metadata_from_query tool registration
SatabrataPaul-GitAc Sep 2, 2025
0a52351
add: custom_metadata_context tool
SatabrataPaul-GitAc Sep 2, 2025
7512a6b
update: custom_metadata_context tool import
SatabrataPaul-GitAc Sep 2, 2025
93cc2f6
add: get_custom_metadata_context_tool registration in server.py
SatabrataPaul-GitAc Sep 8, 2025
017d738
update: procesisng logic for custom metadata filters
SatabrataPaul-GitAc Sep 8, 2025
dafac19
Merge branch 'main' into MCP-8
SatabrataPaul-GitAc Sep 8, 2025
e2f2654
fix: return type of get_custom_metadata_context_tool
SatabrataPaul-GitAc Sep 8, 2025
a08271c
fix: use active client loaded with env for custom_metadata_field search
SatabrataPaul-GitAc Sep 8, 2025
069a2cc
fix: custom_metadata_conditions in search_assets_tool
SatabrataPaul-GitAc Sep 9, 2025
abe1430
fix: back earlier versions of pre-commit hooks
SatabrataPaul-GitAc Sep 10, 2025
3fa116d
remove: main guard clause from custom_metadata_context.py file
SatabrataPaul-GitAc Sep 10, 2025
9960d50
fix: return type of get_custom_metadata_context_tool
SatabrataPaul-GitAc Sep 10, 2025
b049283
update: base prompt for the entire result set, not every bm
SatabrataPaul-GitAc Sep 10, 2025
2860c2f
update: remove extra examples from get_custom_metadata_context_tool a…
SatabrataPaul-GitAc Sep 10, 2025
37142c5
fix: repitive description
SatabrataPaul-GitAc Sep 12, 2025
582a125
fix: variable name
SatabrataPaul-GitAc Sep 15, 2025
9f58b99
changes
abhinavmathur-atlan Oct 21, 2025
890694f
chore: fix cm tool
abhinavmathur-atlan Oct 21, 2025
89211d6
remove extra docs
abhinavmathur-atlan Oct 21, 2025
300df26
precommit checks
abhinavmathur-atlan Oct 21, 2025
ade0e97
search changes
abhinavmathur-atlan Oct 21, 2025
8d808ab
docstring fix
abhinavmathur-atlan Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions modelcontextprotocol/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
get_assets_by_dsl,
traverse_lineage,
update_assets,
get_custom_metadata_context,
create_glossary_category_assets,
create_glossary_assets,
create_glossary_term_assets,
Expand Down Expand Up @@ -62,9 +63,12 @@ def search_assets_tool(
"""
Advanced asset search using FluentSearch with flexible conditions.

Custom metadata can be referenced directly in conditions using the format "SetName.AttributeName".

Args:
conditions (Dict[str, Any], optional): Dictionary of attribute conditions to match.
Format: {"attribute_name": value} or {"attribute_name": {"operator": operator, "value": value}}
Custom metadata: {"SetName.AttributeName": value} or {"SetName.AttributeName": {"operator": "eq", "value": value}}
negative_conditions (Dict[str, Any], optional): Dictionary of attribute conditions to exclude.
Format: {"attribute_name": value} or {"attribute_name": {"operator": operator, "value": value}}
some_conditions (Dict[str, Any], optional): Conditions for where_some() queries that require min_somes of them to match.
Expand Down Expand Up @@ -110,6 +114,53 @@ def search_assets_tool(
include_attributes=["owner_users", "owner_groups"]
)

# Search for assets with custom metadata
# Use nested "custom_metadata" key for clarity
assets = search_assets(
conditions={
"certificate_status": CertificateStatus.VERIFIED.value,
"custom_metadata": {
"Business Ownership.business_owner": "John"
}
}
)

# Search for assets with custom metadata using operators
assets = search_assets(
conditions={
"custom_metadata": {
"Data Quality.quality_score": {
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't the LLM use the names rather than displayNames for this?

"operator": "gt",
"value": 80
},
"Data Classification.sensitivity_level": {
"operator": "eq",
"value": "sensitive",
"case_insensitive": True
}
}
}
)

# Search with multiple custom metadata and standard conditions
assets = search_assets(
asset_type="Table",
conditions={
"name": {
"operator": "startswith",
"value": "customer_"
},
"custom_metadata": {
"Data Governance.data_owner": "John Smith",
"Data Governance.retention_period": {
"operator": "gte",
"value": 365
}
}
}
)


# Search for columns with specific certificate status
columns = search_assets(
asset_type="Column",
Expand Down Expand Up @@ -694,6 +745,50 @@ def create_glossary_categories(categories) -> List[Dict[str, Any]]:
return create_glossary_category_assets(categories)


@mcp.tool()
def get_custom_metadata_context_tool() -> Dict[str, Any]:
"""
Fetch all available custom metadata (business metadata) definitions from the Atlan instance.

This tool returns information about all custom metadata sets and their attributes,
including attribute names, data types, descriptions, and enum values (if applicable).

Use this tool to discover what custom metadata is available before searching for assets
with custom metadata filters.

Returns:
Dict[str, Any]: Dictionary containing:
- context: Description of the returned data
- business_metadata_results: List of business metadata definitions, each containing:
Copy link
Member

Choose a reason for hiding this comment

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

make this consistent

- prompt: Formatted string with metadata name and attributes
- metadata: Dictionary with:
- name: Internal name of the custom metadata set
- display_name: Display name of the custom metadata set
- description: Description of the custom metadata set
- attributes: List of attribute definitions with name, display_name, data_type,
description, and optional enumEnrichment (with allowed values)
- id: GUID of the custom metadata definition
Copy link
Member

Choose a reason for hiding this comment

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

do we need to send this?


Example:
# Get available custom metadata
context = get_custom_metadata_context_tool()

# The response will show custom metadata sets like "Data Classification", "Business Ownership", etc.
# Then you can use them in search_assets_tool with the format "SetName.AttributeName":

assets = search_assets_tool(
conditions={
"Data Classification.sensitivity_level": "sensitive",
"Business Ownership.business_owner": "John Smith"
}
)
"""
try:
return get_custom_metadata_context()
except Exception as e:
return {"error": f"Error getting custom metadata context: {str(e)}"}


def main():
mcp.run()

Expand Down
1 change: 1 addition & 0 deletions modelcontextprotocol/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Configuration settings for the application."""

from pydantic_settings import BaseSettings

from version import __version__ as MCP_VERSION


Expand Down
2 changes: 2 additions & 0 deletions modelcontextprotocol/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .dsl import get_assets_by_dsl
from .lineage import traverse_lineage
from .assets import update_assets
from .custom_metadata_context import get_custom_metadata_context
from .glossary import (
create_glossary_category_assets,
create_glossary_assets,
Expand All @@ -21,6 +22,7 @@
"get_assets_by_dsl",
"traverse_lineage",
"update_assets",
"get_custom_metadata_context",
"create_glossary_category_assets",
"create_glossary_assets",
"create_glossary_term_assets",
Expand Down
163 changes: 163 additions & 0 deletions modelcontextprotocol/tools/custom_metadata_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import logging
from typing import Any, Dict, List
from client import get_atlan_client
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
from pyatlan.cache.enum_cache import EnumCache

logger = logging.getLogger(__name__)


def process_business_metadata(
Copy link
Member

Choose a reason for hiding this comment

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

Lets keep it consistent to custom_metadata

cm_def: Any,
enum_cache: EnumCache,
) -> Dict[str, Any]:
"""
Generates context prompt for a given Atlan business metadata definition.
Args:
cm_def: CustomMetadataDef object from PyAtlan
enum_cache: EnumCache instance for enriching enum attributes
Returns:
Dictionary containing prompt, metadata details, and id
"""
cm_name = cm_def.name or "N/A"
Copy link
Member

Choose a reason for hiding this comment

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

Are the default values correct?

cm_display_name = cm_def.display_name or "N/A"
description = cm_def.description or "No description available."
guid = cm_def.guid

# For prompt: comma separated attribute names and descriptions
attributes_list_for_prompt: List[str] = []
parsed_attributes_for_metadata: List[Dict[str, Any]] = []

if cm_def.attribute_defs:
Copy link
Member

Choose a reason for hiding this comment

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

please do if not raise

for attr_def in cm_def.attribute_defs:
attr_name = attr_def.display_name or attr_def.name or "Unnamed attribute"
attr_desc = attr_def.description or "No description"
Copy link
Member

Choose a reason for hiding this comment

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

please check default values here as well

attributes_list_for_prompt.append(f"{attr_name}:{attr_desc}")

base_description = attr_def.description or ""
enhanced_description = base_description
Copy link
Member

Choose a reason for hiding this comment

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

what is this supposed to do?


# Check if attribute is an enum type and enrich with enum values
if attr_def.options and attr_def.options.is_enum:
enum_type = attr_def.options.enum_type
if enum_type:
Copy link
Member

Choose a reason for hiding this comment

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

please do if nots to decrease the tabs

try:
enum_def = enum_cache.get_by_name(enum_type)
if enum_def and enum_def.element_defs:
enum_values = [
elem.value
for elem in enum_def.element_defs
if elem.value
]
if enum_values:
quoted_values = ", ".join(
[f"'{value}'" for value in enum_values]
)
enum_suffix = f" This attribute can have enum values: {quoted_values}."
enhanced_description = (
f"{base_description}{enum_suffix}".strip()
)

# Create enum enrichment data
enum_enrichment = {
"status": "ENRICHED",
"enumType": enum_type,
"enumGuid": enum_def.guid,
Copy link
Member

Choose a reason for hiding this comment

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

do we need to send this?

"enumDescription": enum_def.description,
"values": enum_values,
}
except Exception as e:
logger.debug(f"Could not enrich enum type {enum_type}: {e}")
enum_enrichment = None
else:
enum_enrichment = None
else:
enum_enrichment = None

attribute_metadata = {
"name": attr_def.name,
"display_name": attr_def.display_name,
"data_type": attr_def.type_name,
"description": enhanced_description,
}

if enum_enrichment:
attribute_metadata["enumEnrichment"] = enum_enrichment

parsed_attributes_for_metadata.append(attribute_metadata)

attributes_str_for_prompt = (
", ".join(attributes_list_for_prompt) if attributes_list_for_prompt else "None"
)

metadata: Dict[str, Any] = {
"name": cm_name,
"display_name": cm_display_name,
"description": description,
"attributes": parsed_attributes_for_metadata,
}

prompt = f"""{cm_display_name}|{description}|{attributes_str_for_prompt}"""

return {"prompt": prompt, "metadata": metadata, "id": guid}


def get_custom_metadata_context() -> Dict[str, Any]:
"""
Fetch custom metadata context using PyAtlan's native cache classes.
Returns:
Dictionary containing context and business metadata results
"""
business_metadata_results: List[Dict[str, Any]] = []

try:
# Get Atlan client
client = get_atlan_client()

# Initialize caches using PyAtlan's native classes
cm_cache = CustomMetadataCache(client)
enum_cache = EnumCache(client)

# Get all custom metadata attributes (includes full definitions)
all_custom_attributes = cm_cache.get_all_custom_attributes(
include_deleted=False, force_refresh=True
)

# Process each custom metadata set
for set_name in all_custom_attributes.keys():
try:
# Get the full custom metadata definition
cm_def = cm_cache.get_custom_metadata_def(set_name)

# Process and enrich with enum data
result = process_business_metadata(cm_def, enum_cache)
business_metadata_results.append(result)

except Exception as e:
logger.warning(
f"Error processing custom metadata set '{set_name}': {e}"
)
continue

logger.info(
f"Fetched {len(business_metadata_results)} business metadata definitions with enum enrichment."
)

except Exception as e:
logger.error(
f"Error fetching custom metadata context: {e}",
exc_info=True,
)
return {
"context": "Error fetching business metadata definitions",
"business_metadata_results": [],
"error": str(e),
}

return {
"context": "This is the list of business metadata definitions used in the data catalog to add more information to an asset",
"business_metadata_results": business_metadata_results,
Copy link
Member

Choose a reason for hiding this comment

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

use consistent naming please

}
Loading