5 Layers of Safely Connecting AI Agents to Your Data Stack

A practical guide to building secure, governed AI agents that can access your data without compromising security or compliance.


Most AI agents need access to structured data—your CRMs, product databases, data warehouses. The naive approach is to give agents direct database credentials and let them query whatever they need. This creates several critical problems:

Security Risks:

Compliance Challenges:

Operational Issues:

The Solution: A Layered Architecture

Instead of direct access, we need a layered architecture where agents interact with data through controlled, governed interfaces. This gives you security, compliance, and operational control while still enabling powerful AI capabilities.


The 5-Layer Architecture

Layer 1: Data Sources

Your raw data repositories—completely off-limits to AI agents.

What Goes Here:

Key Principle: Agents never get credentials to these sources. They can't query them directly, ever.

Implementation:


Layer 2: Data Governance & Security (The Critical Boundary)

This is where you define exactly what data agents can access. Materialized SQL views act as controlled windows into your data.

What Goes Here:

Key Principle: This is the only access point for agents. No exceptions.

Creating Agent Views

Agent views are different from traditional database views. They're purpose-built for AI agent consumption:

1. Cross-Source Joins

CREATE MATERIALIZED VIEW customer_health_comprehensive AS
SELECT
    -- From Salesforce
    s.account_id,
    s.account_name,
    s.account_owner,
    s.contract_value,
    s.renewal_date,

-- From Product Database
    p.active_users,
    p.feature_adoption_score,
    p.days_since_last_login,
    p.usage_trend,

-- From Snowflake Analytics
    sf.total_support_tickets,
    sf.avg_ticket_resolution_days,
    sf.revenue_last_quarter,
    sf.churn_risk_score,
    sf.churn_risk_indicators,

-- Calculated fields
    CASE
        WHEN sf.churn_risk_score > 0.7 THEN 'High Risk'
        WHEN sf.churn_risk_score > 0.4 THEN 'Medium Risk'
        ELSE 'Low Risk'
    END as risk_category,

-- Timestamp for freshness tracking
    CURRENT_TIMESTAMP as view_refreshed_at

FROM salesforce_accounts s
LEFT JOIN product_usage_metrics p
    ON s.account_id = p.account_id
LEFT JOIN snowflake_customer_analytics sf
    ON s.account_id = sf.account_id
WHERE s.account_status = 'Active'
  AND s.account_type IN ('Enterprise', 'Mid-Market');  -- Row-level filtering

2. Column-Level Security

Exclude sensitive columns from agent access:

CREATE MATERIALIZED VIEW customer_safe_view AS
SELECT
    customer_id,
    customer_name,
    email,
    account_status,
    -- Explicitly exclude: ssn, credit_card, internal_notes
    subscription_tier,
    signup_date
FROM customers
WHERE account_status = 'active';

3. Row-Level Security

Filter rows based on security policies:

CREATE MATERIALIZED VIEW my_territory_customers AS
SELECT
    account_id,
    account_name,
    revenue,
    health_score
FROM all_customers
WHERE account_owner = CURRENT_USER()  -- Only see own accounts
  AND region IN (
      SELECT region
      FROM user_territories
      WHERE user_id = CURRENT_USER()
  );

4. Data Masking

Mask sensitive data while keeping it useful:

CREATE MATERIALIZED VIEW customer_anonymized AS
SELECT
    customer_id,
    -- Mask email: john.doe@company.com -> j***@company.com
    REGEXP_REPLACE(email, '^([^@]{1,3}).*@', '\1***@') as email_masked,
    -- Hash PII for analytics
    MD5(phone_number) as phone_hash,
    account_status,
    subscription_tier
FROM customers;

5. Aggregations and Pre-computation

Pre-compute expensive aggregations:

CREATE MATERIALIZED VIEW customer_metrics_daily AS
SELECT
    customer_id,
    DATE_TRUNC('day', event_timestamp) as date,
    COUNT(*) as event_count,
    COUNT(DISTINCT user_id) as active_users,
    AVG(session_duration) as avg_session_duration,
    SUM(revenue) as daily_revenue
FROM events
GROUP BY customer_id, DATE_TRUNC('day', event_timestamp);

Materialization Strategy

Why Materialize Views:

Refresh Strategy:

-- Daily refresh for most views
REFRESH MATERIALIZED VIEW customer_health_comprehensive;

-- Or incremental refresh for large datasets
REFRESH MATERIALIZED VIEW CONCURRENTLY customer_metrics_daily;

Best Practices:


Layer 3: MCP Tool Interface

Model Context Protocol (MCP) tools expose your views as callable functions that agents can use. Each tool is a self-documenting function with parameters, validation, and policy checks.

What Goes Here:

Key Principle: Tools are the agent's API. They should be discoverable, well-documented, and secure.

Building MCP Tools

1. Basic Tool Structure

