@hirey/hi-mcp-server 0.1.6 → 0.1.8

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.
@@ -0,0 +1,13 @@
1
+ export type OpenClawSessionKeyValidation = {
2
+ ok: true;
3
+ normalized: string;
4
+ agentId: string;
5
+ rest: string;
6
+ } | {
7
+ ok: false;
8
+ reason: 'status_display_value' | 'sessions_display_value' | 'non_canonical';
9
+ input: string;
10
+ };
11
+ export declare function looksLikeOpenClawSessionKey(input: unknown): boolean;
12
+ export declare function validateOpenClawSessionKey(input: unknown): OpenClawSessionKeyValidation | null;
13
+ //# sourceMappingURL=openclaw-session-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openclaw-session-key.d.ts","sourceRoot":"","sources":["../src/openclaw-session-key.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,4BAA4B,GACpC;IACE,EAAE,EAAE,IAAI,CAAC;IACT,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,sBAAsB,GAAG,wBAAwB,GAAG,eAAe,CAAC;IAC5E,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AA0CN,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQnE;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,4BAA4B,GAAG,IAAI,CAiC9F"}
@@ -0,0 +1,76 @@
1
+ function parseCanonicalAgentSessionKey(input) {
2
+ const raw = input.trim().toLowerCase();
3
+ if (!raw) {
4
+ return null;
5
+ }
6
+ const parts = raw.split(':').filter(Boolean);
7
+ if (parts.length < 3 || parts[0] !== 'agent') {
8
+ return null;
9
+ }
10
+ const agentId = parts[1]?.trim();
11
+ const rest = parts.slice(2).join(':');
12
+ if (!agentId || !rest) {
13
+ return null;
14
+ }
15
+ return {
16
+ normalized: `agent:${agentId}:${rest}`,
17
+ agentId,
18
+ rest,
19
+ };
20
+ }
21
+ function looksLikeStatusDisplayValue(input) {
22
+ // `openclaw status` textual tables shorten long keys with a trailing Unicode ellipsis.
23
+ return input.includes('…');
24
+ }
25
+ function looksLikeSessionsDisplayValue(input) {
26
+ // `openclaw sessions` currently renders long keys as:
27
+ // <16-char head>...<6-char tail>
28
+ // Keep the detector tight so we only reject the known display artifact.
29
+ const trimmed = input.trim();
30
+ return trimmed.toLowerCase().startsWith('agent:')
31
+ && trimmed.length === 25
32
+ && trimmed.slice(16, 19) === '...';
33
+ }
34
+ export function looksLikeOpenClawSessionKey(input) {
35
+ const trimmed = String(input ?? '').trim();
36
+ if (!trimmed) {
37
+ return false;
38
+ }
39
+ return trimmed.toLowerCase().startsWith('agent:')
40
+ || looksLikeStatusDisplayValue(trimmed)
41
+ || looksLikeSessionsDisplayValue(trimmed);
42
+ }
43
+ export function validateOpenClawSessionKey(input) {
44
+ const trimmed = String(input ?? '').trim();
45
+ if (!trimmed) {
46
+ return null;
47
+ }
48
+ if (looksLikeStatusDisplayValue(trimmed)) {
49
+ return {
50
+ ok: false,
51
+ reason: 'status_display_value',
52
+ input: trimmed,
53
+ };
54
+ }
55
+ if (looksLikeSessionsDisplayValue(trimmed)) {
56
+ return {
57
+ ok: false,
58
+ reason: 'sessions_display_value',
59
+ input: trimmed,
60
+ };
61
+ }
62
+ const parsed = parseCanonicalAgentSessionKey(trimmed);
63
+ if (!parsed) {
64
+ return {
65
+ ok: false,
66
+ reason: 'non_canonical',
67
+ input: trimmed,
68
+ };
69
+ }
70
+ return {
71
+ ok: true,
72
+ normalized: parsed.normalized,
73
+ agentId: parsed.agentId,
74
+ rest: parsed.rest,
75
+ };
76
+ }
@@ -0,0 +1,9 @@
1
+ export declare function resolveCanonicalOpenClawReceiverBinaryPath(homeDir?: string): string;
2
+ export declare function buildInstallReceiverCommandArgv(args: {
3
+ explicitArgv: string[];
4
+ receiverCommand: string;
5
+ hostKind: 'openclaw' | 'generic';
6
+ enableLocalReceiver: boolean;
7
+ homeDir?: string;
8
+ }): string[];
9
+ //# sourceMappingURL=receiver-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receiver-command.d.ts","sourceRoot":"","sources":["../src/receiver-command.ts"],"names":[],"mappings":"AAGA,wBAAgB,0CAA0C,CAAC,OAAO,GAAE,MAAqB,GAAG,MAAM,CAEjG;AAED,wBAAgB,+BAA+B,CAAC,IAAI,EAAE;IACpD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,UAAU,GAAG,SAAS,CAAC;IACjC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,EAAE,CAQX"}
@@ -0,0 +1,16 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export function resolveCanonicalOpenClawReceiverBinaryPath(homeDir = os.homedir()) {
4
+ return path.join(homeDir, '.openclaw', 'vendor', 'hi', 'node_modules', '.bin', 'hi-agent-receiver');
5
+ }
6
+ export function buildInstallReceiverCommandArgv(args) {
7
+ if (args.explicitArgv.length > 0)
8
+ return args.explicitArgv;
9
+ if (args.receiverCommand)
10
+ return [args.receiverCommand];
11
+ // OpenClaw's public install path is user-local vendor install, not global PATH.
12
+ if (args.hostKind === 'openclaw' && args.enableLocalReceiver) {
13
+ return [resolveCanonicalOpenClawReceiverBinaryPath(args.homeDir)];
14
+ }
15
+ return ['hi-agent-receiver'];
16
+ }
package/dist/server.js CHANGED
@@ -12,6 +12,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
12
12
  import { AGENT_GATEWAY_EVENT_TOPICS, normalizeAgentEndpointList, normalizeAgentInstallationDeliveryDeclaration, normalizeAgentSubscriptionList, normalizeText, } from '@hirey/hi-agent-contracts';
