Skip to main content
Glama
jedshen123

App Data MCP

by jedshen123

App Data MCP

这是一个面向内部数据平台的 MCP 服务。Metabase、PostHog 等元信息同步到 PostgreSQL,由管理员在后台决定是否对 MCP 用户开放,让 Codex、Claude Code 等 AI 客户端完成资产发现、详情查看、数据出处追踪和受控取数。

当前能力

  • search_assets: 搜索看板、卡片、Model、insight、指标、表、事件。

  • get_asset: 查看单个资产完整元信息。

  • trace_asset: 查看资产的 SQL / 事件 / 上游表 / 原始链接。

  • run_asset: 只读返回支持资产的真实数据;不支持时读取本地 sampleData 兜底。

  • query_audience: 使用统一 uid 对 2-10 个 Metabase Model 做交集、并集或差集计算。

  • export_audience: 将完整 UID 人群导出为有期限的服务端 CSV 下载文件。

  • query_starrocks: 治理资产无法回答后的受控 StarRocks SQL 回退;执行前会再次搜索 Metric/Model/Card。

  • list_domains: 查看配置里已有的业务域。

  • auth_status: 查看当前请求用户和 Metabase 授权状态。

  • catalog_status: 查看当前个人 MCP 用户可见的元信息数量和分类统计。

  • connector_status: 查看连接器配置状态和只读访问策略。

只读访问策略

这个 MCP 只做只读数据网关,不提供任何创建、更新、删除、写回能力。

允许的操作:

  • 搜索和读取 PostgreSQL 中已开放的元信息。

  • 读取看板、卡片、insight、指标口径和来源链接。

  • 追踪上游表、事件、SQL、原平台 URL。

  • 执行 Metabase/PostHog 查询时,只允许调用读取类 API,并限制为只读查询结果。

  • 执行 StarRocks 查询时,只允许单条 SELECTWITH ... SELECTSHOWDESCRIBE/DESCEXPLAIN

禁止的操作:

  • 创建、编辑、删除 Metabase dashboard/card/question。

  • 创建、编辑、删除 PostHog dashboard/insight/cohort/action。

  • 写数据库、执行 DDL/DML、保存 AI 生成的 SQL 到平台。

  • 通过 MCP 修改任何业务数据、平台配置或权限配置。

后续新增 connector 或 tool 时,命名和实现都必须维持这个边界。connector_status 会返回 accessMode: "read-only",用于让 AI 客户端确认当前服务策略。

数据量限制

默认会限制返回数据量,避免 AI 平台一次拉取过大的结果。

  • search_assets 默认且最多向 AI 返回 5 个资产;服务端内部仍可召回更多候选,并完整写入审计日志。

  • run_asset 默认返回 100 行,最多 500 行。

  • 单次 MCP 响应默认最大约 1 MB,超过会返回 response_too_large,提示缩小查询范围。

  • query_starrocksrun_asset 共用行数上限;同时在 StarRocks 会话设置 sql_select_limitquery_timeout

可以通过环境变量调整,但建议生产环境保持保守:

DATA_DEFAULT_SEARCH_LIMIT=5
DATA_MAX_SEARCH_LIMIT=50
DATA_DEFAULT_RESULT_ROW_LIMIT=100
DATA_MAX_RESULT_ROW_LIMIT=500
DATA_MAX_RESPONSE_BYTES=1000000

Metabase/PostHog connector 会继续在返回侧做行数和响应字节限制。connector_status 会返回当前 dataLimits

真实数据读取

run_asset 现在会优先调用平台只读 API 获取真实数据:

  • metabase:card:* / metabase:model:* / metabase:metric:*: 调用 Metabase card query endpoint;Model 和 Metric 在 Metabase API 中同样通过 card query 执行。

  • metabase:dashboard:*: 读取 dashboard 下的卡片,并按 dashboard 筛选器映射逐个执行卡片查询。

  • posthog:insight:*: 调用 PostHog insight read endpoint,并请求刷新/返回当前结果。

仍然不支持写入操作,也不会保存查询、修改看板或更新 insight。

注意:

  • Dashboard 取数会限制最多执行 20 张卡片,可用 DATA_MAX_DASHBOARD_CARDS 调整。

  • 每张卡片/insight 结果继续受 DATA_MAX_RESULT_ROW_LIMITDATA_MAX_RESPONSE_BYTES 限制。

  • 资产声明了 parameters 时,可以给 run_asset 传维度/日期参数;未声明的友好参数会被拒绝,避免 AI 临时拼接不受控条件。

  • Metabase dashboard 会读取 parameter_mappings,只把已经映射到对应 card 的筛选参数下发,并在返回里给出每张 card 的 parameterMappingStatus

  • 如果 live connector 失败且资产里配置了 sampleData,会回退返回 sampleData。

  • 如果既无法 live 查询又没有 sampleData,会返回 live_connector_failedasset_not_runnable

Metabase 友好参数示例:

{
  "asset_id": "metabase:card:81",
  "params": {
    "date": {
      "from": "2026-07-01",
      "to": "2026-07-09"
    },
    "country": "US"
  },
  "limit": 100
}

Model / Metric / Card 指标集语义查询

run_asset.semantic 可以在不修改 Metabase 对象、不拼接 SQL 的情况下,对已经同步的 Model、Metric 和声明为 semantic.role=metric_set 的 Card 进行受控分析。所有字段都必须来自 get_asset 返回的治理元数据,服务端会校验字段引用、操作符、数量上限和数据类型。

Metric 保持已经治理的公式不变,只允许追加筛选和替换拆分维度:

{
  "asset_id": "metabase:metric:480",
  "semantic": {
    "filters": [
      { "field": "country_ad_ch", "operator": "eq", "value": "CN" }
    ],
    "breakouts": [
      { "field": "data_source" },
      { "field": "date(t1.event_date)", "unit": "month" }
    ]
  },
  "limit": 100
}

传入 "breakouts": [] 可以移除 Metric 的默认时间分组并返回总值;省略 breakouts 则保留 Metric 原有默认分组。

Model 可以选择明细字段:

