@hirey/hi-mcp-server 0.1.6 → 0.1.7
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
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -12,6 +12,7 @@ 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';
|
|
15
16
|
const CAPABILITY_CACHE_TTL_MS = 30_000;
|
|
16
17
|
const resolvedProfile = normalizeStateProfile(process.env.HI_MCP_PROFILE);
|
|
17
18
|
const config = {
|
|
@@ -107,7 +108,7 @@ function controlTools() {
|
|
|
107
108
|
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
109
|
host_adapter_bearer_token: { type: 'string', description: 'host_adapter_kind=openclaw_hooks 时必填:本地 hooks bearer token。' },
|
|
109
110
|
openresponses_model: { type: 'string', description: 'host_adapter_kind=openresponses 时必填:receiver 发给本地入口使用的 model。' },
|
|
110
|
-
host_session_key: { type: 'string', description: '可选:把当前 OpenClaw
|
|
111
|
+
host_session_key: { type: 'string', description: '可选:把当前 OpenClaw 当前会话显式设为 default continuation route 的 canonical full session_key。只能使用结构化宿主来源返回的完整 key;不要从 openclaw status / openclaw sessions / TUI footer 这类展示文本里抄值。' },
|
|
111
112
|
route_missing_policy: { type: 'string', description: "可选:'use_explicit_default_route'|'fail_closed'。默认在提供 host_session_key/default_reply_* 时使用 use_explicit_default_route,否则为 null。" },
|
|
112
113
|
default_reply_channel: { type: 'string', description: '可选:default continuation route 的 channel。' },
|
|
113
114
|
default_reply_to: { type: 'string', description: '可选:default continuation route 的宿主 target。' },
|
|
@@ -535,30 +536,91 @@ function buildInstallationDeliveryDeclaration(args) {
|
|
|
535
536
|
function hasActiveInstallationCapability(declaration, kind) {
|
|
536
537
|
return !!declaration?.capabilities?.some((entry) => entry.kind === kind && entry.status !== 'disabled');
|
|
537
538
|
}
|
|
538
|
-
function
|
|
539
|
+
function buildInvalidOpenClawSessionKeyError(reason, input) {
|
|
540
|
+
const error = new Error('invalid_openclaw_host_session_key');
|
|
541
|
+
const hint = reason === 'status_display_value'
|
|
542
|
+
? 'Use a structured host source for the full session key; do not copy the truncated key shown by openclaw status.'
|
|
543
|
+
: reason === 'sessions_display_value'
|
|
544
|
+
? 'Use a structured host source for the full session key; do not copy the truncated key shown by openclaw sessions.'
|
|
545
|
+
: 'Expected an OpenClaw canonical full session key shaped like agent:<agent_id>:<rest>.';
|
|
546
|
+
error.detail = {
|
|
547
|
+
reason,
|
|
548
|
+
input,
|
|
549
|
+
hint,
|
|
550
|
+
};
|
|
551
|
+
return error;
|
|
552
|
+
}
|
|
553
|
+
function normalizeReplyRouteSessionKey(input, options = {}) {
|
|
554
|
+
const sessionKey = normalizeText(input) || null;
|
|
555
|
+
if (!sessionKey) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const shouldValidateAsOpenClaw = options.requireOpenClawSessionKey || looksLikeOpenClawSessionKey(sessionKey);
|
|
559
|
+
if (!shouldValidateAsOpenClaw) {
|
|
560
|
+
return sessionKey;
|
|
561
|
+
}
|
|
562
|
+
const validation = validateOpenClawSessionKey(sessionKey);
|
|
563
|
+
if (!validation) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
if (!validation.ok) {
|
|
567
|
+
throw buildInvalidOpenClawSessionKeyError(validation.reason, validation.input);
|
|
568
|
+
}
|
|
569
|
+
return validation.normalized;
|
|
570
|
+
}
|
|
571
|
+
function normalizeReplyRouteWithInstallationId(input, installationId, options = {}) {
|
|
539
572
|
if (!isPlainObject(input))
|
|
540
573
|
return null;
|
|
541
574
|
const normalizedInstallationId = normalizeText(installationId) || null;
|
|
542
575
|
const existingInstallationId = normalizeText(input.installation_id) || null;
|
|
543
|
-
|
|
576
|
+
const hasSessionKey = Object.prototype.hasOwnProperty.call(input, 'session_key')
|
|
577
|
+
|| Object.prototype.hasOwnProperty.call(input, 'sessionKey');
|
|
578
|
+
const normalizedSessionKey = hasSessionKey
|
|
579
|
+
? normalizeReplyRouteSessionKey(Object.prototype.hasOwnProperty.call(input, 'session_key')
|
|
580
|
+
? input.session_key
|
|
581
|
+
: input.sessionKey, options)
|
|
582
|
+
: undefined;
|
|
583
|
+
if (!normalizedInstallationId && !hasSessionKey)
|
|
544
584
|
return input;
|
|
545
|
-
|
|
585
|
+
const output = {
|
|
546
586
|
...input,
|
|
547
|
-
installation_id: normalizedInstallationId,
|
|
548
587
|
};
|
|
588
|
+
if (normalizedInstallationId && !existingInstallationId) {
|
|
589
|
+
output.installation_id = normalizedInstallationId;
|
|
590
|
+
}
|
|
591
|
+
if (hasSessionKey) {
|
|
592
|
+
if (Object.prototype.hasOwnProperty.call(output, 'session_key')) {
|
|
593
|
+
output.session_key = normalizedSessionKey;
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
output.sessionKey = normalizedSessionKey;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return output;
|
|
549
600
|
}
|
|
550
|
-
function
|
|
551
|
-
if (!
|
|
552
|
-
return
|
|
601
|
+
function applyNormalizedDefaultReplyRoute(input, installationId, options = {}) {
|
|
602
|
+
if (!Object.prototype.hasOwnProperty.call(input, 'default_reply_route')) {
|
|
603
|
+
return input;
|
|
553
604
|
}
|
|
554
|
-
|
|
605
|
+
return {
|
|
555
606
|
...input,
|
|
556
|
-
default_reply_route:
|
|
607
|
+
default_reply_route: normalizeReplyRouteWithInstallationId(input.default_reply_route, installationId, options),
|
|
557
608
|
};
|
|
558
|
-
return normalizeAgentInstallationDeliveryDeclaration(hydrated);
|
|
559
609
|
}
|
|
560
|
-
function
|
|
561
|
-
|
|
610
|
+
function normalizeDeliveryCapabilitiesWithInstallationId(input, installationId) {
|
|
611
|
+
if (isPlainObject(input)) {
|
|
612
|
+
return applyNormalizedDefaultReplyRoute(input, installationId);
|
|
613
|
+
}
|
|
614
|
+
const normalized = normalizeAgentInstallationDeliveryDeclaration(input);
|
|
615
|
+
if (!isPlainObject(normalized)) {
|
|
616
|
+
return normalized;
|
|
617
|
+
}
|
|
618
|
+
return applyNormalizedDefaultReplyRoute(normalized, installationId);
|
|
619
|
+
}
|
|
620
|
+
function buildDefaultReplyRoute(args, options = {}) {
|
|
621
|
+
const sessionKey = normalizeReplyRouteSessionKey(args.host_session_key, {
|
|
622
|
+
requireOpenClawSessionKey: options.requireOpenClawSessionKey,
|
|
623
|
+
});
|
|
562
624
|
const deliveryContext = {
|
|
563
625
|
channel: normalizeText(args.default_reply_channel) || null,
|
|
564
626
|
to: normalizeText(args.default_reply_to) || null,
|
|
@@ -572,7 +634,7 @@ function buildDefaultReplyRoute(args, installationId) {
|
|
|
572
634
|
if (!sessionKey && !hasDeliveryContext)
|
|
573
635
|
return null;
|
|
574
636
|
return {
|
|
575
|
-
installation_id: normalizeText(installationId) || null,
|
|
637
|
+
installation_id: normalizeText(options.installationId) || null,
|
|
576
638
|
session_key: sessionKey,
|
|
577
639
|
delivery_context: hasDeliveryContext ? deliveryContext : null,
|
|
578
640
|
};
|
|
@@ -693,9 +755,11 @@ function buildDoctorSummary(args) {
|
|
|
693
755
|
const defaultReplyRoute = isPlainObject(deliveryDeclaration?.default_reply_route)
|
|
694
756
|
? deliveryDeclaration.default_reply_route
|
|
695
757
|
: null;
|
|
696
|
-
const continuityState = defaultReplyRoute
|
|
697
|
-
? '
|
|
698
|
-
:
|
|
758
|
+
const continuityState = defaultReplyRoute && !args.defaultReplyRouteValidation.ok
|
|
759
|
+
? 'explicit_default_route_invalid'
|
|
760
|
+
: defaultReplyRoute
|
|
761
|
+
? 'explicit_default_route_ready'
|
|
762
|
+
: (routeMissingPolicy === 'fail_closed' ? 'origin_capture_only' : 'continuity_not_ready');
|
|
699
763
|
return {
|
|
700
764
|
ok: args.blockers.length === 0,
|
|
701
765
|
profile: config.profile,
|
|
@@ -715,6 +779,7 @@ function buildDoctorSummary(args) {
|
|
|
715
779
|
delivery_capabilities: deliveryDeclaration,
|
|
716
780
|
route_missing_policy: routeMissingPolicy,
|
|
717
781
|
default_reply_route: defaultReplyRoute,
|
|
782
|
+
default_reply_route_validation: args.defaultReplyRouteValidation,
|
|
718
783
|
delivery_probe: args.deliveryProbe,
|
|
719
784
|
delivery_probe_diagnostics: args.deliveryProbeDiagnostics,
|
|
720
785
|
};
|
|
@@ -806,10 +871,8 @@ async function handleRegister(args) {
|
|
|
806
871
|
capabilities: args.capabilities,
|
|
807
872
|
metadata: isPlainObject(args.metadata) ? args.metadata : null,
|
|
808
873
|
// register 阶段 installation_id 由 gateway 正式生成;
|
|
809
|
-
//
|
|
810
|
-
delivery_capabilities:
|
|
811
|
-
? args.delivery_capabilities
|
|
812
|
-
: normalizeAgentInstallationDeliveryDeclaration(args.delivery_capabilities),
|
|
874
|
+
// 这里仍然保留 default_reply_route / route_missing_policy,但会先做 reply route 校验。
|
|
875
|
+
delivery_capabilities: normalizeDeliveryCapabilitiesWithInstallationId(args.delivery_capabilities, null),
|
|
813
876
|
status: normalizeText(args.status) || undefined,
|
|
814
877
|
};
|
|
815
878
|
if (agentId)
|
|
@@ -963,6 +1026,11 @@ async function handleDoctor(args) {
|
|
|
963
1026
|
let remote = null;
|
|
964
1027
|
let deliveryProbe = null;
|
|
965
1028
|
let deliveryProbeDiagnostics = null;
|
|
1029
|
+
let defaultReplyRouteValidation = {
|
|
1030
|
+
ok: true,
|
|
1031
|
+
reason: null,
|
|
1032
|
+
session_key: null,
|
|
1033
|
+
};
|
|
966
1034
|
if (!state.identity) {
|
|
967
1035
|
blockers.push('missing_agent_identity');
|
|
968
1036
|
}
|
|
@@ -985,6 +1053,35 @@ async function handleDoctor(args) {
|
|
|
985
1053
|
subscriptions: subscriptions.subscriptions,
|
|
986
1054
|
};
|
|
987
1055
|
const declaration = installation.installation.delivery_capabilities || null;
|
|
1056
|
+
const defaultReplyRoute = isPlainObject(declaration?.default_reply_route)
|
|
1057
|
+
? declaration.default_reply_route
|
|
1058
|
+
: null;
|
|
1059
|
+
const defaultReplyRouteSessionKey = normalizeText(defaultReplyRoute?.session_key || defaultReplyRoute?.sessionKey) || null;
|
|
1060
|
+
if (defaultReplyRouteSessionKey && looksLikeOpenClawSessionKey(defaultReplyRouteSessionKey)) {
|
|
1061
|
+
const validation = validateOpenClawSessionKey(defaultReplyRouteSessionKey);
|
|
1062
|
+
if (validation?.ok) {
|
|
1063
|
+
defaultReplyRouteValidation = {
|
|
1064
|
+
ok: true,
|
|
1065
|
+
reason: null,
|
|
1066
|
+
session_key: validation.normalized,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
else if (validation) {
|
|
1070
|
+
defaultReplyRouteValidation = {
|
|
1071
|
+
ok: false,
|
|
1072
|
+
reason: validation.reason,
|
|
1073
|
+
session_key: validation.input,
|
|
1074
|
+
};
|
|
1075
|
+
blockers.push(`openclaw_default_reply_route_session_key_invalid:${validation.reason}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
defaultReplyRouteValidation = {
|
|
1080
|
+
ok: true,
|
|
1081
|
+
reason: null,
|
|
1082
|
+
session_key: defaultReplyRouteSessionKey,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
988
1085
|
const activeTopics = new Set((subscriptions.subscriptions || [])
|
|
989
1086
|
.filter((entry) => normalizeText(entry?.status) === 'active')
|
|
990
1087
|
.map((entry) => normalizeText(entry?.topic))
|
|
@@ -1044,6 +1141,7 @@ async function handleDoctor(args) {
|
|
|
1044
1141
|
warnings,
|
|
1045
1142
|
deliveryProbe,
|
|
1046
1143
|
deliveryProbeDiagnostics,
|
|
1144
|
+
defaultReplyRouteValidation,
|
|
1047
1145
|
}));
|
|
1048
1146
|
}
|
|
1049
1147
|
async function handleInstall(args) {
|
|
@@ -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,
|
|
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
|
-
//
|
|
1293
|
-
? { 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.
|
|
1626
|
+
version: '0.1.7',
|
|
1531
1627
|
}, {
|
|
1532
1628
|
capabilities: {
|
|
1533
1629
|
tools: {},
|