@lightcone-ai/daemon 0.12.0 → 0.13.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/chat-bridge.js +614 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -14,9 +14,19 @@ function getArg(name) {
14
14
  return idx !== -1 && cliArgs[idx + 1] ? cliArgs[idx + 1] : '';
15
15
  }
16
16
 
17
+ function parseBooleanFlag(value, fallback = false) {
18
+ if (value == null) return fallback;
19
+ if (typeof value === 'boolean') return value;
20
+ const normalized = String(value).trim().toLowerCase();
21
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
22
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
23
+ return fallback;
24
+ }
25
+
17
26
  const SERVER_URL = process.env.SERVER_URL || getArg('--server-url') || 'http://localhost:9779';
18
27
  const MACHINE_API_KEY = process.env.MACHINE_API_KEY || getArg('--auth-token') || '';
19
28
  const AGENT_ID = process.env.AGENT_ID || getArg('--agent-id') || '';
29
+ const HOST_AGENT_HINT = process.env.IS_HOST_AGENT || getArg('--is-host-agent') || '';
20
30
  const WORKSPACE_ID = process.env.WORKSPACE_ID || getArg('--workspace-id') || ''; // injected per-workspace at spawn time
21
31
  const WORKSPACE_DIR = path.resolve(process.env.WORKSPACE_DIR || getArg('--workspace-dir') || process.cwd());
22
32
  const WORKSPACE_ROOT_DIR = path.dirname(WORKSPACE_DIR);
