Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
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
187 changes: 187 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 @@ -42,6 +43,7 @@
@mcp.tool()
def search_assets_tool(
conditions=None,
custom_metadata_conditions=None,
negative_conditions=None,
some_conditions=None,
min_somes=1,
Expand All @@ -65,6 +67,8 @@ def search_assets_tool(
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_conditions (List[Dict[str, Any]], optional): List of custom metadata conditions to match.
Format: [{"custom_metadata_filter": {"display_name": "Business Metadata Name", "property_filters": [{"property_name": "property", "property_value": "value", "operator": "eq"}]}}]
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,86 @@ def search_assets_tool(
include_attributes=["owner_users", "owner_groups"]
)

# Search for assets with custom metadata having a specific property filter (eq)
assets = search_assets(
custom_metadata_conditions=[{
Copy link
Member

Choose a reason for hiding this comment

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

why create a specific filter for this here? Why not make it part of the normal conditions?

Copy link
Member

Choose a reason for hiding this comment

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

you wouldn't have to define all the context and convertors as well. If the LLM understands how to use the CM context, they can use the unique ids as well

Copy link
Contributor Author

@SatabrataPaul-GitAc SatabrataPaul-GitAc Sep 12, 2025

Choose a reason for hiding this comment

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

Current Search Implementation

The search implementation in /Users/satabrata.paul/Desktop/atlan-github-repos/agent-toolkit-internal/modelcontextprotocol/tools/search.py processes conditions through:

Standard Asset Attributes Processing

  • Attribute Resolution: Uses SearchUtils._get_asset_attribute(attr_name) which calls getattr(Asset, attr_name.upper(), None) to get built-in Asset class attributes
  • Condition Processing: Uses SearchUtils._process_condition() which applies operators like eq, contains, startswith, etc. directly on Asset attributes

Why Custom Metadata Conditions Need Separate Handling

The separation between normal conditions and custom_metadata_conditions is necessary because they use fundamentally different PyAtlan APIs and attribute resolution mechanisms:

Comparison Table

Aspect Normal Conditions (Standard Attributes) Custom Metadata Conditions
Attribute Resolution Asset.NAME, Asset.DESCRIPTION
(direct class attributes)
CustomMetadataField(set_name="...", attribute_name="...")
(requires set name + field name)
API Classes Uses Asset class attributes directly Requires CustomMetadataField class instantiation
Search Query Construction Built-in Elasticsearch fields Nested field queries with different syntax

This architectural difference in PyAtlan necessitates separate processing logic

@firecast

"custom_metadata_filter": {
"display_name": "Business Ownership", # This is the display name of the business metadata
"property_filters": [{
"property_name": "business_owner", # This is the display name of the property
"property_value": "John", # This is the value of the property
"operator": "eq"
}]
}
}],
include_attributes=["name", "qualified_name", "type_name", "description", "certificate_status"]
)

# Search for assets with custom metadata having a specific property filter (gt)
assets = search_assets(
custom_metadata_conditions=[{
"custom_metadata_filter": {
"display_name": "Data Quality",
"property_filters": [{
"property_name": "quality_score",
"property_value": 80,
"operator": "gt"
}]
}
}],
include_attributes=["name", "qualified_name", "type_name", "description", "certificate_status"]
)

# Search for assets with custom metadata having multiple property filters (eq and gte)
assets = search_assets(
custom_metadata_conditions=[{
"custom_metadata_filter": {
"display_name": "Data Governance",
"property_filters": [
{
"property_name": "data_owner",
"property_value": "John Smith",
"operator": "eq"
},
{
"property_name": "retention_period",
"property_value": 365,
"operator": "gte"
}
]
}
}],
include_attributes=["name", "qualified_name", "type_name", "description", "certificate_status"]
)

# Search for assets with custom metadata having multiple business metadata filters (eq and gte)
assets = search_assets(
custom_metadata_conditions=[
{
"custom_metadata_filter": {
"display_name": "Data Classification",
"property_filters": [{
"property_name": "sensitivity_level",
"property_value": "sensitive",
"operator": "eq"
}]
}
},
{
"custom_metadata_filter": {
"display_name": "Data Quality",
"property_filters": [{
"property_name": "quality_score",
"property_value": 80,
"operator": "gte"
}]
}
}
],
include_attributes=["name", "qualified_name", "type_name", "description", "certificate_status"]
)


# Search for columns with specific certificate status
columns = search_assets(
asset_type="Column",
Expand Down Expand Up @@ -234,6 +318,7 @@ def search_assets_tool(
try:
# Parse JSON string parameters if needed
conditions = parse_json_parameter(conditions)
custom_metadata_conditions = parse_json_parameter(custom_metadata_conditions)
negative_conditions = parse_json_parameter(negative_conditions)
some_conditions = parse_json_parameter(some_conditions)
date_range = parse_json_parameter(date_range)
Expand All @@ -244,6 +329,7 @@ def search_assets_tool(

return search_assets(
conditions,
custom_metadata_conditions,
negative_conditions,
some_conditions,
min_somes,
Expand Down Expand Up @@ -694,6 +780,107 @@ 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 the custom metadata context for all business metadata definitions in the Atlan instance.

This tool is used to get the custom metadata context for all business metadata definitions
present in the Atlan instance.

Eventually, this tool helps to prepare the payload for search_assets tool, when users
want to search for assets with filters on custom metadata.

This tool can only be called once in a chat conversation.
Copy link
Member

Choose a reason for hiding this comment

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

Why is this required?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wanted to make sure through docstring that the get_custom_metadata_context_tool() is not called everytime a user wants to search an asset with respect to custom metadata filters , in one chat window

If by default the context is maintained in a chat window by the LLM ( MCP Clients ), we can remove this
@firecast Do let me know, if we need to remove it


Returns:
List[Dict[str, Any]]: List of business metadata definitions, each containing:
- prompt: Formatted string prompt for the business metadata definition
- metadata: Dictionary with business metadata details including:
- name: Internal name of the business metadata
- display_name: Display name of the business metadata
- description: Description of the business metadata
- attributes: List of attribute definitions with name, display_name, data_type, description, and optional enumEnrichment
- id: GUID of the business metadata definition

Raises:
Exception: If there's an error retrieving the custom metadata context

Examples:
# Step 1: Get custom metadata context to understand available business metadata
context = get_custom_metadata_context_tool()

# Step 2: Use the context to prepare custom_metadata_conditions for search_assets_tool
# Example context result might show business metadata like "Data Classification" with attributes
Copy link
Member

Choose a reason for hiding this comment

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

Also is there a need for adding these here compared to the search tool?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are generic examples which are added as a part of the search_assets tool
However, in order for the LLM to have better context on what should be the next step be, after the custom metadata definitions are fetched, is why some examples of calling the search_assets tool are provided as a part of the docstring for get_custom_metadata_context tool

@firecast


# Example 1: Equality operator (eq) - exact match
assets = search_assets_tool(
asset_type="Table",
custom_metadata_conditions=[{
"custom_metadata_filter": {
"display_name": "Data Classification", # This is the display name of the business metadata
"property_filters": [{
"property_name": "sensitivity_level", # This is the display name of the property
"property_value": "sensitive", # This is the value of the property
"operator": "eq"
}]
}
}],
include_attributes=["name", "qualified_name", "type_name", "description", "certificate_status"]
Copy link
Member

Choose a reason for hiding this comment

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

If they are searching on the CMs add them to the include attributes as well

)

# Example 2: Equality with case insensitive matching
assets = search_assets_tool(
custom_metadata_conditions=[{
"custom_metadata_filter": {
"display_name": "Data Classification",
"property_filters": [{
"property_name": "sensitivity_level",
"property_value": "SENSITIVE",
"operator": "eq",
"case_insensitive": True
}]
}
}],
include_attributes=["name", "qualified_name", "type_name", "description", "certificate_status"]
)

# Example 3: Starts with operator with case insensitive matching
assets = search_assets_tool(
custom_metadata_conditions=[{
"custom_metadata_filter": {
"display_name": "Business Ownership",
"property_filters": [{
"property_name": "business_owner",
"property_value": "john",
"operator": "startswith",
"case_insensitive": True
}]
}
}],
include_attributes=["name", "qualified_name", "type_name", "description", "certificate_status"]
)

# Example 4: Has any value operator (has_any_value) - check if field is populated
assets = search_assets_tool(
custom_metadata_conditions=[{
"custom_metadata_filter": {
"display_name": "Business Ownership",
"property_filters": [{
"property_name": "business_owner",
"operator": "has_any_value"
}]
}
}],
include_attributes=["name", "qualified_name", "type_name", "description", "certificate_status"]
)
"""
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
70 changes: 70 additions & 0 deletions modelcontextprotocol/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"""Configuration settings for the application."""

import requests
from typing import Any, Dict, Optional
from urllib.parse import urlencode

from pydantic_settings import BaseSettings

from version import __version__ as MCP_VERSION


Expand All @@ -12,6 +17,7 @@ class Settings(BaseSettings):
ATLAN_AGENT_ID: str = "NA"
ATLAN_AGENT: str = "atlan-mcp"
ATLAN_MCP_USER_AGENT: str = f"Atlan MCP Server {MCP_VERSION}"
ATLAN_TYPEDEF_API_ENDPOINT: Optional[str] = "/api/meta/types/typedefs/"

@property
def headers(self) -> dict:
Expand All @@ -23,6 +29,70 @@ def headers(self) -> dict:
"X-Atlan-Client-Origin": self.ATLAN_AGENT,
}

@staticmethod
def build_api_url(path: str, query_params: Optional[Dict[str, Any]] = None) -> str:
current_settings = Settings()
if not current_settings:
raise ValueError(
"Atlan API URL (ATLAN_API_URL) is not configured in settings."
)

base_url = current_settings.ATLAN_BASE_URL.rstrip("/")

if (
path
and not path.startswith("/")
and not base_url.endswith("/")
and not path.startswith(("http://", "https://"))
):
full_path = f"{base_url}/{path.lstrip('/')}"
elif path.startswith(("http://", "https://")):
full_path = path
else:
full_path = f"{base_url}{path}"

if query_params:
active_query_params = {
k: v for k, v in query_params.items() if v is not None
}
if active_query_params:
query_string = urlencode(active_query_params)
return f"{full_path}?{query_string}"
return full_path

@staticmethod
def get_atlan_typedef_api_endpoint(param: str) -> str:
current_settings = Settings()
if not current_settings.ATLAN_TYPEDEF_API_ENDPOINT:
raise ValueError(
"Default API endpoint for typedefs (api_endpoint) is not configured in settings."
)

return Settings.build_api_url(
path=current_settings.ATLAN_TYPEDEF_API_ENDPOINT,
query_params={"type": param},
)

@staticmethod
def make_request(url: str) -> Optional[Dict[str, Any]]:
current_settings = Settings()
Copy link
Collaborator

Choose a reason for hiding this comment

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

why is this initialization required?

Copy link
Contributor Author

@SatabrataPaul-GitAc SatabrataPaul-GitAc Sep 10, 2025

Choose a reason for hiding this comment

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

The following variables are defined as class variables :

ATLAN_BASE_URL: str
ATLAN_API_KEY: str
ATLAN_AGENT_ID: str = "NA"
ATLAN_AGENT: str = "atlan-mcp"
ATLAN_MCP_USER_AGENT: str = f"Atlan MCP Server {MCP_VERSION}"
ATLAN_TYPEDEF_API_ENDPOINT: Optional[str] = "/api/meta/types/typedefs/"

The values for ATLAN_BASE_URL and ATLAN_API_KEY are loaded from env variables
Since Settings inherits from BaseSettings (Pydantic), the environment variables (ATLAN_BASE_URL, ATLAN_API_KEY) are only loaded when we create an instance, because that's when Pydantic reads the environment/.env file.

In the following @staticmethods :

  • build_api_url () -> ATLAN_BASE_URL is required
  • make_request () -> ATLAN_API_KEY is required

Hence, the initialization ( instance creation ) is necessary

headers = {
"Authorization": f"Bearer {current_settings.ATLAN_API_KEY}",
"x-atlan-client-origin": "atlan-search-app",
Copy link
Member

Choose a reason for hiding this comment

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

Why add these and not just leverage the CustomMetadataCache pyatlan class to fetch them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The API call mechanism is added to get additional context of custom metadata attributes which are of Enum type ( i.e.: Options ) -> which have a fixed set of values

The CustomMetadataCache does have method to get information of on all custom metadata definitions, including attribute definitions, but no context of enum defs can be retrieved

Hence, the API call mechanism addresses both custom metadata definitions ( with attribute defs ) and provide additional context of attribute defs which are of Enum Type

@firecast

Copy link
Member

Choose a reason for hiding this comment

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

}
try:
response = requests.get(
url,
headers=headers,
)
if response.status_code != 200:
raise Exception(
f"Failed to make request to {url}: {response.status_code} {response.text}"
)
return response.json()
except Exception as e:
raise Exception(f"Failed to make request to {url}: {e}")

class Config:
env_file = ".env"
env_file_encoding = "utf-8"
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
Loading
Loading