Skip to main content
Glama
sinanpl

mcp-demo-aad-viz

by sinanpl

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 许可。

提醒一下: 这是一个演示项目,不是产品。它内置了十个公开的示例数据集和一套刻意简化、刻意可读的授权模型,好让授权流程一目了然。

The interactive Altair widget, you understand


不连 Azure 试用

不需要租户、不需要认证、不需要部署 —— 足以看到 widget 如何工作:

uv sync && uv run python scripts/fetch_datasets.py
MCP_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)

auth.py

组成员身份决定目录里有什么数据集,而不是先列出再拒绝

MCP Apps (io.modelcontextprotocol/ui)

chart_builder.html

下拉框就地渲染出图

仅应用可用的工具visibility: ["app"]

render_chart

widget 重绘 只花零 token

input_required

plot_dataset

没带 widget 的宿主得到一个表单

scope 升级403 insufficient_scope

export_chart

第一次导出触发再次授权

资源 + 模板

data://upload

按权限过滤

补全

数据集参数

自动完成的候选里永远不会出现你无权打开的数据集

Prompts

explore

有引导的第一次阅读

这份规范在这个版本里分割除了 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

Datasets.Open

iris、penguins、電 只是 setup 开

open

Datasets.Operations

seattle-weather, us-employment, gapminder

confidential

Datasets.Confidential

diamonds, movies, titanic

Flush table:

层级

角色

数据集

open

Datasets.Open

iris, penguins, cars, barley

operations

Datasets.Operations

seattle-weather, us-employment, gapminder

confidential

Datasets.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 每张图都会进入模型的上下文。所以这里改成了:

  1. plot_dataset 返回的是一个约 73 字节的 handle,写入编码、行数、警告等元信息;不包含 spec。

  2. 宿主渲染 ui:// 应用并把 handle 交给它。

  3. widget 再从应用里调用 render_chart(一个 app-only 工具)去真正取得 spec。

因为第 3 步由应用发起、而不是由模型发起,spec 永远不会进入对话。更改一下拉框只是服务器和 widget 的小往返,也不耗 token。

授权模型在这里同样成立:render_chartapp_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 (示例含义)

iris

seattle-weather

diamonds — 单价

penguins

us-employment

movies — 票房

cars

gapminder

titanic — 个人记录

barley


目录结构

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-COMPATIBILITY

Python 包魔法保持旧名 mcp_dataviz(以及加编号的环境变量前缀),即使仓库名为 mcp-demo-aad-viz;如果改东改西要把每个 Azure 资源名和环境变量命名都得改,没有好处。

测试

uv run pytest          # 168 tests, no Azure needed
uv run ruff check src tests scripts

tests/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.sh

License

MIT

-
license - not tested
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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.

View all MCP Connectors

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/sinanpl/mcp-demo-aad-viz'

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