{
  "asset_id": "metabase:model:479",
  "semantic": {
    "fields": ["uid", "data_source", "date(t1.event_date)"],
    "filters": [
      { "field": "is_active_on_date", "operator": "eq", "value": 1 }
    ]
  },
  "limit": 100
}

也可以动态聚合和拆分:

{
  "asset_id": "metabase:model:479",
  "semantic": {
    "filters": [
      { "field": "is_active_on_date", "operator": "eq", "value": 1 }
    ],
    "aggregations": [
      { "operator": "distinct", "field": "uid", "alias": "有效用户数" }
    ],
    "breakouts": [
      { "field": "data_source" }
    ]
  },
  "limit": 100
}

Card 指标集由管理员在资产编辑页通过可视化表单维护:可以直接为同步到的 Card 输出列勾选“维度”或“指标”,配置基础粒度、默认时间维度、时间粒度、单位、同义词、Rollup、重算分子/分母和运行累计。后台仍保存下面的结构化语义覆盖;“高级:JSON 预览与导入”只用于排障或批量迁移,不要求日常手写。

{
  "role": "metric_set",
  "baseGrain": ["stat_date", "region"],
  "defaultTimeDimension": {
    "field": "stat_date",
    "defaultUnit": "day",
    "supportedUnits": ["day", "week", "month"],
    "timezone": "Asia/Shanghai",
    "dateMeaning": "支付成功日期"
  },
  "dimensions": [
    { "field": "stat_date", "label": "统计日期" },
    { "field": "region", "label": "地区" }
  ],
  "measures": [
    {
      "name": "revenue",
      "label": "支付金额",
      "synonyms": ["成交金额"],
      "unit": "CNY",
      "rollup": {
        "strategy": "sum",
        "allowedGroupBy": ["stat_date", "region"]
      },
      "timeAggregation": {
        "metricType": "flow",
        "strategy": "sum",
        "defaultMode": "require_explicit",
        "reason": "每笔支付只归属一个支付成功日期"
      },
      "cumulative": {
        "supported": true,
        "strategy": "running_sum"
      }
    },
    {
      "name": "paid_users",
      "label": "支付用户数",
      "rollup": {
        "strategy": "forbidden",
        "reason": "基础结果已经去重,跨日期或地区相加会重复计算用户"
      },
      "timeAggregation": {
        "metricType": "distinct_count",
        "strategy": "forbidden",
        "defaultMode": "latest_date",
        "reason": "同一用户可能出现在多个日期"
      }
    }
  ]
}

查询 Card 指标集:

{
  "asset_id": "metabase:card:123",
  "semantic": {
    "measures": ["revenue"],
    "timeScope": {
      "mode": "range",
      "start": "2026-07-01",
      "end": "2026-07-31",
      "unit": "day"
    },
    "resultMode": "cumulative_trend",
    "breakouts": [{ "field": "region" }]
  },
  "limit": 100
}

每个 Card 指标都必须配置 timeAggregation,不再通过 breakouts 或旧配置猜测时间语义。查询必须使用 timeScope,或者由指标的 defaultMode 提供默认范围;resultMode=value/total/trend/cumulative_trend 分别表示单值、跨期总量、时间趋势和累计趋势。服务端只允许 sumrecompute 指标生成跨期总量,只允许 sum 且声明 cumulative.running_sum 的指标生成累计趋势;快照和日去重人数不会被静默跨日期相加。

timeAggregation.metricType 提供四类管理预设:flow 使用 sumsnapshot 使用 latestdistinct_count 使用 recompute_distinctforbiddenratio 使用 recompute。其中“最新日期”是时间选择,不是对指标取 maxlatest_date 会先执行 max(date) where date <= 业务今天 - completeLagDays,再用所得日期筛选正式指标查询。completeLagDays 默认是 1(T+1),所以不会选择今天;若昨天尚无数据,则自动回退到更早的最大可用日期。管理页中的累计开关仅表示“允许生成累计趋势(running sum)”,不控制普通跨日期汇总。

筛选操作符包括 eqneqgtgteltlteinnot_incontainsis_nullnot_nullbetween;Model 聚合包括 countdistinctsumavgminmax。语义查询与 params 不能同时使用,普通 Card、Dashboard 和 PostHog Insight 不接受 semantic

用户人群组合查询

Metabase 同步会为包含 uid 字段的 Model 生成 audience 元数据。query_audience 要求所有输入 Model 位于同一个 Metabase database,并使用当前用户的 Metabase Session 在 /api/dataset 内完成集合计算。每个 Model 会先在数据源侧应用筛选并按 uid 去重,再执行集合连接,不会先把各 Model 的 UID 拉回 MCP 客户端。

{
  "operator": "intersection",
  "models": [
    {
      "asset_id": "metabase:model:101",
      "filters": [
        { "field": "event_time", "operator": "gte", "value": "2026-07-01" }
      ]
    },
    {
      "asset_id": "metabase:model:102",
      "filters": [
        { "field": "topic", "operator": "eq", "value": "母婴" }
      ]
    }
  ],
  "output": "count",
  "limit": 100
}

operator 支持 intersectionuniondifferencedifference 表示第一个 Model 减去后续所有 Model。output=count 默认只返回去重用户数;output=uids 返回去重 UID,并受全局最大行数和响应大小限制。每个 Model 最多包含 20 个受控语义筛选条件。

需要完整 UID 文件时使用 export_audience,不要循环调用有限的 query_audience 结果:

{
  "operator": "intersection",
  "models": [
    { "asset_id": "metabase:model:493", "filters": [{ "field": "deleted", "operator": "eq", "value": 0 }] },
    { "asset_id": "metabase:model:496", "filters": [{ "field": "status", "operator": "eq", "value": 1 }] }
  ],
  "filename": "community-active-users.csv"
}

服务端通过 Metabase CSV endpoint 获取完整的单列 UID 结果,生成随机 capability 下载地址,默认 24 小时后失效,并由后台定时清理。超过行数或文件字节上限时整个导出失败,不会生成截断文件。下载地址相当于临时访问凭证,不应公开分享。

