@apnex/network-adapter 0.1.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.
- package/dist/hub-error.d.ts +42 -0
- package/dist/hub-error.js +74 -0
- package/dist/hub-error.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/agent-client.d.ts +192 -0
- package/dist/kernel/agent-client.js +44 -0
- package/dist/kernel/agent-client.js.map +1 -0
- package/dist/kernel/event-router.d.ts +49 -0
- package/dist/kernel/event-router.js +150 -0
- package/dist/kernel/event-router.js.map +1 -0
- package/dist/kernel/handshake.d.ts +161 -0
- package/dist/kernel/handshake.js +257 -0
- package/dist/kernel/handshake.js.map +1 -0
- package/dist/kernel/instance.d.ts +40 -0
- package/dist/kernel/instance.js +79 -0
- package/dist/kernel/instance.js.map +1 -0
- package/dist/kernel/mcp-agent-client.d.ts +139 -0
- package/dist/kernel/mcp-agent-client.js +505 -0
- package/dist/kernel/mcp-agent-client.js.map +1 -0
- package/dist/kernel/poll-backstop.d.ts +108 -0
- package/dist/kernel/poll-backstop.js +243 -0
- package/dist/kernel/poll-backstop.js.map +1 -0
- package/dist/kernel/session-claim.d.ts +67 -0
- package/dist/kernel/session-claim.js +106 -0
- package/dist/kernel/session-claim.js.map +1 -0
- package/dist/kernel/state-sync.d.ts +43 -0
- package/dist/kernel/state-sync.js +85 -0
- package/dist/kernel/state-sync.js.map +1 -0
- package/dist/logger.d.ts +82 -0
- package/dist/logger.js +114 -0
- package/dist/logger.js.map +1 -0
- package/dist/notification-log.d.ts +29 -0
- package/dist/notification-log.js +66 -0
- package/dist/notification-log.js.map +1 -0
- package/dist/prompt-format.d.ts +38 -0
- package/dist/prompt-format.js +220 -0
- package/dist/prompt-format.js.map +1 -0
- package/dist/tool-manager/dispatcher.d.ts +180 -0
- package/dist/tool-manager/dispatcher.js +379 -0
- package/dist/tool-manager/dispatcher.js.map +1 -0
- package/dist/tool-manager/tool-catalog-cache.d.ts +85 -0
- package/dist/tool-manager/tool-catalog-cache.js +137 -0
- package/dist/tool-manager/tool-catalog-cache.js.map +1 -0
- package/dist/wire/mcp-transport.d.ts +120 -0
- package/dist/wire/mcp-transport.js +447 -0
- package/dist/wire/mcp-transport.js.map +1 -0
- package/dist/wire/transport.d.ts +174 -0
- package/dist/wire/transport.js +43 -0
- package/dist/wire/transport.js.map +1 -0
- package/package.json +32 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatcher.ts — tool-manager handler factory (Layer 1c).
|
|
3
|
+
*
|
|
4
|
+
* Host-independent shared abstraction that owns the MCP server's
|
|
5
|
+
* Initialize / ListTools / CallTool handlers and the supporting
|
|
6
|
+
* pending-action-queueItemId tracking + tool-catalog cache fallback +
|
|
7
|
+
* clientInfo capture + error-envelope normalization.
|
|
8
|
+
*
|
|
9
|
+
* Mounted by per-host shims (Layer 3) which add host-specific transport
|
|
10
|
+
* plumbing (stdio / Bun-HTTP / future) and host-specific render-surface
|
|
11
|
+
* via the `notificationHooks` callback bag (Universal Adapter
|
|
12
|
+
* notification contract).
|
|
13
|
+
*
|
|
14
|
+
* This module is the "tool-manager" per Design v1.2 §4 naming discipline
|
|
15
|
+
* (Director-ratified rename from "MCP-boundary dispatcher" 2026-04-26)
|
|
16
|
+
* — distinct from the "Message-router" which is sovereign-package #6
|
|
17
|
+
* (`@apnex/message-router`) landing in M-Push-Foundation W4. Always
|
|
18
|
+
* qualify ("tool-manager" or "Message-router") in new code; avoid bare
|
|
19
|
+
* "dispatcher".
|
|
20
|
+
*/
|
|
21
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
|
+
import type { AgentClientCallbacks, AgentEvent, SessionState, SessionReconnectReason } from "../kernel/agent-client.js";
|
|
23
|
+
import type { McpAgentClient } from "../kernel/mcp-agent-client.js";
|
|
24
|
+
import { PollBackstop, type PollBackstopOptions } from "../kernel/poll-backstop.js";
|
|
25
|
+
import type { DrainedPendingAction } from "../kernel/state-sync.js";
|
|
26
|
+
import type { CachedCatalog } from "./tool-catalog-cache.js";
|
|
27
|
+
export interface DispatcherClientInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
version: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Universal Adapter notification contract — generic shim-injection
|
|
33
|
+
* callback bag. Layer 3 (per-host shim) implements these to bind
|
|
34
|
+
* dispatcher events into host-specific render-surfaces (claude
|
|
35
|
+
* `<channel>` / opencode `promptAsync` / future hosts).
|
|
36
|
+
*
|
|
37
|
+
* Spec: docs/specs/universal-adapter-notification-contract.md
|
|
38
|
+
*/
|
|
39
|
+
export interface DispatcherNotificationHooks {
|
|
40
|
+
onActionableEvent?: (event: AgentEvent) => void;
|
|
41
|
+
onInformationalEvent?: (event: AgentEvent) => void;
|
|
42
|
+
onStateChange?: (state: SessionState, previous: SessionState, reason?: SessionReconnectReason) => void;
|
|
43
|
+
onPendingActionItem?: (item: DrainedPendingAction) => void;
|
|
44
|
+
}
|
|
45
|
+
export interface SharedDispatcherOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Late-binding agent accessor. Some hosts (opencode) construct the
|
|
48
|
+
* dispatcher before the McpAgentClient connection is established.
|
|
49
|
+
* Returning `null` means "not connected yet" — the dispatcher
|
|
50
|
+
* surfaces a "Hub not connected" error envelope on CallTool and an
|
|
51
|
+
* empty tool-list on ListTools.
|
|
52
|
+
*/
|
|
53
|
+
getAgent: () => McpAgentClient | null;
|
|
54
|
+
/** Adapter version reported in MCP serverInfo. */
|
|
55
|
+
proxyVersion: string;
|
|
56
|
+
/** MCP server name. Default: "proxy". */
|
|
57
|
+
serverName?: string;
|
|
58
|
+
/**
|
|
59
|
+
* MCP server capabilities advertised at Initialize. Default:
|
|
60
|
+
* `{ tools: {}, logging: {} }`. Hosts with extra capabilities
|
|
61
|
+
* (e.g. claude `experimental.claude/channel`) override.
|
|
62
|
+
*/
|
|
63
|
+
serverCapabilities?: Record<string, unknown>;
|
|
64
|
+
/** Diagnostic logger. No-op default. */
|
|
65
|
+
log?: (msg: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Resolves when the underlying McpAgentClient handshake completes
|
|
68
|
+
* (transport connected + identity asserted). Gates the ListTools
|
|
69
|
+
* handler so the host's catalog fetch doesn't block on the slow
|
|
70
|
+
* full-sync phase. Omit when no gating is needed (tests).
|
|
71
|
+
*
|
|
72
|
+
* Was: `handshakeComplete` (per-plugin dispatchers). Renamed in
|
|
73
|
+
* mission-55 cleanup per Design v1.2 Q5: name what is gated, not
|
|
74
|
+
* what is complete.
|
|
75
|
+
*/
|
|
76
|
+
listToolsGate?: Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Resolves when the McpAgentClient session is claim-eligible
|
|
79
|
+
* (eager mode: claim_session returned; lazy mode: identity ready).
|
|
80
|
+
* Gates the CallTool handler so tool dispatch waits until the
|
|
81
|
+
* Hub will accept it. Omit when no gating is needed.
|
|
82
|
+
*
|
|
83
|
+
* Was: `agentReady` (per-plugin dispatchers). Renamed in mission-55
|
|
84
|
+
* cleanup per Design v1.2 Q5.
|
|
85
|
+
*/
|
|
86
|
+
callToolGate?: Promise<void>;
|
|
87
|
+
getCachedCatalog?: () => CachedCatalog | null;
|
|
88
|
+
getIsIdentityReady?: () => boolean;
|
|
89
|
+
getCurrentHubVersion?: () => string | null;
|
|
90
|
+
persistCatalog?: (catalog: unknown[]) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Optional cache-validity check. When omitted, cache-fallback
|
|
93
|
+
* conservatively treats every cached entry as invalid (live-fetch
|
|
94
|
+
* dominates). Wire `isCacheValid` from `./tool-catalog-cache.js`
|
|
95
|
+
* to enable Hub-version-keyed validity.
|
|
96
|
+
*/
|
|
97
|
+
isCacheValid?: (cached: CachedCatalog, currentHubVersion: string | null | undefined) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Universal Adapter notification contract. Host shim attaches its
|
|
100
|
+
* render-surface bindings here.
|
|
101
|
+
*/
|
|
102
|
+
notificationHooks?: DispatcherNotificationHooks;
|
|
103
|
+
/**
|
|
104
|
+
* Mission-56 W3.3: opt-in adapter-side hybrid poll backstop. When
|
|
105
|
+
* supplied, the dispatcher constructs a PollBackstop (Design v1.2
|
|
106
|
+
* commitment #5) that periodically calls `list_messages` with a
|
|
107
|
+
* `since` cursor + `status: "new"` filter and surfaces each delta
|
|
108
|
+
* Message via the same MessageRouter as the SSE inline path
|
|
109
|
+
* (preserving seen-id LRU dedup across both paths).
|
|
110
|
+
*
|
|
111
|
+
* Pass `{ role: "engineer" | "architect" }` minimum; cadence
|
|
112
|
+
* defaults to 5min (`OIS_ADAPTER_POLL_BACKSTOP_S` env override).
|
|
113
|
+
* Omit to disable polling (push-only mode).
|
|
114
|
+
*/
|
|
115
|
+
pollBackstop?: Omit<PollBackstopOptions, "onPolledMessage">;
|
|
116
|
+
}
|
|
117
|
+
export interface SharedDispatcher {
|
|
118
|
+
/** ADR-017 queueItemId tracking map. Keyed by `${dispatchType}:${entityRef}`. */
|
|
119
|
+
pendingActionMap: Map<string, string>;
|
|
120
|
+
/**
|
|
121
|
+
* Lazy MCP server factory. Hosts call this to obtain a fresh Server
|
|
122
|
+
* instance for each host transport (stdio: once at startup;
|
|
123
|
+
* Bun-HTTP: once per HTTP session).
|
|
124
|
+
*/
|
|
125
|
+
createMcpServer: () => Server;
|
|
126
|
+
/**
|
|
127
|
+
* AgentClientCallbacks suitable for `agent.setCallbacks(...)`. Wires
|
|
128
|
+
* pendingActionMap-population + propagates to notificationHooks for
|
|
129
|
+
* host-specific render-surface.
|
|
130
|
+
*/
|
|
131
|
+
callbacks: AgentClientCallbacks;
|
|
132
|
+
/** Returns last-captured Initialize-time clientInfo. */
|
|
133
|
+
getClientInfo: () => DispatcherClientInfo;
|
|
134
|
+
/**
|
|
135
|
+
* Builds an `onPendingActionItem` handshake callback that populates
|
|
136
|
+
* pendingActionMap (drain-path parity with the SSE inline-queueItemId
|
|
137
|
+
* path) and forwards to the supplied hooks.
|
|
138
|
+
*/
|
|
139
|
+
makePendingActionItemHandler: (hooks?: DispatcherNotificationHooks) => (item: DrainedPendingAction) => void;
|
|
140
|
+
/**
|
|
141
|
+
* Mission-56 W3.3: PollBackstop instance, present iff `opts.pollBackstop`
|
|
142
|
+
* was supplied. Hosts MAY call `pollBackstop.start(getAgent)` at the
|
|
143
|
+
* appropriate lifecycle moment (typically post-handshake, when the
|
|
144
|
+
* agent reaches `streaming`). Hosts MUST call `pollBackstop.stop()`
|
|
145
|
+
* on shutdown to clear the timer. Omitted (`undefined`) when polling
|
|
146
|
+
* is disabled (push-only mode).
|
|
147
|
+
*/
|
|
148
|
+
pollBackstop?: PollBackstop;
|
|
149
|
+
/**
|
|
150
|
+
* Mission-56 W3.3: explicit-ack-on-action surface. Host shims call
|
|
151
|
+
* this when the consumer (LLM) has acted on or actively-deferred a
|
|
152
|
+
* Message that was previously claimed (per Option (i) ratified at
|
|
153
|
+
* thread-325 round-2). Idempotent: re-acks on already-acked Messages
|
|
154
|
+
* are no-ops. Errors swallowed (logged, non-fatal) — a missed ack
|
|
155
|
+
* leaves the Message at status `received`, which the next poll-tick
|
|
156
|
+
* naturally excludes from `status: "new"` so the consumer doesn't
|
|
157
|
+
* re-render it.
|
|
158
|
+
*/
|
|
159
|
+
ackMessage: (messageId: string) => Promise<void>;
|
|
160
|
+
}
|
|
161
|
+
/** Compose the pendingActionMap key. Pure helper; exported for tests. */
|
|
162
|
+
/**
|
|
163
|
+
* Mission-62 W3: tools that should NOT trigger signal_working_*
|
|
164
|
+
* wrapping. The signal_* tools themselves would recurse infinitely;
|
|
165
|
+
* register_role + claim_session + drain_pending_actions are lifecycle
|
|
166
|
+
* tools, not semantic tool-call-work.
|
|
167
|
+
*/
|
|
168
|
+
export declare const TOOL_CALL_SIGNAL_SKIP: ReadonlySet<string>;
|
|
169
|
+
export declare function pendingKey(dispatchType: string, entityRef: string): string;
|
|
170
|
+
/**
|
|
171
|
+
* Inject `sourceQueueItemId` into a settling tool call's arguments
|
|
172
|
+
* when a pendingActionMap entry is registered for the call's target.
|
|
173
|
+
* Currently only `create_thread_reply` settles a thread_message
|
|
174
|
+
* dispatch; extend this set as new auto-injection rules are ratified.
|
|
175
|
+
*
|
|
176
|
+
* Side effect: deletes the consumed map entry. Explicit
|
|
177
|
+
* sourceQueueItemId in the args wins over the map (no rewrite).
|
|
178
|
+
*/
|
|
179
|
+
export declare function injectQueueItemId(name: string, args: Record<string, unknown>, pendingActionMap: Map<string, string>): Record<string, unknown>;
|
|
180
|
+
export declare function createSharedDispatcher(opts: SharedDispatcherOptions): SharedDispatcher;
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatcher.ts — tool-manager handler factory (Layer 1c).
|
|
3
|
+
*
|
|
4
|
+
* Host-independent shared abstraction that owns the MCP server's
|
|
5
|
+
* Initialize / ListTools / CallTool handlers and the supporting
|
|
6
|
+
* pending-action-queueItemId tracking + tool-catalog cache fallback +
|
|
7
|
+
* clientInfo capture + error-envelope normalization.
|
|
8
|
+
*
|
|
9
|
+
* Mounted by per-host shims (Layer 3) which add host-specific transport
|
|
10
|
+
* plumbing (stdio / Bun-HTTP / future) and host-specific render-surface
|
|
11
|
+
* via the `notificationHooks` callback bag (Universal Adapter
|
|
12
|
+
* notification contract).
|
|
13
|
+
*
|
|
14
|
+
* This module is the "tool-manager" per Design v1.2 §4 naming discipline
|
|
15
|
+
* (Director-ratified rename from "MCP-boundary dispatcher" 2026-04-26)
|
|
16
|
+
* — distinct from the "Message-router" which is sovereign-package #6
|
|
17
|
+
* (`@apnex/message-router`) landing in M-Push-Foundation W4. Always
|
|
18
|
+
* qualify ("tool-manager" or "Message-router") in new code; avoid bare
|
|
19
|
+
* "dispatcher".
|
|
20
|
+
*/
|
|
21
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import { MessageRouter, SeenIdCache } from "@apnex/message-router";
|
|
24
|
+
import { PollBackstop } from "../kernel/poll-backstop.js";
|
|
25
|
+
/** Compose the pendingActionMap key. Pure helper; exported for tests. */
|
|
26
|
+
/**
|
|
27
|
+
* Mission-62 W3: tools that should NOT trigger signal_working_*
|
|
28
|
+
* wrapping. The signal_* tools themselves would recurse infinitely;
|
|
29
|
+
* register_role + claim_session + drain_pending_actions are lifecycle
|
|
30
|
+
* tools, not semantic tool-call-work.
|
|
31
|
+
*/
|
|
32
|
+
export const TOOL_CALL_SIGNAL_SKIP = new Set([
|
|
33
|
+
"signal_working_started",
|
|
34
|
+
"signal_working_completed",
|
|
35
|
+
"signal_quota_blocked",
|
|
36
|
+
"signal_quota_recovered",
|
|
37
|
+
"register_role",
|
|
38
|
+
"claim_session",
|
|
39
|
+
"drain_pending_actions",
|
|
40
|
+
]);
|
|
41
|
+
export function pendingKey(dispatchType, entityRef) {
|
|
42
|
+
return `${dispatchType}:${entityRef}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Inject `sourceQueueItemId` into a settling tool call's arguments
|
|
46
|
+
* when a pendingActionMap entry is registered for the call's target.
|
|
47
|
+
* Currently only `create_thread_reply` settles a thread_message
|
|
48
|
+
* dispatch; extend this set as new auto-injection rules are ratified.
|
|
49
|
+
*
|
|
50
|
+
* Side effect: deletes the consumed map entry. Explicit
|
|
51
|
+
* sourceQueueItemId in the args wins over the map (no rewrite).
|
|
52
|
+
*/
|
|
53
|
+
export function injectQueueItemId(name, args, pendingActionMap) {
|
|
54
|
+
if (name !== "create_thread_reply")
|
|
55
|
+
return args;
|
|
56
|
+
const threadId = args.threadId;
|
|
57
|
+
if (typeof threadId !== "string")
|
|
58
|
+
return args;
|
|
59
|
+
if ("sourceQueueItemId" in args)
|
|
60
|
+
return args;
|
|
61
|
+
const queueItemId = pendingActionMap.get(pendingKey("thread_message", threadId));
|
|
62
|
+
if (!queueItemId)
|
|
63
|
+
return args;
|
|
64
|
+
pendingActionMap.delete(pendingKey("thread_message", threadId));
|
|
65
|
+
return { ...args, sourceQueueItemId: queueItemId };
|
|
66
|
+
}
|
|
67
|
+
export function createSharedDispatcher(opts) {
|
|
68
|
+
const log = opts.log ?? (() => { });
|
|
69
|
+
const serverName = opts.serverName ?? "proxy";
|
|
70
|
+
const serverCapabilities = opts.serverCapabilities ?? { tools: {}, logging: {} };
|
|
71
|
+
const pendingActionMap = new Map();
|
|
72
|
+
let capturedClientInfo = {
|
|
73
|
+
name: "unknown",
|
|
74
|
+
version: "0.0.0",
|
|
75
|
+
};
|
|
76
|
+
const getClientInfo = () => capturedClientInfo;
|
|
77
|
+
function isUsableAgent(agent) {
|
|
78
|
+
return !!agent && agent.isConnected !== false;
|
|
79
|
+
}
|
|
80
|
+
// ADR-017 Phase 1.1: SSE thread_message events carry queueItemId
|
|
81
|
+
// inline. Capture into pendingActionMap so the next settling
|
|
82
|
+
// create_thread_reply can auto-inject sourceQueueItemId — even if
|
|
83
|
+
// no drain ever populated the map. Eliminates the SSE-vs-drain race
|
|
84
|
+
// that caused false-positive escalations on early thread tests.
|
|
85
|
+
const captureQueueItemFromEvent = (event) => {
|
|
86
|
+
if (event.event !== "thread_message")
|
|
87
|
+
return;
|
|
88
|
+
const data = event.data;
|
|
89
|
+
const qid = data.queueItemId;
|
|
90
|
+
const threadId = data.threadId;
|
|
91
|
+
if (typeof qid === "string" && typeof threadId === "string") {
|
|
92
|
+
pendingActionMap.set(pendingKey("thread_message", threadId), qid);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
// Mission-56 W2.2: Layer-2 routing. Every classified event goes
|
|
96
|
+
// through `@apnex/message-router` so Message-ID dedup (push+poll
|
|
97
|
+
// race) + kind→hook mapping live in one place. The host's
|
|
98
|
+
// `notificationHooks` bag is the router's hook surface — no
|
|
99
|
+
// shape adapter needed (the router's NotificationHooks interface
|
|
100
|
+
// mirrors DispatcherNotificationHooks exactly).
|
|
101
|
+
//
|
|
102
|
+
// The seen-id cache is shared across the construction-time router
|
|
103
|
+
// and any per-call routers minted by `makePendingActionItemHandler`,
|
|
104
|
+
// so a Message ID seen on the SSE inline path will dedup a later
|
|
105
|
+
// drain-path replay (and vice-versa).
|
|
106
|
+
const seenIdCache = new SeenIdCache();
|
|
107
|
+
const router = new MessageRouter({
|
|
108
|
+
hooks: opts.notificationHooks ?? {},
|
|
109
|
+
seenIdCache,
|
|
110
|
+
});
|
|
111
|
+
// Mission-56 W3.3: post-render claim. Extracts the Message ID from
|
|
112
|
+
// `message_arrived` events (W1a SSE shape: event.data.message.id)
|
|
113
|
+
// and fires `claim_message(id)` against the Hub via the agent. Per
|
|
114
|
+
// architect-issued W3 directive: claim happens AFTER the host hook
|
|
115
|
+
// renders (the ordering matches "shim calls after successful render
|
|
116
|
+
// to host" from Design v1.2 commitment #6). Errors are swallowed +
|
|
117
|
+
// logged — claim failure is non-fatal (the SSE path still rendered;
|
|
118
|
+
// the next poll-tick will pick up any unclaimed Message in the
|
|
119
|
+
// status === "new" set if needed).
|
|
120
|
+
//
|
|
121
|
+
// Multi-agent same-role: the Hub-side CAS enforces winner-takes-all
|
|
122
|
+
// (mission-56 W3.2). The wonClaim signal is informational; even if
|
|
123
|
+
// we lost, the host has already rendered (claim is post-render), so
|
|
124
|
+
// the loser still sees the Message — but only the winner's claim
|
|
125
|
+
// flips status to `received`, gating subsequent ack to a single
|
|
126
|
+
// canonical actor.
|
|
127
|
+
function fireClaimMessage(event) {
|
|
128
|
+
if (event.event !== "message_arrived")
|
|
129
|
+
return;
|
|
130
|
+
const data = event.data;
|
|
131
|
+
const message = data?.message;
|
|
132
|
+
const messageId = message?.id;
|
|
133
|
+
if (typeof messageId !== "string")
|
|
134
|
+
return;
|
|
135
|
+
const agent = opts.getAgent();
|
|
136
|
+
if (!agent || agent.state !== "streaming")
|
|
137
|
+
return;
|
|
138
|
+
void agent
|
|
139
|
+
.call("claim_message", { id: messageId })
|
|
140
|
+
.catch((err) => {
|
|
141
|
+
log(`[claim_message] non-fatal failure for ${messageId}: ${err?.message ?? String(err)}`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const callbacks = {
|
|
145
|
+
onActionableEvent: (event) => {
|
|
146
|
+
captureQueueItemFromEvent(event);
|
|
147
|
+
router.route({ kind: "notification.actionable", event });
|
|
148
|
+
// Mission-56 W3.3: post-render claim (replaces W2.2 stub-claim TODO).
|
|
149
|
+
fireClaimMessage(event);
|
|
150
|
+
},
|
|
151
|
+
onInformationalEvent: (event) => {
|
|
152
|
+
router.route({ kind: "notification.informational", event });
|
|
153
|
+
},
|
|
154
|
+
onStateChange: (state, previous, reason) => {
|
|
155
|
+
log(`Connection: ${previous} → ${state}${reason ? ` (${reason})` : ""}`);
|
|
156
|
+
router.route({ kind: "state.change", state, previous, reason });
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
// Mission-56 W3.3: PollBackstop construction (opt-in via opts.pollBackstop).
|
|
160
|
+
// The backstop fires `list_messages({status:"new", since:<lastSeen>})`
|
|
161
|
+
// periodically and routes each delta Message through the same
|
|
162
|
+
// MessageRouter as the SSE inline path so seen-id LRU dedup catches
|
|
163
|
+
// push+poll race overlap. Polled Messages also fire claim_message
|
|
164
|
+
// (the router invocation goes through onActionableEvent which
|
|
165
|
+
// already includes fireClaimMessage).
|
|
166
|
+
const pollBackstop = opts.pollBackstop
|
|
167
|
+
? new PollBackstop({
|
|
168
|
+
...opts.pollBackstop,
|
|
169
|
+
log: opts.pollBackstop.log ?? log,
|
|
170
|
+
onPolledMessage: (event) => {
|
|
171
|
+
router.route({ kind: "notification.actionable", event });
|
|
172
|
+
fireClaimMessage(event);
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
: undefined;
|
|
176
|
+
// Mission-56 W3.3: explicit-ack-on-action helper. Host shims wire
|
|
177
|
+
// this to fire when the LLM consumer has acted on (or actively
|
|
178
|
+
// deferred) a Message — per Option (i) ratified at thread-325 round-2,
|
|
179
|
+
// ack is tied to consumer-action, not auto-on-render.
|
|
180
|
+
async function ackMessage(messageId) {
|
|
181
|
+
const agent = opts.getAgent();
|
|
182
|
+
if (!agent || agent.state !== "streaming")
|
|
183
|
+
return;
|
|
184
|
+
try {
|
|
185
|
+
await agent.call("ack_message", { id: messageId });
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
log(`[ack_message] non-fatal failure for ${messageId}: ${err?.message ?? String(err)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const makePendingActionItemHandler = (hooks) => {
|
|
192
|
+
// Per-call hooks override the construction-time bag for the
|
|
193
|
+
// drain path (preserves the original makePendingActionItemHandler
|
|
194
|
+
// contract — claude-plugin shim uses this to bind a custom log
|
|
195
|
+
// sink). Share the seen-id cache so drain-path replays dedup
|
|
196
|
+
// against SSE-path inline deliveries.
|
|
197
|
+
const drainRouter = new MessageRouter({
|
|
198
|
+
hooks: hooks ?? {},
|
|
199
|
+
seenIdCache,
|
|
200
|
+
});
|
|
201
|
+
return (item) => {
|
|
202
|
+
pendingActionMap.set(pendingKey(item.dispatchType, item.entityRef), item.id);
|
|
203
|
+
drainRouter.route({ kind: "pending-action.dispatch", item });
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
function createMcpServer() {
|
|
207
|
+
const server = new Server({ name: serverName, version: opts.proxyVersion }, { capabilities: serverCapabilities });
|
|
208
|
+
// Initialize handler is intentionally NOT gated — host MCP clients
|
|
209
|
+
// (e.g. Claude Code) have a tight initialize timeout that's faster
|
|
210
|
+
// than a full Hub handshake. The Initialize handler captures
|
|
211
|
+
// clientInfo for downstream handshake passthrough.
|
|
212
|
+
server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
213
|
+
try {
|
|
214
|
+
const ci = request.params
|
|
215
|
+
.clientInfo;
|
|
216
|
+
if (ci &&
|
|
217
|
+
typeof ci.name === "string" &&
|
|
218
|
+
typeof ci.version === "string") {
|
|
219
|
+
capturedClientInfo = { name: ci.name, version: ci.version };
|
|
220
|
+
log(`[Handshake] Captured clientInfo: ${ci.name}@${ci.version}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
log(`[Handshake] clientInfo capture failed (non-fatal): ${err}`);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
protocolVersion: request.params.protocolVersion,
|
|
228
|
+
capabilities: serverCapabilities,
|
|
229
|
+
serverInfo: { name: serverName, version: opts.proxyVersion },
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
233
|
+
// Probe-safe cache fallback. When identity hasn't yet resolved
|
|
234
|
+
// (e.g. `claude mcp list` spawned the adapter and will exit
|
|
235
|
+
// before the handshake completes), serve the persisted catalog
|
|
236
|
+
// if available + valid against the current Hub version. Probe
|
|
237
|
+
// returns in <50ms with zero Hub round-trips.
|
|
238
|
+
if (opts.getCachedCatalog &&
|
|
239
|
+
opts.getIsIdentityReady &&
|
|
240
|
+
!opts.getIsIdentityReady()) {
|
|
241
|
+
const cached = opts.getCachedCatalog();
|
|
242
|
+
if (cached) {
|
|
243
|
+
const currentVersion = opts.getCurrentHubVersion?.() ?? null;
|
|
244
|
+
const valid = (opts.isCacheValid ?? (() => false))(cached, currentVersion);
|
|
245
|
+
if (valid) {
|
|
246
|
+
log("[ListTools] served from cache");
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
248
|
+
return { tools: cached.catalog };
|
|
249
|
+
}
|
|
250
|
+
log(`[ListTools] cache stale (cached.hubVersion=${cached.hubVersion}, current=${currentVersion ?? "unknown"}) — bootstrapping`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
log("[ListTools] no cache (bootstrapping cache from Hub)");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (opts.listToolsGate)
|
|
257
|
+
await opts.listToolsGate;
|
|
258
|
+
const agent = opts.getAgent();
|
|
259
|
+
if (!isUsableAgent(agent))
|
|
260
|
+
return { tools: [] };
|
|
261
|
+
// Route through agent.listTools() so any configured cognitive
|
|
262
|
+
// pipeline's onListTools middleware (ToolDescriptionEnricher,
|
|
263
|
+
// ResponseSummarizer, etc.) observes and modifies the surface.
|
|
264
|
+
const tools = await agent.listTools();
|
|
265
|
+
// Best-effort cache write-back. Failures log + continue; the
|
|
266
|
+
// primary response is already on its way to the host.
|
|
267
|
+
if (opts.persistCatalog) {
|
|
268
|
+
try {
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
270
|
+
opts.persistCatalog(tools);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
log(`[ListTools] persistCatalog hook threw (non-fatal): ${err.message ?? err}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
277
|
+
return { tools: tools };
|
|
278
|
+
});
|
|
279
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
280
|
+
const callStartedAt = Date.now();
|
|
281
|
+
const requestedTool = request.params.name;
|
|
282
|
+
log(`[CallTool] ${requestedTool} entered`);
|
|
283
|
+
try {
|
|
284
|
+
if (opts.callToolGate) {
|
|
285
|
+
log(`[CallTool] ${requestedTool} awaiting callToolGate`);
|
|
286
|
+
await opts.callToolGate;
|
|
287
|
+
log(`[CallTool] ${requestedTool} gate passed (+${Date.now() - callStartedAt}ms)`);
|
|
288
|
+
}
|
|
289
|
+
const agent = opts.getAgent();
|
|
290
|
+
if (!isUsableAgent(agent)) {
|
|
291
|
+
log(`[CallTool] ${requestedTool} aborted — Hub not connected`);
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: "text",
|
|
296
|
+
text: JSON.stringify({
|
|
297
|
+
error: "Hub not connected",
|
|
298
|
+
message: "The Hub adapter is not currently connected.",
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
isError: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const { name } = request.params;
|
|
306
|
+
const incomingArgs = (request.params.arguments ?? {});
|
|
307
|
+
const outgoingArgs = injectQueueItemId(name, incomingArgs, pendingActionMap);
|
|
308
|
+
// ── Mission-62 W3 — activity FSM signal wrapping ─────────────
|
|
309
|
+
//
|
|
310
|
+
// Wrap each LLM-driven tool call with signal_working_started +
|
|
311
|
+
// signal_working_completed RPCs (fire-and-forget; eventual-
|
|
312
|
+
// consistency on Hub-side activity FSM). Per Design v1.0 §5.2:
|
|
313
|
+
// implicit-only inference infeasible (LLM-to-MCP-tool-call
|
|
314
|
+
// path doesn't enqueue items per ADR-017 §M1-M2); explicit
|
|
315
|
+
// signaling required for routing peers to see this agent's
|
|
316
|
+
// working state.
|
|
317
|
+
//
|
|
318
|
+
// Skip-list prevents infinite recursion (signal_* calls would
|
|
319
|
+
// wrap themselves) + handshake/lifecycle tools that are not
|
|
320
|
+
// semantically tool-call-work (register_role, claim_session,
|
|
321
|
+
// drain_pending_actions).
|
|
322
|
+
const wrapWithSignal = !TOOL_CALL_SIGNAL_SKIP.has(name);
|
|
323
|
+
if (wrapWithSignal) {
|
|
324
|
+
// Fire-and-forget: don't await; Hub-side eventual-consistency
|
|
325
|
+
// is acceptable for v1.0 routing-cache use case.
|
|
326
|
+
agent.call("signal_working_started", { toolName: name }).catch((err) => {
|
|
327
|
+
log(`[mission-62] signal_working_started fire-and-forget failed (non-fatal): ${err?.message ?? err}`);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
let result;
|
|
331
|
+
const agentCallStart = Date.now();
|
|
332
|
+
log(`[CallTool] ${name} dispatching to agent.call (wrapWithSignal=${wrapWithSignal})`);
|
|
333
|
+
try {
|
|
334
|
+
result = await agent.call(name, outgoingArgs);
|
|
335
|
+
log(`[CallTool] ${name} agent.call returned in ${Date.now() - agentCallStart}ms`);
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
if (wrapWithSignal) {
|
|
339
|
+
agent.call("signal_working_completed", {}).catch((err) => {
|
|
340
|
+
log(`[mission-62] signal_working_completed fire-and-forget failed (non-fatal): ${err?.message ?? err}`);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
log(`[CallTool] ${name} completed in ${Date.now() - callStartedAt}ms total`);
|
|
345
|
+
return {
|
|
346
|
+
content: [
|
|
347
|
+
{
|
|
348
|
+
type: "text",
|
|
349
|
+
text: typeof result === "string"
|
|
350
|
+
? result
|
|
351
|
+
: JSON.stringify(result, null, 2),
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
358
|
+
log(`[CallTool] ${requestedTool} threw after ${Date.now() - callStartedAt}ms: ${message}`);
|
|
359
|
+
return {
|
|
360
|
+
content: [
|
|
361
|
+
{ type: "text", text: JSON.stringify({ error: message }) },
|
|
362
|
+
],
|
|
363
|
+
isError: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return server;
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
pendingActionMap,
|
|
371
|
+
createMcpServer,
|
|
372
|
+
callbacks,
|
|
373
|
+
getClientInfo,
|
|
374
|
+
makePendingActionItemHandler,
|
|
375
|
+
pollBackstop,
|
|
376
|
+
ackMessage,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../src/tool-manager/dispatcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-catalog-cache.ts — per-WORK_DIR Hub tool catalog cache.
|
|
3
|
+
*
|
|
4
|
+
* Probe-safe ListTools support: when the host calls tools/list before
|
|
5
|
+
* the adapter's identityReady has resolved (e.g. `claude mcp list`
|
|
6
|
+
* spawning the adapter just to enumerate available tools), the
|
|
7
|
+
* dispatcher serves the catalog from a persisted cache without
|
|
8
|
+
* touching the Hub. Together with the lazy session-claim path this
|
|
9
|
+
* makes probes fully Hub-free against a warm cache.
|
|
10
|
+
*
|
|
11
|
+
* Storage: $WORK_DIR/.ois/tool-catalog.json
|
|
12
|
+
* {
|
|
13
|
+
* schemaVersion: 1,
|
|
14
|
+
* hubVersion: "1.0.0",
|
|
15
|
+
* fetchedAt: "2026-04-22T...Z",
|
|
16
|
+
* catalog: [...]
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Invalidation: Hub-version mismatch only. No TTL — the catalog is
|
|
20
|
+
* static between Hub deploys; TTL would add noise without correctness
|
|
21
|
+
* value. Schema-version mismatch on read returns null (cache treated
|
|
22
|
+
* as invalid; future schema evolution bumps CATALOG_SCHEMA_VERSION).
|
|
23
|
+
*
|
|
24
|
+
* Atomicity: writeCache uses tmp-file + rename so partial writes on
|
|
25
|
+
* crash don't corrupt the cache. Parse errors on read also return
|
|
26
|
+
* null — the cache self-heals on next bootstrap.
|
|
27
|
+
*
|
|
28
|
+
* Failure modes (best-effort; readCache + writeCache never throw on
|
|
29
|
+
* the primary flow):
|
|
30
|
+
* - missing file: readCache returns null
|
|
31
|
+
* - parse error: readCache returns null + logs
|
|
32
|
+
* - schema-version mismatch: readCache returns null
|
|
33
|
+
* - $WORK_DIR readonly: writeCache logs + no-ops
|
|
34
|
+
* - disk full: writeCache logs + no-ops
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Bumping CATALOG_SCHEMA_VERSION forces all existing cache files to
|
|
38
|
+
* be treated as invalid + re-bootstrapped. Use when changing the
|
|
39
|
+
* cache file shape.
|
|
40
|
+
*/
|
|
41
|
+
export declare const CATALOG_SCHEMA_VERSION = 1;
|
|
42
|
+
/**
|
|
43
|
+
* MCP tool catalog entry shape. Kept loose (`unknown[]`) since the
|
|
44
|
+
* cache is opaque storage; the dispatcher hands the catalog back to
|
|
45
|
+
* the host without re-shaping.
|
|
46
|
+
*/
|
|
47
|
+
export type ToolCatalog = unknown[];
|
|
48
|
+
export interface CachedCatalog {
|
|
49
|
+
schemaVersion: number;
|
|
50
|
+
hubVersion: string;
|
|
51
|
+
fetchedAt: string;
|
|
52
|
+
catalog: ToolCatalog;
|
|
53
|
+
}
|
|
54
|
+
/** Compute the canonical cache path for a given WORK_DIR. */
|
|
55
|
+
export declare function cachePathFor(workDir: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Read the cache file. Returns null on missing file, parse error,
|
|
58
|
+
* schema-version mismatch, or shape mismatch. Never throws — the
|
|
59
|
+
* primary ListTools flow always falls through to a live Hub fetch
|
|
60
|
+
* when readCache returns null.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readCache(workDir: string, log?: (msg: string) => void): CachedCatalog | null;
|
|
63
|
+
/**
|
|
64
|
+
* Persist the catalog atomically. Writes a sibling tmp file then
|
|
65
|
+
* renames — so a crash mid-write leaves either the previous cache
|
|
66
|
+
* intact OR the new cache fully landed. Best-effort: failures log
|
|
67
|
+
* and return; the caller's primary flow continues.
|
|
68
|
+
*/
|
|
69
|
+
export declare function writeCache(workDir: string, catalog: ToolCatalog, hubVersion: string, log?: (msg: string) => void): void;
|
|
70
|
+
/**
|
|
71
|
+
* Check cache validity against the current Hub version.
|
|
72
|
+
*
|
|
73
|
+
* Two semantics:
|
|
74
|
+
* - currentHubVersion is a non-empty string: strict equality vs
|
|
75
|
+
* cached.hubVersion. Mismatch → invalid → caller re-bootstraps.
|
|
76
|
+
* - currentHubVersion is null/undefined/empty: caller doesn't know
|
|
77
|
+
* the current Hub version yet (e.g. /health fetch in flight at
|
|
78
|
+
* startup). Trust the cache (probe-friendly default) — worst
|
|
79
|
+
* case is serving a stale catalog ONCE until the next /health
|
|
80
|
+
* fetch completes and a real session refreshes the cache.
|
|
81
|
+
*
|
|
82
|
+
* Schema-version check is enforced inside readCache, so isCacheValid
|
|
83
|
+
* never sees a wrong-schema cached object.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isCacheValid(cached: CachedCatalog, currentHubVersion: string | null | undefined): boolean;
|