-
Notifications
You must be signed in to change notification settings - Fork 15
MCP-8 Add: Asset Search with Custom Metadata Filters in Atlan MCP Server #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 12 commits
22c024b
18c4b04
ff5f808
56d4e6f
99b8d55
8652d58
bf9ccb9
8f0eaf4
7eb389f
d73074a
8bfe222
fcd70a5
150d08b
042babe
0a52351
7512a6b
93cc2f6
017d738
dafac19
e2f2654
a08271c
069a2cc
abe1430
3fa116d
9960d50
b049283
2860c2f
37142c5
582a125
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
|
|
||
|
|
@@ -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: | ||
|
|
@@ -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 | ||
SatabrataPaul-GitAc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this initialization required? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The following variables are defined as class variables : ATLAN_BASE_URL: str The values for In the following @staticmethods :
Hence, the initialization ( instance creation ) is necessary |
||
| headers = { | ||
| "Authorization": f"Bearer {current_settings.ATLAN_API_KEY}", | ||
| "x-atlan-client-origin": "atlan-search-app", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why add these and not just leverage the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import logging | ||
| from typing import Any, Dict | ||
| from utils.custom_metadata_detector import detect_custom_metadata_with_singleton | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def detect_custom_metadata_trigger(query_text: str) -> Dict[str, Any]: | ||
| """ | ||
| Detect custom metadata triggers from natural language queries. | ||
| This function analyzes natural language text to identify when users are referencing | ||
| custom metadata (business metadata) and automatically provides context about | ||
| available custom metadata definitions. | ||
| Args: | ||
| query_text (str): Natural language query text to analyze for custom metadata references | ||
| Returns: | ||
| Dict[str, Any]: Dictionary containing: | ||
| - detected: Boolean indicating if custom metadata was detected | ||
SatabrataPaul-GitAc marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| - context: Custom metadata context if detected (list of metadata definitions) | ||
| - detection_reasons: List of reasons why custom metadata was detected | ||
| - suggested_attributes: List of suggested custom metadata attributes | ||
| Examples: | ||
| # Query mentioning data classification | ||
| result = detect_custom_metadata_trigger("Find all tables with sensitive data classification") | ||
| # Query about data quality | ||
| result = detect_custom_metadata_trigger("Show me assets with poor data quality scores") | ||
| # Query about business ownership | ||
| result = detect_custom_metadata_trigger("Which datasets have John as the business owner?") | ||
| # Query about compliance | ||
| result = detect_custom_metadata_trigger("Find all PII data that needs GDPR compliance review") | ||
| """ | ||
| logger.info(f"Detecting custom metadata triggers in query: {query_text[:100]}...") | ||
|
|
||
| try: | ||
| result = detect_custom_metadata_with_singleton(query_text) | ||
|
|
||
| if result["detected"]: | ||
| logger.info( | ||
| f"Custom metadata detected with reasons: {result['detection_reasons']}" | ||
| ) | ||
| context_count = len(result.get("context", [])) | ||
| logger.info( | ||
| f"Provided {context_count} custom metadata definitions for context enrichment" | ||
| ) | ||
| else: | ||
| logger.debug("No custom metadata triggers detected in the query") | ||
|
|
||
| return result | ||
|
|
||
| except Exception as e: | ||
| logger.error(f"Error detecting custom metadata triggers: {str(e)}") | ||
| return { | ||
| "detected": False, | ||
| "context": None, | ||
| "detection_reasons": [], | ||
| "error": str(e), | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.