内容
# GesagtGetan.NeosMcp
MCP(模型上下文协议)服务器,用于 Neos 9 内容存储库。为 LLMs 提供对内容节点、节点类型和工作区的结构化访问。
## HTTP 传输(OAuth)
该软件包通过 HTTP 在 `POST /api/mcp` 上公开 MCP 服务器,并使用 OAuth 2.0(授权代码授予 + PKCE)进行保护。Claude.ai 的远程 MCP 连接器和 ChatGPT 使用此功能。
每个经过身份验证的用户都有自己的个人工作区(与他们在 Neos UI 中使用的同一个工作区)。更改是针对每个用户隔离的,可以独立审查和发布。JWT `sub` 声明包含 Neos `UserId`(UUID),而不是用户名 — 在令牌中不会泄露任何凭证。
### 设置
1. 生成客户端凭证(或如果已经存在,则从密码管理器中检索)
```bash
openssl rand -hex 16 # client_id
openssl rand -hex 32 # client_secret
```
2. 在 Claude.ai 或 ChatGPT 连接器的设置中输入它们,以及 MCP 端点 URL:`https://example.com/api/mcp`。
3. 配置 `Configuration/Production/Settings.yaml`:
```yaml
GesagtGetan:
NeosMcp:
oauth:
enabled: true
issuer: 'https://example.com'
client:
id: '<generated client_id>'
secret: '<generated client_secret>'
```
4. 运行 `./flow mcp:setup` 以创建 stdio 工作区,注册 OAuth 客户端,并生成 RSA 密钥:
```bash
./flow mcp:setup
```
> ⚠️ 重新运行 `mcp:setup` 以更新客户端凭证、重定向 URI 或任何其他 `oauth.client.*` 设置时。数据库不会自动更新。
5. 将 `GesagtGetan.NeosMcp:McpUser` 角色分配给应能够授权 MCP 访问的 Neos 帐户。
6. (仅适用于自定义后端登录)OAuth 同意屏幕通过 Flow 安全上下文识别登录的后端用户。默认 `Neos.Neos:Backend` 提供程序通过 `requestPatterns` 限定到 Neos 后端和服务控制器,因此在授权控制器上处于非活动状态。此软件包附带了一个请求模式,该模式也在那里激活,因此纯 Neos 安装无需额外配置即可工作。如果您的后端登录使用自定义身份验证提供程序(例如单点登录软件包)而不是默认提供程序,则该提供程序限定为其自己的控制器,并在同意屏幕上保持非活动状态,因此用户似乎已注销。在这种情况下,请在项目的 `Settings.yaml` 中为您的提供程序添加相同的请求模式,例如:
```yaml
Neos:
Flow:
security:
authentication:
providers:
'Vendor.SingleSignOn:Backend':
requestPatterns:
'GesagtGetan.NeosMcp:OAuthAuthorize':
pattern: 'ControllerObjectName'
patternOptions:
controllerObjectNamePattern: 'GesagtGetan\NeosMcp\OAuth\Controller\OAuthAuthorizeController'
```
7. 确保以下端点是公开可访问的(无基本身份验证,无防火墙限制):`/.well-known/oauth-protected-resource`、`/.well-known/oauth-authorization-server`、`/oauth/token`、`/api/mcp`。如果您的服务器使用基本身份验证或 IP 限制,请豁免这些路由。授权端点(`GET /api/mcp`)也被豁免,但需要 Neos 会话,因此没有安全漏洞。
您豁免它们的方式取决于您的服务器(例如虚拟主机中的条件语句或 `Web/.htaccess`)。精确匹配这些路径,以免您无意中扩大公共访问范围。
8. 启用 `mod_proxy_fcgi` 的 Apache 会在请求到达 PHP 之前剥离 `Authorization` 头,导致所有带有 bearer 令牌的请求都以 `401` 静默失败。在 `<IfModule mod_rewrite.c>` 块内的 `Web/.htaccess` 中添加以下行,位于 `RewriteBase /` 之后:
```apache
# 将 Authorization 头转发给 PHP — 否则 Apache mod_proxy_fcgi 会剥离它。
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
```
这会将 `Authorization` 头复制到 `HTTP_AUTHORIZATION` 环境变量中,以便 PHP 可以读取它。`RewriteCond` 确保仅在存在该头时才触发。
9. 在 Claude.ai 或 ChatGPT 中连接 MCP 服务器,并使用类似“列出连接的 Neos 网站的所有节点类型”的提示进行测试,以验证连接是否有效。
## CLI 传输(stdio,可选)
用于本地开发的可选传输。适用于直接测试工具或在不通过 OAuth 的情况下连接本地编码代理(例如 Claude Code)。
### 设置
```bash
./flow mcp:setup
```
这会创建共享的 stdio 工作区,生成 OAuth RSA 密钥,并在数据库中注册 OAuth 客户端。在初始设置期间和**每次配置更改**(凭证,重定向 URI 等)后运行它,以应用新值。
### 用法
```bash
./flow mcp:server
```
Flow 命令,从标准输入读取 MCP 请求,并将响应写入标准输出。需要 stdio 工作区存在 — 首次运行前请运行 `mcp:setup`。
来自 stdio 传输的内容写入会在共享的 stdio 工作区(`Settings.yaml` 中的 `stdioWorkspaceName`,默认为 `llm-review`)中进行,直到发布。要在 Neos 后端审查或发布它们,请将 `GesagtGetan.NeosMcp:McpUser` 角色分配给相关 Neos 帐户 — 否则工作区存在但在编辑器 UI 中不可见。
### Claude Code 配置
在项目根目录的 `.mcp.json` 中添加:
```json
{
"mcpServers": {
"neos": {
"command": "./flow",
"args": ["mcp:server"]
}
}
}
```
然后在 Claude Code 中运行 `/mcp` 以连接服务器。
### 可选工作区配置
在 `Settings.yaml` 中:
```yaml
GesagtGetan:
NeosMcp:
contentRepositoryId: 'default'
stdioWorkspaceName: 'llm-review'
stdioWorkspaceTitle: 'MCP Stdio'
stdioWorkspaceDescription: 'MCP stdio 传输的共享工作区'
stdioBaseWorkspaceName: 'live'
```
## 可用工具
### 读取
- `getContentRepositoryInfo` — 维度,工作区,维度空间点
- `listNodeTypes` — 列出非抽象节点类型(可选过滤)
- `getNodeTypeSchema` — 包括属性,子节点和引用的完整节点类型架构。从 NodeTypes.yaml 中的每个属性的 `ui.label` 和 `ui.help.message` 中获取每属性的 `label` 和 `description` — 在 Neos 编辑器工具提示中编写一次,为 LLM 获取相同的提示。
- `findNodes` — 按类型和/或搜索词搜索
- `getNode` — 获取具有所有属性的单个节点
- `getChildren` — 列出子节点,可选按类型过滤
- `getNodeReferences` — 外出引用(此节点指向哪些节点?)。在 Neos 9 中,引用位于 `properties` 之外,因此 `getNode` 不会返回它们。
- `getNodeBackReferences` — 进入引用(哪些节点指向此节点?)。在删除/移动之前进行影响分析非常有用。
- `getWorkspaceStatus` — 工作区状态,包括待处理更改计数
### 写入(在工作区中分阶段,需要人工发布)
- `createNode` — 在父节点下创建节点(自动生成 ID)
- `setNodeProperties` — 部分属性更新
- `setNodeReference` — 替换一个引用名称的所有引用(空 `targets` 删除它们)
- `moveNode` — 移动到新父节点
- `hideNode` — 从公共站点隐藏节点(可逆)
- `unhideNode` — 取消隐藏先前隐藏的节点
- `findAndReplace` — 跨内容树进行批量查找/替换
- `removeNode` — 软删除节点(可以恢复)
### 工作区
- `discardWorkspaceChanges` — 丢弃所有待处理更改
## LLM 的提示(`ui.help.message`)
LLM 偶尔会选择相似的错误属性。在 NodeTypes.yaml 中为属性留下提示:
```yaml
'Vendor.Pkg:Mixin.SomeMixin':
properties:
titleOverride:
ui:
help:
message: >-
一句话描述属性的用途以及何时(或
何时不)使用它。 在 `backticks` 中引用其他属性的机器
名称。
```
`getNodeTypeSchema` 以每属性 `description` 字段的形式返回 `ui.help.message`,因此 LLM 在每次检查架构后都会读取它。相同的字符串已经在 Neos 编辑器后端渲染为工具提示,因此编辑器和 LLM 共享一个真实来源 — 编写一次指导即可为两者带来好处。
何时添加提示:
- 两个属性具有相似的名称但目的不同(`title` / `titleOverride`,`image` / `fallbackImage` 等)
- 属性具有非明显的格式约束或与另一个属性的交互
- 属性看起来很有诱惑力,但实际上已弃用或仅由一个渲染路径使用
何时不添加提示:
- 属性名称不言自明,与编辑器字段标签匹配
- 每个属性都不需要工具提示;只有真正令人困惑的属性
## 工作区变基
Neos 内容存储库不会自动将更改从基准工作区(例如 `live`)传播到派生工作区。这意味着 MCP 工作区可能会过时:节点在 `live` 中删除或修改后,工作区中仍可见,直到发生显式变基。
为了保持 LLM 的视图新鲜,MCP 服务器在**每次工具调用前**都会变基工作区。这确保读取反映最新的实时状态,写入不会针对不再存在的节点。
### 冲突处理
如果 MCP 工作区中的未发布更改与实时更改冲突(例如,节点在工作区中编辑,但在实时中删除),则变基失败。发生这种情况时,工具调用仍会针对过时的工作区执行,但响应中会包含冲突警告,并提供有关哪些节点受影响的详细信息。LLM 然后可以决定通过 `discardWorkspaceChanges` 丢弃冲突更改或通知用户。
### 变基性能(1000 个实时节点,MariaDB,10 次运行)
| 场景 | 平均值 | 最小值 | 最大值 |
|----------|-----|-----|-----|
| 无操作(工作区已更新) | 4.6 毫秒 | 3.6 毫秒 | 5.8 毫秒 |
| 空工作区(过时,无未发布更改) | 41.2 毫秒 | 36.5 毫秒 | 51.8 毫秒 |
| 10 个未发布更改 | 285.7 毫秒 | 264.1 毫秒 | 366.1 毫秒 |
| 50 个未发布更改 | 1223.9 毫秒 | 1158.9 毫秒 | 1320.7 毫秒 |
常见情况(无操作)为每个工具调用增加约 5 毫秒。空但过时的情况(通常在发布后)增加约 40 毫秒。具有许多未发布更改的工作区更为昂贵,但在正常 MCP 使用中不常见。
## 架构
生产代码使用 `ContentRepositoryFacade` 接口(而不是最终的 `ContentRepository` 类)来实现通过模拟进行单元测试。`DefaultContentRepositoryFacade` 包装实用的 CR。
工具通过 `Tool\McpToolProvider` 实现提供。`Tool\McpToolProviderRegistry` Flow 单例使用 Flow 的 `ReflectionService` 来自动发现应用程序中的每个实现,并在每次请求时调度每个实现 — 没有 Settings.yaml 注册表需要维护。两个控制器(`Command\McpCommandController`,`Controller\McpHttpController`)在构建其服务器时都调用 `registerAll()`。
```
Classes/
Command/McpCommandController.php # CLI 入口点(stdio 传输,设置命令)
Controller/McpHttpController.php # HTTP 传输(OAuth 安全)
ContentRepositoryFacade.php # 接口,用于测试
DefaultContentRepositoryFacade.php # 包装真实 ContentRepository
Service/
NodeReadService.php # 读取操作(查找,获取,子节点)
NodeTypeService.php # 节点类型列表和架构
NodeWriteService.php # 写入操作(创建,更新,移动,删除)
Tool/
McpToolProvider.php # 接口 - 实现以贡献工具
McpToolProviderRegistry.php # 自动发现 + 调度(Flow 单例)
McpRequestContext.php # 每个请求的 DTO 交给每个提供者
McpToolReflector.php # 扫描 #[McpTool] 方法在处理程序上
WorkspaceRebaser.php # 调用前变基 + 冲突警告
McpNodeToolProvider.php # 内置:节点工具(读取/写入/类型)
McpWorkspaceToolProvider.php # 内置:工作区工具
```
## 使用自定义工具扩展
任何 Flow 软件包都可以通过实现 `GesagtGetan\NeosMcp\Tool\McpToolProvider` 来贡献 MCP 工具。注册表通过 `ReflectionService` 自动发现实现,因此无需在 `Settings.yaml` 中注册。
一个最小的无状态提供者 — 对于不需要工作区或 CR 访问的工具 — 将 `#[McpTool]` 方法保留在自身:
```php
use GesagtGetan\NeosMcp\Tool\McpRequestContext;
use GesagtGetan\NeosMcp\Tool\McpToolProvider;
use GesagtGetan\NeosMcp\Tool\McpToolReflector;
use Neos\Flow\Annotations as Flow;
use PhpMcp\Server\Attributes\McpTool;
use PhpMcp\Server\Defaults\BasicContainer;
use PhpMcp\Server\ServerBuilder;
#[Flow\Scope('singleton')]
final readonly class EchoToolProvider implements McpToolProvider
{
public function registerTools(ServerBuilder $builder, BasicContainer $container, McpRequestContext $context): ServerBuilder
{
$container->set(self::class, $this);
return McpToolReflector::register($builder, self::class);
}
#[McpTool(description: '将字符串回声回来')]
public function echo(string $message): string
{
return $message;
}
}
```
如果您的工具需要每个请求的 工作区/CR 状态,请在 `registerTools()` 中从 `McpRequestContext` 构建一个小型处理程序(就像 `McpNodeToolProvider` 一样)— 单例不应直接持有每个请求的 状态。
## 配套软件包
- **`gesagtgetan/neosmcpimages`** — 贡献 `findImagesByAltText` 和 `setImageProperty`,以便 LLM 可以通过其替代文本描述选取现有的媒体库图像,并将它们分配给 `ImageInterface` 类型的节点属性。与 `gesagtgetan/imagealttext` 配合使用,即 Neos 端的桥梁,从外部 Altaca 服务填充替代文本表。
## 开发
所有命令都在软件包目录(`DistributionPackages/GesagtGetan.NeosMcp/`)中运行。需要 [Just](https://just.systems/) >= 1.38.0。
```bash
just check # 运行所有静态分析(phpcs + php-cs-fixer + phpstan)
just fix # 自动修复代码样式问题
just test # 运行所有测试(单元 + 功能)
just test-unit # 仅运行单元测试
just test-functional # 仅运行功能测试
```
### 开发依赖项
开发工具在该软件包的 `composer.json` 的 `require-dev` 中声明。
### 功能测试先决条件
功能测试需要一个 MySQL/MariaDB 测试数据库(在主机项目的 `Configuration/Testing/Settings.yaml` 中配置)。内容存储库自己的表(事件存储,投影)由测试基类自动创建。
### 常见问题
**为什么有两个 PHPUnit 配置?** Flow 在 `Build/BuildEssentials/PhpUnit/` 的全局 `FunctionalTests.xml` 中提供,但它使用 PHPUnit 9 XML 架构。该项目运行 PHPUnit 10,它更改了 `<exclude>` 处理并导致符号链接的软件包被发现两次。我们的 `phpunit-functional.xml.dist` 使用 PHPUnit 10 架构,仅扫描此软件包,避免重复执行和弃用警告。
**为什么不使用 SQLite?** Neos 内容存储库的 DoctrineDbal 适配器使用 MySQL 特有的 SQL(例如 `INSERT IGNORE`),而 SQLite 不支持。需要一个真实的 MySQL/MariaDB 数据库。
### 提示:在开发过程中连接 MCP 服务器
在开发此软件包时,使用具有 MCP 能力的编码代理(例如 Claude Code)连接到正在运行的 MCP 服务器实例 — 通过 stdio 或远程通过 HTTP 传输 — 以便它可以查询实际节点,检查工作区状态,并针对真实数据验证工具行为。(在本地代码更改后,重新连接 MCP 服务器 — 例如,通过 Claude Code 中的 `/mcp`)以便代理拾取更新的 PHP 文件。
为了在工具更改后获得更快的反馈循环,请将 JSON-RPC 直接输送到 `./flow mcp:server` 而不是重新连接 MCP 客户端 — 每次调用都读取当前的 PHP 文件,因此您可以运行一个快速的 `initialize` → `notifications/initialized` → `tools/call` 序列来验证工具的原始 `TextContent` JSON 输出。写入会进入与连接客户端流程相同的 stdio 工作区。