@journal.one/gateway-client 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/server.d.ts +84 -0
- package/dist/server.js +465 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/package.json +34 -0
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Integration, ToolDefinition, ToolResult, Skill } from "./types.js";
|
|
2
|
+
export interface TokenValidationResult {
|
|
3
|
+
organizationId: string;
|
|
4
|
+
organizationName?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GatewayServerOptions {
|
|
7
|
+
port?: number;
|
|
8
|
+
validateToken: (token: string) => Promise<TokenValidationResult | null>;
|
|
9
|
+
pingIntervalMs?: number;
|
|
10
|
+
pullTimeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ConnectedGateway {
|
|
13
|
+
id: string;
|
|
14
|
+
organizationId: string;
|
|
15
|
+
protocolVersion: number;
|
|
16
|
+
gatewayVersion: string;
|
|
17
|
+
integrations: Integration[];
|
|
18
|
+
mcpVersion: string | null;
|
|
19
|
+
skillsVersion: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare class GatewayServer {
|
|
22
|
+
private options;
|
|
23
|
+
private wss;
|
|
24
|
+
private gateways;
|
|
25
|
+
private _port;
|
|
26
|
+
private pingTimer;
|
|
27
|
+
private connCounter;
|
|
28
|
+
private reqCounter;
|
|
29
|
+
private pullCounter;
|
|
30
|
+
private pullTimeoutMs;
|
|
31
|
+
constructor(options: GatewayServerOptions);
|
|
32
|
+
get port(): number;
|
|
33
|
+
get url(): string;
|
|
34
|
+
get connectedGateways(): ConnectedGateway[];
|
|
35
|
+
get availableTools(): Array<{
|
|
36
|
+
integrationId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
}>;
|
|
40
|
+
onGatewayConnected?: (gateway: ConnectedGateway) => void;
|
|
41
|
+
onGatewayUpdated?: (gateway: ConnectedGateway) => void;
|
|
42
|
+
onGatewayDisconnected?: (gateway: ConnectedGateway) => void;
|
|
43
|
+
start(): Promise<void>;
|
|
44
|
+
stop(): Promise<void>;
|
|
45
|
+
callTool(integrationId: string, toolName: string, args: Record<string, unknown>, timeoutMs?: number): Promise<ToolResult>;
|
|
46
|
+
/** Check if any gateway is connected for the given organization. */
|
|
47
|
+
hasGatewayForOrg(organizationId: string): boolean;
|
|
48
|
+
/** Get all connected gateways for an organization. */
|
|
49
|
+
getGatewaysForOrg(organizationId: string): ConnectedGateway[];
|
|
50
|
+
/** Get deduplicated tools across all gateways for an organization. */
|
|
51
|
+
getToolsForOrg(organizationId: string): Array<{
|
|
52
|
+
integrationId: string;
|
|
53
|
+
tool: ToolDefinition;
|
|
54
|
+
}>;
|
|
55
|
+
/** Pull current versions from a gateway. */
|
|
56
|
+
getVersions(gatewayId: string): Promise<{
|
|
57
|
+
mcpVersion: string | null;
|
|
58
|
+
skillsVersion: string | null;
|
|
59
|
+
}>;
|
|
60
|
+
/** Pull tools from a gateway. */
|
|
61
|
+
getTools(gatewayId: string): Promise<{
|
|
62
|
+
integrations: Integration[];
|
|
63
|
+
mcpVersion: string | null;
|
|
64
|
+
}>;
|
|
65
|
+
/** Pull skills from a gateway. */
|
|
66
|
+
getSkills(gatewayId: string): Promise<{
|
|
67
|
+
skills: Skill[];
|
|
68
|
+
skillsVersion: string | null;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Call a tool on any gateway for the given organization that provides the
|
|
72
|
+
* requested integration. Picks a random candidate for load balancing and
|
|
73
|
+
* retries on a different one if the call fails with a connection error.
|
|
74
|
+
*/
|
|
75
|
+
callToolForOrg(organizationId: string, integrationId: string, toolName: string, args: Record<string, unknown>, timeoutMs?: number): Promise<ToolResult>;
|
|
76
|
+
private callToolOnEntry;
|
|
77
|
+
private sendPull;
|
|
78
|
+
private resolvePull;
|
|
79
|
+
private autoPull;
|
|
80
|
+
private pullTools;
|
|
81
|
+
private pullSkills;
|
|
82
|
+
private handleConnection;
|
|
83
|
+
private sendPings;
|
|
84
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import { GatewayMessageSchema } from "./types.js";
|
|
3
|
+
export class GatewayServer {
|
|
4
|
+
options;
|
|
5
|
+
wss = null;
|
|
6
|
+
gateways = new Map();
|
|
7
|
+
_port;
|
|
8
|
+
pingTimer = null;
|
|
9
|
+
connCounter = 0;
|
|
10
|
+
reqCounter = 0;
|
|
11
|
+
pullCounter = 0;
|
|
12
|
+
pullTimeoutMs;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this._port = options.port ?? 0;
|
|
16
|
+
this.pullTimeoutMs = options.pullTimeoutMs ?? 30_000;
|
|
17
|
+
}
|
|
18
|
+
get port() {
|
|
19
|
+
return this._port;
|
|
20
|
+
}
|
|
21
|
+
get url() {
|
|
22
|
+
return `ws://localhost:${this._port}`;
|
|
23
|
+
}
|
|
24
|
+
get connectedGateways() {
|
|
25
|
+
return Array.from(this.gateways.values()).map((g) => g.gateway);
|
|
26
|
+
}
|
|
27
|
+
get availableTools() {
|
|
28
|
+
const tools = [];
|
|
29
|
+
for (const { gateway } of this.gateways.values()) {
|
|
30
|
+
for (const integration of gateway.integrations) {
|
|
31
|
+
for (const tool of integration.tools) {
|
|
32
|
+
tools.push({
|
|
33
|
+
integrationId: integration.id,
|
|
34
|
+
name: tool.name,
|
|
35
|
+
description: tool.description,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return tools;
|
|
41
|
+
}
|
|
42
|
+
onGatewayConnected;
|
|
43
|
+
onGatewayUpdated;
|
|
44
|
+
onGatewayDisconnected;
|
|
45
|
+
async start() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
this.wss = new WebSocketServer({ port: this._port }, () => {
|
|
48
|
+
const addr = this.wss.address();
|
|
49
|
+
if (typeof addr === "object" && addr !== null) {
|
|
50
|
+
this._port = addr.port;
|
|
51
|
+
}
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
this.wss.on("connection", (ws) => this.handleConnection(ws));
|
|
55
|
+
const interval = this.options.pingIntervalMs ?? 30_000;
|
|
56
|
+
if (interval > 0) {
|
|
57
|
+
this.pingTimer = setInterval(() => this.sendPings(), interval);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async stop() {
|
|
62
|
+
if (this.pingTimer) {
|
|
63
|
+
clearInterval(this.pingTimer);
|
|
64
|
+
this.pingTimer = null;
|
|
65
|
+
}
|
|
66
|
+
for (const entry of this.gateways.values()) {
|
|
67
|
+
if (entry.pongTimer)
|
|
68
|
+
clearTimeout(entry.pongTimer);
|
|
69
|
+
for (const pending of entry.pending.values()) {
|
|
70
|
+
clearTimeout(pending.timer);
|
|
71
|
+
pending.reject(new Error("Server shutting down"));
|
|
72
|
+
}
|
|
73
|
+
for (const pull of entry.pendingPulls.values()) {
|
|
74
|
+
clearTimeout(pull.timer);
|
|
75
|
+
pull.reject(new Error("Server shutting down"));
|
|
76
|
+
}
|
|
77
|
+
entry.ws.close();
|
|
78
|
+
}
|
|
79
|
+
this.gateways.clear();
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
if (this.wss) {
|
|
82
|
+
this.wss.close(() => resolve());
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
resolve();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async callTool(integrationId, toolName, args, timeoutMs = 90_000) {
|
|
90
|
+
let targetEntry;
|
|
91
|
+
for (const entry of this.gateways.values()) {
|
|
92
|
+
if (entry.gateway.integrations.some((i) => i.id === integrationId)) {
|
|
93
|
+
targetEntry = entry;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!targetEntry) {
|
|
98
|
+
throw new Error(`No gateway has integration "${integrationId}"`);
|
|
99
|
+
}
|
|
100
|
+
return this.callToolOnEntry(targetEntry, integrationId, toolName, args, timeoutMs);
|
|
101
|
+
}
|
|
102
|
+
/** Check if any gateway is connected for the given organization. */
|
|
103
|
+
hasGatewayForOrg(organizationId) {
|
|
104
|
+
for (const { gateway } of this.gateways.values()) {
|
|
105
|
+
if (gateway.organizationId === organizationId)
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
/** Get all connected gateways for an organization. */
|
|
111
|
+
getGatewaysForOrg(organizationId) {
|
|
112
|
+
const result = [];
|
|
113
|
+
for (const { gateway } of this.gateways.values()) {
|
|
114
|
+
if (gateway.organizationId === organizationId) {
|
|
115
|
+
result.push(gateway);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
/** Get deduplicated tools across all gateways for an organization. */
|
|
121
|
+
getToolsForOrg(organizationId) {
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const tools = [];
|
|
124
|
+
for (const { gateway } of this.gateways.values()) {
|
|
125
|
+
if (gateway.organizationId !== organizationId)
|
|
126
|
+
continue;
|
|
127
|
+
for (const integration of gateway.integrations) {
|
|
128
|
+
for (const tool of integration.tools) {
|
|
129
|
+
const key = `${integration.id}.${tool.name}`;
|
|
130
|
+
if (!seen.has(key)) {
|
|
131
|
+
seen.add(key);
|
|
132
|
+
tools.push({ integrationId: integration.id, tool });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return tools;
|
|
138
|
+
}
|
|
139
|
+
/** Pull current versions from a gateway. */
|
|
140
|
+
async getVersions(gatewayId) {
|
|
141
|
+
const data = await this.sendPull(gatewayId, "get_versions");
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
144
|
+
/** Pull tools from a gateway. */
|
|
145
|
+
async getTools(gatewayId) {
|
|
146
|
+
const data = await this.sendPull(gatewayId, "get_tools");
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
/** Pull skills from a gateway. */
|
|
150
|
+
async getSkills(gatewayId) {
|
|
151
|
+
const data = await this.sendPull(gatewayId, "get_skills");
|
|
152
|
+
return data;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Call a tool on any gateway for the given organization that provides the
|
|
156
|
+
* requested integration. Picks a random candidate for load balancing and
|
|
157
|
+
* retries on a different one if the call fails with a connection error.
|
|
158
|
+
*/
|
|
159
|
+
async callToolForOrg(organizationId, integrationId, toolName, args, timeoutMs = 90_000) {
|
|
160
|
+
const candidates = [];
|
|
161
|
+
for (const entry of this.gateways.values()) {
|
|
162
|
+
if (entry.gateway.organizationId === organizationId &&
|
|
163
|
+
entry.gateway.integrations.some((i) => i.id === integrationId)) {
|
|
164
|
+
candidates.push(entry);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (candidates.length === 0) {
|
|
168
|
+
throw new Error(`No gateway for org "${organizationId}" has integration "${integrationId}"`);
|
|
169
|
+
}
|
|
170
|
+
// Shuffle candidates for load balancing
|
|
171
|
+
for (let i = candidates.length - 1; i > 0; i--) {
|
|
172
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
173
|
+
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
|
|
174
|
+
}
|
|
175
|
+
let lastError;
|
|
176
|
+
for (const entry of candidates) {
|
|
177
|
+
try {
|
|
178
|
+
return await this.callToolOnEntry(entry, integrationId, toolName, args, timeoutMs);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
182
|
+
// Only retry on connection-level errors
|
|
183
|
+
if (!lastError.message.includes("Gateway disconnected")) {
|
|
184
|
+
throw lastError;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw lastError ?? new Error("All gateway candidates failed");
|
|
189
|
+
}
|
|
190
|
+
callToolOnEntry(entry, integrationId, toolName, args, timeoutMs) {
|
|
191
|
+
const requestId = `req_${++this.reqCounter}`;
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
entry.pending.delete(requestId);
|
|
195
|
+
reject(new Error(`Tool call timed out after ${timeoutMs}ms`));
|
|
196
|
+
}, timeoutMs);
|
|
197
|
+
entry.pending.set(requestId, { resolve, reject, timer });
|
|
198
|
+
entry.ws.send(JSON.stringify({
|
|
199
|
+
type: "tool_call",
|
|
200
|
+
requestId,
|
|
201
|
+
integrationId,
|
|
202
|
+
toolName,
|
|
203
|
+
arguments: args,
|
|
204
|
+
}));
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
sendPull(gatewayId, type) {
|
|
208
|
+
const entry = this.gateways.get(gatewayId);
|
|
209
|
+
if (!entry) {
|
|
210
|
+
return Promise.reject(new Error(`Gateway "${gatewayId}" not found`));
|
|
211
|
+
}
|
|
212
|
+
const requestId = `pull_${++this.pullCounter}`;
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
entry.pendingPulls.delete(requestId);
|
|
216
|
+
reject(new Error(`Pull ${type} timed out`));
|
|
217
|
+
}, this.pullTimeoutMs);
|
|
218
|
+
entry.pendingPulls.set(requestId, { resolve, reject, timer });
|
|
219
|
+
entry.ws.send(JSON.stringify({ type, requestId }));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
resolvePull(connId, requestId, data) {
|
|
223
|
+
const entry = this.gateways.get(connId);
|
|
224
|
+
if (!entry)
|
|
225
|
+
return;
|
|
226
|
+
const pull = entry.pendingPulls.get(requestId);
|
|
227
|
+
if (pull) {
|
|
228
|
+
clearTimeout(pull.timer);
|
|
229
|
+
entry.pendingPulls.delete(requestId);
|
|
230
|
+
pull.resolve(data);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async autoPull(connId) {
|
|
234
|
+
const entry = this.gateways.get(connId);
|
|
235
|
+
if (!entry)
|
|
236
|
+
return;
|
|
237
|
+
const pulls = [];
|
|
238
|
+
if (entry.gateway.mcpVersion !== null) {
|
|
239
|
+
pulls.push(this.pullTools(connId));
|
|
240
|
+
}
|
|
241
|
+
if (entry.gateway.skillsVersion !== null) {
|
|
242
|
+
pulls.push(this.pullSkills(connId));
|
|
243
|
+
}
|
|
244
|
+
await Promise.all(pulls);
|
|
245
|
+
}
|
|
246
|
+
async pullTools(connId) {
|
|
247
|
+
const entry = this.gateways.get(connId);
|
|
248
|
+
if (!entry)
|
|
249
|
+
return;
|
|
250
|
+
const data = await this.sendPull(connId, "get_tools");
|
|
251
|
+
entry.gateway.integrations = [
|
|
252
|
+
...data.integrations,
|
|
253
|
+
...entry.gateway.integrations.filter((i) => i.id === "skills"),
|
|
254
|
+
];
|
|
255
|
+
entry.gateway.mcpVersion = data.mcpVersion;
|
|
256
|
+
}
|
|
257
|
+
async pullSkills(connId) {
|
|
258
|
+
const entry = this.gateways.get(connId);
|
|
259
|
+
if (!entry)
|
|
260
|
+
return;
|
|
261
|
+
const data = await this.sendPull(connId, "get_skills");
|
|
262
|
+
// Build skills integration if any skills exist
|
|
263
|
+
const nonSkillIntegrations = entry.gateway.integrations.filter((i) => i.id !== "skills");
|
|
264
|
+
if (data.skills.length > 0) {
|
|
265
|
+
nonSkillIntegrations.push({
|
|
266
|
+
id: "skills",
|
|
267
|
+
name: "Skills",
|
|
268
|
+
description: "Gateway skills",
|
|
269
|
+
tools: [],
|
|
270
|
+
skills: data.skills,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
entry.gateway.integrations = nonSkillIntegrations;
|
|
274
|
+
entry.gateway.skillsVersion = data.skillsVersion;
|
|
275
|
+
}
|
|
276
|
+
handleConnection(ws) {
|
|
277
|
+
const connId = `gw_${++this.connCounter}`;
|
|
278
|
+
let authenticated = false;
|
|
279
|
+
let organizationId = "";
|
|
280
|
+
let protocolVersion = 0;
|
|
281
|
+
let gatewayVersion = "unknown";
|
|
282
|
+
const authTimer = setTimeout(() => {
|
|
283
|
+
if (!authenticated) {
|
|
284
|
+
ws.close();
|
|
285
|
+
}
|
|
286
|
+
}, 10_000);
|
|
287
|
+
ws.on("message", async (data) => {
|
|
288
|
+
let msg;
|
|
289
|
+
try {
|
|
290
|
+
msg = GatewayMessageSchema.parse(JSON.parse(data.toString()));
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
switch (msg.type) {
|
|
296
|
+
case "authenticate": {
|
|
297
|
+
const result = await this.options.validateToken(msg.token);
|
|
298
|
+
if (result) {
|
|
299
|
+
clearTimeout(authTimer);
|
|
300
|
+
authenticated = true;
|
|
301
|
+
organizationId = result.organizationId;
|
|
302
|
+
protocolVersion = msg.protocolVersion;
|
|
303
|
+
gatewayVersion = msg.gatewayVersion;
|
|
304
|
+
ws.send(JSON.stringify({
|
|
305
|
+
type: "authenticated",
|
|
306
|
+
organizationId: result.organizationId,
|
|
307
|
+
...(result.organizationName
|
|
308
|
+
? { organizationName: result.organizationName }
|
|
309
|
+
: {}),
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
clearTimeout(authTimer);
|
|
314
|
+
ws.send(JSON.stringify({
|
|
315
|
+
type: "auth_error",
|
|
316
|
+
error: "Invalid token",
|
|
317
|
+
}));
|
|
318
|
+
ws.close();
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case "version_changed": {
|
|
323
|
+
if (!authenticated) {
|
|
324
|
+
ws.close();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const mcpVersion = msg.mcpVersion;
|
|
328
|
+
const skillsVersion = msg.skillsVersion;
|
|
329
|
+
const existing = this.gateways.get(connId);
|
|
330
|
+
if (existing) {
|
|
331
|
+
// Subsequent version_changed: update versions and pull what changed
|
|
332
|
+
const mcpChanged = mcpVersion !== existing.gateway.mcpVersion;
|
|
333
|
+
const skillsChanged = skillsVersion !== existing.gateway.skillsVersion;
|
|
334
|
+
existing.gateway.mcpVersion = mcpVersion;
|
|
335
|
+
existing.gateway.skillsVersion = skillsVersion;
|
|
336
|
+
const pulls = [];
|
|
337
|
+
if (mcpChanged && mcpVersion !== null) {
|
|
338
|
+
pulls.push(this.pullTools(connId));
|
|
339
|
+
}
|
|
340
|
+
else if (mcpChanged && mcpVersion === null) {
|
|
341
|
+
// MCP removed: clear tool integrations
|
|
342
|
+
existing.gateway.integrations = existing.gateway.integrations.filter((i) => i.id === "skills");
|
|
343
|
+
}
|
|
344
|
+
if (skillsChanged && skillsVersion !== null) {
|
|
345
|
+
pulls.push(this.pullSkills(connId));
|
|
346
|
+
}
|
|
347
|
+
else if (skillsChanged && skillsVersion === null) {
|
|
348
|
+
// Skills removed: clear skills integrations
|
|
349
|
+
existing.gateway.integrations = existing.gateway.integrations.filter((i) => i.id !== "skills");
|
|
350
|
+
}
|
|
351
|
+
if (pulls.length > 0) {
|
|
352
|
+
await Promise.all(pulls);
|
|
353
|
+
}
|
|
354
|
+
this.onGatewayUpdated?.(existing.gateway);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// First version_changed: create gateway entry, auto-pull, then fire connected
|
|
358
|
+
const gateway = {
|
|
359
|
+
id: connId,
|
|
360
|
+
organizationId,
|
|
361
|
+
protocolVersion,
|
|
362
|
+
gatewayVersion,
|
|
363
|
+
integrations: [],
|
|
364
|
+
mcpVersion,
|
|
365
|
+
skillsVersion,
|
|
366
|
+
};
|
|
367
|
+
const entry = {
|
|
368
|
+
ws,
|
|
369
|
+
gateway,
|
|
370
|
+
pending: new Map(),
|
|
371
|
+
pendingPulls: new Map(),
|
|
372
|
+
pongTimer: null,
|
|
373
|
+
};
|
|
374
|
+
this.gateways.set(connId, entry);
|
|
375
|
+
// Auto-pull and then fire connected callback
|
|
376
|
+
await this.autoPull(connId);
|
|
377
|
+
this.onGatewayConnected?.(gateway);
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case "tool_result": {
|
|
382
|
+
const entry = this.gateways.get(connId);
|
|
383
|
+
if (!entry)
|
|
384
|
+
return;
|
|
385
|
+
const pending = entry.pending.get(msg.requestId);
|
|
386
|
+
if (pending) {
|
|
387
|
+
clearTimeout(pending.timer);
|
|
388
|
+
entry.pending.delete(msg.requestId);
|
|
389
|
+
pending.resolve(msg.result);
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case "tool_error": {
|
|
394
|
+
const entry = this.gateways.get(connId);
|
|
395
|
+
if (!entry)
|
|
396
|
+
return;
|
|
397
|
+
const pending = entry.pending.get(msg.requestId);
|
|
398
|
+
if (pending) {
|
|
399
|
+
clearTimeout(pending.timer);
|
|
400
|
+
entry.pending.delete(msg.requestId);
|
|
401
|
+
pending.reject(new Error(`Tool error [${msg.error.code}]: ${msg.error.message}`));
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "pong": {
|
|
406
|
+
const entry = this.gateways.get(connId);
|
|
407
|
+
if (entry?.pongTimer) {
|
|
408
|
+
clearTimeout(entry.pongTimer);
|
|
409
|
+
entry.pongTimer = null;
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case "versions": {
|
|
414
|
+
this.resolvePull(connId, msg.requestId, {
|
|
415
|
+
mcpVersion: msg.mcpVersion,
|
|
416
|
+
skillsVersion: msg.skillsVersion,
|
|
417
|
+
});
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "tools": {
|
|
421
|
+
this.resolvePull(connId, msg.requestId, {
|
|
422
|
+
integrations: msg.integrations,
|
|
423
|
+
mcpVersion: msg.mcpVersion,
|
|
424
|
+
});
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
case "skills": {
|
|
428
|
+
this.resolvePull(connId, msg.requestId, {
|
|
429
|
+
skills: msg.skills,
|
|
430
|
+
skillsVersion: msg.skillsVersion,
|
|
431
|
+
});
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
ws.on("close", () => {
|
|
437
|
+
clearTimeout(authTimer);
|
|
438
|
+
const entry = this.gateways.get(connId);
|
|
439
|
+
if (entry) {
|
|
440
|
+
if (entry.pongTimer)
|
|
441
|
+
clearTimeout(entry.pongTimer);
|
|
442
|
+
for (const pending of entry.pending.values()) {
|
|
443
|
+
clearTimeout(pending.timer);
|
|
444
|
+
pending.reject(new Error("Gateway disconnected"));
|
|
445
|
+
}
|
|
446
|
+
for (const pull of entry.pendingPulls.values()) {
|
|
447
|
+
clearTimeout(pull.timer);
|
|
448
|
+
pull.reject(new Error("Gateway disconnected"));
|
|
449
|
+
}
|
|
450
|
+
this.gateways.delete(connId);
|
|
451
|
+
this.onGatewayDisconnected?.(entry.gateway);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
sendPings() {
|
|
456
|
+
for (const entry of this.gateways.values()) {
|
|
457
|
+
entry.ws.send(JSON.stringify({ type: "ping" }));
|
|
458
|
+
if (entry.pongTimer)
|
|
459
|
+
clearTimeout(entry.pongTimer);
|
|
460
|
+
entry.pongTimer = setTimeout(() => {
|
|
461
|
+
entry.ws.close();
|
|
462
|
+
}, 10_000);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export type { Integration, ToolDefinition, ToolResult, ContentBlock, TextContent, ImageContent, Skill, GatewayError, GatewayErrorCode, GatewayMessage, VersionChangedMessage, VersionsMessage, ToolsMessage, SkillsMessage, GatewayVersions, ServiceMessage, } from "@journal.one/gateway-protocol";
|
|
2
|
+
export { GatewayMessageSchema, VersionChangedMessageSchema, VersionsMessageSchema, ToolsMessageSchema, SkillsMessageSchema, ServiceMessageSchema, IntegrationSchema, ToolResultSchema, } from "@journal.one/gateway-protocol";
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GatewayMessageSchema, VersionChangedMessageSchema, VersionsMessageSchema, ToolsMessageSchema, SkillsMessageSchema, ServiceMessageSchema, IntegrationSchema, ToolResultSchema, } from "@journal.one/gateway-protocol";
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@journal.one/gateway-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/server.js",
|
|
6
|
+
"types": "./dist/server.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/server.d.ts",
|
|
13
|
+
"import": "./dist/server.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ws": "^8",
|
|
21
|
+
"zod": "^3.24",
|
|
22
|
+
"@journal.one/gateway-protocol": "0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22",
|
|
26
|
+
"@types/ws": "^8",
|
|
27
|
+
"typescript": "^5.7",
|
|
28
|
+
"vitest": "^3"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"test": "vitest run"
|
|
33
|
+
}
|
|
34
|
+
}
|