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 协议实时流转。

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)

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_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 (示例含义)

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

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables 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.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables 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 npm
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    Interactive 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.
    8
    60 npm
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Provides 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.
    -