内容
# mcp-product-infra
适用于应用程序的MCP一体化解决方案。
`mcp-product-infra`是一个为开发者提供的小型工具包,允许他们的应用程序通过MCP被代理操作,而无需每次都重新构建设置、生命周期、配置和恢复逻辑。
它是**Rust原生的**,因为第一个提取来自Ishoo的Rust MCP运行时。它并不打算仅限于Rust。目标形状是:
```text
Rust应用程序
直接导入 crate
非Rust应用程序
运行一个小的mcp-product-infra sidecar或应用程序拥有的辅助二进制文件
通过清单/本地进程边界连接处理程序
```
这不是一个代理框架、市场、通用MCP管理器、桌面应用程序或云服务。它是应用程序拥有的MCP层。
## 状态
**Ishoo运行在此crate上。** 提取已完成服务器+ sidecar层:Ishoo的MCP运行时(`src/mcp/`)现在是`mcp-product-infra`上的薄产品组合,Ishoo的完整预提取MCP行为套件(126个测试:握手、调度顺序、所有者恢复、失败关闭写入)未经修改地通过它。
从生产事件中继承的硬性行为,与当前Ishoo保持一致:
- 有限的所有者生命周期:一个分离的驻留所有者空闲回收,而不是永远保持安装的二进制文件锁定
- 优雅的升级交接:一个重建/重新安装的应用程序通过其构建指纹(应用程序版本+二进制签名)退休过时的构建所有者
- 重新安装后执行解析:一个`current_exe()`在就地重新安装后读取`… (已删除)`,仍然解析为替换二进制文件
- Unix上的僵尸意识:一个退出的但未被回收的所有者读取为死,因此恢复重新选举而不是永远楔入写入(并且生成的子进程被回收,因此僵尸永远不会形成)
- Windows上的退出代码意识:一个`OpenProcess`句柄到一个终止的pid不是生命的证明
- 失败关闭写入:一个实时但不可达的所有者拒绝突变而不是产生一个竞争写入者
## 它为您提供
- stdio MCP JSON-RPC服务器循环
- 类型化的工具注册表
- `initialize`、`tools/list`、`tools/call`、`ping`和通知处理
- 可选的MCP资源(`resources/list`/`resources/read`)从应用程序提供的封闭集
- `tools/list`注释:`readOnlyHint`从每个工具的读写分类中派生,每个工具的其余部分都有重写
- 带有文本回退的结构化MCP响应+`structuredContent`
- 读写调度:读取工具可以并发运行,变异工具按到达顺序序列化
- 恐慌遏制:一个恐慌的处理程序用JSON-RPC内部错误响应,而不是挂起请求或杀死突变工作线程
- `before_tool`预调度钩子(每个调用的准备/新鲜度门控,可以短路调用)
- 进程内stdout捕获(`capture::capture_stdout`),以便print-first CLI核心函数可以作为工具处理程序而无需重写
- 纯文本工具结果(`Value::String`结果作为原始文本提供,无`structuredContent`)
- `ServerConfig::serial()`适用于其处理程序永远不会重叠响应写入的应用程序(捕获情况)
- 可选的驻留侧车所有者,用于需要单个写入器的应用程序状态
- 通过锁定文件实现崩溃安全的选举
- 重建/重新安装后过时的所有者退休
- 不会覆盖的Claude Code和Codex配置安装程序
- 存储库范围或用户范围的主机设置
- `CLAUDE.md`和`AGENTS.md`的可选托管块
- 主机就绪事实
- 未来的语言无关侧车路径的清单结构体
## 为什么存在
MCP为宿主和服务器提供协议。它并没有为应用程序开发者提供必要的操作层,使应用程序对代理来说更易用。
第一个工具处理程序通常很容易。令人讨厌的部分是:
- 宿主如何发现服务器?
- 配置放在哪里?
- 如何避免默认情况下乱七八糟的仓库?
- 当应用程序二进制文件更改时会发生什么?
- 当服务器进程成为孤儿时会发生什么?
- 写操作如何避免相互竞争?
- 工具结果如何告诉代理实际发生了什么?
- 如何在不手动编辑JSON/TOML的情况下修复设置?
`mcp-product-infra`打包了这些枯燥的难点。
## 来源
此存储库是从**Ishoo**中的MCP工作中提取的,该应用程序的代理界面是MCP-first。
从Ishoo复制的源文件位于`origin/ishoo/`:
```text
origin/ishoo/src/mcp/mod.rs
origin/ishoo/src/mcp/transport.rs
origin/ishoo/src/mcp/registry.rs
origin/ishoo/src/mcp/tests.rs
origin/ishoo/src/model/adapters.rs
origin/ishoo/src/main_cli.rs
origin/ishoo/src/main_dispatch.rs
origin/ishoo/src/main_dispatch/handlers.rs
origin/ishoo/config/.mcp.json
origin/ishoo/config/codex-config.toml
origin/ishoo/config/claude-settings.local.json
origin/ishoo/config/CLAUDE.md
```
`origin/ishoo/SHA256SUMS`记录了复制文件的哈希值。
有关源映射,请参见[`EXTRACTION.md`](./EXTRACTION.md)。
## 安装
对于本地开发:
```toml
[dependencies]
mcp-product-infra = { path = "../mcp-product-infra" }
serde_json = "1"
```
发布后:
```toml
[dependencies]
mcp-product-infra = "0.1"
serde_json = "1"
```
## Rust原生用法
```rust
use serde_json::json;
use mcp_product_infra::{McpServer, ServerConfig, ToolError, ToolSpec};
fn main() {
let server = McpServer::new(
ServerConfig::new("todo", env!("CARGO_PKG_VERSION"), ".")
.instructions("使用todo_* MCP工具。从todo_status开始.")
.tool(ToolSpec::read(
"todo_status",
"返回应用程序状态。",
json!({ "type": "object", "properties": {}, "additionalProperties": false }),
|ctx, _args| Ok(json!({ "app": ctx.app_name, "ok": true })),
))
.tool(ToolSpec::write(
"todo_create",
"创建待办事项。",
json!({
"type": "object",
"properties": { "title": { "type": "string" } },
"required": ["title"],
"additionalProperties": false
}),
|_ctx, args| {
let title = args
.get("title")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::invalid_params("todo_create requires title"))?;
Ok(json!({ "id": "TODO-1", "title": title }))
},
)),
);
std::process::exit(server.run_stdio());
}
```
运行示例:
```bash
cargo run --example minimal
```
主机可以像这样启动应用程序的MCP入口点:
```json
{
"mcpServers": {
"todo": {
"command": "todo",
"args": ["mcp"]
}
}
}
```
## 非Rust应用程序
Rust crate并不意味着只有Rust应用程序才能使用这个想法。
该项目应该支持两种集成模式:
### 模式1:应用程序拥有的辅助二进制文件
任何应用程序都可以提供一个小的MCP辅助二进制文件,使用`mcp-product-infra`构建:
```text
Electron / Python / Go / Swift 应用程序
拥有应用程序行为
调用或与
小Rust MCP辅助程序使用mcp-product-infra构建
向
Claude Code / Codex / Cursor / 其他主机
公开MCP
```
这通常是生产中最干净的形状:您的应用程序仍然用自己的语言编写,而MCP生命周期层是一个小的本地侧车。
### 模式2:清单/进程桥
预期的语言无关的下一步是一个清单模式:
```bash
mcp-product-infra serve --manifest ./mcp.manifest.json
```
清单示例:
```json
{
"name": "todo",
"version": "0.1.0",
"instructions": "使用todo_*工具操作此应用程序。",
"handler": {
"command": "python",
"args": ["./todo_handlers.py"]
},
"tools": [
{
"name": "todo_status",
"description": "返回应用程序状态。",
"mutation": "never",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false
}
},
{
"name": "todo_create",
"description": "创建待办事项。",
"mutation": "always",
"inputSchema": {
"type": "object",
"properties": { "title": { "type": "string" } },
"required": ["title"],
"additionalProperties": false
}
}
]
}
```
侧车拥有MCP帧、主机配置、生命周期和调度。应用程序拥有的处理程序进程拥有行为。
此软件包包括清单类型和文档。生产清单运行器是下一个实现步骤。
## 工具结果
处理程序返回类型化的JSON:
```rust
Ok(json!({ "id": "TODO-1", "title": "Ship it" }))
```
服务器将其包装为MCP成功有效载体:
```json
{
"content": [
{ "type": "text", "text": "{\n \"id\": \"TODO-1\"\n}" }
],
"structuredContent": {
"id": "TODO-1",
"title": "Ship it"
},
"isError": false
}
```
这为每个主机提供文本回退,同时为使用它的主机保留结构化数据。
## 读写调度
`ToolSpec::read`工具并发处理。`ToolSpec::write`工具通过一个FIFO突变工作线程。
这为应用程序拥有的状态提供了有用的默认设置:
- 慢速写入不会阻塞状态/读取工具
- 依赖写入不能重新排序
- 变异调用有一个序列化点
对于依赖于参数的工具,请使用`ToolSpec::dynamic`:
```rust
ToolSpec::dynamic(
"todo_item",
"根据操作读取或变异todos。",
schema,
|args| args.get("op").and_then(|v| v.as_str()) != Some("show"),
handler,
)
```
## 资源、注释和预调度门控
将应用程序工件作为MCP资源提供(**封闭集** - `resources/read`仅提供提供者枚举的URI,从不任意路径):
```rust
use mcp_product_infra::{ResourceContent, ResourceEntry};
let config = config.resources(|ctx| {
let map = ctx.workspace_root.join("MAP.md");
let mut entries = Vec::new();
if map.is_file() {
entries.push(ResourceEntry::file(map, "MAP.md", "渲染的地图.", "text/markdown"));
}
entries
});
```
`tools/list`始终宣传注释:`readOnlyHint`从工具的分派分类中派生(`ToolSpec::read`→`true`),并且`ToolSpec::with_annotations(json!({ "destructiveHint": true }))`合并分类无法知道的任何内容。请注意,`ToolSpec::dynamic`工具宣传`readOnlyHint: false` - 静态主机提示不能依赖调用的参数 - 因此使用`ToolSpec::read`注册一个从不变异的工具。
使用`before_tool`(例如semmap样式的索引新鲜度:刷新导航工具之前的索引,如果刷新失败,则大声失败)为每个调用设置应用程序就绪状态:
```rust
let config = config.before_tool(|ctx, name, _args| {
if name == "myapp_generate" {
return Ok(()); // 生成索引的工具是豁免的
}
refresh_index(&ctx.workspace_root)
.map_err(|e| mcp_product_infra::ToolError::server(format!("auto-refresh failed: {e}")))
});
```
## 打印优先的CLI:stdout捕获
如果您的核心函数打印结果而不是返回类型化值,`capture::capture_stdout(cwd, f)`运行`f`,将进程全局stdout重定向到管道(unix `dup2`,windows `SetStdHandle`),工作目录设置为`cwd`,一个排空线程对抗管道缓冲区死锁,和一个恐慌安全的恢复。调用由内部全局锁序列化。 这就是CLI的精确输出成为工具的文本内容而无需重写每个命令。 警告:重定向在`cargo test`的输出捕获下无法观察到 - 使用二进制文件进行测试。
基于捕获的处理程序需要两个配置选项:返回捕获的文本作为`Value::String`(作为原始文本块提供,无`structuredContent`),并使用`ServerConfig::serial()`运行传输 - 文件描述符重定向是进程全局的,因此并发响应写入主循环将进入捕获管道(及其文本进入JSON-RPC流)。
## 主机设置
`HostInstall`为应用程序拥有的MCP服务器提供不会覆盖的配置写入器:
```rust
use mcp_product_infra::{HostInstall, HostServer};
let install = HostInstall::new("todo")
.server(HostServer::stdio("todo", "/usr/local/bin/todo", ["mcp"]));
install.install_repo(std::path::Path::new("."))?;
install.install_user()?;
```
默认情况下,主机配置应仅由显式安装命令更改,而不是由随机应用程序启动或工具调用更改。
## 侧车所有者
驻留所有者模式适用于具有需要单个写入器的本地状态的应用程序:
```text
主机生成的MCP stdio进程
附加到/选举
驻留所有者进程
拥有应用程序状态和序列化突变
```
该模式直接来自Ishoo的MCP传输。通用模块保留端点、令牌、锁、指纹和恢复件,同时将应用程序特定的状态启动留给应用程序。
在主机生成的MCP进程中,使用侧车配置服务器:
```rust
use mcp_product_infra::{McpServer, ServerConfig, SidecarConfig};
let sidecar = SidecarConfig::new("todo", ".", ".todo/cache")
.owner_args(["--path", ".", "mcp-owner"]);
let server = McpServer::new(
ServerConfig::new("todo", env!("CARGO_PKG_VERSION"), ".")
.sidecar(sidecar)
// .tool(...)
);
std::process::exit(server.run_stdio());
```
在隐藏的所有者进程中,构建相同的服务器**不带**.sidecar(...)`,在`init`钩子中运行您的状态启动(它在单个锁被赢得并且套接字存在之前执行,因此失败的第二个所有者永远不会运行竞争启动工作),并将`handle_line`传递给所有者运行时:
```rust
use mcp_product_infra::{sidecar, McpServer, ServerConfig, SidecarConfig};
let sidecar_config = SidecarConfig::new("todo", ".", ".todo/cache")
.app_version(env!("CARGO_PKG_VERSION"))
.owner_args(["--path", ".", "mcp-owner"]);
let owner_server = McpServer::new(
ServerConfig::new("todo", env!("CARGO_PKG_VERSION"), ".")
// .tool(...)
);
sidecar::run_owner_server(
sidecar_config,
|| {
// 应用程序状态启动:同步、后台工作线程、打开存储…
Ok(())
},
move |line| owner_server.handle_line(line),
)?;
```
该拆分是Ishoo的关键非回归目标:stdio进程是可处置的主机胶水;驻留所有者是应用程序/存储的权限。
`SidecarConfig`携带生命周期旋钮:
- `.app_version(...)` - 折叠到所有者构建指纹中,以便应用程序发布会退休过时的所有者
- `.idle_timeout(...)` / `.idle_timeout_env("MYAPP_OWNER_IDLE_TIMEOUT_MS")` - 有限的所有者生命周期(默认为10分钟而没有客户端请求)
- `.liveness_path(...)` - 所有者退出时此路径消失(例如您的状态目录),因此删除的工作区的所有者永远不会徘徊
- `.drain(|| …)` - 在任何所有者退出(空闲回收或关闭交接)之前运行,以便完成正在进行的序列化工作
## 说您的应用程序语言
代理应该看到您的应用程序词汇,而不是这个库的。`ServerConfig`公开产品缝隙:
```rust
use mcp_product_infra::OwnerProse;
ServerConfig::new("todo", env!("CARGO_PKG_VERSION"), ".")
// 失败关闭的所有者不可用的错误,以您的词句:
.owner_prose(OwnerProse {
code: -32010,
prefix: "todo服务不可用 - 写入拒绝; 未进行任何更改.".into(),
owner_noun: "驻留的todo所有者".into(),
restart_hint: "重启Todo(或运行 `todo mcp-owner`)".into(),
})
// {PREFIX}_MCP_SHUTDOWN_DRAIN_MS / {PREFIX}_MCP_PARENT_WATCHDOG_MS的前缀: .env_prefix("TODO")
// 注释仅当读取降级超过不可达的所有者时,请注释您的状态工具(默认为:每个降级读取都被注释): .annotate_degraded_reads(["todo_status"])
.read_annotation_source("todo_transport")
// 容忍失败的所有者选举,当您的应用程序没有状态时(“打开一个新的存储库”情况)而不是退出:
.sidecar_optional()
// 在成功的突变结果中附加耐用性事实: .mutation_hook(|_ctx, _tool, value| {
// 例如快照应用程序状态,然后将结果折叠到`value`
Ok(())
});
```
## 测试您的集成
`mcp_product_infra::sidecar::test_support` 为您的套件提供所有者恢复形状而无需子进程:`unreachable_endpoint`(一个注册表,其套接字拒绝),`write_endpoint`(持久化注册),和`start_owner_thread`(脚本化的进程所有者)。 `Dispatch`/`ServerEvent`运行时内部公开`#[doc(hidden)]`以便移植现有的MCP回归套件。
## 项目边界
该软件包应保持小尺寸。
它不应成为:
- 代理框架
- 通用MCP市场
- MCP服务器的图形管理器
- 云服务
- 工作流引擎
- LLM库
它应该保持:
```text
应用程序拥有的MCP的枯燥部分,封装起来。
```
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器