@mclawnet/mcp-server 0.1.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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +55 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/evolution.d.ts +76 -0
- package/dist/tools/evolution.d.ts.map +1 -0
- package/dist/tools/evolution.js +142 -0
- package/dist/tools/evolution.js.map +1 -0
- package/dist/tools/index.d.ts +229 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +26 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory.d.ts +137 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +221 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/skill.d.ts +36 -0
- package/dist/tools/skill.d.ts.map +1 -0
- package/dist/tools/skill.js +69 -0
- package/dist/tools/skill.js.map +1 -0
- package/package.json +33 -0
- package/src/__tests__/e2e-memory-pipeline.test.ts +627 -0
- package/src/__tests__/evolution-tools.test.ts +94 -0
- package/src/__tests__/memory-tools.test.ts +259 -0
- package/src/__tests__/skill-tools.test.ts +78 -0
- package/src/index.ts +3 -0
- package/src/server.ts +77 -0
- package/src/tools/evolution.ts +157 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/memory.ts +280 -0
- package/src/tools/skill.ts +79 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { EvolutionPipeline } from "@mclawnet/skill-manager";
|
|
6
|
+
import {
|
|
7
|
+
getEvolutionToolDefinitions,
|
|
8
|
+
handleEvolutionToolCall,
|
|
9
|
+
} from "../tools/evolution.js";
|
|
10
|
+
|
|
11
|
+
describe("Evolution MCP Tools", () => {
|
|
12
|
+
let tempDir: string;
|
|
13
|
+
let pipeline: EvolutionPipeline;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempDir = mkdtempSync(join(tmpdir(), "evo-mcp-"));
|
|
17
|
+
pipeline = new EvolutionPipeline(tempDir);
|
|
18
|
+
// seed a skill
|
|
19
|
+
const skillDir = join(tempDir, ".claude", "skills", "demo");
|
|
20
|
+
mkdirSync(skillDir, { recursive: true });
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(skillDir, "SKILL.md"),
|
|
23
|
+
`---\nname: demo\ndescription: demo skill\n---\n# demo`,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should define expected tools", () => {
|
|
32
|
+
const defs = getEvolutionToolDefinitions();
|
|
33
|
+
const names = defs.map((d) => d.name);
|
|
34
|
+
expect(names).toEqual([
|
|
35
|
+
"skill_evolve",
|
|
36
|
+
"skill_pending_list",
|
|
37
|
+
"skill_pending_approve",
|
|
38
|
+
"skill_pending_reject",
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("skill_evolve should process a proposal and auto-approve small change", async () => {
|
|
43
|
+
const result = await handleEvolutionToolCall(
|
|
44
|
+
"skill_evolve",
|
|
45
|
+
{
|
|
46
|
+
skillName: "demo",
|
|
47
|
+
signal: "skill-error-fixed",
|
|
48
|
+
problem: "missing note",
|
|
49
|
+
fix: "added note",
|
|
50
|
+
patch: `---\nname: demo\ndescription: demo skill\n---\n# demo\nnew`,
|
|
51
|
+
},
|
|
52
|
+
{ pipeline },
|
|
53
|
+
);
|
|
54
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
55
|
+
expect(["auto-approve", "needs-review"]).toContain(parsed.action);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("skill_pending_list should return empty initially", async () => {
|
|
59
|
+
const result = await handleEvolutionToolCall(
|
|
60
|
+
"skill_pending_list",
|
|
61
|
+
{},
|
|
62
|
+
{ pipeline },
|
|
63
|
+
);
|
|
64
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
65
|
+
expect(parsed.pending).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("skill_pending_approve should fail for unknown id", async () => {
|
|
69
|
+
const result = await handleEvolutionToolCall(
|
|
70
|
+
"skill_pending_approve",
|
|
71
|
+
{ id: "nope" },
|
|
72
|
+
{ pipeline },
|
|
73
|
+
);
|
|
74
|
+
expect(result.isError).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("skill_pending_reject should fail for unknown id", async () => {
|
|
78
|
+
const result = await handleEvolutionToolCall(
|
|
79
|
+
"skill_pending_reject",
|
|
80
|
+
{ id: "nope" },
|
|
81
|
+
{ pipeline },
|
|
82
|
+
);
|
|
83
|
+
expect(result.isError).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("invalid proposal should return error", async () => {
|
|
87
|
+
const result = await handleEvolutionToolCall(
|
|
88
|
+
"skill_evolve",
|
|
89
|
+
{ skillName: "", signal: "skill-error-fixed", problem: "", fix: "", patch: "" },
|
|
90
|
+
{ pipeline },
|
|
91
|
+
);
|
|
92
|
+
expect(result.isError).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { EmbeddingService, initDatabase, MemoryStore } from "@mclawnet/memory";
|
|
3
|
+
import {
|
|
4
|
+
getMemoryToolDefinitions as getToolDefinitions,
|
|
5
|
+
handleMemoryToolCall as handleToolCall,
|
|
6
|
+
type MemoryToolContext as ToolContext,
|
|
7
|
+
} from "../tools/memory.js";
|
|
8
|
+
import type { Database as DatabaseType } from "better-sqlite3";
|
|
9
|
+
|
|
10
|
+
let db: DatabaseType;
|
|
11
|
+
let store: MemoryStore;
|
|
12
|
+
let embeddingService: EmbeddingService;
|
|
13
|
+
let context: ToolContext;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
db = initDatabase(":memory:");
|
|
17
|
+
store = new MemoryStore(db);
|
|
18
|
+
embeddingService = new EmbeddingService(db);
|
|
19
|
+
context = { store, embeddingService };
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ── Tool definitions ───────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe("getToolDefinitions", () => {
|
|
25
|
+
it("should return 4 tool definitions", () => {
|
|
26
|
+
const tools = getToolDefinitions();
|
|
27
|
+
expect(tools).toHaveLength(4);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should have the correct tool names", () => {
|
|
31
|
+
const tools = getToolDefinitions();
|
|
32
|
+
const names = tools.map((t) => t.name);
|
|
33
|
+
expect(names).toEqual([
|
|
34
|
+
"memory_search",
|
|
35
|
+
"memory_store",
|
|
36
|
+
"memory_stats",
|
|
37
|
+
"memory_reflect",
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should have inputSchema on each tool", () => {
|
|
42
|
+
const tools = getToolDefinitions();
|
|
43
|
+
for (const tool of tools) {
|
|
44
|
+
expect(tool.inputSchema).toBeDefined();
|
|
45
|
+
expect(tool.inputSchema.type).toBe("object");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── memory_search ──────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
describe("memory_search handler", () => {
|
|
53
|
+
it("should find a stored memory by semantic search", async () => {
|
|
54
|
+
// Store a memory with embedding first
|
|
55
|
+
await store.addMemoryWithEmbedding(
|
|
56
|
+
{
|
|
57
|
+
roleId: "role-developer",
|
|
58
|
+
content: "React hooks require careful dependency arrays",
|
|
59
|
+
type: "pattern",
|
|
60
|
+
domain: "frontend",
|
|
61
|
+
},
|
|
62
|
+
embeddingService,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const result = await handleToolCall(
|
|
66
|
+
"memory_search",
|
|
67
|
+
{ roleId: "role-developer", query: "React hooks require careful dependency arrays" },
|
|
68
|
+
context,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(result.content).toHaveLength(1);
|
|
72
|
+
expect(result.content[0].type).toBe("text");
|
|
73
|
+
expect(result.isError).toBeUndefined();
|
|
74
|
+
|
|
75
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
76
|
+
expect(parsed).toBeInstanceOf(Array);
|
|
77
|
+
expect(parsed.length).toBeGreaterThan(0);
|
|
78
|
+
expect(parsed[0].content).toBe("React hooks require careful dependency arrays");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should return empty array when no memories exist", async () => {
|
|
82
|
+
const result = await handleToolCall(
|
|
83
|
+
"memory_search",
|
|
84
|
+
{ roleId: "role-developer", query: "nonexistent topic" },
|
|
85
|
+
context,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
89
|
+
expect(parsed).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should respect crossRole flag", async () => {
|
|
93
|
+
await store.addMemoryWithEmbedding(
|
|
94
|
+
{ roleId: "role-reviewer", content: "cross role memory", type: "experience" },
|
|
95
|
+
embeddingService,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Without crossRole, searching as developer should not find reviewer memory
|
|
99
|
+
const result1 = await handleToolCall(
|
|
100
|
+
"memory_search",
|
|
101
|
+
{ roleId: "role-developer", query: "cross role memory", crossRole: false },
|
|
102
|
+
context,
|
|
103
|
+
);
|
|
104
|
+
const parsed1 = JSON.parse(result1.content[0].text);
|
|
105
|
+
expect(parsed1).toHaveLength(0);
|
|
106
|
+
|
|
107
|
+
// With crossRole, should find it
|
|
108
|
+
const result2 = await handleToolCall(
|
|
109
|
+
"memory_search",
|
|
110
|
+
{ roleId: "role-developer", query: "cross role memory", crossRole: true },
|
|
111
|
+
context,
|
|
112
|
+
);
|
|
113
|
+
const parsed2 = JSON.parse(result2.content[0].text);
|
|
114
|
+
expect(parsed2.length).toBeGreaterThan(0);
|
|
115
|
+
expect(parsed2[0].content).toBe("cross role memory");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── memory_store ───────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe("memory_store handler", () => {
|
|
122
|
+
it("should store a memory and return success with id", async () => {
|
|
123
|
+
const result = await handleToolCall(
|
|
124
|
+
"memory_store",
|
|
125
|
+
{
|
|
126
|
+
roleId: "role-developer",
|
|
127
|
+
content: "Always run tests before committing",
|
|
128
|
+
type: "pattern",
|
|
129
|
+
tags: ["workflow", "testing"],
|
|
130
|
+
domain: "ci",
|
|
131
|
+
importance: 0.8,
|
|
132
|
+
},
|
|
133
|
+
context,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(result.content).toHaveLength(1);
|
|
137
|
+
expect(result.content[0].type).toBe("text");
|
|
138
|
+
|
|
139
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
140
|
+
expect(parsed.success).toBe(true);
|
|
141
|
+
expect(parsed.id).toBeDefined();
|
|
142
|
+
expect(typeof parsed.id).toBe("string");
|
|
143
|
+
|
|
144
|
+
// Verify stored in DB
|
|
145
|
+
const mem = store.getMemory(parsed.id);
|
|
146
|
+
expect(mem).not.toBeNull();
|
|
147
|
+
expect(mem!.content).toBe("Always run tests before committing");
|
|
148
|
+
expect(mem!.type).toBe("pattern");
|
|
149
|
+
expect(mem!.tags).toEqual(["workflow", "testing"]);
|
|
150
|
+
expect(mem!.domain).toBe("ci");
|
|
151
|
+
expect(mem!.importance).toBe(0.8);
|
|
152
|
+
expect(mem!.roleId).toBe("role-developer");
|
|
153
|
+
expect(mem!.level).toBe("long-term");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should store memory with minimal args", async () => {
|
|
157
|
+
const result = await handleToolCall(
|
|
158
|
+
"memory_store",
|
|
159
|
+
{ roleId: "role-developer", content: "Something learned", type: "experience" },
|
|
160
|
+
context,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
164
|
+
expect(parsed.success).toBe(true);
|
|
165
|
+
|
|
166
|
+
const mem = store.getMemory(parsed.id);
|
|
167
|
+
expect(mem!.tags).toEqual([]);
|
|
168
|
+
expect(mem!.domain).toBeUndefined();
|
|
169
|
+
expect(mem!.importance).toBe(0.5); // default
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should have an embedding after store", async () => {
|
|
173
|
+
const result = await handleToolCall(
|
|
174
|
+
"memory_store",
|
|
175
|
+
{ roleId: "role-developer", content: "Memory with embedding", type: "pattern" },
|
|
176
|
+
context,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
180
|
+
const mem = store.getMemory(parsed.id);
|
|
181
|
+
expect(mem!.embedding).toBeInstanceOf(Float32Array);
|
|
182
|
+
expect(mem!.embedding!.length).toBe(128);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ── memory_stats ───────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
describe("memory_stats handler", () => {
|
|
189
|
+
it("should return stats for current role", async () => {
|
|
190
|
+
// Add some memories
|
|
191
|
+
store.addMemory({ roleId: "role-developer", content: "exp1", type: "experience" });
|
|
192
|
+
store.addMemory({ roleId: "role-developer", content: "pat1", type: "pattern", domain: "frontend" });
|
|
193
|
+
store.addMemory({ roleId: "role-developer", content: "err1", type: "error", domain: "frontend" });
|
|
194
|
+
|
|
195
|
+
const result = await handleToolCall("memory_stats", { roleId: "role-developer" }, context);
|
|
196
|
+
|
|
197
|
+
expect(result.content).toHaveLength(1);
|
|
198
|
+
expect(result.content[0].type).toBe("text");
|
|
199
|
+
|
|
200
|
+
const stats = JSON.parse(result.content[0].text);
|
|
201
|
+
expect(stats.totalMemories).toBe(3);
|
|
202
|
+
expect(stats.byType.experience).toBe(1);
|
|
203
|
+
expect(stats.byType.pattern).toBe(1);
|
|
204
|
+
expect(stats.byType.error).toBe(1);
|
|
205
|
+
expect(stats.topDomains).toContainEqual({ domain: "frontend", count: 2 });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should return zero stats for role with no memories", async () => {
|
|
209
|
+
const result = await handleToolCall("memory_stats", { roleId: "role-developer" }, context);
|
|
210
|
+
const stats = JSON.parse(result.content[0].text);
|
|
211
|
+
expect(stats.totalMemories).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ── memory_reflect ─────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
describe("memory_reflect handler", () => {
|
|
218
|
+
it("should perform light pruning and return results", async () => {
|
|
219
|
+
const result = await handleToolCall(
|
|
220
|
+
"memory_reflect",
|
|
221
|
+
{ roleId: "role-developer", scope: "light" },
|
|
222
|
+
context,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(result.content).toHaveLength(1);
|
|
226
|
+
expect(result.content[0].type).toBe("text");
|
|
227
|
+
|
|
228
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
229
|
+
expect(parsed.scope).toBe("light");
|
|
230
|
+
expect(typeof parsed.recalculated).toBe("number");
|
|
231
|
+
expect(typeof parsed.demotedToLongTerm).toBe("number");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should perform full reflect across all roles", async () => {
|
|
235
|
+
const result = await handleToolCall(
|
|
236
|
+
"memory_reflect",
|
|
237
|
+
{ roleId: "role-developer", scope: "full" },
|
|
238
|
+
context,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
242
|
+
expect(parsed.scope).toBe("full");
|
|
243
|
+
expect(typeof parsed.recalculated).toBe("number");
|
|
244
|
+
expect(typeof parsed.rolesProcessed).toBe("number");
|
|
245
|
+
expect(typeof parsed.profilesRefreshed).toBe("number");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ── Unknown tool ───────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
describe("unknown tool", () => {
|
|
252
|
+
it("should return isError for unknown tool name", async () => {
|
|
253
|
+
const result = await handleToolCall("nonexistent_tool", {}, context);
|
|
254
|
+
expect(result.isError).toBe(true);
|
|
255
|
+
|
|
256
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
257
|
+
expect(parsed.error).toContain("Unknown tool");
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { SkillStore } from "@mclawnet/skill-manager";
|
|
6
|
+
import { getSkillToolDefinitions, handleSkillToolCall } from "../tools/skill.js";
|
|
7
|
+
|
|
8
|
+
describe("Skill MCP Tools", () => {
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
let store: SkillStore;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = mkdtempSync(join(tmpdir(), "skill-mcp-test-"));
|
|
14
|
+
store = new SkillStore(tempDir);
|
|
15
|
+
const skillDir = join(tempDir, ".claude", "skills", "test-skill");
|
|
16
|
+
mkdirSync(skillDir, { recursive: true });
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(skillDir, "SKILL.md"),
|
|
19
|
+
`---
|
|
20
|
+
name: test-skill
|
|
21
|
+
description: A test skill for testing
|
|
22
|
+
---
|
|
23
|
+
# Test`,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should define skill_list and skill_read tools", () => {
|
|
32
|
+
const defs = getSkillToolDefinitions();
|
|
33
|
+
const names = defs.map((d) => d.name);
|
|
34
|
+
expect(names).toContain("skill_list");
|
|
35
|
+
expect(names).toContain("skill_read");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("skill_list should return installed skills", async () => {
|
|
39
|
+
const result = await handleSkillToolCall("skill_list", {}, { store });
|
|
40
|
+
expect(result.isError).toBeUndefined();
|
|
41
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
42
|
+
expect(parsed.skills).toHaveLength(1);
|
|
43
|
+
expect(parsed.skills[0].name).toBe("test-skill");
|
|
44
|
+
expect(parsed.total).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("skill_read should return SKILL.md content", async () => {
|
|
48
|
+
const result = await handleSkillToolCall(
|
|
49
|
+
"skill_read",
|
|
50
|
+
{ name: "test-skill" },
|
|
51
|
+
{ store },
|
|
52
|
+
);
|
|
53
|
+
expect(result.isError).toBeUndefined();
|
|
54
|
+
expect(result.content[0].text).toContain("# Test");
|
|
55
|
+
expect(result.content[0].text).toContain("name: test-skill");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("skill_read should return error for unknown skill", async () => {
|
|
59
|
+
const result = await handleSkillToolCall(
|
|
60
|
+
"skill_read",
|
|
61
|
+
{ name: "nonexistent" },
|
|
62
|
+
{ store },
|
|
63
|
+
);
|
|
64
|
+
expect(result.isError).toBe(true);
|
|
65
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
66
|
+
expect(parsed.error).toContain("not found");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should return error for missing name arg", async () => {
|
|
70
|
+
const result = await handleSkillToolCall("skill_read", {}, { store });
|
|
71
|
+
expect(result.isError).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return error for unknown tool", async () => {
|
|
75
|
+
const result = await handleSkillToolCall("skill_unknown", {}, { store });
|
|
76
|
+
expect(result.isError).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/index.ts
ADDED
package/src/server.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { createLogger } from "@mclawnet/logger";
|
|
11
|
+
import {
|
|
12
|
+
createEmbeddingProviders,
|
|
13
|
+
EmbeddingService,
|
|
14
|
+
initDatabase,
|
|
15
|
+
MemoryStore,
|
|
16
|
+
} from "@mclawnet/memory";
|
|
17
|
+
import { SkillStore, EvolutionPipeline } from "@mclawnet/skill-manager";
|
|
18
|
+
import { getAllToolDefinitions, routeToolCall, type ToolContext } from "./tools/index.js";
|
|
19
|
+
|
|
20
|
+
const log = createLogger({ module: "mcp-server" });
|
|
21
|
+
|
|
22
|
+
const dbPath = process.env.CLAWNET_MEMORY_DB ?? join(homedir(), ".clawnet", "memory.db");
|
|
23
|
+
const db = initDatabase(dbPath);
|
|
24
|
+
|
|
25
|
+
log.info({ dbPath }, "ClawNet MCP server starting");
|
|
26
|
+
|
|
27
|
+
const store = new MemoryStore(db);
|
|
28
|
+
const providers = createEmbeddingProviders();
|
|
29
|
+
const embeddingService = new EmbeddingService(db, providers);
|
|
30
|
+
|
|
31
|
+
const clawnetDir = process.env.CLAWNET_DIR ?? join(homedir(), ".clawnet");
|
|
32
|
+
const skillStore = new SkillStore(clawnetDir);
|
|
33
|
+
const evolutionPipeline = new EvolutionPipeline(clawnetDir);
|
|
34
|
+
|
|
35
|
+
const context: ToolContext = {
|
|
36
|
+
memory: { store, embeddingService },
|
|
37
|
+
skill: { store: skillStore },
|
|
38
|
+
evolution: { pipeline: evolutionPipeline },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const server = new Server(
|
|
42
|
+
{ name: "clawnet-mcp", version: "0.1.0" },
|
|
43
|
+
{ capabilities: { tools: {} } },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
47
|
+
tools: getAllToolDefinitions(),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
51
|
+
const { name, arguments: args } = request.params;
|
|
52
|
+
return routeToolCall(name, (args ?? {}) as Record<string, unknown>, context);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const transport = new StdioServerTransport();
|
|
56
|
+
await server.connect(transport);
|
|
57
|
+
|
|
58
|
+
log.info("ClawNet MCP server connected via stdio");
|
|
59
|
+
|
|
60
|
+
function cleanup() {
|
|
61
|
+
log.info("ClawNet MCP server shutting down");
|
|
62
|
+
try {
|
|
63
|
+
db.close();
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.on("exit", cleanup);
|
|
70
|
+
process.on("SIGINT", () => {
|
|
71
|
+
cleanup();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
process.on("SIGTERM", () => {
|
|
75
|
+
cleanup();
|
|
76
|
+
process.exit(0);
|
|
77
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createLogger } from "@mclawnet/logger";
|
|
2
|
+
import type { EvolutionPipeline } from "@mclawnet/skill-manager";
|
|
3
|
+
|
|
4
|
+
const logger = createLogger({ module: "mcp-server/tools/evolution" });
|
|
5
|
+
|
|
6
|
+
export interface EvolutionToolContext {
|
|
7
|
+
pipeline: EvolutionPipeline;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Layer 1 实时进化默认置信度
|
|
11
|
+
const LAYER1_DEFAULT_CONFIDENCE = 0.85;
|
|
12
|
+
|
|
13
|
+
export function getEvolutionToolDefinitions() {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
name: "skill_evolve",
|
|
17
|
+
description: `提交 Skill 进化提案。仅在以下强信号出现时调用:
|
|
18
|
+
1. 你按照某个 skill 执行但遇到错误,修正后才成功 (signal: skill-error-fixed)
|
|
19
|
+
2. 用户指出某个 skill 的指引有误,并给出了正确做法 (signal: user-correction)
|
|
20
|
+
3. 你发现某个 skill 缺少关键步骤,实践中你补上了 (signal: missing-step)
|
|
21
|
+
4. 某个 skill 的指引在当前平台/环境不适用 (signal: platform-mismatch)
|
|
22
|
+
|
|
23
|
+
不要在以下情况调用:
|
|
24
|
+
- skill 正常工作没有问题
|
|
25
|
+
- 只是"可能更好"的优化建议
|
|
26
|
+
- 用户没有使用任何 skill 的普通对话`,
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object" as const,
|
|
29
|
+
properties: {
|
|
30
|
+
skillName: { type: "string", description: "要进化的 skill 名称" },
|
|
31
|
+
signal: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: [
|
|
34
|
+
"skill-error-fixed",
|
|
35
|
+
"user-correction",
|
|
36
|
+
"missing-step",
|
|
37
|
+
"platform-mismatch",
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
problem: { type: "string", description: "遇到了什么问题" },
|
|
41
|
+
fix: { type: "string", description: "怎么修正的" },
|
|
42
|
+
patch: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "修改后的完整 SKILL.md 内容",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ["skillName", "signal", "problem", "fix", "patch"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "skill_pending_list",
|
|
52
|
+
description: "列出所有待审核的 Skill 进化提案。",
|
|
53
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "skill_pending_approve",
|
|
57
|
+
description: "批准一条待审核的 Skill 进化提案并应用到本地 skill。",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object" as const,
|
|
60
|
+
properties: {
|
|
61
|
+
id: { type: "string", description: "pending 提案 id" },
|
|
62
|
+
},
|
|
63
|
+
required: ["id"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "skill_pending_reject",
|
|
68
|
+
description: "拒绝一条待审核的 Skill 进化提案(不应用,从队列移除)。",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object" as const,
|
|
71
|
+
properties: {
|
|
72
|
+
id: { type: "string", description: "pending 提案 id" },
|
|
73
|
+
},
|
|
74
|
+
required: ["id"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function handleEvolutionToolCall(
|
|
81
|
+
name: string,
|
|
82
|
+
args: Record<string, unknown>,
|
|
83
|
+
context: EvolutionToolContext,
|
|
84
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
85
|
+
try {
|
|
86
|
+
switch (name) {
|
|
87
|
+
case "skill_evolve": {
|
|
88
|
+
const result = await context.pipeline.process({
|
|
89
|
+
skillName: String(args.skillName ?? ""),
|
|
90
|
+
signal: args.signal as
|
|
91
|
+
| "skill-error-fixed"
|
|
92
|
+
| "user-correction"
|
|
93
|
+
| "missing-step"
|
|
94
|
+
| "platform-mismatch",
|
|
95
|
+
problem: String(args.problem ?? ""),
|
|
96
|
+
fix: String(args.fix ?? ""),
|
|
97
|
+
patch: String(args.patch ?? ""),
|
|
98
|
+
confidence: LAYER1_DEFAULT_CONFIDENCE,
|
|
99
|
+
source: "layer1-realtime",
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
103
|
+
isError: result.action === "rejected" || result.action === "blocked",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
case "skill_pending_list": {
|
|
107
|
+
const pending = context.pipeline.listPending();
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{ type: "text", text: JSON.stringify({ pending, total: pending.length }) },
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case "skill_pending_approve": {
|
|
115
|
+
const id = String(args.id ?? "");
|
|
116
|
+
if (!id) {
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: JSON.stringify({ error: "id is required" }) }],
|
|
119
|
+
isError: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const result = context.pipeline.approvePending(id);
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
125
|
+
isError: !result.applied,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
case "skill_pending_reject": {
|
|
129
|
+
const id = String(args.id ?? "");
|
|
130
|
+
if (!id) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: JSON.stringify({ error: "id is required" }) }],
|
|
133
|
+
isError: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const removed = context.pipeline.rejectPending(id);
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text", text: JSON.stringify({ removed }) }],
|
|
139
|
+
isError: !removed,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{ type: "text", text: JSON.stringify({ error: `Unknown evolution tool: ${name}` }) },
|
|
146
|
+
],
|
|
147
|
+
isError: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.error({ err, tool: name }, "evolution tool error");
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: "text", text: JSON.stringify({ error: String(err) }) }],
|
|
154
|
+
isError: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|