libero-mcp 0.2.12 → 0.2.13

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.
@@ -1,7 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { SqliteCategoryRepo, SqliteTimeBlockRepo, SqliteStatsRepo, SqliteTimerRepo, SqliteProfileRepo, SqliteReminderRepo, SqliteTimelineRepo, } from "./src/repo/sqlite.js";
4
- import { SqliteIdentityRepo } from "./src/repo/sqlite-identity.js";
3
+ import { createRepos } from "./src/repo/factory.js";
5
4
  import { createCategoryTools } from "./src/categories/index.js";
6
5
  import { createTimeBlockTools } from "./src/timeblock/index.js";
7
6
  import { createStatsTools } from "./src/stats/index.js";
@@ -47,47 +46,36 @@ export function errorContent(msg) {
47
46
  };
48
47
  }
49
48
  /**
50
- * Build the libero MCP server on a given DB connection.
51
- * Tools throw Error on business violations; handlers let them propagate —
52
- * the SDK turns thrown errors into MCP tool errors (isError) preserving the message.
49
+ * Build the full tool registry (48 tools). Each entry is a data-fied version of
50
+ * the pre-refactor inline registration calls — handlers are byte-for-byte copies
51
+ * preserving declaration order for diagnostic readability.
53
52
  */
54
- export function buildServer(db) {
55
- // MV-COACH-3 Slice 2: resolve default user_id once at server build
56
- const defaultUser = resolveDefaultUserId({ db, env: process.env });
57
- logResolvedDefaultUserId(defaultUser);
58
- const injector = makeUserIdInjector(defaultUser.userId);
59
- // ── construct repos (S10 §3.3) ──
60
- const categoryRepo = new SqliteCategoryRepo(db);
61
- const timeblockRepo = new SqliteTimeBlockRepo(db);
62
- const statsRepo = new SqliteStatsRepo(db);
63
- const timerRepo = new SqliteTimerRepo(db);
64
- const profileRepo = new SqliteProfileRepo(db);
65
- const reminderRepo = new SqliteReminderRepo(db);
66
- const timelineRepo = new SqliteTimelineRepo(db);
67
- const identityRepo = new SqliteIdentityRepo(db);
68
- // ── inject tool factories ──
69
- const categories = createCategoryTools(categoryRepo);
70
- const tb = createTimeBlockTools(timeblockRepo, db);
71
- const stats = createStatsTools(statsRepo);
72
- const matrix = createMatrixTools(statsRepo); // matrix reuses StatsRepo
73
- const timeline = createTimelineTools(timelineRepo);
74
- const timer = createTimerTools(timerRepo, timeblockRepo, db);
75
- const profile = createProfileTools(profileRepo, identityRepo);
76
- const profileConfirm = createProfileConfirmTools(db);
77
- const reminder = createReminderTools(reminderRepo, undefined, db);
78
- const scheduler = createSchedulerTools(timeblockRepo, reminderRepo, categoryRepo);
79
- const task = createTaskTools(db);
80
- // instructions 必须放在第 2 参 ServerOptions(不是 serverInfo)—— SDK 只从 options 读
81
- const server = new McpServer({ name: "libero-mcp", version: "0.2.3" }, { instructions: buildServerInstructions() });
53
+ function buildToolRegistry(ctx) {
54
+ const { categories, tb, stats, matrix, timeline, timer, profile, profileConfirm, reminder, scheduler, task, injector, } = ctx;
55
+ // identityRepo is referenced directly inside handlers below (closure scope),
56
+ // matching the pre-refactor behavior.
57
+ const identityRepo = ctx.repos.identityRepo;
58
+ const tools = [];
82
59
  // ============================================================
83
60
  // categories (2 tools)
84
61
  // ============================================================
85
- server.registerTool("categories.getAll", { description: "获取全部时间分类(15 类)", inputSchema: {} }, async () => asContent(await categories.getAllCategories()));
86
- server.registerTool("categories.getById", { description: "按 id 获取单个分类", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await categories.getCategoryById(id)));
62
+ tools.push({
63
+ name: "categories.getAll",
64
+ description: "获取全部时间分类(15 类)",
65
+ inputSchema: {},
66
+ handler: async () => asContent(await categories.getAllCategories()),
67
+ });
68
+ tools.push({
69
+ name: "categories.getById",
70
+ description: "按 id 获取单个分类",
71
+ inputSchema: { id: z.string().min(1) },
72
+ handler: async ({ id }) => asContent(await categories.getCategoryById(id)),
73
+ });
87
74
  // ============================================================
88
- // timer (6 tools — positional userId args, R4 pattern)
75
+ // timer (9 tools — positional userId args, R4 pattern)
89
76
  // ============================================================
90
- server.registerTool("timer.start", {
77
+ tools.push({
78
+ name: "timer.start",
91
79
  description: "开始一个计时会话",
92
80
  inputSchema: {
93
81
  user_id: z.string().optional(),
@@ -96,55 +84,90 @@ export function buildServer(db) {
96
84
  categoryId: z.string().optional(),
97
85
  task_id: z.string().optional(),
98
86
  },
99
- }, async (rawInput) => {
100
- const r = injector(rawInput);
101
- if ("error" in r)
102
- return errorContent(r.error);
103
- return asContent(await timer.start(r.user_id, r.event, r.categoryId, r.task_id, r.target_user_id));
104
- });
105
- server.registerTool("timer.pause", { description: "暂停当前计时", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
106
- const r = injector(rawInput);
107
- if ("error" in r)
108
- return errorContent(r.error);
109
- return asContent(await timer.pause(r.user_id));
110
- });
111
- server.registerTool("timer.resume", { description: "恢复已暂停的计时", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
112
- const r = injector(rawInput);
113
- if ("error" in r)
114
- return errorContent(r.error);
115
- return asContent(await timer.resume(r.user_id));
116
- });
117
- server.registerTool("timer.stop", {
87
+ needsUserIdInjection: true,
88
+ handler: async (rawInput) => {
89
+ const r = injector(rawInput);
90
+ if ("error" in r)
91
+ return errorContent(r.error);
92
+ return asContent(await timer.start(r.user_id, r.event, r.categoryId, r.task_id, r.target_user_id));
93
+ },
94
+ });
95
+ tools.push({
96
+ name: "timer.pause",
97
+ description: "暂停当前计时",
98
+ inputSchema: { user_id: z.string().optional() },
99
+ needsUserIdInjection: true,
100
+ handler: async (rawInput) => {
101
+ const r = injector(rawInput);
102
+ if ("error" in r)
103
+ return errorContent(r.error);
104
+ return asContent(await timer.pause(r.user_id));
105
+ },
106
+ });
107
+ tools.push({
108
+ name: "timer.resume",
109
+ description: "恢复已暂停的计时",
110
+ inputSchema: { user_id: z.string().optional() },
111
+ needsUserIdInjection: true,
112
+ handler: async (rawInput) => {
113
+ const r = injector(rawInput);
114
+ if ("error" in r)
115
+ return errorContent(r.error);
116
+ return asContent(await timer.resume(r.user_id));
117
+ },
118
+ });
119
+ tools.push({
120
+ name: "timer.stop",
118
121
  description: "停止计时并返回记录(传 rating 时自动落盘 timeblock)",
119
122
  inputSchema: {
120
123
  user_id: z.string().optional(),
121
124
  rating: z.number().min(1).max(5).optional(),
122
125
  rating_reason: z.string().optional(),
123
126
  },
124
- }, async (rawInput) => {
125
- const r = injector(rawInput);
126
- if ("error" in r)
127
- return errorContent(r.error);
128
- return asContent(await timer.stop(r.user_id, {
129
- rating: r.rating,
130
- rating_reason: r.rating_reason,
131
- }));
132
- });
133
- server.registerTool("timer.status", { description: "查询某用户当前计时状态", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
134
- const r = injector(rawInput);
135
- if ("error" in r)
136
- return errorContent(r.error);
137
- return asContent(await timer.status(r.user_id));
138
- });
139
- server.registerTool("timer.elapsed", { description: "查询当前计时已过毫秒数", inputSchema: { user_id: z.string().optional() } }, async (rawInput) => {
140
- const r = injector(rawInput);
141
- if ("error" in r)
142
- return errorContent(r.error);
143
- return asContent(await timer.elapsed(r.user_id));
144
- });
145
- server.registerTool("timer.statusAll", { description: "查询所有用户的当前计时状态(调试/兜底用)", inputSchema: {} }, async () => asContent(await timer.statusAll()));
127
+ needsUserIdInjection: true,
128
+ handler: async (rawInput) => {
129
+ const r = injector(rawInput);
130
+ if ("error" in r)
131
+ return errorContent(r.error);
132
+ return asContent(await timer.stop(r.user_id, {
133
+ rating: r.rating,
134
+ rating_reason: r.rating_reason,
135
+ }));
136
+ },
137
+ });
138
+ tools.push({
139
+ name: "timer.status",
140
+ description: "查询某用户当前计时状态",
141
+ inputSchema: { user_id: z.string().optional() },
142
+ needsUserIdInjection: true,
143
+ handler: async (rawInput) => {
144
+ const r = injector(rawInput);
145
+ if ("error" in r)
146
+ return errorContent(r.error);
147
+ return asContent(await timer.status(r.user_id));
148
+ },
149
+ });
150
+ tools.push({
151
+ name: "timer.elapsed",
152
+ description: "查询当前计时已过毫秒数",
153
+ inputSchema: { user_id: z.string().optional() },
154
+ needsUserIdInjection: true,
155
+ handler: async (rawInput) => {
156
+ const r = injector(rawInput);
157
+ if ("error" in r)
158
+ return errorContent(r.error);
159
+ return asContent(await timer.elapsed(r.user_id));
160
+ },
161
+ });
162
+ tools.push({
163
+ name: "timer.statusAll",
164
+ description: "查询所有用户的当前计时状态(调试/兜底用)",
165
+ inputSchema: {},
166
+ handler: async () => asContent(await timer.statusAll()),
167
+ });
146
168
  // ── S26: pomodoro v1 tools ──
147
- server.registerTool("timer.startFocus", {
169
+ tools.push({
170
+ name: "timer.startFocus",
148
171
  description: "开始一个专注计时(番茄钟),带目标时长",
149
172
  inputSchema: {
150
173
  user_id: z.string().optional(),
@@ -153,27 +176,33 @@ export function buildServer(db) {
153
176
  categoryId: z.string().optional(),
154
177
  task_id: z.string().optional(),
155
178
  },
156
- }, async (rawInput) => {
157
- const r = injector(rawInput);
158
- if ("error" in r)
159
- return errorContent(r.error);
160
- return asContent(await timer.startFocus(r.user_id, r.event, r.focusMin, r.categoryId, r.task_id));
179
+ needsUserIdInjection: true,
180
+ handler: async (rawInput) => {
181
+ const r = injector(rawInput);
182
+ if ("error" in r)
183
+ return errorContent(r.error);
184
+ return asContent(await timer.startFocus(r.user_id, r.event, r.focusMin, r.categoryId, r.task_id));
185
+ },
161
186
  });
162
- server.registerTool("timer.remaining", {
187
+ tools.push({
188
+ name: "timer.remaining",
163
189
  description: "查询专注计时剩余时间(负数=超时)",
164
190
  inputSchema: {
165
191
  user_id: z.string().optional(),
166
192
  },
167
- }, async (rawInput) => {
168
- const r = injector(rawInput);
169
- if ("error" in r)
170
- return errorContent(r.error);
171
- return asContent(await timer.remaining(r.user_id));
193
+ needsUserIdInjection: true,
194
+ handler: async (rawInput) => {
195
+ const r = injector(rawInput);
196
+ if ("error" in r)
197
+ return errorContent(r.error);
198
+ return asContent(await timer.remaining(r.user_id));
199
+ },
172
200
  });
173
201
  // ============================================================
174
- // timeblock (5 tools — single-object args)
202
+ // timeblock (6 tools — single-object args)
175
203
  // ============================================================
176
- server.registerTool("timeblock.create", {
204
+ tools.push({
205
+ name: "timeblock.create",
177
206
  description: "创建一条时间记录",
178
207
  inputSchema: {
179
208
  user_id: z.string().optional(),
@@ -187,26 +216,32 @@ export function buildServer(db) {
187
216
  rating_reason: z.string().optional(),
188
217
  notes: z.string().optional(),
189
218
  },
190
- }, async (rawInput) => {
191
- const r = injector(rawInput);
192
- if ("error" in r)
193
- return errorContent(r.error);
194
- return asContent(await tb.create(r));
219
+ needsUserIdInjection: true,
220
+ handler: async (rawInput) => {
221
+ const r = injector(rawInput);
222
+ if ("error" in r)
223
+ return errorContent(r.error);
224
+ return asContent(await tb.create(r));
225
+ },
195
226
  });
196
- server.registerTool("timeblock.getById", {
227
+ tools.push({
228
+ name: "timeblock.getById",
197
229
  description: "按 id 获取单条时间记录。" +
198
230
  "S41 多用户:默认按当前 user_id 过滤,跨用户查不到返回 null。",
199
231
  inputSchema: {
200
232
  id: z.string().min(1),
201
233
  user_id: z.string().optional(),
202
234
  },
203
- }, async (rawInput) => {
204
- const r = injector(rawInput);
205
- if ("error" in r)
206
- return errorContent(r.error);
207
- return asContent(await tb.getById(rawInput.id, r.user_id));
235
+ needsUserIdInjection: true,
236
+ handler: async (rawInput) => {
237
+ const r = injector(rawInput);
238
+ if ("error" in r)
239
+ return errorContent(r.error);
240
+ return asContent(await tb.getById(rawInput.id, r.user_id));
241
+ },
208
242
  });
209
- server.registerTool("timeblock.update", {
243
+ tools.push({
244
+ name: "timeblock.update",
210
245
  description: "更新一条时间记录(标题/时间/评分/status 等)。" +
211
246
  "MV-COACH-3 Slice 4 用户主权状态机:" +
212
247
  "① 开工 cron 触发到点 → status='unverified'(等用户核实,**不计入实绩**);" +
@@ -227,16 +262,25 @@ export function buildServer(db) {
227
262
  notes: z.string().optional(),
228
263
  status: z.enum(["planned", "unverified", "completed"]).optional(),
229
264
  },
230
- }, async ({ id, ...rest }) => asContent(await tb.update(id, rest)));
231
- server.registerTool("timeblock.remove", {
265
+ handler: async ({ id, ...rest }) => asContent(await tb.update(id, rest)),
266
+ });
267
+ tools.push({
268
+ name: "timeblock.remove",
232
269
  description: "软删除一条时间记录",
233
270
  inputSchema: {
234
271
  id: z.string().min(1),
235
272
  target_user_id: z.string().min(1).optional(),
236
273
  },
237
- }, async ({ id, target_user_id }) => asContent(await tb.remove(id, { target_user_id })));
238
- server.registerTool("timeblock.restore", { description: "恢复一条已软删除的时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.restore(id)));
239
- server.registerTool("timeblock.list", {
274
+ handler: async ({ id, target_user_id }) => asContent(await tb.remove(id, { target_user_id })),
275
+ });
276
+ tools.push({
277
+ name: "timeblock.restore",
278
+ description: "恢复一条已软删除的时间记录",
279
+ inputSchema: { id: z.string().min(1) },
280
+ handler: async ({ id }) => asContent(await tb.restore(id)),
281
+ });
282
+ tools.push({
283
+ name: "timeblock.list",
240
284
  description: "按条件查询时间记录列表",
241
285
  inputSchema: {
242
286
  user_id: z.string().optional(),
@@ -249,16 +293,19 @@ export function buildServer(db) {
249
293
  limit: z.number().optional(),
250
294
  offset: z.number().optional(),
251
295
  },
252
- }, async (rawInput) => {
253
- const r = injector(rawInput);
254
- if ("error" in r)
255
- return errorContent(r.error);
256
- return asContent(await tb.list(r));
296
+ needsUserIdInjection: true,
297
+ handler: async (rawInput) => {
298
+ const r = injector(rawInput);
299
+ if ("error" in r)
300
+ return errorContent(r.error);
301
+ return asContent(await tb.list(r));
302
+ },
257
303
  });
258
304
  // ============================================================
259
305
  // stats (2 tools — single-object filters)
260
306
  // ============================================================
261
- server.registerTool("stats.summarize", {
307
+ tools.push({
308
+ name: "stats.summarize",
262
309
  description: "按分类汇总时间统计(占比/记录数)",
263
310
  inputSchema: {
264
311
  user_id: z.string().optional(),
@@ -267,13 +314,16 @@ export function buildServer(db) {
267
314
  to: z.string().optional(),
268
315
  category_id: z.string().optional(),
269
316
  },
270
- }, async (rawInput) => {
271
- const r = injector(rawInput);
272
- if ("error" in r)
273
- return errorContent(r.error);
274
- return asContent(await stats.summarize(r));
317
+ needsUserIdInjection: true,
318
+ handler: async (rawInput) => {
319
+ const r = injector(rawInput);
320
+ if ("error" in r)
321
+ return errorContent(r.error);
322
+ return asContent(await stats.summarize(r));
323
+ },
275
324
  });
276
- server.registerTool("stats.dailyBreakdown", {
325
+ tools.push({
326
+ name: "stats.dailyBreakdown",
277
327
  description: "按天×分类拆分时间明细",
278
328
  inputSchema: {
279
329
  user_id: z.string().optional(),
@@ -282,32 +332,38 @@ export function buildServer(db) {
282
332
  to: z.string().optional(),
283
333
  category_id: z.string().optional(),
284
334
  },
285
- }, async (rawInput) => {
286
- const r = injector(rawInput);
287
- if ("error" in r)
288
- return errorContent(r.error);
289
- return asContent(await stats.dailyBreakdown(r));
335
+ needsUserIdInjection: true,
336
+ handler: async (rawInput) => {
337
+ const r = injector(rawInput);
338
+ if ("error" in r)
339
+ return errorContent(r.error);
340
+ return asContent(await stats.dailyBreakdown(r));
341
+ },
290
342
  });
291
343
  // ============================================================
292
344
  // matrix (1 tool — single-object filters, reuses StatsRepo)
293
345
  // ============================================================
294
- server.registerTool("matrix.renderMatrix", {
346
+ tools.push({
347
+ name: "matrix.renderMatrix",
295
348
  description: "时间×评分四象限矩阵(能耗黑洞/保持区/亮点区/可放弃)",
296
349
  inputSchema: {
297
350
  user_id: z.string().optional(),
298
351
  from: z.string().optional(),
299
352
  to: z.string().optional(),
300
353
  },
301
- }, async (rawInput) => {
302
- const r = injector(rawInput);
303
- if ("error" in r)
304
- return errorContent(r.error);
305
- return asContent(await matrix.renderMatrix(r));
354
+ needsUserIdInjection: true,
355
+ handler: async (rawInput) => {
356
+ const r = injector(rawInput);
357
+ if ("error" in r)
358
+ return errorContent(r.error);
359
+ return asContent(await matrix.renderMatrix(r));
360
+ },
306
361
  });
307
362
  // ============================================================
308
363
  // schedule.timeline (emoji matrix + detail table)
309
364
  // ============================================================
310
- server.registerTool("schedule.timeline", {
365
+ tools.push({
366
+ name: "schedule.timeline",
311
367
  description: "看日程/复盘的首选视图:emoji 矩阵+细项表格,含计划/计时中/未回应/实绩全状态。" +
312
368
  "用户说「看今天/看日程/在干啥/复盘/接下来 N 小时」时调本工具,并直接 echo rendered_text(不重写、不改格式)。" +
313
369
  "不要调 scheduler.listSchedules(那是内部排程查询,不是用户视图)。",
@@ -318,45 +374,58 @@ export function buildServer(db) {
318
374
  to: z.string().optional(),
319
375
  format: z.enum(["emoji", "ascii"]).optional(),
320
376
  },
321
- }, async (rawInput) => {
322
- const r = injector(rawInput);
323
- if ("error" in r)
324
- return errorContent(r.error);
325
- return asContent(await timeline.renderTimeline(r));
377
+ needsUserIdInjection: true,
378
+ handler: async (rawInput) => {
379
+ const r = injector(rawInput);
380
+ if ("error" in r)
381
+ return errorContent(r.error);
382
+ return asContent(await timeline.renderTimeline(r));
383
+ },
326
384
  });
327
385
  // ============================================================
328
386
  // clock (3 tools — pure functions, no repo)
329
387
  // ============================================================
330
- server.registerTool("clock.now", {
388
+ tools.push({
389
+ name: "clock.now",
331
390
  description: "返回当前 ISO 时间字符串(可选时区偏移)",
332
391
  inputSchema: {
333
392
  offsetMinutes: z.number().optional(),
334
393
  },
335
- }, async ({ offsetMinutes }) => asContent(offsetMinutes !== undefined ? nowLocal(offsetMinutes) : nowLocal()));
336
- server.registerTool("clock.formatTime", {
394
+ handler: async ({ offsetMinutes }) => asContent(offsetMinutes !== undefined ? nowLocal(offsetMinutes) : nowLocal()),
395
+ });
396
+ tools.push({
397
+ name: "clock.formatTime",
337
398
  description: "按指定格式格式化 ISO 时间",
338
399
  inputSchema: {
339
400
  iso: z.string().min(1),
340
401
  format: z.string().min(1),
341
402
  },
342
- }, async ({ iso, format }) => asContent(formatTime(iso, format)));
343
- server.registerTool("clock.parseTime", {
403
+ handler: async ({ iso, format }) => asContent(formatTime(iso, format)),
404
+ });
405
+ tools.push({
406
+ name: "clock.parseTime",
344
407
  description: "解析 ISO 时间字符串为结构化字段",
345
408
  inputSchema: { iso: z.string().min(1) },
346
- }, async ({ iso }) => asContent(parseTime(iso)));
409
+ handler: async ({ iso }) => asContent(parseTime(iso)),
410
+ });
347
411
  // ============================================================
348
- // profile (2 tools — get / upsert)
412
+ // profile (8 tools — get / upsert / markInactivityNudge / claimNudge /
413
+ // confirm_default / bindAlias / listMyChannels / createUser)
349
414
  // ============================================================
350
- server.registerTool("profile.get", {
415
+ tools.push({
416
+ name: "profile.get",
351
417
  description: "获取用户 profile(含结构化约定)",
352
418
  inputSchema: { user_id: z.string().optional() },
353
- }, async (rawInput) => {
354
- const r = injector(rawInput);
355
- if ("error" in r)
356
- return errorContent(r.error);
357
- return asContent(await profile.get(r.user_id));
419
+ needsUserIdInjection: true,
420
+ handler: async (rawInput) => {
421
+ const r = injector(rawInput);
422
+ if ("error" in r)
423
+ return errorContent(r.error);
424
+ return asContent(await profile.get(r.user_id));
425
+ },
358
426
  });
359
- server.registerTool("profile.upsert", {
427
+ tools.push({
428
+ name: "profile.upsert",
360
429
  description: "创建或更新用户 profile(display_name / expectations / settings)。首次确认请用 profile.confirm_default。",
361
430
  inputSchema: {
362
431
  user_id: z.string().optional(),
@@ -367,13 +436,16 @@ export function buildServer(db) {
367
436
  .passthrough()
368
437
  .optional(),
369
438
  },
370
- }, async (rawInput) => {
371
- const r = injector(rawInput);
372
- if ("error" in r)
373
- return errorContent(r.error);
374
- return asContent(await profile.upsert(r));
439
+ needsUserIdInjection: true,
440
+ handler: async (rawInput) => {
441
+ const r = injector(rawInput);
442
+ if ("error" in r)
443
+ return errorContent(r.error);
444
+ return asContent(await profile.upsert(r));
445
+ },
375
446
  });
376
- server.registerTool("profile.markInactivityNudge", {
447
+ tools.push({
448
+ name: "profile.markInactivityNudge",
377
449
  description: "MV-COACH-3 slice 2(防重复失效专用):触发「5 天没记录轻触」问候后必调。" +
378
450
  "专门写 settings.last_inactivity_nudge_at = now。" +
379
451
  "⚠️ 不要用 profile.upsert 写这个字段——upsert 是 whole-replace 会清掉其它 settings key。" +
@@ -381,13 +453,16 @@ export function buildServer(db) {
381
453
  inputSchema: {
382
454
  user_id: z.string().optional(),
383
455
  },
384
- }, async (rawInput) => {
385
- const r = injector(rawInput);
386
- if ("error" in r)
387
- return errorContent(r.error);
388
- return asContent(await profile.markInactivityNudge({ user_id: r.user_id }));
456
+ needsUserIdInjection: true,
457
+ handler: async (rawInput) => {
458
+ const r = injector(rawInput);
459
+ if ("error" in r)
460
+ return errorContent(r.error);
461
+ return asContent(await profile.markInactivityNudge({ user_id: r.user_id }));
462
+ },
389
463
  });
390
- server.registerTool("profile.claimNudge", {
464
+ tools.push({
465
+ name: "profile.claimNudge",
391
466
  description: "S40 slice 2(多 cron 并发 atomic 抢占):nag fan-out 多渠道时用。" +
392
467
  "window_minutes 内只有第一个调用赢(won=true),其它返回 won=false。" +
393
468
  "**调用方必须看返回的 won 字段**:" +
@@ -397,28 +472,33 @@ export function buildServer(db) {
397
472
  user_id: z.string().optional(),
398
473
  window_minutes: z.number().optional(),
399
474
  },
400
- }, async (rawInput) => {
401
- const r = injector(rawInput);
402
- if ("error" in r)
403
- return errorContent(r.error);
404
- return asContent(await profile.claimNudge({
405
- user_id: r.user_id,
406
- window_minutes: typeof rawInput.window_minutes === "number"
407
- ? rawInput.window_minutes
408
- : undefined,
409
- }));
410
- });
411
- server.registerTool("profile.confirm_default", {
475
+ needsUserIdInjection: true,
476
+ handler: async (rawInput) => {
477
+ const r = injector(rawInput);
478
+ if ("error" in r)
479
+ return errorContent(r.error);
480
+ return asContent(await profile.claimNudge({
481
+ user_id: r.user_id,
482
+ window_minutes: typeof rawInput.window_minutes === "number"
483
+ ? rawInput.window_minutes
484
+ : undefined,
485
+ }));
486
+ },
487
+ });
488
+ tools.push({
489
+ name: "profile.confirm_default",
412
490
  description: "首次写入前确认默认用户:写入 user_profile,设置 display_name 与 default_confirmed_at。display_name 重名则拒绝。",
413
491
  inputSchema: {
414
492
  platform_user_id: z.string().min(1),
415
493
  display_name: z.string().min(1),
416
494
  },
417
- }, async (input) => asContent(await profileConfirm.confirmDefault(input)));
495
+ handler: async (input) => asContent(await profileConfirm.confirmDefault(input)),
496
+ });
418
497
  // ============================================================
419
- // S40: cross-platform identity (2 tools)
498
+ // S40: cross-platform identity (bindAlias / listMyChannels)
420
499
  // ============================================================
421
- server.registerTool("profile.bindAlias", {
500
+ tools.push({
501
+ name: "profile.bindAlias",
422
502
  description: "S40 跨平台身份层:把一个平台 chat_id 绑定到已有 libero 用户身份。" +
423
503
  "场景:用户在新设备(如飞书)首次开口,主会话问『要和已有账号(如企微)合并吗』,得到肯定后调这个。" +
424
504
  "libero_user_id 为空 = 首次绑定,自动建一个 libero_user_id。" +
@@ -430,59 +510,64 @@ export function buildServer(db) {
430
510
  chat_id: z.string().optional(),
431
511
  display_name: z.string().optional(),
432
512
  },
433
- }, async (input) => {
434
- const crypto = await import("node:crypto");
435
- let lid = input.libero_user_id;
436
- if (!lid) {
437
- lid = `u_libero_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
438
- await identityRepo.createIdentity({
439
- libero_user_id: lid,
440
- display_name: input.display_name ?? input.platform_user_id.slice(0, 16),
441
- });
442
- }
443
- else {
444
- const existing = await identityRepo.getIdentity(lid);
445
- if (!existing) {
446
- return errorContent(`libero_user_id not found: ${lid}`);
513
+ handler: async (input) => {
514
+ const crypto = await import("node:crypto");
515
+ let lid = input.libero_user_id;
516
+ if (!lid) {
517
+ lid = `u_libero_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
518
+ await identityRepo.createIdentity({
519
+ libero_user_id: lid,
520
+ display_name: input.display_name ?? input.platform_user_id.slice(0, 16),
521
+ });
447
522
  }
448
- }
449
- try {
450
- await identityRepo.bindAlias({
451
- libero_user_id: lid,
452
- platform: input.platform,
453
- platform_user_id: input.platform_user_id,
454
- chat_id: input.chat_id,
455
- });
456
- }
457
- catch (e) {
458
- return errorContent(e.message);
459
- }
460
- const aliases = await identityRepo.listAliases(lid);
461
- return asContent({ libero_user_id: lid, bound_aliases: aliases });
462
- });
463
- server.registerTool("profile.listMyChannels", {
523
+ else {
524
+ const existing = await identityRepo.getIdentity(lid);
525
+ if (!existing) {
526
+ return errorContent(`libero_user_id not found: ${lid}`);
527
+ }
528
+ }
529
+ try {
530
+ await identityRepo.bindAlias({
531
+ libero_user_id: lid,
532
+ platform: input.platform,
533
+ platform_user_id: input.platform_user_id,
534
+ chat_id: input.chat_id,
535
+ });
536
+ }
537
+ catch (e) {
538
+ return errorContent(e.message);
539
+ }
540
+ const aliases = await identityRepo.listAliases(lid);
541
+ return asContent({ libero_user_id: lid, bound_aliases: aliases });
542
+ },
543
+ });
544
+ tools.push({
545
+ name: "profile.listMyChannels",
464
546
  description: "S40 跨平台身份层:列出当前用户绑定的所有平台渠道。" +
465
547
  "用途:nag fan-out(按渠道建独立 cron)、跨平台数据查询、身份确认。",
466
548
  inputSchema: {
467
549
  user_id: z.string().optional(),
468
550
  },
469
- }, async (rawInput) => {
470
- const r = injector(rawInput);
471
- if ("error" in r)
472
- return errorContent(r.error);
473
- const lid = await resolveUserId(identityRepo, r.user_id);
474
- const aliases = await identityRepo.listAliases(lid);
475
- return asContent({
476
- libero_user_id: lid,
477
- channels: aliases.map((a) => ({
478
- platform: a.platform,
479
- platform_user_id: a.platform_user_id,
480
- chat_id: a.chat_id,
481
- bound_at: a.bound_at,
482
- })),
483
- });
484
- });
485
- server.registerTool("profile.createUser", {
551
+ needsUserIdInjection: true,
552
+ handler: async (rawInput) => {
553
+ const r = injector(rawInput);
554
+ if ("error" in r)
555
+ return errorContent(r.error);
556
+ const lid = await resolveUserId(identityRepo, r.user_id);
557
+ const aliases = await identityRepo.listAliases(lid);
558
+ return asContent({
559
+ libero_user_id: lid,
560
+ channels: aliases.map((a) => ({
561
+ platform: a.platform,
562
+ platform_user_id: a.platform_user_id,
563
+ chat_id: a.chat_id,
564
+ bound_at: a.bound_at,
565
+ })),
566
+ });
567
+ },
568
+ });
569
+ tools.push({
570
+ name: "profile.createUser",
486
571
  description: "S41 多用户建档:建立新 libero 用户(父母代管模式)。" +
487
572
  "用户体感:「帮我建一个新用户」「我要给我儿子建档」时调用。" +
488
573
  "返回新 libero_user_id,建档后该 user_id 立即可用于所有 libero 工具调用。" +
@@ -498,26 +583,28 @@ export function buildServer(db) {
498
583
  platform_user_id: z.string().optional(),
499
584
  chat_id: z.string().optional(),
500
585
  },
501
- }, async (rawInput) => {
502
- try {
503
- const result = await profile.createUser({
504
- display_name: rawInput.display_name,
505
- focus_areas: rawInput.focus_areas,
506
- goals: rawInput.goals,
507
- platform: rawInput.platform,
508
- platform_user_id: rawInput.platform_user_id,
509
- chat_id: rawInput.chat_id,
510
- });
511
- return asContent(result);
512
- }
513
- catch (e) {
514
- return errorContent(e.message);
515
- }
586
+ handler: async (rawInput) => {
587
+ try {
588
+ const result = await profile.createUser({
589
+ display_name: rawInput.display_name,
590
+ focus_areas: rawInput.focus_areas,
591
+ goals: rawInput.goals,
592
+ platform: rawInput.platform,
593
+ platform_user_id: rawInput.platform_user_id,
594
+ chat_id: rawInput.chat_id,
595
+ });
596
+ return asContent(result);
597
+ }
598
+ catch (e) {
599
+ return errorContent(e.message);
600
+ }
601
+ },
516
602
  });
517
603
  // ============================================================
518
604
  // reminder (6 tools — S11)
519
605
  // ============================================================
520
- server.registerTool("reminder.create", {
606
+ tools.push({
607
+ name: "reminder.create",
521
608
  description: "⚠️ 仅写 DB,不主动推送(libero-mcp 无 background sender)。" +
522
609
  "在 hermes 等有 cronjob 工具的平台上,用户要'X 时间提醒我 Y'请调宿主 cronjob(action=create, deliver=origin),不要用本工具。" +
523
610
  "本工具仅用于:① 无 cron 能力的平台兜底;② 查询场景配合 getDue/getMissed。",
@@ -529,48 +616,62 @@ export function buildServer(db) {
529
616
  channel: z.string().optional(),
530
617
  timeblock_id: z.string().optional(),
531
618
  },
532
- }, async (rawInput) => {
533
- const r = injector(rawInput);
534
- if ("error" in r)
535
- return errorContent(r.error);
536
- return asContent(await reminder.create(r));
619
+ needsUserIdInjection: true,
620
+ handler: async (rawInput) => {
621
+ const r = injector(rawInput);
622
+ if ("error" in r)
623
+ return errorContent(r.error);
624
+ return asContent(await reminder.create(r));
625
+ },
537
626
  });
538
- server.registerTool("reminder.getDue", {
627
+ tools.push({
628
+ name: "reminder.getDue",
539
629
  description: "获取到期且待发送的提醒列表",
540
630
  inputSchema: {
541
631
  user_id: z.string().optional(),
542
632
  until: z.string().optional(),
543
633
  },
544
- }, async (input) => asContent(await reminder.getDueReminders(input)));
545
- server.registerTool("reminder.getNextDue", {
634
+ handler: async (input) => asContent(await reminder.getDueReminders(input)),
635
+ });
636
+ tools.push({
637
+ name: "reminder.getNextDue",
546
638
  description: "获取下一条待发送提醒的时间",
547
639
  inputSchema: {
548
640
  user_id: z.string().optional(),
549
641
  },
550
- }, async (input) => asContent(await reminder.getNextDue(input)));
551
- server.registerTool("reminder.getMissed", {
642
+ handler: async (input) => asContent(await reminder.getNextDue(input)),
643
+ });
644
+ tools.push({
645
+ name: "reminder.getMissed",
552
646
  description: "获取过期但未送达的提醒(被动补发用)",
553
647
  inputSchema: {
554
648
  user_id: z.string().optional(),
555
649
  },
556
- }, async (rawInput) => {
557
- const r = injector(rawInput);
558
- if ("error" in r)
559
- return errorContent(r.error);
560
- return asContent(await reminder.getMissedReminders({ user_id: r.user_id }));
650
+ needsUserIdInjection: true,
651
+ handler: async (rawInput) => {
652
+ const r = injector(rawInput);
653
+ if ("error" in r)
654
+ return errorContent(r.error);
655
+ return asContent(await reminder.getMissedReminders({ user_id: r.user_id }));
656
+ },
561
657
  });
562
- server.registerTool("reminder.markSent", {
658
+ tools.push({
659
+ name: "reminder.markSent",
563
660
  description: "标记提醒为已发送(幂等)",
564
661
  inputSchema: { id: z.string().min(1) },
565
- }, async ({ id }) => asContent(await reminder.markSent(id)));
566
- server.registerTool("reminder.cancel", {
662
+ handler: async ({ id }) => asContent(await reminder.markSent(id)),
663
+ });
664
+ tools.push({
665
+ name: "reminder.cancel",
567
666
  description: "取消一条提醒(幂等)",
568
667
  inputSchema: { id: z.string().min(1) },
569
- }, async ({ id }) => asContent(await reminder.cancelReminder(id)));
668
+ handler: async ({ id }) => asContent(await reminder.cancelReminder(id)),
669
+ });
570
670
  // ============================================================
571
671
  // scheduler (3 tools — S27, reuses timeblockRepo)
572
672
  // ============================================================
573
- server.registerTool("scheduler.createSchedule", {
673
+ tools.push({
674
+ name: "scheduler.createSchedule",
574
675
  description: "创建一个日程(planned 时间块)。" +
575
676
  "⚠️ MV-COACH-3 Slice 4 v2 教练闭环(用户主权模型):建完 schedule 后,**必须**在同一回复里完成:" +
576
677
  "① 调 cronjob(action=create, deliver=origin) 建开工 cron(schedule=日程开始时间),cron prompt 必须用 libero-coach skill 的'开工 cron 标准 prompt 模板'——**不允许自创结构**。" +
@@ -589,13 +690,16 @@ export function buildServer(db) {
589
690
  rating_reason: z.string().optional(),
590
691
  notes: z.string().optional(),
591
692
  },
592
- }, async (rawInput) => {
593
- const r = injector(rawInput);
594
- if ("error" in r)
595
- return errorContent(r.error);
596
- return asContent(await scheduler.createSchedule(r));
693
+ needsUserIdInjection: true,
694
+ handler: async (rawInput) => {
695
+ const r = injector(rawInput);
696
+ if ("error" in r)
697
+ return errorContent(r.error);
698
+ return asContent(await scheduler.createSchedule(r));
699
+ },
597
700
  });
598
- server.registerTool("scheduler.listSchedules", {
701
+ tools.push({
702
+ name: "scheduler.listSchedules",
599
703
  description: "内部工具:查询 planned 日程块用于排程冲突检查。" +
600
704
  "⚠️ 用户「看今天/看日程/复盘/在干啥」请调 schedule.timeline(含矩阵+表格+全状态视图,不要调本工具)。" +
601
705
  "本工具仅用于排程编排时的内部查询。",
@@ -604,29 +708,35 @@ export function buildServer(db) {
604
708
  from: z.string().optional(),
605
709
  to: z.string().optional(),
606
710
  },
607
- }, async (rawInput) => {
608
- const r = injector(rawInput);
609
- if ("error" in r)
610
- return errorContent(r.error);
611
- return asContent(await scheduler.listSchedules(r));
711
+ needsUserIdInjection: true,
712
+ handler: async (rawInput) => {
713
+ const r = injector(rawInput);
714
+ if ("error" in r)
715
+ return errorContent(r.error);
716
+ return asContent(await scheduler.listSchedules(r));
717
+ },
612
718
  });
613
- server.registerTool("scheduler.checkConflict", {
719
+ tools.push({
720
+ name: "scheduler.checkConflict",
614
721
  description: "检查时间段是否与已有日程冲突",
615
722
  inputSchema: {
616
723
  user_id: z.string().optional(),
617
724
  start_time: z.string(),
618
725
  end_time: z.string(),
619
726
  },
620
- }, async (rawInput) => {
621
- const r = injector(rawInput);
622
- if ("error" in r)
623
- return errorContent(r.error);
624
- return asContent(await scheduler.checkConflict(r));
727
+ needsUserIdInjection: true,
728
+ handler: async (rawInput) => {
729
+ const r = injector(rawInput);
730
+ if ("error" in r)
731
+ return errorContent(r.error);
732
+ return asContent(await scheduler.checkConflict(r));
733
+ },
625
734
  });
626
735
  // ============================================================
627
- // task (7 tools — S29 P2-3 + 事一 draft/commit, takes raw db)
736
+ // task (6 tools — S29 P2-3 + 事一 draft/commit, takes raw db)
628
737
  // ============================================================
629
- server.registerTool("task.create", {
738
+ tools.push({
739
+ name: "task.create",
630
740
  description: "建单个独立任务(不挂任何 timeblock)。⚠️ 拆解请用 decompose.split,本工具一次只能建一个任务、不能建子任务。",
631
741
  // S33 v3: 用 z.object().strict() 显式拒 parent_id(zod 默认 strip 不报错,
632
742
  // 严格模式让多余键触发 safeParse 失败 → MCP 返回 isError)。
@@ -639,30 +749,38 @@ export function buildServer(db) {
639
749
  est_minutes: z.number().optional(),
640
750
  })
641
751
  .strict(),
642
- }, async (rawInput) => {
643
- const r = injector(rawInput);
644
- if ("error" in r)
645
- return errorContent(r.error);
646
- return asContent(await task.createTask(r));
752
+ needsUserIdInjection: true,
753
+ handler: async (rawInput) => {
754
+ const r = injector(rawInput);
755
+ if ("error" in r)
756
+ return errorContent(r.error);
757
+ return asContent(await task.createTask(r));
758
+ },
647
759
  });
648
- server.registerTool("task.get", {
760
+ tools.push({
761
+ name: "task.get",
649
762
  description: "按 id 获取任务(含递归子树)",
650
763
  inputSchema: { id: z.string().min(1) },
651
- }, async ({ id }) => asContent(await task.getTask(id)));
652
- server.registerTool("task.list", {
764
+ handler: async ({ id }) => asContent(await task.getTask(id)),
765
+ });
766
+ tools.push({
767
+ name: "task.list",
653
768
  description: "列出任务(parent_id=null 仅根;给定则该父的子;省略则全部)",
654
769
  inputSchema: {
655
770
  user_id: z.string().optional(),
656
771
  target_user_id: z.string().min(1).optional(),
657
772
  parent_id: z.string().nullable().optional(),
658
773
  },
659
- }, async (rawInput) => {
660
- const r = injector(rawInput);
661
- if ("error" in r)
662
- return errorContent(r.error);
663
- return asContent(await task.listTasks(r));
774
+ needsUserIdInjection: true,
775
+ handler: async (rawInput) => {
776
+ const r = injector(rawInput);
777
+ if ("error" in r)
778
+ return errorContent(r.error);
779
+ return asContent(await task.listTasks(r));
780
+ },
664
781
  });
665
- server.registerTool("task.update", {
782
+ tools.push({
783
+ name: "task.update",
666
784
  description: "更新任务(title/est_minutes/status;状态机校验非法流转)",
667
785
  inputSchema: {
668
786
  id: z.string().min(1),
@@ -670,25 +788,31 @@ export function buildServer(db) {
670
788
  est_minutes: z.number().optional(),
671
789
  status: z.string().optional(),
672
790
  },
673
- }, async ({ id, ...rest }) => asContent(await task.updateTask(id, rest)));
674
- server.registerTool("task.linkTimeblock", {
791
+ handler: async ({ id, ...rest }) => asContent(await task.updateTask(id, rest)),
792
+ });
793
+ tools.push({
794
+ name: "task.linkTimeblock",
675
795
  description: "把任务关联到一个时间块(子任务开工执行时)",
676
796
  inputSchema: {
677
797
  task_id: z.string().min(1),
678
798
  block_id: z.string().min(1),
679
799
  },
680
- }, async ({ task_id, block_id }) => asContent(await task.linkTimeblock(task_id, block_id)));
681
- server.registerTool("task.findPlan", {
800
+ handler: async ({ task_id, block_id }) => asContent(await task.linkTimeblock(task_id, block_id)),
801
+ });
802
+ tools.push({
803
+ name: "task.findPlan",
682
804
  description: "按事件名 exact-match 查拆解计划:返回根任务 + 第一块未完成子任务(record 开工时链 task_id,驱动闭环正反馈)",
683
805
  inputSchema: {
684
806
  user_id: z.string().optional(),
685
807
  title: z.string().min(1),
686
808
  },
687
- }, async (rawInput) => {
688
- const r = injector(rawInput);
689
- if ("error" in r)
690
- return errorContent(r.error);
691
- return asContent(await task.findPlan(r));
809
+ needsUserIdInjection: true,
810
+ handler: async (rawInput) => {
811
+ const r = injector(rawInput);
812
+ if ("error" in r)
813
+ return errorContent(r.error);
814
+ return asContent(await task.findPlan(r));
815
+ },
692
816
  });
693
817
  // S33 v3: task.draftPlan / task.commitPlan 从 MCP 工具列表移除(不再暴露给 mimo)。
694
818
  // 拆解唯一入口 = decompose.split(见下方 decompose 段)。
@@ -698,7 +822,8 @@ export function buildServer(db) {
698
822
  // decompose.split = 拆解唯一入口。状态机强制 draft→confirm→commit。
699
823
  // 错误返回 isError content(不 throw),避 hermes 熔断器(v2 实证坑)。
700
824
  // ============================================================
701
- server.registerTool("decompose.split", {
825
+ tools.push({
826
+ name: "decompose.split",
702
827
  description: "拆解 timeblock 为结构化子任务树(拆解唯一入口)。第一次调用出方案(不写库),第二次带 confirm_token 落地。状态机强制 draft→confirm→commit。⚠️ 拆子任务请用本工具,不要用 task.create。",
703
828
  inputSchema: {
704
829
  user_id: z.string().optional(),
@@ -711,24 +836,75 @@ export function buildServer(db) {
711
836
  .min(1),
712
837
  confirm_token: z.string().optional(),
713
838
  },
714
- }, async (rawInput) => {
715
- const r = injector(rawInput);
716
- if ("error" in r)
717
- return errorContent(r.error);
718
- const result = await task.splitDecompose(r);
719
- // DecomposeErrorResult → isError content(不 throw,避 hermes 熔断器)
720
- if ("ok" in result && result.ok === false) {
721
- return {
722
- content: [
723
- {
724
- type: "text",
725
- text: JSON.stringify(result),
726
- },
727
- ],
728
- isError: true,
729
- };
730
- }
731
- return asContent(result);
839
+ needsUserIdInjection: true,
840
+ handler: async (rawInput) => {
841
+ const r = injector(rawInput);
842
+ if ("error" in r)
843
+ return errorContent(r.error);
844
+ const result = await task.splitDecompose(r);
845
+ // DecomposeErrorResult → isError content(不 throw,避 hermes 熔断器)
846
+ if ("ok" in result && result.ok === false) {
847
+ return {
848
+ content: [
849
+ {
850
+ type: "text",
851
+ text: JSON.stringify(result),
852
+ },
853
+ ],
854
+ isError: true,
855
+ };
856
+ }
857
+ return asContent(result);
858
+ },
732
859
  });
860
+ return tools;
861
+ }
862
+ /**
863
+ * Build the libero MCP server on a given DB connection.
864
+ * Tools throw Error on business violations; handlers let them propagate —
865
+ * the SDK turns thrown errors into MCP tool errors (isError) preserving the message.
866
+ */
867
+ export function buildServer(db) {
868
+ // MV-COACH-3 Slice 2: resolve default user_id once at server build
869
+ const defaultUser = resolveDefaultUserId({ db, env: process.env });
870
+ logResolvedDefaultUserId(defaultUser);
871
+ const injector = makeUserIdInjector(defaultUser.userId);
872
+ // ── construct repos via MC-0a factory (default sqlite; postgres reserved) ──
873
+ const repos = createRepos(db);
874
+ // ── inject tool factories ──
875
+ const categories = createCategoryTools(repos.categoryRepo);
876
+ const tb = createTimeBlockTools(repos.timeblockRepo, db);
877
+ const stats = createStatsTools(repos.statsRepo);
878
+ const matrix = createMatrixTools(repos.statsRepo); // matrix reuses StatsRepo
879
+ const timeline = createTimelineTools(repos.timelineRepo);
880
+ const timer = createTimerTools(repos.timerRepo, repos.timeblockRepo, db);
881
+ const profile = createProfileTools(repos.profileRepo, repos.identityRepo);
882
+ const profileConfirm = createProfileConfirmTools(db);
883
+ const reminder = createReminderTools(repos.reminderRepo, undefined, db);
884
+ const scheduler = createSchedulerTools(repos.timeblockRepo, repos.reminderRepo, repos.categoryRepo);
885
+ const task = createTaskTools(db);
886
+ // instructions 必须放在第 2 参 ServerOptions(不是 serverInfo)—— SDK 只从 options 读
887
+ const server = new McpServer({ name: "libero-mcp", version: "0.2.3" }, { instructions: buildServerInstructions() });
888
+ // ── MC-0a: build registry once, register in a single traversal ──
889
+ const ctx = {
890
+ repos,
891
+ categories,
892
+ tb,
893
+ stats,
894
+ matrix,
895
+ timeline,
896
+ timer,
897
+ profile,
898
+ profileConfirm,
899
+ reminder,
900
+ scheduler,
901
+ task,
902
+ injector,
903
+ db,
904
+ };
905
+ const registry = buildToolRegistry(ctx);
906
+ for (const tool of registry) {
907
+ server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, tool.handler);
908
+ }
733
909
  return server;
734
910
  }