integrate-sdk 0.3.7 → 0.3.9
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/index.js +207 -11
- package/dist/server.js +182 -11
- package/dist/src/adapters/nextjs-oauth-redirect.d.ts +34 -0
- package/dist/src/adapters/nextjs-oauth-redirect.d.ts.map +1 -0
- package/dist/src/client.d.ts +43 -5
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/config/types.d.ts +15 -0
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/oauth/manager.d.ts +4 -0
- package/dist/src/oauth/manager.d.ts.map +1 -1
- package/dist/src/oauth/types.d.ts +30 -6
- package/dist/src/oauth/types.d.ts.map +1 -1
- package/dist/src/oauth/window-manager.d.ts +1 -1
- package/dist/src/oauth/window-manager.d.ts.map +1 -1
- package/package.json +3 -15
- package/dist/src/adapters/nextjs-callback.d.ts +0 -44
- package/dist/src/adapters/nextjs-callback.d.ts.map +0 -1
- package/src/adapters/auto-routes.ts +0 -217
- package/src/adapters/base-handler.ts +0 -212
- package/src/adapters/nextjs-callback.tsx +0 -160
- package/src/adapters/nextjs.ts +0 -318
- package/src/adapters/tanstack-start.ts +0 -264
- package/src/client.ts +0 -952
- package/src/config/types.ts +0 -180
- package/src/errors.ts +0 -207
- package/src/index.ts +0 -110
- package/src/integrations/vercel-ai.ts +0 -104
- package/src/oauth/manager.ts +0 -307
- package/src/oauth/pkce.ts +0 -127
- package/src/oauth/types.ts +0 -101
- package/src/oauth/window-manager.ts +0 -322
- package/src/plugins/generic.ts +0 -119
- package/src/plugins/github-client.ts +0 -345
- package/src/plugins/github.ts +0 -122
- package/src/plugins/gmail-client.ts +0 -114
- package/src/plugins/gmail.ts +0 -108
- package/src/plugins/server-client.ts +0 -20
- package/src/plugins/types.ts +0 -89
- package/src/protocol/jsonrpc.ts +0 -88
- package/src/protocol/messages.ts +0 -145
- package/src/server.ts +0 -117
- package/src/transport/http-session.ts +0 -322
- package/src/transport/http-stream.ts +0 -331
- package/src/utils/naming.ts +0 -52
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP Session-Based Transport for MCP
|
|
3
|
-
* Uses POST for request/response and optional SSE for notifications
|
|
4
|
-
* Compatible with MCP StreamableHTTPServer with sessions
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
JSONRPCRequest,
|
|
9
|
-
JSONRPCResponse,
|
|
10
|
-
JSONRPCNotification,
|
|
11
|
-
} from "../protocol/messages.js";
|
|
12
|
-
import { parseMessage } from "../protocol/jsonrpc.js";
|
|
13
|
-
|
|
14
|
-
export type MessageHandler = (
|
|
15
|
-
message: JSONRPCResponse | JSONRPCNotification
|
|
16
|
-
) => void;
|
|
17
|
-
|
|
18
|
-
export interface HttpSessionTransportOptions {
|
|
19
|
-
url: string;
|
|
20
|
-
headers?: Record<string, string>;
|
|
21
|
-
/** Timeout for requests in milliseconds */
|
|
22
|
-
timeout?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* HTTP Session Transport
|
|
27
|
-
* Maintains a session with the MCP server
|
|
28
|
-
* - Sends requests via POST
|
|
29
|
-
* - Receives responses in POST response body
|
|
30
|
-
* - Optionally listens for notifications via SSE
|
|
31
|
-
*/
|
|
32
|
-
export class HttpSessionTransport {
|
|
33
|
-
private url: string;
|
|
34
|
-
private headers: Record<string, string>;
|
|
35
|
-
private timeout: number;
|
|
36
|
-
private messageHandlers: Set<MessageHandler> = new Set();
|
|
37
|
-
private sessionId?: string;
|
|
38
|
-
private sseController?: AbortController;
|
|
39
|
-
private connected = false;
|
|
40
|
-
|
|
41
|
-
constructor(options: HttpSessionTransportOptions) {
|
|
42
|
-
this.url = options.url;
|
|
43
|
-
this.headers = options.headers || {};
|
|
44
|
-
this.timeout = options.timeout || 30000;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Initialize session (no persistent connection needed)
|
|
49
|
-
*/
|
|
50
|
-
async connect(): Promise<void> {
|
|
51
|
-
if (this.connected) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Session will be established on first request
|
|
56
|
-
this.connected = true;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Send a request to the server and get immediate response
|
|
61
|
-
*/
|
|
62
|
-
async sendRequest<T = unknown>(
|
|
63
|
-
method: string,
|
|
64
|
-
params?: unknown
|
|
65
|
-
): Promise<T> {
|
|
66
|
-
if (!this.connected) {
|
|
67
|
-
throw new Error("Not connected to server");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const request: JSONRPCRequest = {
|
|
71
|
-
jsonrpc: "2.0",
|
|
72
|
-
id: Date.now() + Math.random(),
|
|
73
|
-
method,
|
|
74
|
-
params: params as Record<string, unknown> | unknown[] | undefined,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const headers: Record<string, string> = {
|
|
78
|
-
...this.headers,
|
|
79
|
-
"Content-Type": "application/json",
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Include session ID if we have one
|
|
83
|
-
if (this.sessionId) {
|
|
84
|
-
headers["mcp-session-id"] = this.sessionId;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const controller = new AbortController();
|
|
89
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
90
|
-
|
|
91
|
-
const response = await fetch(this.url, {
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers,
|
|
94
|
-
body: JSON.stringify(request),
|
|
95
|
-
signal: controller.signal,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
clearTimeout(timeoutId);
|
|
99
|
-
|
|
100
|
-
if (!response.ok) {
|
|
101
|
-
// Preserve HTTP status code for auth error detection
|
|
102
|
-
const error = new Error(`Request failed: ${response.statusText}`) as Error & { statusCode?: number };
|
|
103
|
-
error.statusCode = response.status;
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Capture session ID from first response
|
|
108
|
-
if (!this.sessionId) {
|
|
109
|
-
const sid = response.headers.get("mcp-session-id");
|
|
110
|
-
if (sid) {
|
|
111
|
-
this.sessionId = sid;
|
|
112
|
-
console.log("Session established:", sid);
|
|
113
|
-
|
|
114
|
-
// Start SSE listener for notifications
|
|
115
|
-
this.startSSEListener();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const jsonResponse = (await response.json()) as JSONRPCResponse<T>;
|
|
120
|
-
|
|
121
|
-
if ("error" in jsonResponse) {
|
|
122
|
-
// Preserve the full error object for the client to parse
|
|
123
|
-
const error = new Error(
|
|
124
|
-
`JSON-RPC Error ${jsonResponse.error.code}: ${jsonResponse.error.message}`
|
|
125
|
-
) as Error & { code?: number; data?: unknown };
|
|
126
|
-
error.code = jsonResponse.error.code;
|
|
127
|
-
if (jsonResponse.error.data) {
|
|
128
|
-
error.data = jsonResponse.error.data;
|
|
129
|
-
}
|
|
130
|
-
// Attach the original JSON-RPC error for detailed parsing
|
|
131
|
-
(error as any).jsonrpcError = jsonResponse.error;
|
|
132
|
-
throw error;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return jsonResponse.result as T;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
// If it's a fetch error (network, timeout, etc.), wrap it appropriately
|
|
138
|
-
if (error instanceof Error) {
|
|
139
|
-
if (error.name === "AbortError") {
|
|
140
|
-
const timeoutError = new Error("Request timeout") as Error & { code?: number };
|
|
141
|
-
timeoutError.code = -32000; // Custom timeout code
|
|
142
|
-
throw timeoutError;
|
|
143
|
-
}
|
|
144
|
-
// Re-throw errors with preserved information
|
|
145
|
-
throw error;
|
|
146
|
-
}
|
|
147
|
-
throw new Error(String(error));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Start SSE listener for server-initiated notifications
|
|
153
|
-
*/
|
|
154
|
-
private async startSSEListener(): Promise<void> {
|
|
155
|
-
if (!this.sessionId || this.sseController) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
this.sseController = new AbortController();
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const response = await fetch(this.url, {
|
|
163
|
-
method: "GET",
|
|
164
|
-
headers: {
|
|
165
|
-
...this.headers,
|
|
166
|
-
"Accept": "text/event-stream",
|
|
167
|
-
"mcp-session-id": this.sessionId,
|
|
168
|
-
},
|
|
169
|
-
signal: this.sseController.signal,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (!response.ok || !response.body) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Process SSE stream for notifications
|
|
177
|
-
this.processSSEStream(response.body);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
180
|
-
// Connection was intentionally closed
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
console.error("SSE connection error:", error);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Process SSE stream for server notifications
|
|
189
|
-
*/
|
|
190
|
-
private async processSSEStream(body: ReadableStream<Uint8Array>): Promise<void> {
|
|
191
|
-
const reader = body.getReader();
|
|
192
|
-
const decoder = new TextDecoder();
|
|
193
|
-
let buffer = "";
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
while (true) {
|
|
197
|
-
const { done, value } = await reader.read();
|
|
198
|
-
if (done) break;
|
|
199
|
-
|
|
200
|
-
buffer += decoder.decode(value, { stream: true });
|
|
201
|
-
const lines = buffer.split("\n");
|
|
202
|
-
buffer = lines.pop() || "";
|
|
203
|
-
|
|
204
|
-
for (const line of lines) {
|
|
205
|
-
if (line.startsWith("data: ")) {
|
|
206
|
-
const data = line.slice(6).trim();
|
|
207
|
-
if (data) {
|
|
208
|
-
this.handleNotification(data);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
} catch (error) {
|
|
214
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
console.error("SSE stream error:", error);
|
|
218
|
-
} finally {
|
|
219
|
-
reader.releaseLock();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Handle incoming notification from SSE
|
|
225
|
-
*/
|
|
226
|
-
private handleNotification(data: string): void {
|
|
227
|
-
try {
|
|
228
|
-
const message = parseMessage(data);
|
|
229
|
-
|
|
230
|
-
// Notify all message handlers
|
|
231
|
-
this.messageHandlers.forEach((handler) => {
|
|
232
|
-
try {
|
|
233
|
-
handler(message);
|
|
234
|
-
} catch (error) {
|
|
235
|
-
console.error("Error in message handler:", error);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
} catch (error) {
|
|
239
|
-
console.error("Failed to parse notification:", error);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Register a message handler for notifications
|
|
245
|
-
*/
|
|
246
|
-
onMessage(handler: MessageHandler): () => void {
|
|
247
|
-
this.messageHandlers.add(handler);
|
|
248
|
-
|
|
249
|
-
// Return unsubscribe function
|
|
250
|
-
return () => {
|
|
251
|
-
this.messageHandlers.delete(handler);
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Disconnect from the server
|
|
257
|
-
*/
|
|
258
|
-
async disconnect(): Promise<void> {
|
|
259
|
-
if (!this.connected) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Close SSE connection if open
|
|
264
|
-
if (this.sseController) {
|
|
265
|
-
this.sseController.abort();
|
|
266
|
-
this.sseController = undefined;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Clear handlers
|
|
270
|
-
this.messageHandlers.clear();
|
|
271
|
-
|
|
272
|
-
this.sessionId = undefined;
|
|
273
|
-
this.connected = false;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Check if transport is connected
|
|
278
|
-
*/
|
|
279
|
-
isConnected(): boolean {
|
|
280
|
-
return this.connected;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Get current session ID
|
|
285
|
-
*/
|
|
286
|
-
getSessionId(): string | undefined {
|
|
287
|
-
return this.sessionId;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Set a custom header for all requests
|
|
292
|
-
* Used for session tokens and other auth headers
|
|
293
|
-
*
|
|
294
|
-
* @param key - Header name
|
|
295
|
-
* @param value - Header value
|
|
296
|
-
*
|
|
297
|
-
* @example
|
|
298
|
-
* ```typescript
|
|
299
|
-
* transport.setHeader('X-Session-Token', 'abc123');
|
|
300
|
-
* ```
|
|
301
|
-
*/
|
|
302
|
-
setHeader(key: string, value: string): void {
|
|
303
|
-
this.headers[key] = value;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Remove a custom header
|
|
308
|
-
*
|
|
309
|
-
* @param key - Header name to remove
|
|
310
|
-
*/
|
|
311
|
-
removeHeader(key: string): void {
|
|
312
|
-
delete this.headers[key];
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Get all current headers
|
|
317
|
-
*/
|
|
318
|
-
getHeaders(): Record<string, string> {
|
|
319
|
-
return { ...this.headers };
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP Streaming Transport for MCP
|
|
3
|
-
* Implements HTTP streaming with newline-delimited JSON (NDJSON)
|
|
4
|
-
* Compatible with MCP's StreamableHTTPServer
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
JSONRPCRequest,
|
|
9
|
-
JSONRPCResponse,
|
|
10
|
-
JSONRPCNotification,
|
|
11
|
-
} from "../protocol/messages.js";
|
|
12
|
-
import { parseMessage } from "../protocol/jsonrpc.js";
|
|
13
|
-
|
|
14
|
-
export type MessageHandler = (
|
|
15
|
-
message: JSONRPCResponse | JSONRPCNotification
|
|
16
|
-
) => void;
|
|
17
|
-
|
|
18
|
-
export interface HttpStreamTransportOptions {
|
|
19
|
-
url: string;
|
|
20
|
-
headers?: Record<string, string>;
|
|
21
|
-
/** Timeout for requests in milliseconds */
|
|
22
|
-
timeout?: number;
|
|
23
|
-
/** Heartbeat interval in milliseconds (default: 30000) */
|
|
24
|
-
heartbeatInterval?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* HTTP Streaming Transport
|
|
29
|
-
* Handles bidirectional communication with MCP server via HTTP streaming
|
|
30
|
-
* Uses newline-delimited JSON (NDJSON) over a single persistent connection
|
|
31
|
-
*/
|
|
32
|
-
export class HttpStreamTransport {
|
|
33
|
-
private url: string;
|
|
34
|
-
private headers: Record<string, string>;
|
|
35
|
-
private timeout: number;
|
|
36
|
-
private heartbeatInterval: number;
|
|
37
|
-
private messageHandlers: Set<MessageHandler> = new Set();
|
|
38
|
-
private pendingRequests: Map<
|
|
39
|
-
string | number,
|
|
40
|
-
{
|
|
41
|
-
resolve: (value: JSONRPCResponse) => void;
|
|
42
|
-
reject: (error: Error) => void;
|
|
43
|
-
timeoutId?: ReturnType<typeof setTimeout>;
|
|
44
|
-
}
|
|
45
|
-
> = new Map();
|
|
46
|
-
private streamController?: AbortController;
|
|
47
|
-
private connected = false;
|
|
48
|
-
private heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
49
|
-
|
|
50
|
-
constructor(options: HttpStreamTransportOptions) {
|
|
51
|
-
this.url = options.url;
|
|
52
|
-
this.headers = options.headers || {};
|
|
53
|
-
this.timeout = options.timeout || 30000;
|
|
54
|
-
this.heartbeatInterval = options.heartbeatInterval || 30000;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Connect to the MCP server and establish SSE connection
|
|
59
|
-
*/
|
|
60
|
-
async connect(): Promise<void> {
|
|
61
|
-
if (this.connected) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
this.streamController = new AbortController();
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
// Establish SSE connection for receiving messages
|
|
69
|
-
const response = await fetch(this.url, {
|
|
70
|
-
method: "GET",
|
|
71
|
-
headers: {
|
|
72
|
-
...this.headers,
|
|
73
|
-
"Accept": "text/event-stream",
|
|
74
|
-
"Cache-Control": "no-cache",
|
|
75
|
-
},
|
|
76
|
-
signal: this.streamController.signal,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
throw new Error(`Failed to connect: ${response.statusText}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!response.body) {
|
|
84
|
-
throw new Error("Response body is null");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
this.connected = true;
|
|
88
|
-
|
|
89
|
-
// Process incoming SSE stream
|
|
90
|
-
this.processStream(response.body);
|
|
91
|
-
|
|
92
|
-
// Start heartbeat
|
|
93
|
-
this.startHeartbeat();
|
|
94
|
-
} catch (error) {
|
|
95
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
96
|
-
// Connection was intentionally closed
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Process incoming SSE stream
|
|
105
|
-
*/
|
|
106
|
-
private async processStream(body: ReadableStream<Uint8Array>): Promise<void> {
|
|
107
|
-
const reader = body.getReader();
|
|
108
|
-
const decoder = new TextDecoder();
|
|
109
|
-
let buffer = "";
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
while (true) {
|
|
113
|
-
const { done, value } = await reader.read();
|
|
114
|
-
|
|
115
|
-
if (done) {
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Decode and add to buffer
|
|
120
|
-
buffer += decoder.decode(value, { stream: true });
|
|
121
|
-
|
|
122
|
-
// Split by newlines
|
|
123
|
-
const lines = buffer.split("\n");
|
|
124
|
-
|
|
125
|
-
// Keep the last incomplete line in the buffer
|
|
126
|
-
buffer = lines.pop() || "";
|
|
127
|
-
|
|
128
|
-
// Process SSE format: "data: {json}\n\n"
|
|
129
|
-
for (const line of lines) {
|
|
130
|
-
if (line.startsWith("data: ")) {
|
|
131
|
-
const data = line.slice(6).trim();
|
|
132
|
-
if (data) {
|
|
133
|
-
this.handleMessage(data);
|
|
134
|
-
}
|
|
135
|
-
} else if (line.trim() === "") {
|
|
136
|
-
// Empty line separates events
|
|
137
|
-
continue;
|
|
138
|
-
} else if (line.startsWith(":")) {
|
|
139
|
-
// SSE comment, ignore
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
} catch (error) {
|
|
145
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
146
|
-
// Stream was intentionally closed
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
console.error("Stream error:", error);
|
|
150
|
-
} finally {
|
|
151
|
-
reader.releaseLock();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Start sending periodic heartbeats to keep connection alive
|
|
157
|
-
*/
|
|
158
|
-
private startHeartbeat(): void {
|
|
159
|
-
if (this.heartbeatTimer) {
|
|
160
|
-
clearInterval(this.heartbeatTimer);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
this.heartbeatTimer = setInterval(() => {
|
|
164
|
-
if (this.connected) {
|
|
165
|
-
// Send a ping/heartbeat (you can customize this based on your server's expectations)
|
|
166
|
-
this.sendRawMessage({ jsonrpc: "2.0", method: "ping" }).catch((error) => {
|
|
167
|
-
console.error("Heartbeat failed:", error);
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}, this.heartbeatInterval);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Handle incoming message from server
|
|
175
|
-
*/
|
|
176
|
-
private handleMessage(data: string): void {
|
|
177
|
-
try {
|
|
178
|
-
const message = parseMessage(data);
|
|
179
|
-
|
|
180
|
-
// Check if this is a response to a pending request
|
|
181
|
-
if ("id" in message && message.id !== undefined) {
|
|
182
|
-
const pending = this.pendingRequests.get(message.id);
|
|
183
|
-
if (pending) {
|
|
184
|
-
if (pending.timeoutId) {
|
|
185
|
-
clearTimeout(pending.timeoutId);
|
|
186
|
-
}
|
|
187
|
-
this.pendingRequests.delete(message.id);
|
|
188
|
-
pending.resolve(message as JSONRPCResponse);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Otherwise, notify all message handlers (notifications, etc.)
|
|
194
|
-
this.messageHandlers.forEach((handler) => {
|
|
195
|
-
try {
|
|
196
|
-
handler(message);
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.error("Error in message handler:", error);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.error("Failed to parse message:", error);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Send a raw message to the server via POST (internal use)
|
|
208
|
-
*/
|
|
209
|
-
private async sendRawMessage(message: unknown): Promise<void> {
|
|
210
|
-
// Send each message as a separate POST request
|
|
211
|
-
const response = await fetch(this.url, {
|
|
212
|
-
method: "POST",
|
|
213
|
-
headers: {
|
|
214
|
-
...this.headers,
|
|
215
|
-
"Content-Type": "application/json",
|
|
216
|
-
},
|
|
217
|
-
body: JSON.stringify(message),
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
if (!response.ok) {
|
|
221
|
-
throw new Error(`Failed to send message: ${response.statusText}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Send a request to the server
|
|
227
|
-
*/
|
|
228
|
-
async sendRequest<T = unknown>(
|
|
229
|
-
method: string,
|
|
230
|
-
params?: unknown
|
|
231
|
-
): Promise<T> {
|
|
232
|
-
if (!this.connected) {
|
|
233
|
-
throw new Error("Not connected to server");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const request: JSONRPCRequest = {
|
|
237
|
-
jsonrpc: "2.0",
|
|
238
|
-
id: Date.now() + Math.random(),
|
|
239
|
-
method,
|
|
240
|
-
params: params as Record<string, unknown> | unknown[] | undefined,
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// Create promise for response
|
|
244
|
-
const responsePromise = new Promise<JSONRPCResponse>((resolve, reject) => {
|
|
245
|
-
const timeoutId = setTimeout(() => {
|
|
246
|
-
this.pendingRequests.delete(request.id);
|
|
247
|
-
reject(new Error(`Request timeout: ${method}`));
|
|
248
|
-
}, this.timeout);
|
|
249
|
-
|
|
250
|
-
this.pendingRequests.set(request.id, {
|
|
251
|
-
resolve,
|
|
252
|
-
reject,
|
|
253
|
-
timeoutId,
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// Send request over the stream
|
|
258
|
-
try {
|
|
259
|
-
await this.sendRawMessage(request);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
this.pendingRequests.delete(request.id);
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Wait for response over the same stream
|
|
266
|
-
const response = await responsePromise;
|
|
267
|
-
|
|
268
|
-
if ("error" in response) {
|
|
269
|
-
throw new Error(
|
|
270
|
-
`JSON-RPC Error ${response.error.code}: ${response.error.message}`
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return response.result as T;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Register a message handler for notifications and other messages
|
|
279
|
-
*/
|
|
280
|
-
onMessage(handler: MessageHandler): () => void {
|
|
281
|
-
this.messageHandlers.add(handler);
|
|
282
|
-
|
|
283
|
-
// Return unsubscribe function
|
|
284
|
-
return () => {
|
|
285
|
-
this.messageHandlers.delete(handler);
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Disconnect from the server
|
|
291
|
-
*/
|
|
292
|
-
async disconnect(): Promise<void> {
|
|
293
|
-
if (!this.connected) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Stop heartbeat
|
|
298
|
-
if (this.heartbeatTimer) {
|
|
299
|
-
clearInterval(this.heartbeatTimer);
|
|
300
|
-
this.heartbeatTimer = undefined;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Cancel SSE stream connection
|
|
304
|
-
if (this.streamController) {
|
|
305
|
-
this.streamController.abort();
|
|
306
|
-
this.streamController = undefined;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Reject all pending requests
|
|
310
|
-
this.pendingRequests.forEach(({ reject, timeoutId }) => {
|
|
311
|
-
if (timeoutId) {
|
|
312
|
-
clearTimeout(timeoutId);
|
|
313
|
-
}
|
|
314
|
-
reject(new Error("Connection closed"));
|
|
315
|
-
});
|
|
316
|
-
this.pendingRequests.clear();
|
|
317
|
-
|
|
318
|
-
// Clear handlers
|
|
319
|
-
this.messageHandlers.clear();
|
|
320
|
-
|
|
321
|
-
this.connected = false;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Check if transport is connected
|
|
326
|
-
*/
|
|
327
|
-
isConnected(): boolean {
|
|
328
|
-
return this.connected;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
package/src/utils/naming.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Naming conversion utilities
|
|
3
|
-
* Helpers for converting between camelCase and snake_case
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Convert camelCase to snake_case
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* camelToSnake('getRepo') // 'get_repo'
|
|
11
|
-
* camelToSnake('listOwnRepos') // 'list_own_repos'
|
|
12
|
-
*/
|
|
13
|
-
export function camelToSnake(str: string): string {
|
|
14
|
-
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Convert snake_case to camelCase
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* snakeToCamel('get_repo') // 'getRepo'
|
|
22
|
-
* snakeToCamel('list_own_repos') // 'listOwnRepos'
|
|
23
|
-
*/
|
|
24
|
-
export function snakeToCamel(str: string): string {
|
|
25
|
-
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Convert a method name to a full tool name with plugin prefix
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* methodToToolName('getRepo', 'github') // 'github_get_repo'
|
|
33
|
-
* methodToToolName('sendEmail', 'gmail') // 'gmail_send_email'
|
|
34
|
-
*/
|
|
35
|
-
export function methodToToolName(methodName: string, pluginId: string): string {
|
|
36
|
-
const snakeCaseMethod = camelToSnake(methodName);
|
|
37
|
-
return `${pluginId}_${snakeCaseMethod}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Convert a tool name to a method name (removes plugin prefix and converts to camelCase)
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* toolNameToMethod('github_get_repo') // 'getRepo'
|
|
45
|
-
* toolNameToMethod('gmail_send_email') // 'sendEmail'
|
|
46
|
-
*/
|
|
47
|
-
export function toolNameToMethod(toolName: string): string {
|
|
48
|
-
// Remove the plugin prefix (everything before the first underscore)
|
|
49
|
-
const withoutPrefix = toolName.replace(/^[^_]+_/, '');
|
|
50
|
-
return snakeToCamel(withoutPrefix);
|
|
51
|
-
}
|
|
52
|
-
|