catpaw-observer-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/store.d.ts +44 -0
- package/dist/store.js +57 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# CatPaw Observer MCP
|
|
2
|
+
|
|
3
|
+
CatPaw 全链路观测平台 MCP Server —— 为 AI Agent(Claude Desktop、Cursor、Cline 等)提供任务与执行链路的记录、查询和统计能力。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
通过 MCP 协议暴露以下工具:
|
|
8
|
+
|
|
9
|
+
| 工具 | 说明 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `create_task` | 创建观测任务 |
|
|
12
|
+
| `record_trace` | 记录一次执行的完整链路(耗时、Token、成本、步骤等) |
|
|
13
|
+
| `list_tasks` | 查询任务列表 |
|
|
14
|
+
| `get_task_detail` | 获取任务详情与执行记录汇总 |
|
|
15
|
+
| `get_trace_detail` | 获取单条链路详情 |
|
|
16
|
+
| `get_statistics` | 全局统计汇总(成功率、平均耗时、总 Token、总成本) |
|
|
17
|
+
| `delete_task` | 删除任务及其所有记录 |
|
|
18
|
+
|
|
19
|
+
数据保存在本机 `~/.catpaw-observer` 目录下的 JSON 文件中。
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g catpaw-observer-mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 使用
|
|
28
|
+
|
|
29
|
+
### 命令行
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
catpaw-observer-mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Claude Desktop
|
|
36
|
+
|
|
37
|
+
在 `claude_desktop_config.json` 中添加:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"catpaw-observer": {
|
|
43
|
+
"command": "catpaw-observer-mcp"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 使用 npx
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"catpaw-observer": {
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["-y", "catpaw-observer-mcp"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 环境要求
|
|
63
|
+
|
|
64
|
+
- Node.js >= 18
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { getTasks, getTask, saveTask, deleteTask, getTraces, getTracesByTask, saveTrace } from './store.js';
|
|
6
|
+
const server = new McpServer({ name: 'catpaw-observer-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
7
|
+
server.registerTool('create_task', {
|
|
8
|
+
description: '创建一个新的 CatPaw 观测任务',
|
|
9
|
+
inputSchema: { name: z.string().describe('任务名称'), category: z.string().optional().describe('任务分类'), description: z.string().optional().describe('任务描述'), prompt: z.string().describe('Prompt 内容') },
|
|
10
|
+
}, async ({ name, category, description, prompt }) => {
|
|
11
|
+
const task = await saveTask({ name, category: category || '其他', description: description || '', prompt, status: 'pending' });
|
|
12
|
+
return { content: [{ type: 'text', text: JSON.stringify({ message: '观测任务创建成功', task }, null, 2) }] };
|
|
13
|
+
});
|
|
14
|
+
server.registerTool('record_trace', {
|
|
15
|
+
description: '记录 CatPaw 执行全链路数据',
|
|
16
|
+
inputSchema: { taskId: z.string().describe('任务ID'), status: z.enum(['success', 'failed', 'running']).describe('状态'), totalDuration: z.number().describe('总耗时(ms)'), inputTokens: z.number().describe('输入Token'), outputTokens: z.number().describe('输出Token'), model: z.string().optional().describe('模型'), cost: z.number().optional().describe('成本'), qualityScore: z.number().optional().describe('质量评分'), steps: z.array(z.object({ name: z.string(), duration: z.number(), inputTokens: z.number(), outputTokens: z.number(), status: z.enum(['success', 'running', 'warning', 'failed']) })).describe('步骤') },
|
|
17
|
+
}, async ({ taskId, status, totalDuration, inputTokens, outputTokens, model, cost, qualityScore, steps }) => {
|
|
18
|
+
const trace = await saveTrace({ taskId, status, totalDuration, totalTokens: inputTokens + outputTokens, inputTokens, outputTokens, model: model || 'CatPaw', cost: cost || 0, qualityScore: qualityScore || null, stepCount: steps.length, steps: steps.map(s => ({ name: s.name, duration: s.duration, inputTokens: s.inputTokens, outputTokens: s.outputTokens, status: s.status })) });
|
|
19
|
+
const task = await getTask(taskId);
|
|
20
|
+
if (task)
|
|
21
|
+
await saveTask({ ...task, status: status === 'success' ? 'completed' : status });
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify({ message: '链路记录保存成功', trace: { id: trace.id, totalDuration: `${(trace.totalDuration / 1000).toFixed(2)}s`, totalTokens: trace.totalTokens.toLocaleString(), cost: `$${trace.cost}` } }, null, 2) }] };
|
|
23
|
+
});
|
|
24
|
+
server.registerTool('list_tasks', {
|
|
25
|
+
description: '查询任务列表', inputSchema: { status: z.enum(['pending', 'running', 'completed', 'failed']).optional(), limit: z.number().optional() },
|
|
26
|
+
}, async ({ status, limit }) => {
|
|
27
|
+
let tasks = await getTasks();
|
|
28
|
+
if (status)
|
|
29
|
+
tasks = tasks.filter(t => t.status === status);
|
|
30
|
+
tasks = tasks.slice(-(limit || 20)).reverse();
|
|
31
|
+
return { content: [{ type: 'text', text: JSON.stringify({ total: tasks.length, tasks: tasks.map(t => ({ id: t.id, name: t.name, category: t.category, status: t.status })) }, null, 2) }] };
|
|
32
|
+
});
|
|
33
|
+
server.registerTool('get_task_detail', {
|
|
34
|
+
description: '获取任务详情和执行记录', inputSchema: { taskId: z.string().describe('任务ID') },
|
|
35
|
+
}, async ({ taskId }) => {
|
|
36
|
+
const task = await getTask(taskId);
|
|
37
|
+
if (!task)
|
|
38
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '任务不存在' }) }], isError: true };
|
|
39
|
+
const traces = await getTracesByTask(taskId);
|
|
40
|
+
return { content: [{ type: 'text', text: JSON.stringify({ task, traces, summary: { totalExecutions: traces.length, totalTokens: traces.reduce((s, t) => s + t.totalTokens, 0).toLocaleString(), totalCost: `$${traces.reduce((s, t) => s + t.cost, 0).toFixed(3)}` } }, null, 2) }] };
|
|
41
|
+
});
|
|
42
|
+
server.registerTool('get_trace_detail', {
|
|
43
|
+
description: '获取链路详情', inputSchema: { traceId: z.string().describe('链路ID') },
|
|
44
|
+
}, async ({ traceId }) => {
|
|
45
|
+
const trace = (await getTraces()).find(t => t.id === traceId);
|
|
46
|
+
if (!trace)
|
|
47
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '链路记录不存在' }) }], isError: true };
|
|
48
|
+
return { content: [{ type: 'text', text: JSON.stringify({ trace, steps: trace.steps.map((s, i) => ({ step: i + 1, name: s.name, duration: `${(s.duration / 1000).toFixed(2)}s`, tokens: (s.inputTokens + s.outputTokens).toLocaleString(), status: s.status })) }, null, 2) }] };
|
|
49
|
+
});
|
|
50
|
+
server.registerTool('get_statistics', {
|
|
51
|
+
description: '获取全局统计汇总', inputSchema: {},
|
|
52
|
+
}, async () => {
|
|
53
|
+
const tasks = await getTasks();
|
|
54
|
+
const traces = await getTraces();
|
|
55
|
+
const ok = traces.filter(t => t.status === 'success');
|
|
56
|
+
return { content: [{ type: 'text', text: JSON.stringify({ statistics: { totalTasks: tasks.length, totalTraces: traces.length, successRate: traces.length ? `${Math.round(ok.length / traces.length * 100)}%` : 'N/A', avgDuration: ok.length ? `${Math.round(ok.reduce((s, t) => s + t.totalDuration, 0) / ok.length / 1000)}s` : 'N/A', totalTokens: traces.reduce((s, t) => s + t.totalTokens, 0).toLocaleString(), totalCost: `$${traces.reduce((s, t) => s + t.cost, 0).toFixed(3)}` } }, null, 2) }] };
|
|
57
|
+
});
|
|
58
|
+
server.registerTool('delete_task', {
|
|
59
|
+
description: '删除任务及所有记录', inputSchema: { taskId: z.string().describe('任务ID') },
|
|
60
|
+
}, async ({ taskId }) => { await deleteTask(taskId); return { content: [{ type: 'text', text: JSON.stringify({ message: '任务已删除', taskId }) }] }; });
|
|
61
|
+
async function main() { await server.connect(new StdioServerTransport()); console.error('CatPaw Observer MCP Server 已启动'); }
|
|
62
|
+
main().catch(console.error);
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface TraceStep {
|
|
2
|
+
name: string;
|
|
3
|
+
duration: number;
|
|
4
|
+
inputTokens: number;
|
|
5
|
+
outputTokens: number;
|
|
6
|
+
status: 'success' | 'running' | 'warning' | 'failed';
|
|
7
|
+
}
|
|
8
|
+
export interface Trace {
|
|
9
|
+
id: string;
|
|
10
|
+
taskId: string;
|
|
11
|
+
status: 'success' | 'failed' | 'running';
|
|
12
|
+
totalDuration: number;
|
|
13
|
+
totalTokens: number;
|
|
14
|
+
inputTokens: number;
|
|
15
|
+
outputTokens: number;
|
|
16
|
+
model: string;
|
|
17
|
+
cost: number;
|
|
18
|
+
qualityScore: number | null;
|
|
19
|
+
stepCount: number;
|
|
20
|
+
steps: TraceStep[];
|
|
21
|
+
createdAt: string;
|
|
22
|
+
}
|
|
23
|
+
export interface Task {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
category: string;
|
|
28
|
+
prompt: string;
|
|
29
|
+
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
30
|
+
createdAt: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function getTasks(): Promise<Task[]>;
|
|
33
|
+
export declare function getTask(id: string): Promise<Task | undefined>;
|
|
34
|
+
export declare function saveTask(t: Omit<Task, 'id' | 'createdAt'> & {
|
|
35
|
+
id?: string;
|
|
36
|
+
createdAt?: string;
|
|
37
|
+
}): Promise<Task>;
|
|
38
|
+
export declare function deleteTask(id: string): Promise<void>;
|
|
39
|
+
export declare function getTraces(): Promise<Trace[]>;
|
|
40
|
+
export declare function getTracesByTask(id: string): Promise<Trace[]>;
|
|
41
|
+
export declare function saveTrace(t: Omit<Trace, 'id' | 'createdAt'> & {
|
|
42
|
+
id?: string;
|
|
43
|
+
createdAt?: string;
|
|
44
|
+
}): Promise<Trace>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
const DATA_DIR = join(homedir(), '.catpaw-observer');
|
|
5
|
+
const TASKS_FILE = join(DATA_DIR, 'tasks.json');
|
|
6
|
+
const TRACES_FILE = join(DATA_DIR, 'traces.json');
|
|
7
|
+
async function ensureDir() {
|
|
8
|
+
try {
|
|
9
|
+
await access(DATA_DIR);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function readJson(p, d) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(await readFile(p, 'utf-8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return d;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function writeJson(p, d) {
|
|
24
|
+
await ensureDir();
|
|
25
|
+
await writeFile(p, JSON.stringify(d, null, 2), 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
function genId(p) { return `${p}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
|
|
28
|
+
export async function getTasks() { return readJson(TASKS_FILE, []); }
|
|
29
|
+
export async function getTask(id) { return (await getTasks()).find(t => t.id === id); }
|
|
30
|
+
export async function saveTask(t) {
|
|
31
|
+
const all = await getTasks();
|
|
32
|
+
const n = { ...t, id: t.id || genId('task'), createdAt: t.createdAt || new Date().toISOString() };
|
|
33
|
+
const i = all.findIndex(x => x.id === n.id);
|
|
34
|
+
if (i >= 0)
|
|
35
|
+
all[i] = n;
|
|
36
|
+
else
|
|
37
|
+
all.push(n);
|
|
38
|
+
await writeJson(TASKS_FILE, all);
|
|
39
|
+
return n;
|
|
40
|
+
}
|
|
41
|
+
export async function deleteTask(id) {
|
|
42
|
+
await writeJson(TASKS_FILE, (await getTasks()).filter(t => t.id !== id));
|
|
43
|
+
await writeJson(TRACES_FILE, (await getTraces()).filter(t => t.taskId !== id));
|
|
44
|
+
}
|
|
45
|
+
export async function getTraces() { return readJson(TRACES_FILE, []); }
|
|
46
|
+
export async function getTracesByTask(id) { return (await getTraces()).filter(t => t.taskId === id); }
|
|
47
|
+
export async function saveTrace(t) {
|
|
48
|
+
const all = await getTraces();
|
|
49
|
+
const n = { ...t, id: t.id || genId('trace'), createdAt: t.createdAt || new Date().toISOString() };
|
|
50
|
+
const i = all.findIndex(x => x.id === n.id);
|
|
51
|
+
if (i >= 0)
|
|
52
|
+
all[i] = n;
|
|
53
|
+
else
|
|
54
|
+
all.push(n);
|
|
55
|
+
await writeJson(TRACES_FILE, all);
|
|
56
|
+
return n;
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "catpaw-observer-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CatPaw 全链路观测平台 MCP Server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"catpaw-observer-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
21
|
+
"zod": "^3.23.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.0.0",
|
|
25
|
+
"typescript": "^5.0.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|