治理资产优先与 SQL 回退

新数据主题或上下文中尚未确定可用资产时,先调用 search_assets。同一会话的后续问题如果只是修改已选资产支持的时间范围、筛选条件、分组维度、排序或返回范围,应复用已有 asset_idget_asset 元数据,直接调用 run_asset;只有主题改变、已有资产缺少所需能力、执行失败或上下文中没有可靠资产信息时才重新搜索。question 始终原样保留当前轮用户问题用于审计。调用方 AI 应优先通过 query_spec 提交一个原子化、可独立理解的结构化数据需求:保留指标、业务对象、时间、筛选、拆分、比较和分析类型,只删除工具名称、礼貌语及回答格式要求;显式语义、会话补全和不确定推断分别标记为 explicitconversationinferredretrieval_queries 只提供别名、事件名、缩写或替代表达用于扩召回,不改变硬能力要求。旧的 queryresolved_queryexpansion_queriesqueries[] 保持兼容;长报告仍通过 queries[] 按章节拆成独立数据问题。排序会直接学习当前同步到目录中的 Metabase 字段、参数、Metric 公式/维度、Card 指标集语义,以及 PostHog 的分析类型、事件、属性和 breakdown:先计算语义覆盖、关键业务词覆盖、时间/拆分/明细能力、元信息完整度和风险惩罚,再结合问题意图与治理优先级形成综合可用性评分。普通指标、趋势和拆分查询优先选择能力确实覆盖问题的指标集 Card 或 Metric;明细查询优先 Model,总览优先 Dashboard,固定报表优先 Card,漏斗/留存/路径分析优先匹配分析类型的 PostHog Insight。AI 首次使用所选资产时仍需用 get_asset 核对字段、维度、参数和公式。返回值中的 selection.intentrecommendedAssetIdcandidateOrder,以及每项资产的 selection.suitability/reasons/missingCapabilities/evaluation 会解释推荐结果。只有管理后台已开放且有效的资产会参与搜索和治理检查。

管理后台的“检索匹配规则”工作台可配置业务关键词到平台、资产类型和 PostHog 分析类型的路由策略。Boost 模式仅增加 0–40 分,多个规则累计仍封顶 40;Prefer 模式会把通过语义与能力门槛的目标资产放入优先组,没有合格目标资产时可回退其他平台。规则支持 All of、Any of、None of、启停、优先级和实时问题预览。默认内置三条可编辑策略:行为分析优先 PostHog、页面/功能/访问/曝光/点击等行为日活优先 PostHog Trends,以及整体/业务/口径/报表语境的经营日活优先 Metabase。命中规则还会生成受控的核心词召回扩展,防止规则在候选召回前失效;最终推荐仍使用完整原问题进行关键业务词和能力校验。

search_assets 使用“权限过滤 → 结构化能力匹配 → 全量评分 → 治理与路由重排 → 执行前核验”链路。当前目录规模下,所有已发布、当前用户可见且符合平台、类型和业务域范围的资产都会参与主问题评分;retrieval_queries 只保留为召回与审计证据,不参与硬能力判定。提供 query_spec 时,服务端直接使用结构化指标、业务对象、拆分维度、筛选字段和时间要求;未提供时继续使用旧的规则分类器兼容现有客户端。能力结果分为 supportedverification_requiredunsupported:只有治理元数据能够明确证明不支持的资产才会被过滤;元数据不足但可能正确的候选会返回给 AI,并要求先通过 get_asset 核验。指标查询中的 Model 只有声明 modelSemantic.aggregationPolicy=guarded 才具备聚合候选资格。每个返回候选都带有 requirements.termClassificationscapabilityMatch.statusexecutionVerification,AI 必须完成对应核验清单后再调用 run_asset

Model 的聚合授权按字段和函数的精确组合配置。count 不传字段表示 COUNT(*),由 allowRowCount 单独控制;传字段表示 COUNT(field)distinct 表示 COUNT(DISTINCT field)。例如:

{
  "role": "detail_dataset",
  "aggregationPolicy": "guarded",
  "baseGrain": ["event_id"],
  "primaryTimeField": "event_time",
  "allowRowCount": true,
  "aggregationRules": [
    {
      "field": "uid",
      "operators": ["count", "distinct"],
      "capabilities": {
        "distinct": {
          "label": "APP用户数",
          "synonyms": ["用户数", "APP人数"]
        }
      }
    },
    { "field": "amount", "operators": ["sum", "min", "max"] },
    { "field": "event_time", "operators": ["min", "max"] }
  ]
}

旧的 entityFieldsadditiveFields 配置仍分别兼容为 distinctsum 授权;管理后台再次保存时会统一写入 aggregationRules

allowRowCount 不依赖 baseGrain:即使没有声明每行粒度,也可以执行 COUNT(*)。此时结果只能解释为物理记录数;只有明确声明了每行的业务粒度后,才可以进一步说明这些记录代表订单、事件等业务对象。用户、设备等实体数量通常应使用已授权字段的 distinct

管理后台的“启用受控聚合”开关直接对应 aggregationPolicy=guarded,不再单独展示聚合策略下拉框。每行粒度从 Model 输出字段中选择;主时间字段和可选的强制过滤条件收在“高级配置”中。关闭开关会删除该 Model 的聚合语义配置,使其回到默认仅明细状态。

每个已授权的字段/聚合函数组合会自动产生保守的检索能力,例如 distinct(uid) 根据“用户ID”推导“用户数、去重用户数”,count(uid) 只推导“用户ID非空数”,count(*) 只表示“记录数”。管理员可以在 capabilities 中补充业务名称和搜索别名;配置值优先用于检索,但不会改变实际聚合函数。管理后台会展示自动识别结果,业务名称和别名均为可选。