13
13
  import { createHiAgentClients, exchangeHiAgentClientCredentialsToken, HiAgentGatewayClient, HiAgentPlatformClient, } from '@hirey/hi-agent-sdk';
14
14
  import { readState, resolveCanonicalOpenClawStateDir, resolveDefaultStateDir, resolveLegacyStateFiles, resolveStateFile, updateState, normalizeStateProfile, } from './state.js';
15
+ import { looksLikeOpenClawSessionKey, validateOpenClawSessionKey, } from './openclaw-session-key.js';
16
+ import { buildInstallReceiverCommandArgv, } from './receiver-command.js';
15
17
  const CAPABILITY_CACHE_TTL_MS = 30_000;
16
18
  const resolvedProfile = normalizeStateProfile(process.env.HI_MCP_PROFILE);
17
19
  const config = {
@@ -107,7 +109,7 @@ function controlTools() {
107
109
  host_adapter_url: { type: 'string', description: 'enable_local_receiver=true 时可选:宿主本地接收入口。OpenClaw 默认 full endpoint 为 http://127.0.0.1:18789/hooks/agent;注意 OpenClaw 配置里的 hooks.path 应保持 /hooks,而不是 /hooks/agent。' },
108
110
  host_adapter_bearer_token: { type: 'string', description: 'host_adapter_kind=openclaw_hooks 时必填:本地 hooks bearer token。' },
109
111
  openresponses_model: { type: 'string', description: 'host_adapter_kind=openresponses 时必填:receiver 发给本地入口使用的 model。' },
110
- host_session_key: { type: 'string', description: '可选:把当前 OpenClaw 可见会话显式设为 default continuation route 的 session_key' },
112
+ host_session_key: { type: 'string', description: '可选:把当前 OpenClaw 当前会话显式设为 default continuation route 的 canonical full session_key。只能使用结构化宿主来源返回的完整 key;不要从 openclaw status / openclaw sessions / TUI footer 这类展示文本里抄值。' },
111
113
  route_missing_policy: { type: 'string', description: "可选:'use_explicit_default_route'|'fail_closed'。默认在提供 host_session_key/default_reply_* 时使用 use_explicit_default_route,否则为 null。" },
112
114
  default_reply_channel: { type: 'string', description: '可选:default continuation route 的 channel。' },
113
115
  default_reply_to: { type: 'string', description: '可选:default continuation route 的宿主 target。' },
@@ -535,30 +537,91 @@ function buildInstallationDeliveryDeclaration(args) {
535
537
  function hasActiveInstallationCapability(declaration, kind) {
536
538
  return !!declaration?.capabilities?.some((entry) => entry.kind === kind && entry.status !== 'disabled');
537
539
  }
538
- function withInstallationIdOnReplyRoute(input, installationId) {
540
+ function buildInvalidOpenClawSessionKeyError(reason, input) {
541
+ const error = new Error('invalid_openclaw_host_session_key');
542
+ const hint = reason === 'status_display_value'
543
+ ? 'Use a structured host source for the full session key; do not copy the truncated key shown by openclaw status.'
544
+ : reason === 'sessions_display_value'
545
+ ? 'Use a structured host source for the full session key; do not copy the truncated key shown by openclaw sessions.'
546
+ : 'Expected an OpenClaw canonical full session key shaped like agent:<agent_id>:<rest>.';
547
+ error.detail = {
548
+ reason,
549
+ input,
550
+ hint,
551
+ };
552
+ return error;
553
+ }
554
+ function normalizeReplyRouteSessionKey(input, options = {}) {
555
+ const sessionKey = normalizeText(input) || null;
556
+ if (!sessionKey) {
557
+ return null;
558
+ }
559
+ const shouldValidateAsOpenClaw = options.requireOpenClawSessionKey || looksLikeOpenClawSessionKey(sessionKey);
560
+ if (!shouldValidateAsOpenClaw) {
561
+ return sessionKey;
562
+ }
563
+ const validation = validateOpenClawSessionKey(sessionKey);
564
+ if (!validation) {
565
+ return null;
566
+ }
567
+ if (!validation.ok) {
568
+ throw buildInvalidOpenClawSessionKeyError(validation.reason, validation.input);
569
+ }
570
+ return validation.normalized;
571
+ }
572
+ function normalizeReplyRouteWithInstallationId(input, installationId, options = {}) {
539
573
  if (!isPlainObject(input))
540
574
  return null;
541
575
  const normalizedInstallationId = normalizeText(installationId) || null;
542
576
  const existingInstallationId = normalizeText(input.installation_id) || null;
543
- if (!normalizedInstallationId || existingInstallationId)
577
+ const hasSessionKey = Object.prototype.hasOwnProperty.call(input, 'session_key')
578
+ || Object.prototype.hasOwnProperty.call(input, 'sessionKey');
579
+ const normalizedSessionKey = hasSessionKey
580
+ ? normalizeReplyRouteSessionKey(Object.prototype.hasOwnProperty.call(input, 'session_key')
581
+ ? input.session_key
582
+ : input.sessionKey, options)
583
+ : undefined;
584
+ if (!normalizedInstallationId && !hasSessionKey)
544
585
  return input;
545
- return {
586
+ const output = {
546
587
  ...input,
547
- installation_id: normalizedInstallationId,
548
588
  };
589
+ if (normalizedInstallationId && !existingInstallationId) {
590
+ output.installation_id = normalizedInstallationId;
591
+ }
592
+ if (hasSessionKey) {
593
+ if (Object.prototype.hasOwnProperty.call(output, 'session_key')) {
594
+ output.session_key = normalizedSessionKey;
595
+ }
596
+ else {
597
+ output.sessionKey = normalizedSessionKey;
598
+ }
599
+ }
600
+ return output;
549
601
  }
550
- function normalizeDeliveryCapabilitiesWithInstallationId(input, installationId) {
551
- if (!isPlainObject(input)) {
552
- return normalizeAgentInstallationDeliveryDeclaration(input);
602
+ function applyNormalizedDefaultReplyRoute(input, installationId, options = {}) {
603
+ if (!Object.prototype.hasOwnProperty.call(input, 'default_reply_route')) {
604
+ return input;
553
605
  }
554
- const hydrated = {
606
+ return {
555
607
  ...input,
556
- default_reply_route: withInstallationIdOnReplyRoute(input.default_reply_route, installationId),
608
+ default_reply_route: normalizeReplyRouteWithInstallationId(input.default_reply_route, installationId, options),
557
609
  };
558
- return normalizeAgentInstallationDeliveryDeclaration(hydrated);
559
610
  }
560
- function buildDefaultReplyRoute(args, installationId) {
561
- const sessionKey = normalizeText(args.host_session_key) || null;
611
+ function normalizeDeliveryCapabilitiesWithInstallationId(input, installationId) {
612
+ if (isPlainObject(input)) {
613
+ return applyNormalizedDefaultReplyRoute(input, installationId);
614
+ }
615
+ const normalized = normalizeAgentInstallationDeliveryDeclaration(input);
616
+ if (!isPlainObject(normalized)) {
617
+ return normalized;
618
+ }
619
+ return applyNormalizedDefaultReplyRoute(normalized, installationId);
620
+ }
621
+ function buildDefaultReplyRoute(args, options = {}) {
622
+ const sessionKey = normalizeReplyRouteSessionKey(args.host_session_key, {
623
+ requireOpenClawSessionKey: options.requireOpenClawSessionKey,
624
+ });
562
625
  const deliveryContext = {
563
626
  channel: normalizeText(args.default_reply_channel) || null,
564
627
  to: normalizeText(args.default_reply_to) || null,
@@ -572,7 +635,7 @@ function buildDefaultReplyRoute(args, installationId) {
572
635
  if (!sessionKey && !hasDeliveryContext)
573
636
  return null;
574
637
  return {
575
- installation_id: normalizeText(installationId) || null,
638
+ installation_id: normalizeText(options.installationId) || null,
576
639
  session_key: sessionKey,
577
640
  delivery_context: hasDeliveryContext ? deliveryContext : null,
578
641
  };
@@ -693,9 +756,11 @@ function buildDoctorSummary(args) {
693
756
  const defaultReplyRoute = isPlainObject(deliveryDeclaration?.default_reply_route)
694
757
  ? deliveryDeclaration.default_reply_route
695
758
  : null;
696
- const continuityState = defaultReplyRoute
697
- ? 'explicit_default_route_ready'
698
- : (routeMissingPolicy === 'fail_closed' ? 'origin_capture_only' : 'continuity_not_ready');
759
+ const continuityState = defaultReplyRoute && !args.defaultReplyRouteValidation.ok
760
+ ? 'explicit_default_route_invalid'
761
+ : defaultReplyRoute
762
+ ? 'explicit_default_route_ready'
763
+ : (routeMissingPolicy === 'fail_closed' ? 'origin_capture_only' : 'continuity_not_ready');
699
764
  return {
700
765
  ok: args.blockers.length === 0,
701
766
  profile: config.profile,
@@ -715,6 +780,7 @@ function buildDoctorSummary(args) {
715
780
  delivery_capabilities: deliveryDeclaration,
716
781
  route_missing_policy: routeMissingPolicy,
717
782
  default_reply_route: defaultReplyRoute,
783
+ default_reply_route_validation: args.defaultReplyRouteValidation,
718
784
  delivery_probe: args.deliveryProbe,
719
785
  delivery_probe_diagnostics: args.deliveryProbeDiagnostics,
720
786
  };
@@ -806,10 +872,8 @@ async function handleRegister(args) {
806
872
  capabilities: args.capabilities,
807
873
  metadata: isPlainObject(args.metadata) ? args.metadata : null,
808
874
  // register 阶段 installation_id 由 gateway 正式生成;
809
- // 这里不能先把 default_reply_route 这种需要 installation_id 的字段在 client 侧拍扁。
810
- delivery_capabilities: isPlainObject(args.delivery_capabilities)
811
- ? args.delivery_capabilities
812
- : normalizeAgentInstallationDeliveryDeclaration(args.delivery_capabilities),
875
+ // 这里仍然保留 default_reply_route / route_missing_policy,但会先做 reply route 校验。
876
+ delivery_capabilities: normalizeDeliveryCapabilitiesWithInstallationId(args.delivery_capabilities, null),
813
877
  status: normalizeText(args.status) || undefined,
814
878
  };
815
879
  if (agentId)
@@ -963,6 +1027,11 @@ async function handleDoctor(args) {
963
1027
  let remote = null;
964
1028
  let deliveryProbe = null;
965
1029
  let deliveryProbeDiagnostics = null;
1030
+ let defaultReplyRouteValidation = {
1031
+ ok: true,
1032
+ reason: null,
1033
+ session_key: null,
1034
+ };
966
1035
  if (!state.identity) {
967
1036
  blockers.push('missing_agent_identity');
968
1037
  }
@@ -985,6 +1054,35 @@ async function handleDoctor(args) {
985
1054
  subscriptions: subscriptions.subscriptions,
986
1055
  };
987
1056
  const declaration = installation.installation.delivery_capabilities || null;
1057
+ const defaultReplyRoute = isPlainObject(declaration?.default_reply_route)
1058
+ ? declaration.default_reply_route
1059
+ : null;
1060
+ const defaultReplyRouteSessionKey = normalizeText(defaultReplyRoute?.session_key || defaultReplyRoute?.sessionKey) || null;
1061
+ if (defaultReplyRouteSessionKey && looksLikeOpenClawSessionKey(defaultReplyRouteSessionKey)) {
1062
+ const validation = validateOpenClawSessionKey(defaultReplyRouteSessionKey);
1063
+ if (validation?.ok) {
1064
+ defaultReplyRouteValidation = {
1065
+ ok: true,
1066
+ reason: null,
1067
+ session_key: validation.normalized,
1068
+ };
1069
+ }
1070
+ else if (validation) {
1071
+ defaultReplyRouteValidation = {
1072
+ ok: false,
1073
+ reason: validation.reason,
1074
+ session_key: validation.input,
1075
+ };
1076
+ blockers.push(`openclaw_default_reply_route_session_key_invalid:${validation.reason}`);
1077
+ }
1078
+ }
1079
+ else {
1080
+ defaultReplyRouteValidation = {
1081
+ ok: true,
1082
+ reason: null,
1083
+ session_key: defaultReplyRouteSessionKey,
1084
+ };
1085
+ }
988
1086
  const activeTopics = new Set((subscriptions.subscriptions || [])
989
1087
  .filter((entry) => normalizeText(entry?.status) === 'active')
990
1088
  .map((entry) => normalizeText(entry?.topic))
@@ -1044,6 +1142,7 @@ async function handleDoctor(args) {
1044
1142
  warnings,
1045
1143
  deliveryProbe,
1046
1144
  deliveryProbeDiagnostics,
1145
+ defaultReplyRouteValidation,
1047
1146
  }));
1048
1147
  }
1049
1148
  async function handleInstall(args) {
@@ -1054,13 +1153,12 @@ async function handleInstall(args) {
1054
1153
  const receiverTransport = normalizeReceiverTransport(args.receiver_transport);
1055
1154
  const subscribeDefaultTopics = args.subscribe_default_topics !== false;
1056
1155
  const receiverShouldStart = enableLocalReceiver ? args.receiver_start !== false : false;
1057
- const receiverCommandArgv = (() => {
1058
- const explicitArgv = normalizeCommandArgv(args.receiver_command_argv);
1059
- if (explicitArgv.length > 0)
1060
- return explicitArgv;
1061
- const command = normalizeText(args.receiver_command) || 'hi-agent-receiver';
1062
- return [command];
1063
- })();
1156
+ const receiverCommandArgv = buildInstallReceiverCommandArgv({
1157
+ explicitArgv: normalizeCommandArgv(args.receiver_command_argv),
1158
+ receiverCommand: normalizeText(args.receiver_command),
1159
+ hostKind,
1160
+ enableLocalReceiver,
1161
+ });
1064
1162
  const hostAdapterKindRaw = normalizeText(args.host_adapter_kind).toLowerCase();
1065
1163
  const hostAdapterKind = hostAdapterKindRaw === 'openresponses'
1066
1164
  ? 'openresponses'
@@ -1103,7 +1201,10 @@ async function handleInstall(args) {
1103
1201
  state = await loadPersistedState();
1104
1202
  }
1105
1203
  const { gateway } = await createAuthorizedClients();
1106
- const defaultReplyRoute = buildDefaultReplyRoute(args, state.identity?.installation_id || null);
1204
+ const defaultReplyRoute = buildDefaultReplyRoute(args, {
1205
+ installationId: state.identity?.installation_id || null,
1206
+ requireOpenClawSessionKey: hostKind === 'openclaw',
1207
+ });
1107
1208
  const routeMissingPolicy = normalizeText(args.route_missing_policy)
1108
1209
  || (defaultReplyRoute ? 'use_explicit_default_route' : '');
1109
1210
  const desiredDeliveryCapabilities = buildInstallationDeliveryDeclaration({
@@ -1289,13 +1390,8 @@ async function handleInstallationUpdate(args) {
1289
1390
  : {}),
1290
1391
  ...(Object.prototype.hasOwnProperty.call(args, 'delivery_capabilities')
1291
1392
  // installation_update 需要保留 default_reply_route / route_missing_policy 这类扩展字段,
1292
- // 不能在 client 侧先被旧版 normalize 链路截断。
1293
- ? { delivery_capabilities: (isPlainObject(args.delivery_capabilities)
1294
- ? {
1295
- ...args.delivery_capabilities,
1296
- default_reply_route: withInstallationIdOnReplyRoute(args.delivery_capabilities.default_reply_route, state.identity?.installation_id || null),
1297
- }
1298
- : normalizeDeliveryCapabilitiesWithInstallationId(args.delivery_capabilities, state.identity?.installation_id || null)) }
1393
+ // reply route 里的 session_key 仍然必须先过 canonical 校验。
1394
+ ? { delivery_capabilities: normalizeDeliveryCapabilitiesWithInstallationId(args.delivery_capabilities, state.identity?.installation_id || null) }
1299
1395
  : {}),
1300
1396
  };
1301
1397
  const updated = await gateway.updateInstallation(updateRequest);
@@ -1527,7 +1623,7 @@ async function listTools() {
1527
1623
  function createMcpServer() {
1528
1624
  const server = new Server({
1529
1625
  name: 'hi-mcp-server',
1530
- version: '0.1.6',
1626
+ version: '0.1.7',
1531
1627
  }, {
1532
1628
  capabilities: {
1533
1629
  tools: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirey/hi-mcp-server",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/server.js",