@yeaft/webchat-agent 0.1.935 → 0.1.936

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.
@@ -65,6 +65,8 @@ import { pairSanitize } from './pair-sanitize.js';
65
65
  import { filterSnapshotForVp } from './snapshot-filter.js';
66
66
  import { createVpStatusBroker } from './vp-status-broker.js';
67
67
  import { classifyThread as defaultClassifyThread, fallbackTitle } from './vp/thread-classifier.js';
68
+ import { listMcpServers, upsertMcpServer, removeMcpServer } from './config-api.js';
69
+ import { buildMcpFlattenedTools } from './tools/mcp-tools.js';
68
70
 
69
71
  /** @type {import('./session.js').Session | null} */
70
72
  let session = null;
@@ -4186,3 +4188,233 @@ export async function resetYeaftSession() {
4186
4188
  console.error('[Yeaft] Failed to re-initialize session after reset:', err.message);
4187
4189
  }
4188
4190
  }
4191
+
4192
+ // ────────────────────────────────────────────────────────────
4193
+ // MCP CRUD wire handlers (Claude-Code-style Settings → MCP tab)
4194
+ //
4195
+ // Wire types: `yeaft_mcp_list` / `yeaft_mcp_add` / `yeaft_mcp_remove` /
4196
+ // `yeaft_mcp_reload`. Each:
4197
+ // 1. Reads / writes ~/.yeaft/config.json `mcpServers` via config-api.
4198
+ // 2. Calls `session.mcpManager.connect|disconnect` to apply at runtime.
4199
+ // 3. Hot-swaps the live `toolRegistry` via `replaceMcpTools(...)` so the
4200
+ // next LLM turn sees the new tool catalogue WITHOUT a session restart.
4201
+ // 4. Broadcasts `yeaft_mcp_updated` so any subscribed web client (the
4202
+ // Settings panel + any open Yeaft view) refreshes its badge without
4203
+ // a manual reload.
4204
+ //
4205
+ // The handlers do NOT block on `ensureSessionLoaded()` — the session may
4206
+ // not yet be initialised when the user opens Settings before sending the
4207
+ // first message. In that case `session` is null and we operate ONLY on
4208
+ // the on-disk config; the live runtime takes effect on the next session
4209
+ // boot. When `session` IS available, we apply the runtime change too.
4210
+ //
4211
+ // Wire shape per response: always `{ type: 'yeaft_mcp_*', servers, runtime?, error? }`.
4212
+ // Frontend reducer should treat `error` as a non-empty string failure.
4213
+ // ────────────────────────────────────────────────────────────
4214
+
4215
+ /**
4216
+ * Snapshot the live MCP runtime so the UI can render per-server
4217
+ * connection state next to the configured servers. Safe to call when
4218
+ * the session hasn't been initialised yet — returns an empty runtime.
4219
+ */
4220
+ function mcpRuntimeSnapshot() {
4221
+ if (!session?.mcpManager) {
4222
+ return { connected: false, toolCount: 0, perServer: [] };
4223
+ }
4224
+ const status = session.mcpManager.status() || [];
4225
+ const toolCount = typeof session.mcpManager.toolCount === 'number'
4226
+ ? session.mcpManager.toolCount
4227
+ : status.reduce((sum, s) => sum + (s.toolCount || 0), 0);
4228
+ return {
4229
+ connected: !!session.mcpManager.hasServers,
4230
+ toolCount,
4231
+ perServer: status.map(s => ({
4232
+ name: s.name,
4233
+ ready: !!s.ready,
4234
+ toolCount: s.toolCount || 0,
4235
+ })),
4236
+ };
4237
+ }
4238
+
4239
+ /**
4240
+ * Re-flatten MCP tools into the live ToolRegistry. No-op when the session
4241
+ * (or its registry) hasn't been created yet — the next session boot will
4242
+ * pick up the change.
4243
+ */
4244
+ function hotSwapMcpTools() {
4245
+ if (!session?.toolRegistry || typeof session.toolRegistry.replaceMcpTools !== 'function') {
4246
+ return { removed: 0, added: 0, skipped: true };
4247
+ }
4248
+ try {
4249
+ const result = session.toolRegistry.replaceMcpTools(session.mcpManager, buildMcpFlattenedTools);
4250
+ return { ...result, skipped: false };
4251
+ } catch (err) {
4252
+ console.warn('[Yeaft] hot-swap MCP tools failed:', err?.message || err);
4253
+ return { removed: 0, added: 0, skipped: true, error: err?.message || String(err) };
4254
+ }
4255
+ }
4256
+
4257
+ /**
4258
+ * Broadcast a `yeaft_mcp_updated` event so any client subscribed to the
4259
+ * Yeaft view (Settings panel, status badge) refreshes without needing
4260
+ * to re-open the panel. The current list+runtime are included so the UI
4261
+ * is single-source (no separate fetch round-trip needed).
4262
+ */
4263
+ function broadcastMcpUpdated(extra = {}) {
4264
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
4265
+ const listed = listMcpServers(yeaftDir);
4266
+ sendToServer({
4267
+ type: 'yeaft_mcp_updated',
4268
+ servers: listed.servers || [],
4269
+ runtime: mcpRuntimeSnapshot(),
4270
+ ...extra,
4271
+ });
4272
+ }
4273
+
4274
+ export function handleYeaftMcpList(msg = {}) {
4275
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
4276
+ const listed = listMcpServers(yeaftDir);
4277
+ sendToServer({
4278
+ type: 'yeaft_mcp_list_result',
4279
+ requestId: msg.requestId || null,
4280
+ servers: listed.servers || [],
4281
+ runtime: mcpRuntimeSnapshot(),
4282
+ error: listed.error || null,
4283
+ });
4284
+ }
4285
+
4286
+ export async function handleYeaftMcpAdd(msg = {}) {
4287
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
4288
+ const result = upsertMcpServer(msg.server || {}, yeaftDir);
4289
+ if (result.error) {
4290
+ sendToServer({
4291
+ type: 'yeaft_mcp_add_result',
4292
+ requestId: msg.requestId || null,
4293
+ servers: [],
4294
+ runtime: mcpRuntimeSnapshot(),
4295
+ error: result.error,
4296
+ });
4297
+ return;
4298
+ }
4299
+
4300
+ // Apply at runtime when the session is live. The MCPManager's
4301
+ // `connect(serverConfig)` already disconnects-and-reconnects if a
4302
+ // server with the same name was already registered.
4303
+ let connectError = null;
4304
+ if (session?.mcpManager) {
4305
+ try {
4306
+ await session.mcpManager.connect(result.server);
4307
+ } catch (err) {
4308
+ connectError = err?.message || String(err);
4309
+ console.warn(`[Yeaft] MCP connect "${result.server.name}" failed:`, connectError);
4310
+ }
4311
+ }
4312
+
4313
+ const swap = hotSwapMcpTools();
4314
+
4315
+ sendToServer({
4316
+ type: 'yeaft_mcp_add_result',
4317
+ requestId: msg.requestId || null,
4318
+ servers: result.servers,
4319
+ runtime: mcpRuntimeSnapshot(),
4320
+ swap,
4321
+ connectError,
4322
+ error: null,
4323
+ });
4324
+ broadcastMcpUpdated({ reason: 'add', name: result.server.name, connectError });
4325
+ }
4326
+
4327
+ export async function handleYeaftMcpRemove(msg = {}) {
4328
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
4329
+ const name = typeof msg.name === 'string' ? msg.name : '';
4330
+ const result = removeMcpServer(name, yeaftDir);
4331
+ if (result.error) {
4332
+ sendToServer({
4333
+ type: 'yeaft_mcp_remove_result',
4334
+ requestId: msg.requestId || null,
4335
+ servers: [],
4336
+ runtime: mcpRuntimeSnapshot(),
4337
+ error: result.error,
4338
+ });
4339
+ return;
4340
+ }
4341
+
4342
+ if (session?.mcpManager) {
4343
+ try {
4344
+ await session.mcpManager.disconnect(name);
4345
+ } catch (err) {
4346
+ console.warn(`[Yeaft] MCP disconnect "${name}" failed:`, err?.message || err);
4347
+ }
4348
+ }
4349
+
4350
+ const swap = hotSwapMcpTools();
4351
+
4352
+ sendToServer({
4353
+ type: 'yeaft_mcp_remove_result',
4354
+ requestId: msg.requestId || null,
4355
+ servers: result.servers,
4356
+ runtime: mcpRuntimeSnapshot(),
4357
+ removed: !!result.removed,
4358
+ swap,
4359
+ error: null,
4360
+ });
4361
+ broadcastMcpUpdated({ reason: 'remove', name });
4362
+ }
4363
+
4364
+ export async function handleYeaftMcpReload(msg = {}) {
4365
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
4366
+ const targetName = typeof msg.name === 'string' && msg.name ? msg.name : null;
4367
+
4368
+ if (!session?.mcpManager) {
4369
+ // Session not yet alive — just echo the current config + an empty
4370
+ // runtime so the UI knows to wait for session boot.
4371
+ const listed = listMcpServers(yeaftDir);
4372
+ sendToServer({
4373
+ type: 'yeaft_mcp_reload_result',
4374
+ requestId: msg.requestId || null,
4375
+ servers: listed.servers || [],
4376
+ runtime: mcpRuntimeSnapshot(),
4377
+ error: null,
4378
+ });
4379
+ return;
4380
+ }
4381
+
4382
+ const listed = listMcpServers(yeaftDir);
4383
+ const configured = listed.servers || [];
4384
+
4385
+ // Per-server reload: disconnect + reconnect the named server only.
4386
+ // Whole-set reload: disconnect everything, then reconnect from current
4387
+ // config.json. The latter is what the user clicks "Reload all" for.
4388
+ const failures = [];
4389
+ try {
4390
+ if (targetName) {
4391
+ const cfg = configured.find(s => s.name === targetName);
4392
+ try { await session.mcpManager.disconnect(targetName); } catch { /* ignore */ }
4393
+ if (cfg) {
4394
+ try { await session.mcpManager.connect(cfg); }
4395
+ catch (err) { failures.push({ name: targetName, error: err?.message || String(err) }); }
4396
+ }
4397
+ } else {
4398
+ try { await session.mcpManager.disconnectAll(); } catch { /* ignore */ }
4399
+ for (const cfg of configured) {
4400
+ try { await session.mcpManager.connect(cfg); }
4401
+ catch (err) { failures.push({ name: cfg.name, error: err?.message || String(err) }); }
4402
+ }
4403
+ }
4404
+ } catch (err) {
4405
+ console.warn('[Yeaft] MCP reload failed:', err?.message || err);
4406
+ }
4407
+
4408
+ const swap = hotSwapMcpTools();
4409
+
4410
+ sendToServer({
4411
+ type: 'yeaft_mcp_reload_result',
4412
+ requestId: msg.requestId || null,
4413
+ servers: configured,
4414
+ runtime: mcpRuntimeSnapshot(),
4415
+ failures,
4416
+ swap,
4417
+ error: null,
4418
+ });
4419
+ broadcastMcpUpdated({ reason: 'reload', name: targetName, failures });
4420
+ }