当 AI 在 Model 上使用 semantic.aggregations 重新计算 countdistinctsum 等指标时,run_asset 会进行第二次服务端检查:

  • 必须传入用户原始 question,否则返回 asset_question_required

  • 如果仍有匹配的 Metric 或 Card,返回 higher_priority_asset_available 并拒绝执行 Model。

  • AI 必须逐个 get_asset 检查候选 Metric 与 Card;确认不适用后,把所有相关 ID 放入 rejected_asset_ids 并提供具体 fallback_reason

  • 拒绝 Metric 或 Card 却不说明原因时返回 fallback_reason_required

  • Model 的 semantic.fields 明细查询不属于重新计算指标,不受这项拦截影响。

例如只有在治理 Metric 和现有 Card 都无法覆盖用户要求的维度时,才允许这样显式降级:

{
  "asset_id": "metabase:model:492",
  "question": "查询按实验分组拆分的中国地区绑定设备用户数",
  "rejected_asset_ids": ["metabase:metric:483", "metabase:card:530"],
  "fallback_reason": "Metric 483 没有实验分组维度,Card 530 也未提供该筛选参数",
  "semantic": {
    "filters": [{ "field": "country_ad_ch", "operator": "eq", "value": "中国" }],
    "aggregations": [{ "operator": "distinct", "field": "uid", "alias": "user_count" }],
    "breakouts": [{ "field": "experiment_group" }]
  }
}

query_starrocks 要求同时传入用户原始问题,并在执行 SQL 前由服务端再次搜索治理资产:

{
  "question": "最近15天有效绑定M9设备的社区活跃用户数趋势",
  "sql": "select ...",
  "purpose": "data_question",
  "limit": 100
}

如果存在匹配的 Metric/Model/Card,服务端返回 governed_assets_available、候选资产和下一步说明,且 sqlExecuted=false。AI 应先使用 get_assetrun_asset。检查后确认候选资产不适用,才可显式拒绝并回退:

{
  "question": "最近15天有效绑定M9设备的社区活跃用户数趋势",
  "sql": "select ...",
  "purpose": "data_question",
  "rejected_asset_ids": ["metabase:metric:480", "metabase:model:479"],
  "fallback_reason": "候选资产没有所需的实验组字段",
  "limit": 100
}

purpose=user_requested_sql 只能用于用户明确要求直接执行 SQL 的场景;purpose=metadata_inspection 只允许 SHOWDESCRIBE/DESCEXPLAIN

Metabase dashboard 参数执行结果会包含筛选器覆盖情况:

{
  "requestedParameters": ["date", "country"],
  "parameterCoverage": [
    {
      "parameter": "date",
      "mappedCardCount": 3,
      "mappedCards": [
        {
          "cardId": "81",
          "title": "APP 日活"
        }
      ]
    }
  ],
  "cards": [
    {
      "cardId": "81",
      "parameterMappingStatus": {
        "status": "partially_mapped",
        "requestedParameters": ["date", "country"],
        "appliedParameters": ["date"],
        "unmappedParameters": ["country"],
        "mappedParametersAvailable": ["date"]
      }
    }
  ]
}

Metabase 原生参数仍可透传:

{
  "asset_id": "metabase:card:81",
  "params": {
    "parameters": [
      {
        "type": "date/range",
        "target": ["variable", ["template-tag", "date"]],
        "value": "2026-07-01~2026-07-09"
      }
    ]
  }
}

PostHog insight 支持常见只读覆盖参数:

{
  "asset_id": "posthog:insight:abc123",
  "params": {
    "date_from": "-30d",
    "date_to": "now",
    "breakdown": "country",
    "properties": [
      {
        "key": "country",
        "value": "US",
        "operator": "exact",
        "type": "event"
      }
    ]
  }
}

PostHog 参数不会再作为裸 date_from / properties 查询参数发送。服务端会读取已经同步并由管理员开放的 PostHog 公共属性,将显示名称、翻译或同义词解析成原生字段名,并校验是否允许过滤/分组。普通筛选使用 Insight Retrieve API 的 filters_override 参数;动态分组基于已保存 Insight 构造临时只读 Query API 请求。未开放、 已下线、歧义、禁止过滤或禁止分组的属性会被拒绝。

例如管理员可以把 $geoip_country_code 配置为“国家代码”,并将“国家码、country”维护为同义词;调用方随后 可以在 properties[].propertybreakdown 中使用任意一个已治理名称。原生 properties[].key 继续兼容, 但也必须对应一条有效且已开放的公共属性。

StarRocks 自助 SQL

query_starrocks 仅用于治理资产无法回答问题后的 SQL 回退。服务端强制执行下面的顺序:

  1. 新主题或没有可复用资产时先调用 search_assets;同一主题已检查过的资产可直接复用。

  2. 能由治理资产回答时使用 get_assetrun_asset

  3. 没有合适资产、已明确拒绝候选资产,或用户明确要求 SQL 时,才使用 query_starrocks

  4. 不熟悉表结构时,先执行 SHOW TABLESDESCRIBE table_nameSHOW CREATE TABLE table_name

  5. 根据真实字段生成带明确日期范围和过滤条件的 SELECT,避免扫描无关数据。

示例:

{
  "question": "最近15天活跃用户数趋势",
  "sql": "SELECT dt, count(*) AS active_users FROM ads_app_daily WHERE dt >= '2026-07-01' GROUP BY dt ORDER BY dt",
  "purpose": "data_question",
  "limit": 100
}

服务会拒绝 DDL、DML、多语句、INTO OUTFILELOAD_FILEFILESSLEEP、可执行注释和自定义查询 Hint 等危险或消耗型 SQL。每次执行还会设置 StarRocks 当前会话的查询超时与最大返回行数,并继续受 MCP 响应字节数限制。

StarRocks 配置放在 .env,不要写入元信息表:

STARROCKS_HOST=127.0.0.1
STARROCKS_PORT=9030
STARROCKS_USER=app_data_mcp_reader
STARROCKS_PASSWORD=your-password
STARROCKS_DATABASE=your_database
STARROCKS_CONNECTION_LIMIT=10
STARROCKS_CONNECT_TIMEOUT_MS=10000
STARROCKS_QUERY_TIMEOUT_MS=30000
STARROCKS_MAX_SQL_LENGTH=50000
STARROCKS_SSL=false

