@mebular/mcp 0.0.0-stage → 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/src/tools.mjs ADDED
@@ -0,0 +1,397 @@
1
+ // MCP 工具面(G6.2 / D35 + R1 统一入口):**27 个** agent 中立工具 = 记忆 11(薄壳委托 MemoryService)
2
+ // + 任务 16(复用 @mebular/fleet 的 TASK_TOOLS handler;同 handler 不复制)。
3
+ //
4
+ // 钳制:limit ≤ 50(默认 10)、depth ≤ 5、batch ≤ 100。
5
+ // 输出:content:[{type:'text'}] + structuredContent(同一对象)。
6
+ //
7
+ // W3 表面一致性:抽出 `TOOL_SPECS`(单一 handler 注册表),MCP 适配器与 `mebular memory_*` CLI 共用。
8
+
9
+ import { z } from 'zod';
10
+ import { json, fail } from './tool-envelope.mjs';
11
+ import { TASK_TOOL_SPECS, TASK_TOOL_SCOPES } from './task-tools.mjs';
12
+ import { NOT_INITIALIZED_CODE, NOT_INITIALIZED_MESSAGE } from './service-facade.mjs';
13
+
14
+ export { TASK_TOOL_SPECS } from './task-tools.mjs';
15
+
16
+ export { json, fail } from './tool-envelope.mjs';
17
+
18
+ /** 每个工具所需的 OAuth scope(D36) */
19
+ export const TOOL_SCOPES = {
20
+ memory_write: 'memory.write',
21
+ memory_write_batch: 'memory.write',
22
+ memory_query: 'memory.read',
23
+ memory_search: 'memory.read',
24
+ memory_profile: 'memory.read',
25
+ memory_skills: 'memory.read',
26
+ memory_history: 'memory.read',
27
+ memory_graph: 'memory.read',
28
+ memory_import: 'memory.admin',
29
+ memory_status: 'memory.read',
30
+ memory_sync: 'memory.admin',
31
+ // G-ML-1 生命周期:删除=admin(不可逆动作);归档=write(可逆标记,按轮次规格)
32
+ memory_delete: 'memory.admin',
33
+ memory_archive: 'memory.write',
34
+ };
35
+
36
+ /** 记忆面工具名(11)。 */
37
+ export const MEMORY_TOOL_NAMES = [
38
+ 'memory_write',
39
+ 'memory_write_batch',
40
+ 'memory_query',
41
+ 'memory_search',
42
+ 'memory_profile',
43
+ 'memory_skills',
44
+ 'memory_history',
45
+ 'memory_graph',
46
+ 'memory_import',
47
+ 'memory_status',
48
+ 'memory_sync',
49
+ 'memory_delete',
50
+ 'memory_archive',
51
+ ];
52
+
53
+ /** 任务面工具名(16,来自 fleet 的 TASK_TOOLS)。 */
54
+ export const TASK_TOOL_NAMES = TASK_TOOL_SPECS.map((spec) => spec.name);
55
+
56
+ const MAX_BATCH = 100;
57
+ const MAX_LIMIT = 50;
58
+ const MAX_DEPTH = 5;
59
+
60
+ export function clampLimit(value, fallback = 10) {
61
+ const n = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : fallback;
62
+ return Math.min(Math.max(n, 1), MAX_LIMIT);
63
+ }
64
+ export function clampDepth(value, fallback = 1) {
65
+ const n = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : fallback;
66
+ return Math.min(Math.max(n, 0), MAX_DEPTH);
67
+ }
68
+
69
+ const memoryInputSchema = z.object({
70
+ type: z.enum(['fact', 'episode', 'skill', 'preference', 'observation']),
71
+ content: z.string(),
72
+ metadata: z
73
+ .object({
74
+ source: z.string().optional(),
75
+ confidence: z.number().min(0).max(1).optional(),
76
+ tags: z.array(z.string()).optional(),
77
+ relatedTo: z.array(z.string()).optional(),
78
+ expiresAt: z.number().optional(),
79
+ episodeType: z.enum(['conversation', 'task', 'decision', 'error', 'observation', 'other']).optional(),
80
+ category: z.string().optional(),
81
+ name: z.string().optional(),
82
+ preferenceType: z.string().optional(),
83
+ namespace: z.string().optional(),
84
+ })
85
+ .optional(),
86
+ });
87
+
88
+ /** namespace 过滤:单分区或分区列表,均可选(缺省不过滤) */
89
+ const namespaceFilter = z.union([z.string(), z.array(z.string())]).optional();
90
+
91
+ const internalTypes = z.array(z.enum(['fact', 'episode', 'skill', 'preference', 'observation']));
92
+
93
+ /** 工具注册表:handler(service, args) → MCP 结果对象(CLI 直接取 `structuredContent`)。 */
94
+ export const TOOL_SPECS = [
95
+ {
96
+ name: 'memory_write',
97
+ title: '写入记忆',
98
+ description: '写入单条记忆(fact/episode/skill/preference/observation)',
99
+ inputSchema: z.object({ items: z.array(memoryInputSchema).min(1) }),
100
+ handler: async (service, { items }) => {
101
+ try {
102
+ return json({ stored: await service.write(items) });
103
+ } catch (error) {
104
+ return fail(String(error?.message ?? error));
105
+ }
106
+ },
107
+ },
108
+ {
109
+ name: 'memory_write_batch',
110
+ title: '批量写入记忆',
111
+ description: `批量写入记忆(最多 ${MAX_BATCH} 条)`,
112
+ inputSchema: z.object({ items: z.array(memoryInputSchema).min(1) }),
113
+ handler: async (service, { items }) => {
114
+ if (items.length > MAX_BATCH) return fail(`批量写入超过上限:${items.length} > ${MAX_BATCH}`);
115
+ try {
116
+ return json({ stored: await service.write(items) });
117
+ } catch (error) {
118
+ return fail(String(error?.message ?? error));
119
+ }
120
+ },
121
+ },
122
+ {
123
+ name: 'memory_query',
124
+ title: '查询记忆',
125
+ description: '语义/关键词召回(配置向量索引时走语义)',
126
+ inputSchema: z.object({
127
+ query: z.string().optional(),
128
+ types: internalTypes.optional(),
129
+ limit: z.number().optional(),
130
+ offset: z.number().optional(),
131
+ includeHistory: z.boolean().optional(),
132
+ /** G-ML-1:默认 false(归档不进入召回);显式 true 才可见已归档 */
133
+ includeArchived: z.boolean().optional(),
134
+ filters: z
135
+ .object({
136
+ tags: z.array(z.string()).optional(),
137
+ createdAfter: z.number().optional(),
138
+ createdBefore: z.number().optional(),
139
+ entity: z.string().optional(),
140
+ minConfidence: z.number().optional(),
141
+ namespace: namespaceFilter,
142
+ })
143
+ .optional(),
144
+ }),
145
+ handler: async (service, args) => {
146
+ try {
147
+ return json(await service.query({
148
+ ...(args.query !== undefined ? { query: args.query } : {}),
149
+ ...(args.types ? { types: args.types } : {}),
150
+ limit: clampLimit(args.limit),
151
+ ...(args.offset !== undefined ? { offset: args.offset } : {}),
152
+ ...(args.includeHistory !== undefined ? { includeHistory: args.includeHistory } : {}),
153
+ ...(args.includeArchived !== undefined ? { includeArchived: args.includeArchived } : {}),
154
+ ...(args.filters ? { filters: args.filters } : {}),
155
+ }));
156
+ } catch (error) {
157
+ return fail(String(error?.message ?? error));
158
+ }
159
+ },
160
+ },
161
+ {
162
+ name: 'memory_search',
163
+ title: '关键词搜索',
164
+ description: '关键词检索基线,可选一跳关系',
165
+ inputSchema: z.object({
166
+ query: z.string(),
167
+ types: z.array(z.string()).optional(),
168
+ limit: z.number().optional(),
169
+ includeRelations: z.boolean().optional(),
170
+ /** G-ML-1:默认 false(归档不进入召回) */
171
+ includeArchived: z.boolean().optional(),
172
+ filters: z
173
+ .object({
174
+ tags: z.array(z.string()).optional(),
175
+ createdAfter: z.number().optional(),
176
+ createdBefore: z.number().optional(),
177
+ namespace: namespaceFilter,
178
+ })
179
+ .optional(),
180
+ }),
181
+ handler: async (service, args) => {
182
+ try {
183
+ return json(await service.search({
184
+ query: args.query,
185
+ ...(args.types ? { types: args.types } : {}),
186
+ limit: clampLimit(args.limit),
187
+ ...(args.includeRelations !== undefined ? { includeRelations: args.includeRelations } : {}),
188
+ ...(args.includeArchived !== undefined ? { includeArchived: args.includeArchived } : {}),
189
+ ...(args.filters ? { filters: args.filters } : {}),
190
+ }));
191
+ } catch (error) {
192
+ return fail(String(error?.message ?? error));
193
+ }
194
+ },
195
+ },
196
+ {
197
+ name: 'memory_profile',
198
+ title: '用户画像',
199
+ description: '返回用户偏好与属性(默认不含已归档)',
200
+ inputSchema: z.object({ includeArchived: z.boolean().optional() }),
201
+ handler: async (service, args) => {
202
+ try {
203
+ return json(await service.profile(args?.includeArchived !== undefined ? { includeArchived: args.includeArchived } : {}));
204
+ } catch (error) {
205
+ return fail(String(error?.message ?? error));
206
+ }
207
+ },
208
+ },
209
+ {
210
+ name: 'memory_skills',
211
+ title: '技能列表',
212
+ description: '按分类/关键词/标签筛选技能',
213
+ inputSchema: z.object({
214
+ category: z.string().optional(),
215
+ search: z.string().optional(),
216
+ tags: z.array(z.string()).optional(),
217
+ includeArchived: z.boolean().optional(),
218
+ }),
219
+ handler: async (service, args) => {
220
+ try {
221
+ const skills = await service.skills({
222
+ ...(args.category ? { category: args.category } : {}),
223
+ ...(args.search ? { search: args.search } : {}),
224
+ ...(args.tags ? { tags: args.tags } : {}),
225
+ ...(args.includeArchived !== undefined ? { includeArchived: args.includeArchived } : {}),
226
+ });
227
+ return json({ skills });
228
+ } catch (error) {
229
+ return fail(String(error?.message ?? error));
230
+ }
231
+ },
232
+ },
233
+ {
234
+ name: 'memory_history',
235
+ title: '会话历史',
236
+ description: '按会话/时间/主题筛选会话情节',
237
+ inputSchema: z.object({
238
+ sessionIds: z.array(z.string()).optional(),
239
+ startTime: z.number().optional(),
240
+ endTime: z.number().optional(),
241
+ topic: z.string().optional(),
242
+ limit: z.number().optional(),
243
+ offset: z.number().optional(),
244
+ includeArchived: z.boolean().optional(),
245
+ }),
246
+ handler: async (service, args) => {
247
+ try {
248
+ return json(await service.history({
249
+ ...(args.sessionIds ? { sessionIds: args.sessionIds } : {}),
250
+ ...(args.includeArchived !== undefined ? { includeArchived: args.includeArchived } : {}),
251
+ ...(args.startTime !== undefined ? { startTime: args.startTime } : {}),
252
+ ...(args.endTime !== undefined ? { endTime: args.endTime } : {}),
253
+ ...(args.topic ? { topic: args.topic } : {}),
254
+ ...(args.limit !== undefined ? { limit: clampLimit(args.limit) } : {}),
255
+ ...(args.offset !== undefined ? { offset: args.offset } : {}),
256
+ }));
257
+ } catch (error) {
258
+ return fail(String(error?.message ?? error));
259
+ }
260
+ },
261
+ },
262
+ {
263
+ name: 'memory_graph',
264
+ title: '图遍历',
265
+ description: `从起点遍历(深度 ≤ ${MAX_DEPTH})`,
266
+ inputSchema: z.object({
267
+ startId: z.string(),
268
+ direction: z.enum(['outgoing', 'incoming', 'both']).optional(),
269
+ maxDepth: z.number().optional(),
270
+ edgeTypes: z.array(z.string()).optional(),
271
+ namespace: namespaceFilter,
272
+ includeArchived: z.boolean().optional(),
273
+ }),
274
+ handler: async (service, args) => {
275
+ try {
276
+ return json(await service.graph(args.startId, {
277
+ ...(args.includeArchived !== undefined ? { includeArchived: args.includeArchived } : {}),
278
+ ...(args.direction ? { direction: args.direction } : {}),
279
+ maxDepth: clampDepth(args.maxDepth),
280
+ ...(args.edgeTypes ? { edgeTypes: args.edgeTypes } : {}),
281
+ ...(args.namespace !== undefined ? { namespace: args.namespace } : {}),
282
+ }));
283
+ } catch (error) {
284
+ return fail(String(error?.message ?? error));
285
+ }
286
+ },
287
+ },
288
+ {
289
+ name: 'memory_import',
290
+ title: '导入记忆',
291
+ description: '经适配器导入异构来源(kv/markdown/…)',
292
+ inputSchema: z.object({ kind: z.string().optional(), data: z.unknown(), origin: z.string().optional() }),
293
+ handler: async (service, args) => {
294
+ try {
295
+ return json(await service.import({
296
+ ...(args.kind ? { kind: args.kind } : {}),
297
+ data: args.data,
298
+ ...(args.origin ? { origin: args.origin } : {}),
299
+ }));
300
+ } catch (error) {
301
+ return fail(String(error?.message ?? error));
302
+ }
303
+ },
304
+ },
305
+ {
306
+ name: 'memory_status',
307
+ title: '记忆状态',
308
+ description: '返回设备/网络/计数/状态哈希/开关',
309
+ inputSchema: z.object({}),
310
+ handler: async (service) => {
311
+ try {
312
+ return json(await service.status());
313
+ } catch (error) {
314
+ return fail(String(error?.message ?? error));
315
+ }
316
+ },
317
+ },
318
+ {
319
+ name: 'memory_sync',
320
+ title: '同步对端',
321
+ description: '连接对端并等待一次双向同步(network.enabled 需为 true)',
322
+ inputSchema: z.object({ peerId: z.string(), address: z.string().optional() }),
323
+ handler: async (service, args) => {
324
+ try {
325
+ return json(await service.sync(args.peerId, args.address));
326
+ } catch (error) {
327
+ return fail(String(error?.message ?? error));
328
+ }
329
+ },
330
+ },
331
+ // ---------- G-ML-1 生命周期(删除 / 归档;MCP 与 CLI 同一 handler) ----------
332
+ {
333
+ name: 'memory_delete',
334
+ title: '删除记忆',
335
+ description: '删除记忆:写墓碑(deletedAt)并随图事件同步到各端;删除后默认不再出现在任何召回。不可逆动作(对端已拷贝的旧快照无法强制回收)。需 memory.admin。',
336
+ inputSchema: z.object({ ids: z.array(z.string()).min(1) }),
337
+ handler: async (service, args) => {
338
+ try {
339
+ return json(await service.delete(args.ids));
340
+ } catch (error) {
341
+ return fail(String(error?.message ?? error));
342
+ }
343
+ },
344
+ },
345
+ {
346
+ name: 'memory_archive',
347
+ title: '归档 / 解除归档记忆',
348
+ description: '归档记忆:打 metadata.archivedAt 标记(随图同步、无损、可逆),归档后默认不进入召回;archived=false 解除归档。删除(墓碑)节点会被跳过。',
349
+ inputSchema: z.object({ ids: z.array(z.string()).min(1), archived: z.boolean().optional() }),
350
+ handler: async (service, args) => {
351
+ try {
352
+ return json(await service.archive(args.ids, args.archived !== false));
353
+ } catch (error) {
354
+ return fail(String(error?.message ?? error));
355
+ }
356
+ },
357
+ },
358
+ ];
359
+
360
+ /** 统一工具注册表(记忆 11 + 任务 16 = 27):`mebular mcp`(stdio)与 HTTP `/mcp` 都注册这一份。 */
361
+ export const ALL_TOOL_SPECS = [...TOOL_SPECS, ...TASK_TOOL_SPECS];
362
+
363
+ /** 统一工具名(27)——控制台「能力清单」与 CLI `mebular tools` 同源。 */
364
+ export const TOOL_NAMES = ALL_TOOL_SPECS.map((spec) => spec.name);
365
+
366
+ /** 统一 scope 表(记忆 + 任务,27)。 */
367
+ export const TOOL_ALL_SCOPES = { ...TOOL_SCOPES, ...TASK_TOOL_SCOPES };
368
+
369
+ /** 工具名 → spec(记忆 + 任务;CLI 与 MCP 同 handler)。 */
370
+ export function toolByName(name) {
371
+ return ALL_TOOL_SPECS.find((t) => t.name === name) ?? null;
372
+ }
373
+
374
+ /**
375
+ * 把工具注册到 McpServer;service 为 MemoryService 实例(或 UNINITIALIZED 时的惰性代理)。
376
+ *
377
+ * `isReady` 是所有工具(含不访问 service 的任务 / 闲聊 / board 工具)的统一就绪门槛:
378
+ * 未就绪时直接返回稳定的 NOT_INITIALIZED 业务错误,绝不让任何 handler 触碰实际 task/memory 数据。
379
+ *
380
+ * `toolContext` 交给每个 handler 作为第三个参数(记忆面忽略)。任务面用它拿运行中 daemon 的
381
+ * live app:对象或 getter(热初始化后 `app` 会切换,用 getter 才能拿到当前实例)。
382
+ */
383
+ export function registerTools(server, service, { isReady, toolContext } = {}) {
384
+ for (const spec of ALL_TOOL_SPECS) {
385
+ server.registerTool(
386
+ spec.name,
387
+ { title: spec.title, description: spec.description, inputSchema: spec.inputSchema },
388
+ (args) => {
389
+ if (typeof isReady === 'function' && !isReady()) {
390
+ return fail(NOT_INITIALIZED_MESSAGE, NOT_INITIALIZED_CODE);
391
+ }
392
+ const context = typeof toolContext === 'function' ? toolContext() : toolContext;
393
+ return spec.handler(service, args, context);
394
+ },
395
+ );
396
+ }
397
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.