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 协议实时流转。
连上 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 installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
MCP Hub: AI service discovery, per-user OAuth, and multi-service workflow orchestration
Official Microsoft MCP Server to query Microsoft Entra data using natural language
Manage SRG+ hubs, channels, content, assets, users, and workspaces from any MCP-aware AI agent.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/sinanpl/mcp-demo-aad-viz'
If you have feedback or need assistance with the MCP directory API, please join our Discord server