{
  "name": "get_customer_health",
  "description": "Retrieves comprehensive health data for a specific customer account including usage metrics, support tickets, revenue, and churn risk indicators. Use this when users ask about customer status, health scores, or account details.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "account_name": {
        "type": "string",
        "description": "The name of the customer account (e.g., 'Acme Corp')"
      }
    },
    "required": ["account_name"]
  },
  "query": "SELECT * FROM customer_health_comprehensive WHERE account_name = $1",
  "policies": [
    "authenticated",
    "role:customer_success",
    "row_level_security:territory_match"
  ]
}

2. Tool with Multiple Parameters

{
  "name": "identify_at_risk_customers",
  "description": "Identifies customer accounts at high or medium risk of churning, ordered by risk score. Returns account details, risk indicators, and key metrics.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "risk_level": {
        "type": "string",
        "enum": ["High", "Medium", "All"],
        "description": "Filter by risk level. Defaults to 'All' if not specified.",
        "default": "All"
      },
      "limit": {
        "type": "integer",
        "description": "Maximum number of results to return",
        "minimum": 1,
        "maximum": 100,
        "default": 20
      },
      "min_revenue": {
        "type": "number",
        "description": "Minimum contract value to include",
        "minimum": 0
      }
    }
  },
  "query": "SELECT * FROM customer_health_comprehensive WHERE risk_category = CASE WHEN $1 = 'All' THEN risk_category ELSE $1 END AND contract_value >= COALESCE($3, 0) ORDER BY churn_risk_score DESC LIMIT $2",
  "policies": [
    "authenticated",
    "role:customer_success OR role:manager"
  ]
}

3. Tool with Date Range Filtering

{
  "name": "analyze_customer_trends",
  "description": "Analyzes usage trends and support patterns for accounts with declining engagement over a specified time period.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "start_date": {
        "type": "string",
        "format": "date",
        "description": "Start date for analysis (YYYY-MM-DD)"
      },
      "end_date": {
        "type": "string",
        "format": "date",
        "description": "End date for analysis (YYYY-MM-DD)"
      }
    },
    "required": ["start_date", "end_date"]
  },
  "query": "SELECT * FROM customer_health_comprehensive WHERE usage_trend = 'declining' AND view_refreshed_at BETWEEN $1::timestamp AND $2::timestamp",
  "policies": [
    "authenticated",
    "role:manager"
  ]
}

Policy Checks

Policies enforce security at the tool level:

Authentication Policy:

def check_authentication(request):
    token = request.headers.get('X-Pylar-API-Key')
    if not token or not validate_token(token):
        raise UnauthorizedError("Invalid or missing authentication token")
    return get_user_from_token(token)

Role-Based Access Control:

def check_role(user, required_roles):
    user_roles = get_user_roles(user.id)
    if not any(role in user_roles for role in required_roles):
        raise ForbiddenError(f"User must have one of: {required_roles}")

Row-Level Security:

def apply_row_level_security(user, query):
    # Modify query to filter by user's territory
    territory_filter = f"account_owner = '{user.id}' OR region IN ({get_user_regions(user.id)})"
    return f"{query} AND {territory_filter}"

Tool Descriptions Matter

The description field is critical—it's what the LLM uses to decide when to call your tool. Be specific:

Bad:

"description": "Gets customer data"

Good:

"description": "Retrieves comprehensive health data for a specific customer account including usage metrics, support tickets, revenue, and churn risk indicators. Use this when users ask about customer status, health scores, account details, or want to understand why a customer might be at risk."

Best Practices:


Layer 4: AI Agent Layer

Your LLM-powered agent (LangGraph, Cursor, n8n, etc.) that interprets user queries, selects appropriate tools, and synthesizes responses.

What Goes Here:

Key Principle: Agents are stateless consumers of MCP tools. They don't know about your data sources—only the tools available to them.

Agent Implementation Example

LangGraph Agent:

from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
import requests

class PylarAgent:
    def __init__(self, pylar_api_key, pylar_endpoint):
        self.api_key = pylar_api_key
        self.endpoint = pylar_endpoint
        self.llm = ChatOpenAI(model="gpt-4", temperature=0)
        self.tools = self._discover_tools()

def _discover_tools(self):
        response = requests.get(
            f"{self.endpoint}/tools",
            headers={"X-Pylar-API-Key": self.api_key}
        )
        return response.json()["tools"]

def _select_tool(self, user_query, tools):
        tool_descriptions = "\n".join([
            f"- {t['name']}: {t['description']}" for t in tools
        ])

prompt = f"""Given the user query, select the most appropriate tool.

Available tools:
{tool_descriptions}

User query: {user_query}

Return only the tool name."""

response = self.llm.invoke([HumanMessage(content=prompt)])
        selected_tool = response.content.strip()
        return next(t for t in tools if t["name"] == selected_tool)