必须使用专用只读账号。MCP token 负责识别和审计查询人,真正的数据表权限由这个 StarRocks 账号控制。管理员可以按实际数据库创建最小权限账号,例如:

CREATE USER 'app_data_mcp_reader' IDENTIFIED BY 'replace-with-a-strong-password';
GRANT SELECT ON ALL TABLES IN DATABASE your_database TO USER 'app_data_mcp_reader';
GRANT SELECT ON ALL VIEWS IN DATABASE your_database TO USER 'app_data_mcp_reader';

不要给该账号授予 INSERTUPDATEDELETECREATEDROPALTEREXPORT 或管理角色。配置后可先调用 connector_status 确认 starrocks.configured=true,再让 AI 执行 SHOW TABLES 验证连接。

本地运行

第一次部署先配置 PostgreSQL。服务或同步脚本首次访问时会自动创建 DB_SCHEMA.METADATA_TABLE,默认是 public.app_data_mcp_assets

stdio 模式,适合本机 Codex / Claude Code 通过命令启动:

npm install
npm run dev

HTTP 模式,适合部署成团队共享服务:

npm run dev:http

默认地址:

http://127.0.0.1:3000/mcp

健康检查:

http://127.0.0.1:3000/health

如果当前用户没有可见资产,catalog_status 会显示 scope: "current_user"assetCount: 0search_assets 会返回空列表。先确认该用户的 Metabase 权限,再检查管理员是否已在 http://127.0.0.1:3000/admin 勾选开放相应资产。

同步平台元信息

可以运行只读同步脚本,把 Metabase/PostHog 元信息 upsert 到 PostgreSQL。

同步 Metabase:

npm run sync:metabase

同步 PostHog:

npm run sync:posthog

全部同步:

npm run sync:all

同步脚本只读取平台 API,然后更新元信息表:

  • 已存在的资产更新 metadata 和同步时间,不覆盖管理员设置的 is_published 和人工配置。

  • 新资产默认 is_published=false;可通过 METADATA_DEFAULT_PUBLISHED=true 调整,但生产环境不建议。

  • 平台中已经消失的资产会标记为 inactive,并自动设置 is_published=false,不再对 MCP 暴露;未来重新同步出现时也不会自动恢复开放。

  • 不会创建、更新、删除 Metabase/PostHog 平台内的任何对象。

PostHog 同步会继续读取 Dashboard 详情,建立 Dashboard 到 Insight 的 children 关系;Insight 会自动抽取 analysisType、事件、拆分字段和过滤属性到 insightSemantic。同步任务还会读取 event/person/session/group 四类 Property Definitions,保存 PostHog 原始元数据并保留后台人工覆盖字段。负责人优先使用 PostHog 用户姓名,其次使用邮箱,只有两者都不存在时才回退到 distinct_id

管理后台的“PostHog 公共属性”页面用于维护属性的开放状态、显示名称、翻译、类型、过滤能力、分组能力、说明和 同义词。PostHog 新发现的属性默认关闭;后续同步只更新源元数据,不覆盖人工配置。PostHog 已消失的属性会标记 为下线并立即停止供 MCP 查询使用。

Metabase 同步会为 dashboard/card/model/metric 写入权限快照 asset.access,包括 collection、creator、archived、personal collection、同步时间等。MCP 使用两级过滤:

Card、Model 与 Metric 同步会在读取 /api/card 列表后,以受限并发继续读取 /api/card/:id 详情,并优先使用详情中的 dataset_queryresult_metadata、参数和更新时间。Model 保存为 metabase:model:<id>,Metric 保存为 metabase:metric:<id>。包含 uid 的 Model 会额外保存 audience 元数据和 database id,用于服务端人群组合查询。Metric 还会保存公式、筛选条件、数据来源、默认时间维度、可拆分维度和上下游资产依赖;字段元信息区分 namedisplayName 与真实 description。对象在 Card、Model、Metric 之间转换时,会迁移原记录并保留开放状态和后台人工配置。详情请求失败时会保留列表数据,并在同步日志和资产 warnings 中标记。

  • search_assets: 用本地权限快照缩小候选集,再对候选资产做实时权限校验。

  • list_domains / catalog_status: 先做快照过滤,再批量读取当前用户可见的 Metabase Card 与 Dashboard ID。

  • get_asset / trace_asset: 先做本地快照过滤,再用当前用户 Metabase session 实时请求 GET /api/card/:idGET /api/dashboard/:id 校验可见性。

  • run_asset: 继续使用用户 Metabase session 执行只读查询,保留平台实时权限判断。

  • query_audience: 对每个输入 Model 做快照和实时权限校验,再使用同一用户 Session 执行组合查询。

需要统计整个可见目录时,服务会优先并行读取 Metabase /api/card/api/dashboard 列表,一次取得当前用户可见 ID,而不是逐资产请求详情;读取失败时才回退逐资产校验。结果默认按用户 Session 缓存 30 秒,可通过 METABASE_LIVE_ACCESS_CACHE_SECONDS 调整,以减少连续调用 list_domainscatalog_status 的重复开销。

Dashboard 的开放状态会被其直接子 Card、Model、Metric 继承。未单独开放的子资产不会出现在 search_assets 中,但只要它仍属于一个已开放 Dashboard,且当前用户能实时访问该 Dashboard, 就可以通过子资产 id 使用 get_assettrace_assetrun_asset。返回结果中的 access.mode=dashboard_inherited 会列出授权来源 Dashboard。Card 数据查询仍由 Metabase 使用 当前用户 Session 做最终权限判断;从 Dashboard 移除子 Card 后,继承访问会在下一次元信息同步后失效。

部署或升级后执行一次完整同步:

npm run sync:metabase

同步频率建议:

METABASE_METADATA_SYNC_INTERVAL_HOURS=6
METABASE_PERMISSION_SYNC_INTERVAL_HOURS=6
POSTHOG_METADATA_SYNC_INTERVAL_HOURS=12

如果 Metabase 权限、collection、核心看板变化频繁,可以把 Metabase 调整到 1 小时;如果变化少,6 小时通常够用。权限大调整、核心看板发布后建议手动运行:

