@haywoodmarx/agent-pager 0.1.1 → 0.1.2
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/src/broker/broker.js +30 -12
- package/dist/src/broker/protocol.d.ts +2 -2
- package/dist/src/implementations/claude/gateway-cli.js +17 -9
- package/dist/src/implementations/claude/gateway.d.ts +15 -1
- package/dist/src/implementations/claude/gateway.js +69 -12
- package/dist/src/messaging/mcp.js +28 -26
- package/dist/src/operator/commands.js +1 -0
- package/package.json +1 -1
|
@@ -712,6 +712,8 @@ export class Broker {
|
|
|
712
712
|
const visited = new Set([String(current.id)]);
|
|
713
713
|
while (Number(current.active) === 0) {
|
|
714
714
|
const nextId = current.superseded_by;
|
|
715
|
+
if (nextId === null)
|
|
716
|
+
throw this.#sessionEndedError(current);
|
|
715
717
|
if (typeof nextId !== "string" || nextId === "" || visited.has(nextId))
|
|
716
718
|
throw new ProtocolError("expired", SESSION_EXPIRED_MESSAGE);
|
|
717
719
|
const next = this.#db.prepare("select * from sessions where id = ?").get(nextId);
|
|
@@ -723,26 +725,42 @@ export class Broker {
|
|
|
723
725
|
return current;
|
|
724
726
|
}
|
|
725
727
|
/**
|
|
726
|
-
* Name why a known session no longer resolves
|
|
727
|
-
*
|
|
728
|
-
* An archive and a newer attachment both deactivate a session while its lease still has time
|
|
729
|
-
* left; reporting either as an expiry sends the holder looking for a timing problem. A session
|
|
730
|
-
* rotated to a new generation of the same attachment carries `superseded_by` and is not a
|
|
731
|
-
* replacement by another holder.
|
|
728
|
+
* Name why a known session no longer resolves.
|
|
732
729
|
* @param row - The session row the caller presented, inactive or past its lease.
|
|
733
|
-
* @returns -
|
|
730
|
+
* @returns - `ended` when the newest generation of the row's rotation lineage was deactivated by an
|
|
731
|
+
* operator archive or by a newer attachment of the same identity and channel, otherwise `expired`.
|
|
734
732
|
*/
|
|
735
733
|
#sessionEndedError(row) {
|
|
736
|
-
const
|
|
734
|
+
const newest = this.#newestGeneration(row);
|
|
735
|
+
if (Number(newest.active) === 1)
|
|
736
|
+
return new ProtocolError("expired", SESSION_EXPIRED_MESSAGE);
|
|
737
|
+
const archived = this.#db.prepare("select archived_at from agents where id = ?").get(String(newest.agent_id));
|
|
737
738
|
if (typeof archived?.archived_at === "number")
|
|
738
|
-
return new ProtocolError("
|
|
739
|
+
return new ProtocolError("ended", "MCP session ended because an operator archived this agent");
|
|
739
740
|
const replaced = this.#db
|
|
740
741
|
.prepare("select 1 as present from sessions where agent_id = ? and channel = ? and rowid > (select rowid from sessions where id = ?) limit 1")
|
|
741
|
-
.get(String(
|
|
742
|
-
if (
|
|
743
|
-
return new ProtocolError("
|
|
742
|
+
.get(String(newest.agent_id), String(newest.channel), String(newest.id));
|
|
743
|
+
if (replaced)
|
|
744
|
+
return new ProtocolError("ended", "MCP session ended because a newer attachment replaced it");
|
|
744
745
|
return new ProtocolError("expired", SESSION_EXPIRED_MESSAGE);
|
|
745
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Follow a session's rotation lineage to its newest generation, active or not.
|
|
749
|
+
* @param row - Any generation of the lineage.
|
|
750
|
+
* @returns - The last generation the lineage reaches, or the row itself when it was never rotated.
|
|
751
|
+
*/
|
|
752
|
+
#newestGeneration(row) {
|
|
753
|
+
let current = row;
|
|
754
|
+
const visited = new Set([String(current.id)]);
|
|
755
|
+
while (typeof current.superseded_by === "string" && !visited.has(current.superseded_by)) {
|
|
756
|
+
const next = this.#db.prepare("select * from sessions where id = ?").get(current.superseded_by);
|
|
757
|
+
if (!next)
|
|
758
|
+
break;
|
|
759
|
+
visited.add(String(next.id));
|
|
760
|
+
current = next;
|
|
761
|
+
}
|
|
762
|
+
return current;
|
|
763
|
+
}
|
|
746
764
|
/**
|
|
747
765
|
* Return the configured lifetime of an authenticated MCP attachment.
|
|
748
766
|
* @returns - Milliseconds from successful authentication until renewal is required.
|
|
@@ -188,7 +188,7 @@ export declare function lifecycleEventState(type: string): MessageState | null |
|
|
|
188
188
|
/** Stable structured protocol error emitted at broker trust boundaries. */
|
|
189
189
|
export declare class ProtocolError extends Error {
|
|
190
190
|
/** Identifies the machine-readable cause of the protocol failure. */
|
|
191
|
-
readonly code: "validation_failed" | "unsupported_version" | "oversized_content" | "unknown_recipient" | "recipient_alias_changed" | "alias_conflict" | "conflict" | "expired";
|
|
191
|
+
readonly code: "validation_failed" | "unsupported_version" | "oversized_content" | "unknown_recipient" | "recipient_alias_changed" | "alias_conflict" | "conflict" | "expired" | "ended";
|
|
192
192
|
/**
|
|
193
193
|
* Constructs an error that carries a protocol failure code.
|
|
194
194
|
* @param code - Stable protocol error category exposed to callers.
|
|
@@ -196,7 +196,7 @@ export declare class ProtocolError extends Error {
|
|
|
196
196
|
*/
|
|
197
197
|
constructor(
|
|
198
198
|
/** Identifies the machine-readable cause of the protocol failure. */
|
|
199
|
-
code: "validation_failed" | "unsupported_version" | "oversized_content" | "unknown_recipient" | "recipient_alias_changed" | "alias_conflict" | "conflict" | "expired", message: string);
|
|
199
|
+
code: "validation_failed" | "unsupported_version" | "oversized_content" | "unknown_recipient" | "recipient_alias_changed" | "alias_conflict" | "conflict" | "expired" | "ended", message: string);
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
202
|
* Generate a collision-resistant opaque token with a non-sensitive prefix.
|
|
@@ -24,7 +24,7 @@ try {
|
|
|
24
24
|
if (resolution.attached) {
|
|
25
25
|
let gateway;
|
|
26
26
|
try {
|
|
27
|
-
gateway = await startAttachedGateway(resolution);
|
|
27
|
+
gateway = await startAttachedGateway(resolution, identityPorts);
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|
|
30
30
|
// Claude Code restarts a stdio server with the environment of the original launch, whose
|
|
@@ -34,11 +34,7 @@ try {
|
|
|
34
34
|
if (sessionKey !== undefined) {
|
|
35
35
|
try {
|
|
36
36
|
const registered = await registerClaudeConversation(identityPorts, resolution.brokerUrl, sessionKey);
|
|
37
|
-
gateway = await startAttachedGateway({
|
|
38
|
-
...resolution,
|
|
39
|
-
reservation: registered.reservation,
|
|
40
|
-
reservationOrigin: "registration",
|
|
41
|
-
});
|
|
37
|
+
gateway = await startAttachedGateway({ ...resolution, reservation: registered.reservation, reservationOrigin: "registration" }, identityPorts);
|
|
42
38
|
}
|
|
43
39
|
catch (retryError) {
|
|
44
40
|
await serveUnattached({
|
|
@@ -59,7 +55,7 @@ try {
|
|
|
59
55
|
});
|
|
60
56
|
if (brokerUrl !== undefined) {
|
|
61
57
|
try {
|
|
62
|
-
gateway = await startAttachedGateway({ ...resolution, brokerUrl });
|
|
58
|
+
gateway = await startAttachedGateway({ ...resolution, brokerUrl }, identityPorts);
|
|
63
59
|
}
|
|
64
60
|
catch {
|
|
65
61
|
throw error;
|
|
@@ -104,12 +100,24 @@ async function serveUnattached(unattached) {
|
|
|
104
100
|
/**
|
|
105
101
|
* Attach to the broker with the given credentials and begin serving Claude over stdio.
|
|
106
102
|
* @param identity - Endpoint, reservation, and conversation key to attach with.
|
|
103
|
+
* @param identityPorts - Workspace and registration boundary the gateway rejoins its identity through,
|
|
104
|
+
* by the conversation key alone, when the broker can no longer resume its session.
|
|
107
105
|
* @returns - Running gateway; rejects, with the upstream released, when the attach is refused.
|
|
108
106
|
*/
|
|
109
|
-
async function startAttachedGateway(identity) {
|
|
107
|
+
async function startAttachedGateway(identity, identityPorts) {
|
|
110
108
|
const deliveryAddress = claudeDeliveryAddress(process.env);
|
|
109
|
+
const { sessionKey } = identity;
|
|
111
110
|
const upstream = createClaudeGatewayUpstream(identity.brokerUrl, identity.reservation, {
|
|
112
|
-
...(
|
|
111
|
+
...(sessionKey
|
|
112
|
+
? {
|
|
113
|
+
sessionKey,
|
|
114
|
+
rejoin: async () => (await identityPorts.register(identity.brokerUrl, {
|
|
115
|
+
workspacePath: identityPorts.workspacePath,
|
|
116
|
+
sessionKey,
|
|
117
|
+
continuesSessionKey: undefined,
|
|
118
|
+
})).reservation,
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
113
121
|
...(deliveryAddress.known ? { deliveryAddress: deliveryAddress.value } : {}),
|
|
114
122
|
scheduler: {
|
|
115
123
|
schedule: (delayMs, task) => {
|
|
@@ -26,6 +26,14 @@ export type ClaudeGatewayUpstreamOptions = {
|
|
|
26
26
|
deliveryAddress?: string;
|
|
27
27
|
/** Owns renewal timing for the lifetime of the upstream connection. */
|
|
28
28
|
scheduler: ClaudeGatewayScheduler;
|
|
29
|
+
/**
|
|
30
|
+
* Register this conversation again by its key, used when the broker refuses to resume the session
|
|
31
|
+
* this process holds as expired; absent when nothing identifies the conversation to register.
|
|
32
|
+
* @returns - Single-use attachment reservation for the identity the key resolves to.
|
|
33
|
+
*/
|
|
34
|
+
rejoin?: () => Promise<string>;
|
|
35
|
+
/** Sends one HTTP request to the broker; the global `fetch` when absent. */
|
|
36
|
+
fetch?: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
29
37
|
};
|
|
30
38
|
/** Supplies the upstream MCP operations forwarded by the Claude stdio server. */
|
|
31
39
|
export interface ClaudeGatewayUpstream {
|
|
@@ -55,9 +63,15 @@ export declare class ClaudeMcpGateway {
|
|
|
55
63
|
}
|
|
56
64
|
/**
|
|
57
65
|
* Construct the SDK client that authenticates to the singleton broker over Streamable HTTP.
|
|
66
|
+
*
|
|
67
|
+
* When the broker refuses the session this process holds as `expired` and `rejoin` is configured, a
|
|
68
|
+
* forwarded call registers the conversation again, attaches with the reservation that returns, and is
|
|
69
|
+
* retried once; calls refused together share one rejoin. A call refused because this process attached
|
|
70
|
+
* again while it was in flight is retried on the new attachment. Any other refusal, `ended` included,
|
|
71
|
+
* reaches the caller unchanged. Closing waits for a rejoin in flight and detaches what it attached.
|
|
58
72
|
* @param brokerUrl - Loopback MCP endpoint published by the singleton service.
|
|
59
73
|
* @param reservation - Single-use attachment credential consumed during first initialization.
|
|
60
|
-
* @param options - Conversation key presented at attach,
|
|
74
|
+
* @param options - Conversation key presented at attach, the renewal scheduler, and the rejoin.
|
|
61
75
|
* @returns - Upstream tool operations for one Claude session.
|
|
62
76
|
*/
|
|
63
77
|
export declare function createClaudeGatewayUpstream(brokerUrl: string, reservation: string, options: ClaudeGatewayUpstreamOptions): ClaudeGatewayUpstream;
|
|
@@ -2,10 +2,13 @@ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/cli
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
3
|
import { LOOPBACK_HOST } from "../../configuration-defaults.js";
|
|
4
4
|
import { ATTACH_HEADER } from "../../broker/protocol.js";
|
|
5
|
-
import { compatibilityToolRequest } from "../../messaging/delivery-client.js";
|
|
5
|
+
import { compatibilityPayload, compatibilityToolRequest } from "../../messaging/delivery-client.js";
|
|
6
6
|
import { DELIVERY_ADDRESS_HEADER } from "../../messaging/mcp.js";
|
|
7
7
|
import { PAGER_VERSION } from "../../version.js";
|
|
8
8
|
import { claudeUnattachedInstructions, claudeWakeInstructions } from "./instructions.js";
|
|
9
|
+
const CONSUMED = "conflict";
|
|
10
|
+
const EXPIRED = "expired";
|
|
11
|
+
const ENDED = "ended";
|
|
9
12
|
/** Connects the Claude-facing stdio server to the singleton Streamable HTTP MCP service. */
|
|
10
13
|
export class ClaudeMcpGateway {
|
|
11
14
|
#upstream;
|
|
@@ -34,30 +37,40 @@ export class ClaudeMcpGateway {
|
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
36
39
|
* Construct the SDK client that authenticates to the singleton broker over Streamable HTTP.
|
|
40
|
+
*
|
|
41
|
+
* When the broker refuses the session this process holds as `expired` and `rejoin` is configured, a
|
|
42
|
+
* forwarded call registers the conversation again, attaches with the reservation that returns, and is
|
|
43
|
+
* retried once; calls refused together share one rejoin. A call refused because this process attached
|
|
44
|
+
* again while it was in flight is retried on the new attachment. Any other refusal, `ended` included,
|
|
45
|
+
* reaches the caller unchanged. Closing waits for a rejoin in flight and detaches what it attached.
|
|
37
46
|
* @param brokerUrl - Loopback MCP endpoint published by the singleton service.
|
|
38
47
|
* @param reservation - Single-use attachment credential consumed during first initialization.
|
|
39
|
-
* @param options - Conversation key presented at attach,
|
|
48
|
+
* @param options - Conversation key presented at attach, the renewal scheduler, and the rejoin.
|
|
40
49
|
* @returns - Upstream tool operations for one Claude session.
|
|
41
50
|
*/
|
|
42
51
|
export function createClaudeGatewayUpstream(brokerUrl, reservation, options) {
|
|
43
52
|
const url = validatedBrokerUrl(brokerUrl);
|
|
44
53
|
if (!reservation)
|
|
45
54
|
throw new Error("Claude gateway authentication is missing");
|
|
55
|
+
const send = options.fetch ?? fetch;
|
|
46
56
|
const client = new Client({
|
|
47
57
|
name: "pager-claude-gateway",
|
|
48
58
|
version: PAGER_VERSION,
|
|
49
59
|
});
|
|
60
|
+
let attachReservation = reservation;
|
|
50
61
|
let session;
|
|
51
62
|
let leaseTtlMs;
|
|
52
63
|
let cancelRenewal;
|
|
53
64
|
let closed = false;
|
|
65
|
+
let rejoining;
|
|
66
|
+
let attachments = 0;
|
|
54
67
|
const scheduleRenewal = () => {
|
|
55
68
|
cancelRenewal?.();
|
|
56
69
|
if (closed || leaseTtlMs === undefined || leaseTtlMs <= 0)
|
|
57
70
|
return;
|
|
58
71
|
cancelRenewal = options.scheduler.schedule(Math.max(1, Math.floor(leaseTtlMs / 2)), async () => {
|
|
59
72
|
try {
|
|
60
|
-
await client.listTools();
|
|
73
|
+
await recovering(() => client.listTools());
|
|
61
74
|
}
|
|
62
75
|
finally {
|
|
63
76
|
scheduleRenewal();
|
|
@@ -68,8 +81,7 @@ export function createClaudeGatewayUpstream(brokerUrl, reservation, options) {
|
|
|
68
81
|
const headers = new Headers(init?.headers);
|
|
69
82
|
if (session)
|
|
70
83
|
headers.set("mcp-session-id", session.sessionId);
|
|
71
|
-
|
|
72
|
-
headers.set(ATTACH_HEADER, reservation);
|
|
84
|
+
headers.set(ATTACH_HEADER, attachReservation);
|
|
73
85
|
if (options.sessionKey)
|
|
74
86
|
headers.set("x-pager-session-key", options.sessionKey);
|
|
75
87
|
if (options.deliveryAddress)
|
|
@@ -80,7 +92,7 @@ export function createClaudeGatewayUpstream(brokerUrl, reservation, options) {
|
|
|
80
92
|
headers.set("x-pager-epoch", session.epoch);
|
|
81
93
|
let response;
|
|
82
94
|
try {
|
|
83
|
-
response = await
|
|
95
|
+
response = await send(input, { ...init, headers });
|
|
84
96
|
}
|
|
85
97
|
catch (cause) {
|
|
86
98
|
throw new Error(`Cannot reach the Pager broker at ${url.href}. It is not running or not listening there; start it with \`pager\`, then retry.`, { cause });
|
|
@@ -97,6 +109,48 @@ export function createClaudeGatewayUpstream(brokerUrl, reservation, options) {
|
|
|
97
109
|
return response;
|
|
98
110
|
};
|
|
99
111
|
const transport = new StreamableHTTPClientTransport(url, { fetch: brokerFetch });
|
|
112
|
+
const register = options.rejoin;
|
|
113
|
+
const rejoin = register
|
|
114
|
+
? () => (rejoining = (async () => {
|
|
115
|
+
const refusedSession = session;
|
|
116
|
+
attachReservation = await register();
|
|
117
|
+
// An attach that presents a session would be taken as that session's, not as a new attachment.
|
|
118
|
+
session = undefined;
|
|
119
|
+
try {
|
|
120
|
+
await compatibilityPayload(await brokerFetch(url, compatibilityToolRequest("attach", {})), "Broker attach");
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
session ??= refusedSession;
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
attachments += 1;
|
|
127
|
+
})().finally(() => {
|
|
128
|
+
rejoining = undefined;
|
|
129
|
+
}))
|
|
130
|
+
: undefined;
|
|
131
|
+
const recovering = async (call) => {
|
|
132
|
+
await rejoining?.catch(() => undefined);
|
|
133
|
+
const presentedAttachment = attachments;
|
|
134
|
+
try {
|
|
135
|
+
return await call();
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const refusal = brokerErrorCode(error);
|
|
139
|
+
if (closed || !rejoin || (refusal !== EXPIRED && refusal !== ENDED))
|
|
140
|
+
throw error;
|
|
141
|
+
if (rejoining)
|
|
142
|
+
await rejoining.catch(() => undefined);
|
|
143
|
+
else if (attachments === presentedAttachment) {
|
|
144
|
+
if (refusal !== EXPIRED)
|
|
145
|
+
throw error;
|
|
146
|
+
await rejoin();
|
|
147
|
+
}
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- close() can run while the rejoin above is awaited
|
|
149
|
+
if (closed)
|
|
150
|
+
throw error;
|
|
151
|
+
return call();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
100
154
|
return {
|
|
101
155
|
connect: async () => {
|
|
102
156
|
await client.connect(transport);
|
|
@@ -105,6 +159,7 @@ export function createClaudeGatewayUpstream(brokerUrl, reservation, options) {
|
|
|
105
159
|
close: async () => {
|
|
106
160
|
closed = true;
|
|
107
161
|
cancelRenewal?.();
|
|
162
|
+
await rejoining?.catch(() => undefined);
|
|
108
163
|
// The model-facing tools/call path refuses connector-only tools, so this travels the
|
|
109
164
|
// compatibility path. A failed notice leaves the lease to lapse, as after a crash.
|
|
110
165
|
if (session) {
|
|
@@ -112,8 +167,8 @@ export function createClaudeGatewayUpstream(brokerUrl, reservation, options) {
|
|
|
112
167
|
}
|
|
113
168
|
await client.close();
|
|
114
169
|
},
|
|
115
|
-
listTools: () => client.listTools(),
|
|
116
|
-
callTool: (params) => client.callTool(params),
|
|
170
|
+
listTools: () => recovering(() => client.listTools()),
|
|
171
|
+
callTool: (params) => recovering(() => client.callTool(params)),
|
|
117
172
|
};
|
|
118
173
|
}
|
|
119
174
|
/**
|
|
@@ -170,13 +225,15 @@ export function rejoinKeyAfterRefusedAttach(identity, error) {
|
|
|
170
225
|
* @returns - True only when the broker reported the reservation as consumed.
|
|
171
226
|
*/
|
|
172
227
|
export function attachRefusedAsConsumed(error) {
|
|
228
|
+
return brokerErrorCode(error) === CONSUMED;
|
|
229
|
+
}
|
|
230
|
+
function brokerErrorCode(error) {
|
|
173
231
|
if (typeof error !== "object" || error === null || !("data" in error))
|
|
174
|
-
return
|
|
232
|
+
return undefined;
|
|
175
233
|
const data = error.data;
|
|
176
234
|
if (typeof data !== "object" || data === null || !("code" in data))
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
return data.code === code;
|
|
235
|
+
return undefined;
|
|
236
|
+
return data.code;
|
|
180
237
|
}
|
|
181
238
|
/**
|
|
182
239
|
* Report whether an upstream connect failed because the broker named at launch could not be reached
|
|
@@ -90,6 +90,8 @@ async function handleRequest(context, request, response) {
|
|
|
90
90
|
const call = readToolCall(body);
|
|
91
91
|
// The pre-attach tools carry no session and no identity.
|
|
92
92
|
const attached = call.tool === "attach" || call.tool === "register_agent" ? undefined : authenticate(broker, request);
|
|
93
|
+
if (attached)
|
|
94
|
+
announceRefreshedSession(response, attached);
|
|
93
95
|
// A detaching connector is leaving, so its push address is not taken up again.
|
|
94
96
|
if (attached && call.tool !== "detach")
|
|
95
97
|
registerDeliveryAddress(context, request, attached.agentId);
|
|
@@ -107,7 +109,7 @@ async function handleRequest(context, request, response) {
|
|
|
107
109
|
...("fencingToken" in result ? { "x-pager-resume-token": String(result.fencingToken) } : {}),
|
|
108
110
|
...("agentId" in result ? { "x-pager-agent-id": String(result.agentId) } : {}),
|
|
109
111
|
}
|
|
110
|
-
:
|
|
112
|
+
: {};
|
|
111
113
|
writeJson(response, 200, { ok: true, result }, headers);
|
|
112
114
|
}
|
|
113
115
|
catch (error) {
|
|
@@ -159,19 +161,19 @@ function readToolCall(body) {
|
|
|
159
161
|
return { tool, args: args ?? {} };
|
|
160
162
|
}
|
|
161
163
|
/**
|
|
162
|
-
* Announce a session generation the caller does not yet hold.
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
164
|
+
* Announce a session generation the caller does not yet hold, on whichever response the request ends with.
|
|
165
|
+
*
|
|
166
|
+
* Called as soon as the request authenticates, so a request that resumed its session in place carries the
|
|
167
|
+
* replacement on every answer below, an error included.
|
|
168
|
+
* @param response - Response to the request that authenticated.
|
|
169
|
+
* @param attached - Authenticated session, carrying `refreshed` when this request resumed it in place.
|
|
166
170
|
*/
|
|
167
|
-
function
|
|
168
|
-
if (!attached
|
|
169
|
-
return
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"x-pager-resume-token": attached.refreshed.fencingToken,
|
|
174
|
-
};
|
|
171
|
+
function announceRefreshedSession(response, attached) {
|
|
172
|
+
if (!attached.refreshed)
|
|
173
|
+
return;
|
|
174
|
+
response.setHeader("mcp-session-id", attached.sessionId);
|
|
175
|
+
response.setHeader("x-pager-epoch", attached.epoch);
|
|
176
|
+
response.setHeader("x-pager-resume-token", attached.refreshed.fencingToken);
|
|
175
177
|
}
|
|
176
178
|
function authenticate(broker, request) {
|
|
177
179
|
const sessionId = String(request.headers["mcp-session-id"] ?? "");
|
|
@@ -342,32 +344,32 @@ function handleMcpRequest(context, request, response, rpc) {
|
|
|
342
344
|
return;
|
|
343
345
|
}
|
|
344
346
|
if (rpc.method === "notifications/initialized") {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// credentials the server superseded on this very request.
|
|
348
|
-
response.writeHead(202, refreshedSessionHeaders(authenticate(broker, request)));
|
|
347
|
+
announceRefreshedSession(response, authenticate(broker, request));
|
|
348
|
+
response.writeHead(202);
|
|
349
349
|
response.end();
|
|
350
350
|
return;
|
|
351
351
|
}
|
|
352
352
|
const attached = authenticate(broker, request);
|
|
353
|
-
|
|
354
|
-
// must carry it. Computed before registerDeliveryAddress for the same reason as initialize's headers
|
|
355
|
-
// above: a throw there must still answer with it.
|
|
356
|
-
const refreshed = refreshedSessionHeaders(attached);
|
|
353
|
+
announceRefreshedSession(response, attached);
|
|
357
354
|
try {
|
|
358
355
|
registerDeliveryAddress(context, request, attached.agentId);
|
|
359
356
|
}
|
|
360
357
|
catch (error) {
|
|
361
358
|
if (error instanceof ProtocolError) {
|
|
362
|
-
writeMcpError(response, rpc.id, JSON_RPC_SERVER_ERROR, error.message, 200,
|
|
359
|
+
writeMcpError(response, rpc.id, JSON_RPC_SERVER_ERROR, error.message, 200, {}, { code: error.code });
|
|
363
360
|
}
|
|
364
361
|
else {
|
|
365
|
-
writeMcpError(response, rpc.id, JSON_RPC_SERVER_ERROR, INTERNAL_ERROR_MESSAGE, 500
|
|
362
|
+
writeMcpError(response, rpc.id, JSON_RPC_SERVER_ERROR, INTERNAL_ERROR_MESSAGE, 500);
|
|
366
363
|
}
|
|
367
364
|
return;
|
|
368
365
|
}
|
|
366
|
+
if (rpc.method.startsWith("notifications/")) {
|
|
367
|
+
response.writeHead(202);
|
|
368
|
+
response.end();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
369
371
|
if (rpc.method === "tools/list") {
|
|
370
|
-
writeMcpResult(response, rpc.id, { tools: mcpTools() }
|
|
372
|
+
writeMcpResult(response, rpc.id, { tools: mcpTools() });
|
|
371
373
|
return;
|
|
372
374
|
}
|
|
373
375
|
if (rpc.method === "tools/call") {
|
|
@@ -377,13 +379,13 @@ function handleMcpRequest(context, request, response, rpc) {
|
|
|
377
379
|
const args = isPlainObject(supplied) ? supplied : {};
|
|
378
380
|
try {
|
|
379
381
|
const result = callTool(context, attached, name, args, "model");
|
|
380
|
-
writeMcpResult(response, rpc.id, { content: [{ type: "text", text: JSON.stringify(result) }] }
|
|
382
|
+
writeMcpResult(response, rpc.id, { content: [{ type: "text", text: JSON.stringify(result) }] });
|
|
381
383
|
}
|
|
382
384
|
catch (error) {
|
|
383
385
|
const failure = error instanceof ProtocolError
|
|
384
386
|
? { code: error.code, message: error.message }
|
|
385
387
|
: { code: INTERNAL_ERROR_CODE, message: INTERNAL_ERROR_MESSAGE };
|
|
386
|
-
writeMcpResult(response, rpc.id, { content: [{ type: "text", text: JSON.stringify(failure) }], isError: true }
|
|
388
|
+
writeMcpResult(response, rpc.id, { content: [{ type: "text", text: JSON.stringify(failure) }], isError: true });
|
|
387
389
|
}
|
|
388
390
|
return;
|
|
389
391
|
}
|