def _call_tool(self, tool, parameters):
        response = requests.post(
            f"{self.endpoint}/tools/{tool['name']}/execute",
            headers={"X-Pylar-API-Key": self.api_key},
            json={"parameters": parameters}
        )
        return response.json()

def process_query(self, user_query, user_context=None):
        tool = self._select_tool(user_query, self.tools)
        parameters = self._extract_parameters(user_query, tool)
        tool_result = self._call_tool(tool, parameters)
        response = self._synthesize_response(user_query, tool_result)
        return response

Tool Selection with Function Calling:

Modern LLMs support function calling, which makes tool selection easier:

def get_tool_schema(tool):
    return {
        "type": "function",
        "function": {
            "name": tool["name"],
            "description": tool["description"],
            "parameters": tool["inputSchema"]
        }
    }

response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": user_query}],
    tools=[get_tool_schema(t) for t in available_tools],
    tool_choice="auto"
)

Layer 5: User Interface

End users interact with your agent through chat interfaces, APIs, or applications.

What Goes Here:

Key Principle: Users don't need to know about the architecture. They just ask questions and get answers.


Complete Flow: From Query to Response

Here's what happens when a user asks a question:

User Query: "Which customers are at high risk?"

  1. Agent Processing:
    • LLM interprets the query
    • Reviews available MCP tools
    • Selects identify_at_risk_customers based on tool description
  2. Tool Invocation:
    • Agent calls Pylar MCP endpoint
    • Includes authentication header
    • Passes parameters: {risk_level: "High"}
  3. Policy Validation:
    • Validates authentication token
    • Checks user role
    • Validates parameter format
    • Applies row-level security if needed
  4. Query Execution:
    • Executes SQL against agent view
    • Materialized view returns pre-computed results
  5. Response Formatting:
    • Formats results as JSON
    • Logs query for audit trail
  6. Response Synthesis:
    • Synthesizes natural language response
  7. User Receives Answer:
    • "I found 12 customers at high risk. The top 3 are: Acme Corp (risk score: 0.85), TechStart (0.78), Global Solutions (0.72). Would you like detailed analysis for any of these?"

Implementation Guide

Step 1: Connect Your Data Sources

Use Pylar (or similar platform):

  1. Navigate to Integrations section
  2. Add data source connections:
    • Salesforce: OAuth connection
    • PostgreSQL: Connection string (use read-only user)
    • Snowflake: Credentials (encrypted storage)
  3. Test connections
  4. Verify read access

Security Checklist:

Step 2: Create Agent Views

In Pylar SQL IDE (or your database):

  1. Write SQL query joining your data sources
  2. Add security filters
  3. Test the query
  4. Save as materialized view
  5. Set refresh schedule

Example View Creation:

CREATE MATERIALIZED VIEW customer_health_comprehensive AS
    -- ... (full query from above)

Step 3: Build MCP Tools

In Pylar (or your MCP server):

  1. Create new MCP tool
  2. Define function name and description
  3. Specify input schema (parameters)
  4. Write SQL query referencing your view
  5. Configure policy checks
  6. Test the tool

Example Tool Definition:

{
  "name": "get_customer_health",
  "description": "Retrieves comprehensive health data for a customer account.",
  "inputSchema": {
      "type": "object",
      "properties": {
          "account_name": {
              "type": "string"
          }
      },
      "required": ["account_name"]
  },
  "query": "SELECT * FROM customer_health_comprehensive WHERE account_name = $1",
  "policies": ["authenticated"]
}

Step 4: Test in Agent Playground

Before publishing, test your tools:

  1. Open agent playground
  2. Try sample queries:
    • "What's the health status of Acme Corp?"
  3. Review observability dashboard:
    • Monitor query execution times and policy check results

Step 5: Publish to Agent Builder

Generate credentials:

Step 6: Monitor and Iterate

Set up monitoring:


Security Best Practices

1. Principle of Least Privilege

2. Row-Level Security

3. Column-Level Security

4. Data Masking

5. Audit Everything

6. Regular Reviews


Performance Optimization

1. Materialize Expensive Queries

2. Index Your Views

3. Incremental Refreshes

4. Query Optimization


Common Patterns

Pattern 1: Customer Health Dashboard

Pattern 2: Sales Pipeline Analysis

Pattern 3: Product Usage Analytics


Troubleshooting

Agent Selects Wrong Tool

Slow Query Performance

Policy Check Failures

Data Freshness Issues


Conclusion

This 5-layer architecture provides a secure, governed way to connect AI agents to your data stack. The key takeaways are:

Next Steps:

  1. Start with one data source and one view
  2. Build a simple MCP tool
  3. Test in an agent playground
  4. Iterate based on feedback
  5. Scale to more sources and use cases

This architecture pattern is implemented in Pylar, but the concepts apply to any system connecting AI agents to structured data.