npm run sync:metabase

生产环境可以用 cron 或调度器定时执行:

0 */6 * * * cd /path/to/app-data-mcp && npm run sync:metabase
15 */12 * * * cd /path/to/app-data-mcp && npm run sync:posthog

catalog_status 需要个人 MCP token,并按该账号的 Metabase 实时权限统计可见资产;同时返回 metadata/access snapshot 的 latestSyncedAtageHoursstale,超过上述间隔时会提示重新同步。

如果同步失败,常见原因是 .env 里平台地址或凭据未填完整。可以先在 MCP 里调用 connector_status 检查配置。

HTTP 模式可配置:

MCP_HTTP_HOST=0.0.0.0 MCP_HTTP_PORT=3000 npm run dev:http

MCP_HTTP_BEARER_TOKEN 是旧的共享 HTTP 保护 token。多人使用时更推荐后面的“Metabase 用户授权”流程,由每位用户登录后生成自己的个人 MCP token。

如果仍设置 MCP_HTTP_BEARER_TOKEN,客户端需要带共享 token 或用户个人 token 才能进入 /mcp。共享 token 只做传输入口保护;当 APP_DATA_REQUIRE_AUTH_TOKEN=true 时,数据类 tools 仍然需要用户个人 token 才能识别具体权限。

Authorization: Bearer <token>

Codex / Claude Code 配置示例

stdio:

{
  "mcpServers": {
    "app-data": {
      "command": "npm",
      "args": ["run", "dev"],
      "cwd": "/Users/lute/code/app-data-mcp"
    }
  }
}

HTTP / Streamable HTTP:

{
  "mcpServers": {
    "app-data": {
      "url": "http://127.0.0.1:3000/mcp",
      "headers": {
        "Authorization": "Bearer appdata_your-personal-token"
      }
    }
  }
}

不同 AI 平台的配置字段名可能略有差异,但核心就是把 MCP endpoint 指到 /mcp

Metabase / PostHog API 配置

不要把 API key、用户名、密码写入元信息表。密钥只通过部署环境变量提供。

复制 .env.example.env,在部署环境里填写:

METABASE_BASE_URL=https://metabase.example.com
METABASE_PUBLIC_URL=https://metabase.example.com
METABASE_API_KEY=...

POSTHOG_BASE_URL=https://posthog.example.com
POSTHOG_PROJECT_ID=...
POSTHOG_PERSONAL_API_KEY=...

run_asset 会使用这些环境变量调用 Metabase/PostHog 只读 API。资产不可 live 查询或连接失败时,才会按配置回退到本地 sampleData

METABASE_BASE_URL 是 MCP 服务端访问 Metabase API 的地址;METABASE_PUBLIC_URL 是返回给 AI 用户点击的数据来源地址。远程部署时如果服务端通过本机端口访问 Metabase,例如:

METABASE_BASE_URL=http://127.0.0.1:3000
METABASE_PUBLIC_URL=http://54.226.190.74:3000

这样 MCP 查询仍走服务器本地 API,但返回的 asset.url / source.url 会是远程可访问地址。修改后建议重新运行 npm run sync:metabase;即使暂时不重同步,服务读取时也会按 METABASE_PUBLIC_URL 重写 Metabase 来源链接。

如果没有拿到 METABASE_API_KEY,可以用 Metabase 用户名密码:

METABASE_BASE_URL=https://metabase.example.com
METABASE_USER=your-user@example.com
METABASE_PASS=your-password

Metabase connector 会用这组配置调用 POST /api/session 换取 session id,再用 X-Metabase-Session 请求 dashboard/card API。也兼容旧变量名 METABASE_USERNAMEMETABASE_PASSWORD

可以通过 MCP tool connector_status 检查配置是否齐全;它只返回是否配置和认证模式,不返回密钥或密码。

Metabase 用户授权与后台管理员登录

每个用户配置个人 MCP token,服务端用 token 反查对应的 Metabase 账号和 Session,并严格按该账号权限查询:

METABASE_BASE_URL=https://app-data.luteos.site
METABASE_LOGIN_URL=https://app-data.luteos.site
METABASE_AUTH_MODE=user-session
METABASE_ALLOW_SERVICE_FALLBACK=false
APP_DATA_MCP_PUBLIC_BASE_URL=http://127.0.0.1:3000
APP_DATA_REQUIRE_AUTH_TOKEN=true
APP_DATA_SESSION_FILE=.data/metabase-sessions.json

个人 MCP token 本身不按时间过期,MCP 也不再使用 METABASE_SESSION_TTL_HOURS 主动判定底层 Session 过期。只要 Metabase 接受该 Session,服务就持续使用它。只有 Metabase 实际返回 HTTP 401 时,才返回 reauth_required,要求对应账号重新授权以替换平台 Session;同一账号重新授权时会生成新的个人 MCP token,并立即使该账号之前的 token 失效,也不会自动切换到统一服务账号绕过用户权限。

用户首次授权打开:

http://127.0.0.1:3000/auth/metabase/login

授权后将页面生成的 Authorization: Bearer appdata_xxx 配置到 AI 助手。服务端只保存 token 哈希和 Metabase Session,不保存用户密码。

管理后台仍然要求使用 Metabase 管理员账号登录:

http://127.0.0.1:3000/admin

只保存随机后台 Session 的哈希、CSRF token 和管理员邮箱到 PostgreSQL,不保存管理员密码。设置 ADMIN_SESSION_PERSISTENT=true 后,后台 Session 默认不设置服务端过期时间,并在管理员访问后台时滚动刷新浏览器 Cookie;服务重启不会要求重新登录。主动退出、清理数据库 Session 或浏览器清除 Cookie 后仍需重新登录。

审计日志

MCP tool 调用会写入 Postgres 审计表,默认表名:

public.app_data_mcp_audit_logs

本地配置示例:

