mcp-demo-aad-viz
mcp-demo-aad-viz
一个把两种 MCP 能力合在一起演示的实例。这两种能力通常都是单独展示的,而放在一起会大有用处:
Microsoft Entra ID(Azure AD)授权 —— 服务器是一个 OAuth 2.1 资源服务器。你的 Entra 组成员身份决定你能看到哪些数据集。不是"列出之后再拒绝"——而是直接不存在。
应用内应用 / 扩展(
io.modelcontextprotocol/ui) —— 图表以在对话中渲染出的 交互式小组件 形式到达,调整它 只花零 token。
两者结合起来,就是值得一看的东西:一个 Altair 图表构建器,它的数据集下拉框里恰好只有你的 Entra 组允许的数据集,并且服务端对这个 widget 的每一次交互都会强制做一次鉴权。
构建基于 MCP 2026-07-28 规范,使用 Python SDK mcp 2.0。部署目标为 Azure Container Apps。MIT 许可。
提醒一下: 这是一个演示项目,不是产品。它内置了十个公开的示例数据集和一套刻意简化、刻意可读的授权模型,好让授权流程一目了然。

不连 Azure 试用
不需要租户、不需要认证、不需要部署 —— 足以看到 widget 如何工作:
uv sync && uv run python scripts/fetch_datasets.pyMCP_DATAVIZ_AUTH_ENABLED=false MCP_DATAVIZ_PORT=3001 uv run python -m mcp_dataviz此时所有调用者都被视为拥有全部三个数据集层级。把任意 MCP Apps 宿主指向 http://localhost:3001/mcp —— 见 本地开发,那里有一个浏览器基宿,可以让你看到整个 ui 协议实时流转。
Related MCP server: Vela MCP Server
连上 Azure 试用
# 1. Directory objects (app registration, scopes, app roles, 3 groups)
./scripts/entra-setup.sh# 2. Put yourself in a group to pick a persona
source entra.env
az ad group member add --group "$MCP_DATAVIZ_GROUP_ANALYSTS_ID" \
--member-id "$(az ad signed-in-user show --query id -o tsv)"# 3. Deploy (builds the image in Azure; no local Docker needed)
./scripts/deploy.sh --tag v1脚本会打印你的 MCP 端点。把它原样照抄到你的客户端 —— /mcp 路径是 OAuth 资源标识符的一部分 → docs/CONNECT.md。
每次部署都传一个 唯一 的
--tag。如果重复使用同一个 tag,Bicep 模板会与现有模板逐字节相同,不会重建新版本,部署会报成功但其实没有发出任何东西。
它演示了什么
MCP 能力 | 对应位置 | 你能看到什么样的现象 |
授权(OAuth 2.1 RS) | 组成员身份决定目录里有什么数据集,而不是先列出再拒绝 | |
MCP Apps ( | 下拉框就地渲染出图 | |
仅应用可用的工具( |
| widget 重绘 只花零 token |
|
| 没带 widget 的宿主得到一个表单 |
scope 升级( |
| 第一次导出触发再次授权 |
资源 + 模板 |
| 按权限过滤 |
补全 | 数据集参数 | 自动完成的候选里永远不会出现你无权打开的数据集 |
Prompts |
| 有引导的第一次阅读 |
这份规范在这个版本里分割除了 sampling,另外还间接废弃了 logging(见 SEP-2577),因此本服务器两者都没有实现。suggest_chart 会根据列类型选一个 mark,而不是去调模型。
授权模型
这里有两个互相独立的维度,把它俩搞混是常见的错误。
WHO YOU ARE WHAT YOU'RE DOING
Entra group ──► app role ──► dataset tier OAuth scope ──► operation
(roles claim) (scp claim)
analysts → Open ( 4) Datasets.Read → everything
engineers → Open + Operations ( 7) Datasets.Export → export_chart
scientists → Open + Confidential ( 7) ↑ withheld at first, so the
...a *different* 7 first export triggers a step-up
(no group) → nothing ( 0)层级 | 角色 | 数据集 |
open |
| iris、penguins、電 只是 setup 开 |
open |
| seattle-weather, us-employment, gapminder |
confidential |
| diamonds, movies, titanic |
Flush table:
层级 | 角色 | 数据集 |
open |
| iris, penguins, cars, barley |
operations |
| seattle-weather, us-employment, gapminder |
confidential |
| diamonds, movies, titanic |
工程师和科学家拥有的数据集总数相同,但不完全重合,两个同事问同样的一个问题,有时会收到不同的答案。
./scripts/assign-persona.sh engineer colleague@example.com --now--now 也会直接把角色指派给你的用户:一个组变更可能要几分钟才能更新用户可用的 token,而直接分配大概二十秒就能生效。
角色是硬性拒绝。 将向你所在层级之外的数据集,不会出现在 tools/list 的结果里、resources/list 里、补全建议里,也不会成为 widget 下拉框的一个项目 —— 不是先列出然后再拒绝。
Scope 是软性拒绝。 缺少 Datasets.Export 时会返回 403,并带 WWW-Authenticate: Bearer error="insufficient_scope" 挑战;客户端随后会重新授权并请求该 scope。
本仓库特意绕开了 Entra 的两个坑,你如果手动配,它会给出很令人费解的错误:Entra 没有动态客户端注册,也没有 RFC 8414 元数据端点;另外 MCP 的 URL 必须注册为一个 Application ID URI,否则 RFC 8707 的 resource= 会报 AADSTS9010010。
完整细节见:docs/AUTHZ.md · docs/此时需要。
为什么这个 widget 有意思
一段内联数据的 Vega-Lite spec 有 30–300 KB。如果图省事从工具返回,这段 spec 每张图都会进入模型的上下文。所以这里改成了:
plot_dataset返回的是一个约 73 字节的 handle,写入编码、行数、警告等元信息;不包含 spec。宿主渲染
ui://应用并把 handle 交给它。widget 再从应用里调用
render_chart(一个 app-only 工具)去真正取得 spec。
因为第 3 步由应用发起、而不是由模型发起,spec 永远不会进入对话。更改一下拉框只是服务器和 widget 的小往返,也不耗 token。
授权模型在这里同样成立:render_chart 和 app_catalogue 每次调用都会重新从当前 token 推导用户的层级,所以即使模型已不在请求链路里,这个 widget 也无法触达 token 不允许的数据集。
设计记录:docs/DESIGN.md。
本地开发
两个测试台,对应两个不同的目的。
"我 widget 里的 HTML/JS 写得对不对?" —— 一个微型宿主,它真正使用 ui/ 的 postMessage 协议并把每条消息打印出来,完全不连 MCP 客户端:
uv run python scripts/preview_widget.py # http://127.0.0.1:8765它会加载一个真实宿主才会加载的严格 CSP,所以 CSP 问题会在本地就浮现,而不是要等部署后才看到。--strip-structured-content 会把[文档][docs/host-compat]里记录的某个宿主缺陷模拟出来,便于你对照验证。
"我的 MCP 对外表面做得对?" —— 来自 MCP Apps 的仓库的参考宿主,直接通过 HTTP 驱动真实服务器:
git clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps && npm install && cd examples/basic-host
SERVERS='["http://localhost:3001/mcp"]' npm start # http://localhost:8080如果你的 widget 渲染空白,这个 harness 最能立即给线索:他能把所有线上宿主会默默吞掉的协议违规全部报出来。在 MCP_DATAVIZ_AUTH_ENABLED=false 时运行服务,SDK 也会放宽 Origin 校验、并加上 CORS 头 —— 浏览器宿主需要这些,而且在启用鉴权时它们是关掉的。
顺便说,widget 的 HTML 在服务器启动时只读取一次,所以改了它要重开一次服务。
宿主兼容性
各宿主对 MCP Apps 的支持参差不齐,有时表现出的症状却看起来一样 —— widget 是空白或折叠的,且没有放任何报错。HOST-COMPATIBILITY.md 记录了实际见到的情况、每次问题是怎么隔离出来的,以及哪些可以在服务端修复(三者之一)而哪些不行。
数据集
四个 open、三个 operations、三个 confidential —— 全部来自 Vega 数据集里最流行的公开示例数据集,并在构建时就被踢进了镜像,所以运行时不依赖任何数据源。层级标注的做法只是拿来示意,为了让访问模型更具体。
open | operations | confidential (示例含义) |
|
|
|
|
|
|
|
|
|
|
目录结构
src/mcp_dataviz/
server.py tools, resources, prompts, completions
auth.py Entra token verification, roles→tiers, scope challenge
catalog.py the 10 datasets and the tier gate
charts.py Altair → Vega-Lite, with aggregation pushed into pandas
config.py environment settings (nothing hardcoded)
widgets/ the MCP App
infra/ Bicep: ACR, Container Apps, Log Analytics
scripts/ entra-setup.sh, deploy.sh, assign-persona.sh, preview_widget.py
tests/ 168 tests, incl. HTTP-level auth and step-up
docs/ AUTHZ, CONNECT, DESIGN, HOST-COMPATIBILITYPython 包魔法保持旧名 mcp_dataviz(以及加编号的环境变量前缀),即使仓库名为 mcp-demo-aad-viz;如果改东改西要把每个 Azure 资源名和环境变量命名都得改,没有好处。
测试
uv run pytest # 168 tests, no Azure neededuv run ruff check src tests scriptstests/test_http.py 起了一个真实的 uvicorn 服务器,断言了 401 挑战、PRM( 권한文档)、403 insufficient_scope 升级以及 input_required 往返流程。
成本
Container Apps 支持缩到零(minReplicas: 0),所以空闲 demo 时不怎么花钱;只有 ACR Basic 和 Log Analytics 是常用常驻的开销(每月几欧元级)。
az group delete --name rg-mcp-dataviz --yes && ./scripts/entra-teardown.shLicense
This server cannot be deployed
Maintenance
Related MCP Connectors
- BasedashOAuthcom.basedash
Governed BI MCP. Ask questions of live company data and list workspace sources via OAuth.
Build multi-tenant apps over MCP. Schemas, CRUD, deploys — access control enforced server-side.
Query your warehouse or a CSV with Claude/ChatGPT over MCP, governed by table-level ACL + audit.
Governed data discovery, exact queries, decisions, simulations, and runtime utilities over MCP.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with Databricks Genie Spaces through MCP tools. Provides secure OAuth-based access to query and interact with Databricks data catalogs and schemas via custom Genie interfaces.-
- AlicenseNot gradedqualityDmaintenanceEnables governed, agent-agnostic data exploration by allowing users to ask natural language questions through MCP-compatible agents, executing safe, permission-scoped queries against data sources and returning interactive charts.18 npmApache 2.0
- AlicenseAqualityBmaintenanceInteractive visualization of Microsoft Entra ID identity relationships, enabling exploration of org charts, groups, attributes, and access assignments through a D3 force-directed graph within MCP clients.860 npmMIT
- FlicenseNot gradedqualityBmaintenanceProvides MCP tools to query Tableau Server/Cloud datasources via REST API and VizQL Data Service, with support for Gemini or OpenAI as the LLM backend. Enables a natural language chat interface that can be embedded in Tableau dashboards, automatically including dashboard filter context.-