ctp-registry
v0.2.0
Published
Component Tool Protocol (CTP) - A standardized protocol for UI components to declaratively expose tool capabilities to LLM Agents
Maintainers
Readme
CTP Registry
Component Tool Protocol (CTP) — 让 UI 组件声明式地向 LLM Agent 暴露工具能力的标准化协议
什么是 CTP
CTP(Component Tool Protocol)是一个标准化协议,定义了前端 UI 组件如何向 LLM Agent 声明和暴露工具能力。
核心思想:组件自治 + Agent 自动发现
每个 UI 组件在挂载时主动声明「我能做什么」,Agent 无需硬编码即可动态发现和调用所有可用工具。这就像前端组件级别的 MCP。
特性
- 框架无关 — 核心 ToolRegistry 零依赖,React/Vue 适配层为可选
- OpenAI 兼容 —
getTools()直接输出 function-calling 格式 - 生命周期感知 — 挂载注册、卸载清理、切换页面工具自动消失
- 命名空间隔离 — 每个 Provider 有独立 scope,工具名不冲突
- 结构化错误 — 6 种 CTPErrorCode,LLM 能区分错误类型并自我修正
- 中间件管线 — 可插拔 middleware,支持日志/审计/限流
- 执行历史 — 可选 call history,支持多轮对话上下文回溯
- 条件可用性 — 工具可动态启用/禁用,Agent 自动感知
安装
npm install ctp-registry
# or
yarn add ctp-registry
# or
pnpm add ctp-registry快速开始
1. 组件注册工具
import { toolRegistry } from 'ctp-registry';
// 视频面板注册两个工具
const unregister = toolRegistry.register('video-panel', {
description: '视频面板 — 相机查询与帧截取',
tools: [
{
name: 'list_camera_topics',
description: '列出当前可用的相机 video topics',
parameters: { type: 'object', properties: {} },
execute: async () => {
const topics = getVideoTopics();
return { topics, count: topics.length };
},
},
{
name: 'capture_frame',
description: '截取指定相机的当前画面',
parameters: {
type: 'object',
properties: {
topic: { type: 'string', description: '相机 topic 名称' },
time_s: { type: 'number', description: '可选,跳转到指定时间后截取' },
},
required: ['topic'],
},
execute: async ({ topic, time_s }) => captureFrame(topic, time_s),
},
],
});
// 组件销毁时调用
unregister();2. Agent 发现工具
const tools = toolRegistry.getTools();
// [
// { type: 'function', function: {
// name: 'video-panel__list_camera_topics',
// description: '[video-panel] 列出当前可用的相机 video topics',
// parameters: { type: 'object', properties: {} }
// }},
// { type: 'function', function: {
// name: 'video-panel__capture_frame',
// ...
// }},
// ]
// 直接传给 LLM — 无需转换
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages,
tools, // ← CTP 生成的工具列表
tool_choice: 'auto',
});3. 路由调用
// LLM 选择了 video-panel__capture_frame
const { name, arguments: rawArgs } = toolCall.function;
if (toolRegistry.has(name)) {
const result = await toolRegistry.call(name, JSON.parse(rawArgs));
// → 自动路由到 video-panel 的 capture_frame
// → 参数校验 → 中间件管线 → execute() → 返回结果
// → 结果回传给 LLM 继续对话
}React 集成
import { useToolProvider, useToolDiscovery } from 'ctp-registry/react';
// 组件内声明工具
function VideoPanel({ videoTopics }) {
useToolProvider('video-panel', {
description: '视频面板',
tools: [
{
name: 'capture_frame',
description: '截取当前画面',
parameters: { type: 'object', properties: {} },
execute: async () => takeScreenshot(),
},
],
}, [videoTopics]); // deps 变化时重新注册
return <div>...</div>;
}
// Agent 面板发现工具
function AgentPanel() {
const ctpTools = useToolDiscovery();
const handleToolCall = async (name: string, args: object) => {
if (toolRegistry.has(name)) {
return await toolRegistry.call(name, args);
}
};
return <Chat tools={ctpTools} onToolCall={handleToolCall} />;
}Vue 集成
<script setup>
import { useToolProvider, useToolDiscovery } from 'ctp-registry/vue';
const props = defineProps(['videoTopics']);
// 组件内声明工具
useToolProvider('video-panel', () => ({
description: '视频面板',
tools: [
{
name: 'capture_frame',
description: '截取当前画面',
parameters: { type: 'object', properties: {} },
execute: async () => takeScreenshot(),
},
],
}), [() => props.videoTopics]);
// Agent 面板发现工具
const tools = useToolDiscovery();
</script>高级特性
结构化错误体系
import { CTPErrorCode } from 'ctp-registry';
// 错误返回 JSON,LLM 可解析
{
"error": {
"code": "INVALID_PARAMS",
"message": "Parameter validation failed",
"details": {
"errors": [{
"field": "topic",
"expected": "present",
"actual": "missing",
"message": "Required parameter \"topic\" is missing"
}]
}
}
}中间件管线
// 日志中间件
const removeLogger = toolRegistry.use(async (ctx, next) => {
console.debug('[CTP] →', ctx.qualifiedName, ctx.args);
const t0 = performance.now();
const result = await next();
const ms = (performance.now() - t0).toFixed(0);
console.debug('[CTP] ←', ctx.qualifiedName, ms + 'ms');
return result;
});
// 权限中间件
toolRegistry.use(async (ctx, next) => {
if (ctx.scope === 'admin' && !currentUser.isAdmin) {
return JSON.stringify({
error: { code: 'FORBIDDEN', message: 'Admin only' }
});
}
return next();
});
// 移除中间件
removeLogger();执行历史
// 启用历史记录(最多保留 50 条)
toolRegistry.enableHistory({ limit: 50 });
// 获取最近 10 条调用记录
const recent = toolRegistry.getHistory({ limit: 10 });
// → [{ qualifiedName, args, result, durationMs, success, timestamp }]
// 传给 LLM 作为上下文
const systemPrompt = `Recent tool calls: ${JSON.stringify(recent)}`;
// 关闭历史
toolRegistry.disableHistory();条件可用性
toolRegistry.register('player', {
tools: [{
name: 'capture',
description: '截取当前画面',
parameters: { type: 'object', properties: {} },
execute: async () => takeScreenshot(),
// 静态禁用
enabled: false,
disabledReason: '当前无视频流',
}, {
name: 'seek',
description: '跳转到指定时间',
parameters: { type: 'object', properties: { time_s: { type: 'number' } } },
execute: async ({ time_s }) => seekTo(time_s),
// 动态判断
enabled: () => playerState === 'playing',
disabledReason: '播放器未处于播放状态',
}],
});
// getTools() 默认只返回 enabled 的工具
// getTools({ includeDisabled: true }) 返回所有API 参考
核心 API
| 方法 | 签名 | 描述 |
|------|------|------|
| register | (scope, options) => () => void | 注册 Provider,返回卸载函数 |
| getTools | (filter?) => OpenAITool[] | 获取工具,OpenAI function-calling 格式 |
| call | (qualifiedName, args?) => Promise<string> | 调用工具(含校验+中间件) |
| has | (qualifiedName) => boolean | 检查工具是否注册 |
| use | (middleware) => () => void | 注册中间件 |
| subscribe | (callback) => () => void | 订阅变化事件 |
| enableHistory | (options?) / disableHistory() | 启用/禁用执行历史 |
| getHistory | (options?) => HistoryEntry[] | 获取执行历史 |
| getProviders | () => ProviderInfo[] | 获取所有 Provider 摘要 |
| updateTool | (scope, name, patch) | 局部更新工具属性 |
React Hooks
| Hook | 描述 |
|------|------|
| useToolProvider | 挂载注册,卸载清理,deps 变化重注册 |
| useToolDiscovery | 响应式工具发现(基于 useSyncExternalStore) |
| useToolHistory | 跟踪工具执行历史 |
| useToolRegistry | 访问 ToolRegistry 实例 |
Vue Composables
| Composable | 描述 |
|------------|------|
| useToolProvider | 挂载注册,卸载清理,deps 变化重注册 |
| useToolDiscovery | 响应式工具发现 |
| useToolHistory | 跟踪工具执行历史 |
| useToolRegistry | 访问 ToolRegistry 实例 |
命名规范
全限定名格式
格式: scope__toolName
分隔: 双下划线 __ (兼容 OpenAI / Anthropic / Bedrock API)
字符: [a-zA-Z0-9_-]
示例:
nsv-data__get_nsv_frame_data
video-panel__capture_frame
page-context__get_task_run_info建议
- Scope: kebab-case,表达组件职责(
nsv-data而非left-panel) - 工具名: snake_case,动词开头(
get_/fetch_/list_/capture_) - 描述: 要对 LLM 友好 — 它是 LLM 理解工具用途的唯一依据
CTP 与 MCP 的关系
| 协议 | 定位 | 使用场景 | |------|------|----------| | MCP (Model Context Protocol) | 跨进程/网络的远程工具协议 | 知识库检索、代码执行服务器 | | CTP (Component Tool Protocol) | 前端组件级别的本地工具协议 | 视频截取、数据查询、UI 操作 |
两者互补,在实际项目中共存:
- CTP 工具:前端组件声明的本地能力
- MCP 工具:远端服务器提供的能力
- Agent 同时发现 CTP + MCP 工具,统一传给 LLM
架构图
┌──────────────┐ ┌──────────────┐ ┌────────────┐
│ 地图面板 │ │ 视频播放器 │ │ 数据表格 │
│ │ │ │ │ │
│ 我能: │ │ 我能: │ │ 我能: │
│ · 定位坐标 │ │ · 截取画面 │ │ · 查询数据 │
│ · 切换图层 │ │ · 跳转时间 │ │ · 导出 CSV │
└──────┬───────┘ └──────┬───────┘ └──────┬──────┘
│ register │ register │ register
▼ ▼ ▼
┌─────────────────────────────────────────────┐
│ ToolRegistry (单例) │
│ │
│ map__locate player__capture table__query │
│ map__switch_layer player__seek table__export │
└────────────────────────┬───────────────────┘
│ getTools() → OpenAI 格式
│ call(name, args) → 路由执行
▼
┌──────────────┐
│ LLM Agent │
│ 发现 6 个工具 │
│ 自主选择调用 │
└──────────────┘协议优化建议
基于 CTP.md 文档,以下是一些协议优化建议:
- 类型安全增强: 使用 TypeScript 泛型确保工具参数和返回值的类型安全
- 批量操作: 支持批量注册/调用工具,减少往返次数
- 工具版本控制: 添加版本字段支持工具演进
- 依赖注入: 支持工具间的依赖关系声明
- 沙箱执行: 可选的沙箱环境执行不可信工具代码
- 性能监控: 内置性能指标收集(执行时间、内存使用等)
- 缓存机制: 支持工具结果的智能缓存
贡献
欢迎提交 Issue 和 PR!