AUDIT_LOG_ENABLED=true
AUDIT_LOG_TABLE=app_data_mcp_audit_logs
AUDIT_REQUEST_PREVIEW_BYTES=12000
AUDIT_RESULT_PREVIEW_BYTES=16000
AUDIT_RESULT_PREVIEW_ITEMS=5
DB_TYPE=postgres
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=superset
DB_PASSWORD=123456
DB_NAME=cubecore
DB_SCHEMA=public
DB_SSL=false
DB_SSL_REJECT_UNAUTHORIZED=true
DB_SSL_CA_FILE=

Amazon RDS 等要求加密连接的 PostgreSQL 需要设置:

DB_SSL=true
DB_SSL_REJECT_UNAUTHORIZED=true
DB_SSL_CA_FILE=/path/to/global-bundle.pem

DB_SSL_CA_FILE 应指向数据库服务商提供的 CA bundle。仅在临时排障且无法立即安装 CA 时,可以设置 DB_SSL_REJECT_UNAUTHORIZED=false;连接仍会加密,但不会验证数据库服务器证书身份,不建议长期使用。

服务首次写入时会自动执行 create table if not exists,表名前缀为 app_data_mcp_,避免和业务表冲突。已存在的审计表会自动补新增列。审计日志记录:

  • 用户邮箱、鉴权方式、AI 助手平台、request id、IP、user agent。

  • tool 名称、asset id、平台、资产类型、用户原始数据问题 question_text、查询文本 query_text、limit。

  • 客户端传入的 conversation_idturn_idtool_call_id;未提供 tool_call_id 时使用服务端 request id。

  • query_starrocks 记录 SQL 文本、默认数据库和 SQL limit,便于追责与排障。

  • 问题、参数和实际返回内容的 SHA-256 哈希,便于判断内容是否变化。

  • 脱敏后的完整工具请求参数预览,包括 question、调用关联 ID、资产、筛选条件和语义查询参数;默认最多 12 KB。

  • 返回行数、返回字节数、耗时、状态、错误摘要和结果预览。

每个 MCP tool 都要求 AI 在 question 参数中原样传递用户当前轮次的完整问题,并要求同一轮问题触发的多次调用复用相同 turn_idconversation_idtool_call_id 在客户端可以提供时一并传入。通用 MCP 客户端仍由模型填写这些参数;如果可以控制 AI 客户端或 MCP 网关,建议在发送 tools/call 前自动注入,以保证问题文本和关联 ID 完全准确。

请求参数预览默认最多保存 12 KB,普通工具的结果预览默认最多保存 16 KB。对象结构、计数、列信息等会保留;数组只保存前 5 项,并附带总项数和省略项数,因此大型 Dashboard、Card 或 SQL 查询不会把全部数据写入审计表。search_assets 是资产治理链路的核心审计信息:AI 侧最多看到 5 个可用候选,审计侧则完整保存权限过滤后的全部召回候选、是否符合精简响应门槛、评分依据和元数据,不受结果预览条数和字节数限制。审计中的 candidateCounteligibleCandidateCountreturnedCount 可用于区分召回量、合格候选量和实际返回量。所有预览中的 Authorization、token、Session、下载 URL、本地路径等敏感字段仍会被遮盖。普通工具的限制可通过 AUDIT_REQUEST_PREVIEW_BYTESAUDIT_RESULT_PREVIEW_BYTESAUDIT_RESULT_PREVIEW_ITEMS 调整。

AI 助手平台优先读取请求头:

X-App-Data-Client: claude-code
X-App-Data-Client-Version: 1.2.3

客户端名称和版本优先读取上述请求头;未提供时,服务会读取 MCP 标准初始化请求中的 clientInfo,并优先按 Mcp-Session-Id、否则按个人 MCP Token 与 User-Agent 的组合指纹,将该身份关联到后续工具请求,避免同一 token 在多个客户端之间串标。Codex 的无状态工具调用还会通过请求 _meta 中的 Codex turn metadata 直接识别;WorkBuddy 会通过 codebuddy.ai 请求元数据直接识别,二者都不依赖身份缓存。其他客户端最后再尝试从 user_agent 推断。当前可识别 WorkBuddy、Claude Code、Claude Desktop、Codex、ChatGPT、Cursor、Windsurf、Roo Code、Cline、Continue、GitHub Copilot、Gemini CLI、Kiro、Amazon Q、Goose、OpenCode、Zed、VS Code、JetBrains、MCP Inspector、Dify、Coze、n8n、LobeChat、Cherry Studio、AnythingLLM、Open WebUI、LangChain,以及常见 HTTP 调试客户端。未知但具有标准 产品名/版本 格式的客户端会保留产品名;确实没有可用标识时才记录为 unknown。历史审计记录如果曾写成 unknown 或未保存版本,后台列表也会根据已保存的 user_agent 重新识别。

不会记录完整的大型数据结果,也不会记录用户密码、Metabase session、个人 MCP token 明文。审计后台的“查看”操作可以看到用户原始问题、调用关联 ID、查询文本、返回大小、结果预览和各类哈希。

查看最近调用:

select
  created_at,
  user_email,
  ai_client,
  tool_name,
  question_text,
  turn_id,
  asset_id,
  status,
  row_count,
  result_bytes,
  result_truncated,
  duration_ms,
  error_code
from public.app_data_mcp_audit_logs
order by created_at desc
limit 50;

如果要临时关闭审计:

AUDIT_LOG_ENABLED=false

PostHog 同步脚本访问的是 private API,例如 dashboards 和 insights,需要 Personal API key。不要使用 Project API key / project token。POSTHOG_API_KEY 仍作为兼容别名支持,但建议新配置统一使用:

POSTHOG_PERSONAL_API_KEY=phx_...

Personal API key 还需要 property_definition:read,用于只读同步公共属性定义。运行 Insight 需要 insight:read;执行动态 breakdown 还需要 query:read

资产 ID 规范

建议使用统一 ID,避免 AI 客户端理解不同平台的内部 ID:

  • metabase:dashboard:101

  • metabase:card:456metabase:model:388

  • posthog:dashboard:abc

  • posthog:insight:activation-funnel

  • metric:activation_rate

后台管理与元信息配置

HTTP 服务启动后访问:

http://127.0.0.1:3000/admin