@@ -102,6 +112,18 @@ const DEFAULT_TOOL_CLASSIFICATION = {
102
112
  request_approval: 'mandatory',
103
113
  execute_approved_action: 'mandatory',
104
114
  promote_context: 'mandatory',
115
+ update_goal_field: 'mandatory',
116
+ supersede_goal_field: 'mandatory',
117
+ request_credential_auth: 'mandatory',
118
+ bind_workspace_scenario: 'mandatory',
119
+ create_workspace: 'mandatory',
120
+ rename_workspace: 'mandatory',
121
+ delete_workspace: 'mandatory',
122
+ revoke_credential: 'mandatory',
123
+ submit_feedback: 'mandatory',
124
+ escalate_to_human: 'mandatory',
125
+ navigate_to_workspace: 'mandatory',
126
+ navigate_to_settings: 'mandatory',
105
127
 
106
128
  search_messages: 'cacheable',
107
129
  list_server: 'cacheable',
@@ -111,6 +133,19 @@ const DEFAULT_TOOL_CLASSIFICATION = {
111
133
  skill_list: 'cacheable',
112
134
  skill_read: 'cacheable',
113
135
  skill_search: 'cacheable',
136
+ search_scenarios: 'cacheable',
137
+ get_scenario: 'cacheable',
138
+ list_scenarios: 'cacheable',
139
+ recommend_scenario: 'cacheable',
140
+ list_workspaces: 'cacheable',
141
+ get_workspace_summary: 'cacheable',
142
+ query_user_history: 'cacheable',
143
+ list_accounts: 'cacheable',
144
+ list_credentials: 'cacheable',
145
+ get_data_locations: 'cacheable',
146
+ explain_concept: 'cacheable',
147
+ get_release_notes: 'cacheable',
148
+ get_known_issues: 'cacheable',
114
149
  };
115
150
 
116
151
  const TOOL_CLASSIFICATION = Object.freeze({
@@ -132,6 +167,18 @@ const CACHE_INVALIDATION_TOOLS = new Set([
132
167
  'request_approval',
133
168
  'execute_approved_action',
134
169
  'promote_context',
170
+ 'update_goal_field',
171
+ 'supersede_goal_field',
172
+ 'request_credential_auth',
173
+ 'bind_workspace_scenario',
174
+ 'create_workspace',
175
+ 'rename_workspace',
176
+ 'delete_workspace',
177
+ 'revoke_credential',
178
+ 'submit_feedback',
179
+ 'escalate_to_human',
180
+ 'navigate_to_workspace',
181
+ 'navigate_to_settings',
135
182
  ]);
136
183
 
137
184
  const governanceContext = {
@@ -196,11 +243,37 @@ function inferToolForApi(method, apiPath, body) {
196
243
  if (method === 'PATCH' && cleanPath.startsWith('/skills/')) return 'skill_update';
197
244
  if (method === 'POST' && cleanPath === '/actions/request') return 'request_approval';
198
245
  if (method === 'POST' && /^\/actions\/[^/]+\/execute$/.test(cleanPath)) return 'execute_approved_action';
246
+ if (method === 'POST' && cleanPath === '/goal-fields/update') return 'update_goal_field';
247
+ if (method === 'POST' && cleanPath === '/goal-fields/supersede') return 'supersede_goal_field';
248
+ if (method === 'POST' && cleanPath === '/credential-auth/request') return 'request_credential_auth';
199
249
  if (method === 'POST' && cleanPath === '/orchestrate/decision') return 'write_governance_decision';
200
250
  if (method === 'POST' && cleanPath === '/orchestrate/correction') return 'write_governance_correction';
201
251
  if (method === 'GET' && cleanPath === '/orchestrate/context') return 'get_orchestrate_context';
202
252
  if (method === 'POST' && cleanPath === '/orchestrate/complete') return 'complete_orchestrate_trigger';
203
253
  if (method === 'POST' && cleanPath === '/context-proposals') return 'promote_context';
254
+
255
+ if (method === 'GET' && cleanPath === '/host/scenarios/search') return 'search_scenarios';
256
+ if (method === 'POST' && cleanPath === '/host/scenarios/recommend') return 'recommend_scenario';
257
+ if (method === 'GET' && /^\/host\/scenarios\/[^/]+$/.test(cleanPath)) return 'get_scenario';
258
+ if (method === 'GET' && cleanPath === '/host/scenarios') return 'list_scenarios';
259
+ if (method === 'POST' && cleanPath === '/host/workspaces/bind-scenario') return 'bind_workspace_scenario';
260
+ if (method === 'GET' && cleanPath === '/host/workspaces') return 'list_workspaces';
261
+ if (method === 'POST' && cleanPath === '/host/workspaces') return 'create_workspace';
262
+ if (method === 'PATCH' && /^\/host\/workspaces\/[^/]+$/.test(cleanPath)) return 'rename_workspace';
263
+ if (method === 'DELETE' && /^\/host\/workspaces\/[^/]+$/.test(cleanPath)) return 'delete_workspace';
264
+ if (method === 'GET' && /^\/host\/workspaces\/[^/]+\/summary$/.test(cleanPath)) return 'get_workspace_summary';
265
+ if (method === 'GET' && cleanPath === '/host/history') return 'query_user_history';
266
+ if (method === 'GET' && cleanPath === '/host/accounts') return 'list_accounts';
267
+ if (method === 'GET' && cleanPath === '/host/credentials') return 'list_credentials';
268
+ if (method === 'POST' && /^\/host\/credentials\/[^/]+\/revoke$/.test(cleanPath)) return 'revoke_credential';
269
+ if (method === 'GET' && cleanPath === '/host/data-locations') return 'get_data_locations';
270
+ if (method === 'GET' && cleanPath === '/host/docs/explain') return 'explain_concept';
271
+ if (method === 'GET' && cleanPath === '/host/docs/release-notes') return 'get_release_notes';
272
+ if (method === 'GET' && cleanPath === '/host/docs/known-issues') return 'get_known_issues';
273
+ if (method === 'POST' && cleanPath === '/host/feedback') return 'submit_feedback';
274
+ if (method === 'POST' && cleanPath === '/host/escalate') return 'escalate_to_human';
275
+ if (method === 'POST' && cleanPath === '/host/navigation/workspace') return 'navigate_to_workspace';
276
+ if (method === 'POST' && cleanPath === '/host/navigation/settings') return 'navigate_to_settings';
204
277
  return null;
205
278
  }
206
279
 
@@ -584,6 +657,29 @@ async function api(method, apiPath, body) {
584
657
  }
585
658
  }
586
659
 
660
+ async function resolveHostAgentFlag() {
661
+ const hinted = parseBooleanFlag(HOST_AGENT_HINT, false);
662
+ if (!AGENT_ID || !SERVER_URL || !MACHINE_API_KEY) return hinted;
663
+
664
+ try {
665
+ const res = await fetch(`${SERVER_URL}/internal/agent/${AGENT_ID}/profile`, {
666
+ method: 'GET',
667
+ headers: {
668
+ 'Content-Type': 'application/json',
669
+ 'Authorization': `Bearer ${MACHINE_API_KEY}`,
670
+ },
671
+ });
672
+ if (!res.ok) return hinted;
673
+ const data = await res.json();
674
+ if (typeof data?.isHostAgent === 'boolean') return data.isHostAgent;
675
+ return hinted;
676
+ } catch {
677
+ return hinted;
678
+ }
679
+ }
680
+
681
+ const IS_HOST_AGENT = await resolveHostAgentFlag();
682
+
587
683
  const server = new McpServer({ name: 'chat', version: '0.1.0' });
588
684
 
589
685
  // ── check_messages ────────────────────────────────────────────────────────────
@@ -921,6 +1017,106 @@ server.tool('upload_image',
921
1017
  }
922
1018
  );
923
1019
 
1020
+ // ── update_goal_field ─────────────────────────────────────────────────────────
1021
+ server.tool('update_goal_field',
1022
+ 'Incrementally update one goal field in workspace context. Use this during IM dialogue to capture user intent continuously, instead of waiting for a full goal form.',
1023
+ {
1024
+ field_path: z.string().describe('Dot path under goal context, e.g. "brand_positioning.voice" or "audience.primary".'),
1025
+ value: z.any().describe('Field value (string/number/object/array).'),
1026
+ confidence: z.number().min(0).max(1).optional().describe('Confidence in [0,1].'),
1027
+ status: z.enum(['candidate', 'user_confirmed']).optional().describe('candidate=tentative, user_confirmed=explicitly confirmed by user.'),
1028
+ source_turn_id: z.string().optional().describe('Optional conversation turn id for traceability.'),
1029
+ },
1030
+ async ({ field_path, value, confidence, status, source_turn_id }) => {
1031
+ if (!currentWorkspaceId) {
1032
+ return { isError: true, content: [{ type: 'text', text: 'No workspace context. Call check_messages first or specify workspace context in the current conversation.' }] };
1033
+ }
1034
+ const data = await api('POST', '/goal-fields/update', {
1035
+ workspace_id: currentWorkspaceId,
1036
+ field_path,
1037
+ value,
1038
+ confidence,
1039
+ status,
1040
+ source_turn_id,
1041
+ });
1042
+ return {
1043
+ content: [{
1044
+ type: 'text',
1045
+ text:
1046
+ `Goal field updated.\n` +
1047
+ `workspace=${data.workspaceId ?? currentWorkspaceId}\n` +
1048
+ `field_path=${data.fieldPath ?? field_path}\n` +
1049
+ `status=${data.status ?? status ?? 'candidate'}\n` +
1050
+ `context_item_id=${data.contextItemId ?? 'unknown'} version=${data.contextItemVersion ?? 'unknown'}`,
1051
+ }],
1052
+ };
1053
+ }
1054
+ );
1055
+
1056
+ // ── supersede_goal_field ──────────────────────────────────────────────────────
1057
+ server.tool('supersede_goal_field',
1058
+ 'Mark an existing goal field value as superseded when the user explicitly changes their mind.',
1059
+ {
1060
+ field_path: z.string().describe('Dot path to supersede, e.g. "audience.primary".'),
1061
+ reason: z.string().optional().describe('Why this field is superseded (optional).'),
1062
+ source_turn_id: z.string().optional().describe('Optional conversation turn id for traceability.'),
1063
+ },
1064
+ async ({ field_path, reason, source_turn_id }) => {
1065
+ if (!currentWorkspaceId) {
1066
+ return { isError: true, content: [{ type: 'text', text: 'No workspace context. Call check_messages first or specify workspace context in the current conversation.' }] };
1067
+ }
1068
+ const data = await api('POST', '/goal-fields/supersede', {
1069
+ workspace_id: currentWorkspaceId,
1070
+ field_path,
1071
+ reason,
1072
+ source_turn_id,
1073
+ });
1074
+ return {
1075
+ content: [{
1076
+ type: 'text',
1077
+ text:
1078
+ `Goal field superseded.\n` +
1079
+ `workspace=${data.workspaceId ?? currentWorkspaceId}\n` +
1080
+ `field_path=${data.fieldPath ?? field_path}\n` +
1081
+ `context_item_id=${data.contextItemId ?? 'unknown'} version=${data.contextItemVersion ?? 'unknown'}`,
1082
+ }],
1083
+ };
1084
+ }
1085
+ );
1086
+
1087
+ // ── request_credential_auth ───────────────────────────────────────────────────
1088
+ server.tool('request_credential_auth',
1089
+ 'Request just-in-time credential authorization for a platform. Returns a one-time OAuth URL that should be sent to the user as a clickable IM message.',
1090
+ {
1091
+ platform: z.string().describe('Target platform, e.g. "x".'),
1092
+ reason: z.string().describe('Human-readable reason why this credential is needed right now.'),
1093
+ source_turn_id: z.string().optional().describe('Optional conversation turn id for traceability.'),
1094
+ },
1095
+ async ({ platform, reason, source_turn_id }) => {
1096
+ if (!currentWorkspaceId) {
1097
+ return { isError: true, content: [{ type: 'text', text: 'No workspace context. Call check_messages first or specify workspace context in the current conversation.' }] };
1098
+ }
1099
+ const data = await api('POST', '/credential-auth/request', {
1100
+ workspace_id: currentWorkspaceId,
1101
+ platform,
1102
+ reason,
1103
+ source_turn_id,
1104
+ });
1105
+ return {
1106
+ content: [{
1107
+ type: 'text',
1108
+ text:
1109
+ `Credential auth requested.\n` +
1110
+ `platform=${data.platform ?? platform}\n` +
1111
+ `expires_at=${data.expiresAt ?? 'unknown'}\n` +
1112
+ `auth_url=${data.authUrl}\n` +
1113
+ `im_message_id=${data.messageId ?? 'unknown'}\n\n` +
1114
+ `A structured credential auth message has been posted to IM. Ask the user to click the authorization button there.`,
1115
+ }],
1116
+ };
1117
+ }
1118
+ );
1119
+
924
1120
  // ── request_approval ──────────────────────────────────────────────────────────
925
1121
  server.tool('request_approval',
926
1122
  'Request human approval before executing a sensitive platform action (posting, sending, publishing). Returns an action_id. After the human approves, call execute_approved_action with that ID.',
@@ -1122,7 +1318,424 @@ server.tool('complete_orchestrate_trigger',
1122
1318
  }
1123
1319
  );
1124
1320
 
1321
+ function hostJsonResult(data) {
1322
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
1323
+ }
1324
+
1325
+ function hostErrorResult(err) {
1326
+ return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
1327
+ }
1328
+
1329
+ function buildQuery(params = {}) {
1330
+ const query = new URLSearchParams();
1331
+ for (const [key, value] of Object.entries(params)) {
1332
+ if (value == null) continue;
1333
+ if (typeof value === 'string' && !value.trim()) continue;
1334
+ query.set(key, String(value));
1335
+ }
1336
+ const text = query.toString();
1337
+ return text ? `?${text}` : '';
1338
+ }
1339
+
1340
+ if (IS_HOST_AGENT) {
1341
+ server.tool('search_scenarios',
1342
+ 'Search scenario candidates by user intent for host routing decisions.',
1343
+ {
1344
+ intent_text: z.string().describe('User intent text to match against scenario catalog.'),
1345
+ category: z.string().optional().describe('Optional category filter.'),
1346
+ visibility: z.string().optional().describe('Optional visibility filter (public/system/user_private/org_shared).'),
1347
+ status: z.string().optional().describe('Optional status filter (draft/published/active/deprecated/archived).'),
1348
+ limit: z.number().int().min(1).max(20).optional().describe('Maximum number of returned scenarios.'),
1349
+ },
1350
+ async ({ intent_text, category, visibility, status, limit }) => {
1351
+ try {
1352
+ const query = buildQuery({ intent_text, category, visibility, status, limit });
1353
+ const data = await api('GET', `/host/scenarios/search${query}`);
1354
+ return hostJsonResult(data);
1355
+ } catch (err) {
1356
+ return hostErrorResult(err);
1357
+ }
1358
+ }
1359
+ );
1360
+
1361
+ server.tool('get_scenario',
1362
+ 'Get full scenario details (manifest + roles) for host explanation.',
1363
+ {
1364
+ scenario_id: z.string().describe('Scenario ID to inspect.'),
1365
+ },
1366
+ async ({ scenario_id }) => {
1367
+ try {
1368
+ const data = await api('GET', `/host/scenarios/${encodeURIComponent(scenario_id)}`);
1369
+ return hostJsonResult(data);
1370
+ } catch (err) {
1371
+ return hostErrorResult(err);
1372
+ }
1373
+ }
1374
+ );
1375
+
1376
+ server.tool('list_scenarios',
1377
+ 'List scenarios visible to host agent (public/system + owner-accessible).',
1378
+ {
1379
+ visibility: z.string().optional().describe('Optional visibility filter.'),
1380
+ category: z.string().optional().describe('Optional category filter.'),
1381
+ status: z.string().optional().describe('Optional status filter.'),
1382
+ include_archived: z.boolean().optional().describe('Include archived scenarios when true.'),
1383
+ limit: z.number().int().min(1).max(200).optional().describe('Maximum rows to return.'),
1384
+ },
1385
+ async ({ visibility, category, status, include_archived, limit }) => {
1386
+ try {
1387
+ const query = buildQuery({
1388
+ visibility,
1389
+ category,
1390
+ status,
1391
+ includeArchived: include_archived,
1392
+ limit,
1393
+ });
1394
+ const data = await api('GET', `/host/scenarios${query}`);
1395
+ return hostJsonResult(data);
1396
+ } catch (err) {
1397
+ return hostErrorResult(err);
1398
+ }
1399
+ }
1400
+ );
1401
+
1402
+ server.tool('bind_workspace_scenario',
1403
+ 'Bind a scenario to an existing workspace and ensure a primary agent.',
1404
+ {
1405
+ workspace_id: z.string().describe('Workspace ID to bind.'),
1406
+ scenario_id: z.string().describe('Scenario ID to bind.'),
1407
+ role_id: z.string().optional().describe('Optional preferred role id/name for primary agent binding.'),
1408
+ repair_role_binding: z.boolean().optional().describe('When true, ignore existing workspace role binding once and re-resolve primary agent for cleanup.'),
1409
+ },
1410
+ async ({ workspace_id, scenario_id, role_id, repair_role_binding }) => {
1411
+ try {
1412
+ const data = await api('POST', '/host/workspaces/bind-scenario', {
1413
+ workspace_id,
1414
+ scenario_id,
1415
+ role_id,
1416
+ repair_role_binding,
1417
+ });
1418
+ return hostJsonResult(data);
1419
+ } catch (err) {
1420
+ return hostErrorResult(err);
1421
+ }
1422
+ }
1423
+ );
1424
+
1425
+ server.tool('recommend_scenario',
1426
+ 'Recommend best-fit scenarios for a given user intent.',
1427
+ {
1428
+ user_intent: z.string().describe('Natural-language user intent.'),
1429
+ },
1430
+ async ({ user_intent }) => {
1431
+ try {
1432
+ const data = await api('POST', '/host/scenarios/recommend', { user_intent });
1433
+ return hostJsonResult(data);
1434
+ } catch (err) {
1435
+ return hostErrorResult(err);
1436
+ }
1437
+ }
1438
+ );
1439
+
1440
+ server.tool('list_workspaces',
1441
+ 'List all owner workspaces with scenario/mode/last activity for host overview.',
1442
+ {
1443
+ owner_id: z.string().optional().describe('Optional owner id (must match current host owner).'),
1444
+ limit: z.number().int().min(1).max(300).optional().describe('Maximum rows to return.'),
1445
+ },
1446
+ async ({ owner_id, limit }) => {
1447
+ try {
1448
+ const query = buildQuery({ owner_id, limit });
1449
+ const data = await api('GET', `/host/workspaces${query}`);
1450
+ return hostJsonResult(data);
1451
+ } catch (err) {
1452
+ return hostErrorResult(err);
1453
+ }
1454
+ }
1455
+ );
1456
+
1457
+ server.tool('create_workspace',
1458
+ 'Create a workspace and optionally bind scenario + primary role.',
1459
+ {
1460
+ name: z.string().optional().describe('Optional workspace name.'),
1461
+ description: z.string().optional().describe('Optional workspace description.'),
1462
+ scenario_id: z.string().optional().describe('Optional scenario id to bind immediately.'),
1463
+ role_id: z.string().optional().describe('Optional preferred role id/name when scenario is bound.'),
1464
+ },
1465
+ async ({ name, description, scenario_id, role_id }) => {
1466
+ try {
1467
+ const data = await api('POST', '/host/workspaces', {
1468
+ name,
1469
+ description,
1470
+ scenario_id,
1471
+ role_id,
1472
+ });
1473
+ return hostJsonResult(data);
1474
+ } catch (err) {
1475
+ return hostErrorResult(err);
1476
+ }
1477
+ }
1478
+ );
1479
+
1480
+ server.tool('rename_workspace',
1481
+ 'Rename an existing workspace.',
1482
+ {
1483
+ workspace_id: z.string().describe('Workspace ID to rename.'),
1484
+ new_name: z.string().describe('New workspace name.'),
1485
+ },
1486
+ async ({ workspace_id, new_name }) => {
1487
+ try {
1488
+ const data = await api('PATCH', `/host/workspaces/${encodeURIComponent(workspace_id)}`, {
1489
+ new_name,
1490
+ });
1491
+ return hostJsonResult(data);
1492
+ } catch (err) {
1493
+ return hostErrorResult(err);
1494
+ }
1495
+ }
1496
+ );
1497
+
1498
+ server.tool('delete_workspace',
1499
+ 'Delete (soft-delete) a workspace owned by the current user.',
1500
+ {
1501
+ workspace_id: z.string().describe('Workspace ID to delete.'),
1502
+ confirm: z.boolean().describe('Must be true to confirm deletion.'),
1503
+ },
1504
+ async ({ workspace_id, confirm }) => {
1505
+ try {
1506
+ const query = buildQuery({ confirm });
1507
+ const data = await api('DELETE', `/host/workspaces/${encodeURIComponent(workspace_id)}${query}`);
1508
+ return hostJsonResult(data);
1509
+ } catch (err) {
1510
+ return hostErrorResult(err);
1511
+ }
1512
+ }
1513
+ );
1514
+
1515
+ server.tool('get_workspace_summary',
1516
+ 'Get summary of workspace scenario, active agents, recent messages and publish stats.',
1517
+ {
1518
+ workspace_id: z.string().describe('Workspace ID to summarize.'),
1519
+ },
1520
+ async ({ workspace_id }) => {
1521
+ try {
1522
+ const data = await api('GET', `/host/workspaces/${encodeURIComponent(workspace_id)}/summary`);
1523
+ return hostJsonResult(data);
1524
+ } catch (err) {
1525
+ return hostErrorResult(err);
1526
+ }
1527
+ }
1528
+ );
1529
+
1530
+ server.tool('query_user_history',
1531
+ 'Search user history across owner workspaces for prior activities/messages.',
1532
+ {
1533
+ topic: z.string().describe('Topic keyword to search across workspaces.'),
1534
+ limit: z.number().int().min(1).max(50).optional().describe('Maximum number of results.'),
1535
+ },
1536
+ async ({ topic, limit }) => {
1537
+ try {
1538
+ const query = buildQuery({ topic, limit });
1539
+ const data = await api('GET', `/host/history${query}`);
1540
+ return hostJsonResult(data);
1541
+ } catch (err) {
1542
+ return hostErrorResult(err);
1543
+ }
1544
+ }
1545
+ );
1546
+
1547
+ server.tool('list_accounts',
1548
+ 'List platform accounts owned by the current user.',
1549
+ {
1550
+ owner_id: z.string().optional().describe('Optional owner id (must match current owner).'),
1551
+ workspace_id: z.string().optional().describe('Optional workspace filter; pass "null" for unbound accounts.'),
1552
+ platform: z.string().optional().describe('Optional platform filter.'),
1553
+ status: z.string().optional().describe('Optional status filter.'),
1554
+ include_archived: z.boolean().optional().describe('Include archived accounts.'),
1555
+ },
1556
+ async ({ owner_id, workspace_id, platform, status, include_archived }) => {
1557
+ try {
1558
+ const query = buildQuery({
1559
+ owner_id,
1560
+ workspace_id,
1561
+ platform,
1562
+ status,
1563
+ include_archived,
1564
+ });
1565
+ const data = await api('GET', `/host/accounts${query}`);
1566
+ return hostJsonResult(data);
1567
+ } catch (err) {
1568
+ return hostErrorResult(err);
1569
+ }
1570
+ }
1571
+ );
1572
+
1573
+ server.tool('list_credentials',
1574
+ 'List credentials as masked metadata (no plaintext).',
1575
+ {
1576
+ owner_id: z.string().optional().describe('Optional owner id (must match current owner).'),
1577
+ },
1578
+ async ({ owner_id }) => {
1579
+ try {
1580
+ const query = buildQuery({ owner_id });
1581
+ const data = await api('GET', `/host/credentials${query}`);
1582
+ return hostJsonResult(data);
1583
+ } catch (err) {
1584
+ return hostErrorResult(err);
1585
+ }
1586
+ }
1587
+ );
1588
+
1589
+ server.tool('revoke_credential',
1590
+ 'Revoke a credential with explicit confirmation.',
1591
+ {
1592
+ credential_id: z.string().describe('Credential ID to revoke.'),
1593
+ confirm: z.boolean().describe('Must be true to confirm revocation.'),
1594
+ },
1595
+ async ({ credential_id, confirm }) => {
1596
+ try {
1597
+ const data = await api('POST', `/host/credentials/${encodeURIComponent(credential_id)}/revoke`, {
1598
+ confirm,
1599
+ });
1600
+ return hostJsonResult(data);
1601
+ } catch (err) {
1602
+ return hostErrorResult(err);
1603
+ }
1604
+ }
1605
+ );
1606
+
1607
+ server.tool('get_data_locations',
1608
+ 'Describe where user/platform data is stored.',
1609
+ {},
1610
+ async () => {
1611
+ try {
1612
+ const data = await api('GET', '/host/data-locations');
1613
+ return hostJsonResult(data);
1614
+ } catch (err) {
1615
+ return hostErrorResult(err);
1616
+ }
1617
+ }
1618
+ );
1619
+
1620
+ server.tool('explain_concept',
1621
+ 'Explain a product concept from docs corpus.',
1622
+ {
1623
+ topic: z.string().describe('Concept/topic text to explain.'),
1624
+ },
1625
+ async ({ topic }) => {
1626
+ try {
1627
+ const query = buildQuery({ topic });
1628
+ const data = await api('GET', `/host/docs/explain${query}`);
1629
+ return hostJsonResult(data);
1630
+ } catch (err) {
1631
+ return hostErrorResult(err);
1632
+ }
1633
+ }
1634
+ );
1635
+
1636
+ server.tool('get_release_notes',
1637
+ 'Get release notes/events since a specified timestamp.',
1638
+ {
1639
+ since: z.string().optional().describe('Optional ISO timestamp (default: last 30 days).'),
1640
+ },
1641
+ async ({ since }) => {
1642
+ try {
1643
+ const query = buildQuery({ since });
1644
+ const data = await api('GET', `/host/docs/release-notes${query}`);
1645
+ return hostJsonResult(data);
1646
+ } catch (err) {
1647
+ return hostErrorResult(err);
1648
+ }
1649
+ }
1650
+ );
1651
+
1652
+ server.tool('get_known_issues',
1653
+ 'Get current known issues and limitations.',
1654
+ {},
1655
+ async () => {
1656
+ try {
1657
+ const data = await api('GET', '/host/docs/known-issues');
1658
+ return hostJsonResult(data);
1659
+ } catch (err) {
1660
+ return hostErrorResult(err);
1661
+ }
1662
+ }
1663
+ );
1664
+
1665
+ server.tool('submit_feedback',
1666
+ 'Submit user feedback into internal backlog workflow.',
1667
+ {
1668
+ category: z.enum(['bug', 'feature', 'ux', 'performance', 'security', 'other']).describe('Feedback category.'),
1669
+ content: z.string().describe('Feedback content.'),
1670
+ workspace_id: z.string().optional().describe('Optional workspace context for this feedback.'),
1671
+ },
1672
+ async ({ category, content, workspace_id }) => {
1673
+ try {
1674
+ const data = await api('POST', '/host/feedback', {
1675
+ category,
1676
+ content,
1677
+ workspace_id,
1678
+ });
1679
+ return hostJsonResult(data);
1680
+ } catch (err) {
1681
+ return hostErrorResult(err);
1682
+ }
1683
+ }
1684
+ );
1685
+
1686
+ server.tool('escalate_to_human',
1687
+ 'Escalate severe issue to human operators.',
1688
+ {
1689
+ issue: z.string().describe('Issue details to escalate.'),
1690
+ priority: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Escalation priority.'),
1691
+ workspace_id: z.string().optional().describe('Optional workspace context for escalation.'),
1692
+ },
1693
+ async ({ issue, priority, workspace_id }) => {
1694
+ try {
1695
+ const data = await api('POST', '/host/escalate', {
1696
+ issue,
1697
+ priority,
1698
+ workspace_id,
1699
+ });
1700
+ return hostJsonResult(data);
1701
+ } catch (err) {
1702
+ return hostErrorResult(err);
1703
+ }
1704
+ }
1705
+ );
1706
+
1707
+ server.tool('navigate_to_workspace',
1708
+ 'Emit navigation intent for UI to open a target workspace.',
1709
+ {
1710
+ workspace_id: z.string().describe('Target workspace ID.'),
1711
+ },
1712
+ async ({ workspace_id }) => {
1713
+ try {
1714
+ const data = await api('POST', '/host/navigation/workspace', { workspace_id });
1715
+ return hostJsonResult(data);
1716
+ } catch (err) {
1717
+ return hostErrorResult(err);
1718
+ }
1719
+ }
1720
+ );
1721
+
1722
+ server.tool('navigate_to_settings',
1723
+ 'Emit navigation intent for UI to open a settings section.',
1724
+ {
1725
+ section: z.string().describe('Settings section key, e.g. "credentials", "profile", "workspace".'),
1726
+ },
1727
+ async ({ section }) => {
1728
+ try {
1729
+ const data = await api('POST', '/host/navigation/settings', { section });
1730
+ return hostJsonResult(data);
1731
+ } catch (err) {
1732
+ return hostErrorResult(err);
1733
+ }
1734
+ }
1735
+ );
1736
+ }
1737
+
1125
1738
  // ── start ─────────────────────────────────────────────────────────────────────
1126
1739
  const transport = new StdioServerTransport();
1127
1740
  await server.connect(transport);
1128
- console.error(`[chat-bridge] MCP Server started (agentId=${AGENT_ID})`);
1741
+ console.error(`[chat-bridge] MCP Server started (agentId=${AGENT_ID}, host=${IS_HOST_AGENT ? 'true' : 'false'})`);