后台使用 Metabase 账号登录,并通过 /api/user/current 校验 is_superuser=true。管理员可以:

  • 在 Metabase、PostHog 导航中查看同步状态和元信息。

  • 勾选或取消 is_published,也可以选择多条后批量开放或关闭;变更会立即影响 MCP 搜索和读取。

  • 点击列表字段标题可按开放状态、标题、类型、业务域、有效状态和同步时间排序。

  • Metabase、PostHog 和审计列表使用固定默认列宽;拖拽表头右侧分隔线可调整列宽,结果保存在当前浏览器。

  • 业务域使用可搜索组合框,支持输入过滤候选项;也可按资产类型和开放状态筛选,并与关键词搜索、排序和分页组合使用。

  • 编辑标题、描述、搜索别名/业务叫法、适用问题正例、不适用问题反例和 Card 指标集语义。Metabase 业务域只读并始终跟随同步的 Collection;PostHog 业务域可选择已有值,也可输入新名称加入候选项。其他人工配置保存在 admin_overrides,后续同步不会覆盖;标签和负责人在编辑弹窗中只读展示。

  • 在详情弹窗查看 URL、SQL/查询定义、字段、参数、Dashboard 映射、血缘、权限快照、警告、样例数据和完整 JSON;同步字段只读。

  • 在审计导航查看 AUDIT_LOG_TABLE 中的用户、AI 客户端、tool、用户数据问题、资产、状态、行数和耗时。新日志直接展示 question_text;旧日志兼容读取 metadata.questionquery_text

  • 在总览查看按 turn_id 聚合的用户对话统计、每日去重用户趋势,以及 get_asset / run_asset 的资产查询与运行次数;统计日期按中国时区自然日计算。

  • 在 MCP 工具管理导航查看工具名称、分类、风险、详情描述和开放状态;关闭后工具不会出现在未授权账号的新 MCP 连接 tools/list 中。

  • query_starrocksexport_audience 支持在操作列按用户邮箱授权。全局开放表示所有账号可见;全局关闭时只有显式授权账号能看见和调用,撤销授权后立即失效。

  • MCP 工具管理页可查看和编辑连接时发送给 AI 的中文全局 instructions,并在工具详情中查看调用时机和参数说明;工具标识和参数名保留英文,确保客户端能准确调用。

  • 未授权工具不会出现在 tools/list 中,实际发送给 AI 的说明也会移除所有提及该工具的语句,不暴露工具名称或禁用状态;服务端调用层会再次校验权限。

  • 在“问题回归测试”中自动汇集已发布资产的适用问题正例,也可新增管理员维护的问题;每个问题可配置多个预期资产,任一预期资产进入 AI 前五即视为通过。一键执行会复用资产检索模拟的生产排序逻辑,保存逐题候选、最佳排名、失败原因和 Top 1/3/5、MRR 等批次指标。

  • 资产正例由资产元信息单向同步,在回归测试页面只读;需要增删正例时应编辑对应资产的元信息。管理员维护的问题可直接在回归测试页面编辑。问题只要出现在列表中就会参与每次回归测试,避免因停用产生评测盲区。

问题回归测试表会随元信息表按相同前缀自动创建,包括 *_search_regression_cases*_search_regression_expectations*_search_regression_runs*_search_regression_results。自动正例首次编辑后会转为管理员维护,后续资产正例同步不会覆盖其预期关系。

管理员登录会话持久化到 PostgreSQL 的 app_data_mcp_admin_sessions 表。浏览器 Cookie 只保存随机令牌,数据库只保存令牌哈希;服务重启后在有效期内无需重新登录。默认有效期为 168 小时,可配置:

ADMIN_SESSION_TTL_HOURS=168
ADMIN_SESSION_TABLE=app_data_mcp_admin_sessions

元信息表默认名为 public.app_data_mcp_assets,主要字段包括 asset_idplatformmetadata jsonbadmin_overrides jsonbis_publishedis_active 和同步时间。metadata 内的核心内容包括:

  • title / description: 给 AI 搜索和理解使用。

  • businessDomain / tags: 用于业务域过滤和召回。

  • owner: 平台同步的负责人姓名或邮箱,也可由后台人工覆盖。

  • retrieval: 人工维护的检索同义词、适用问题正例和不适用问题反例;正例增强召回,反例按问题相似度降权而不硬过滤。

  • analysisType / insightSemantic: PostHog Insight 的分析类型、事件、拆分字段和过滤属性。

  • url: 必填,保证每个结果都有原始出处。

  • queryText: SQL、PostHog insight 描述或指标口径。

  • columns: 字段说明。

  • parameters: 可传给 run_asset 的只读参数说明,例如日期、国家、渠道、PostHog properties 等。

  • semantic: Metabase Card 的指标集定义,包括基础粒度、默认时间维度、维度、指标、Rollup 与累计能力;由后台写入 admin_overrides

  • dashboardParameterMappings: Metabase dashboard 筛选器到下属 card 的映射关系,用于判断某个筛选参数是否会真正影响某张 card。

  • sourceRefs: 上游表、事件、引用资产等血缘信息。

  • sampleData: 本地样例数据;live connector 失败或不支持时可作为兜底。

  • warnings: 数据延迟、口径注意事项、MVP 限制。

MCP 工具配置默认保存在 public.app_data_mcp_tools,全局说明保存在 public.app_data_mcp_settings,按用户授权保存在 public.app_data_mcp_tool_permissions;可以通过 MCP_TOOLS_TABLEMCP_SETTINGS_TABLEMCP_TOOL_PERMISSIONS_TABLE 修改表名。HTTP 模式每次请求都会按个人 MCP token 对应账号读取最新开关、授权和说明;stdio 或已经建立的长连接需要重新连接后才会更新工具清单。

下一步建议

  1. 接入 momcozy-data-agent 的受控语义层查询 API,作为 Metabase/PostHog curated assets 的 fallback。

  2. 增加 token 自助吊销/轮换页面。

  3. 增加敏感字段脱敏策略。

  4. 根据你们实际口径补充指标 registry 和数据负责人信息。

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jedshen123/app-data-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server