@gtkx/mcp 1.4.0 → 1.6.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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/bin/gtkx-mcp.js +2 -2
  3. package/dist/app-router.d.ts +3 -5
  4. package/dist/app-router.d.ts.map +1 -1
  5. package/dist/app-router.js +21 -34
  6. package/dist/app-router.js.map +1 -1
  7. package/dist/connection-registry.d.ts +3 -4
  8. package/dist/connection-registry.d.ts.map +1 -1
  9. package/dist/connection-registry.js +5 -16
  10. package/dist/connection-registry.js.map +1 -1
  11. package/dist/internal.d.ts +3 -2
  12. package/dist/internal.d.ts.map +1 -1
  13. package/dist/internal.js +1 -1
  14. package/dist/internal.js.map +1 -1
  15. package/dist/protocol/errors.d.ts +6 -7
  16. package/dist/protocol/errors.d.ts.map +1 -1
  17. package/dist/protocol/errors.js +12 -8
  18. package/dist/protocol/errors.js.map +1 -1
  19. package/dist/protocol/schemas.d.ts +1 -19
  20. package/dist/protocol/schemas.d.ts.map +1 -1
  21. package/dist/protocol/schemas.js +1 -16
  22. package/dist/protocol/schemas.js.map +1 -1
  23. package/dist/reference.d.ts.map +1 -1
  24. package/dist/reference.js +8 -5
  25. package/dist/reference.js.map +1 -1
  26. package/dist/server.d.ts +8 -2
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +79 -18
  29. package/dist/server.js.map +1 -1
  30. package/dist/socket-server.js +1 -1
  31. package/dist/socket-server.js.map +1 -1
  32. package/dist/tool-filter.d.ts +5 -0
  33. package/dist/tool-filter.d.ts.map +1 -0
  34. package/dist/tool-filter.js +38 -0
  35. package/dist/tool-filter.js.map +1 -0
  36. package/dist/tool.d.ts +1 -0
  37. package/dist/tool.d.ts.map +1 -1
  38. package/dist/tool.js +1 -1
  39. package/dist/tool.js.map +1 -1
  40. package/dist/transport.d.ts +18 -39
  41. package/dist/transport.d.ts.map +1 -1
  42. package/dist/transport.js +78 -127
  43. package/dist/transport.js.map +1 -1
  44. package/package.json +9 -5
  45. package/src/app-router.ts +34 -44
  46. package/src/connection-registry.ts +6 -22
  47. package/src/internal.ts +2 -3
  48. package/src/protocol/errors.ts +20 -10
  49. package/src/protocol/schemas.ts +0 -41
  50. package/src/reference.ts +8 -5
  51. package/src/server.ts +116 -19
  52. package/src/socket-server.ts +1 -1
  53. package/src/tool-filter.ts +54 -0
  54. package/src/tool.ts +2 -1
  55. package/src/transport.ts +106 -171
package/src/transport.ts CHANGED
@@ -1,30 +1,36 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-function -- the app link negotiates no MCP capabilities */
2
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
1
3
  import type { Socket } from "node:net";
2
- import type { Duplex } from "node:stream";
3
- import { ErrorCode, invalidRequestError, isErrorCode, ProtocolError, requestTimeoutError } from "./protocol/errors.js";
4
- import { type Message, type Request, RequestSchema, type Response, ResponseSchema } from "./protocol/schemas.js";
5
-
6
- type ProtocolConnectionEvents = {
7
- request: Request;
8
- invalid: { id: string; error: ProtocolError };
9
- };
10
-
11
- type PendingRequest = {
12
- resolve: (result: unknown) => void;
13
- reject: (error: Error) => void;
14
- timeout: NodeJS.Timeout;
15
- };
4
+ import { normalizeError } from "@gtkx/utils";
5
+ import { Protocol } from "@modelcontextprotocol/sdk/shared/protocol.js";
6
+ import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
7
+ import {
8
+ ErrorCode,
9
+ type JSONRPCMessage,
10
+ type JSONRPCRequest,
11
+ McpError,
12
+ type Notification,
13
+ type Request,
14
+ type Result,
15
+ ResultSchema,
16
+ } from "@modelcontextprotocol/sdk/types.js";
16
17
 
17
18
  type ConnectionEvent = CustomEvent<ProtocolConnection>;
18
- type ConnectionRequestEvent = CustomEvent<{ connection: ProtocolConnection; request: Request }>;
19
19
  type ConnectionErrorEvent = CustomEvent<Error>;
20
+ type ConnectionRequestHandler = (connection: ProtocolConnection, request: JSONRPCRequest) => Promise<Result>;
21
+ type RequestParams = Request["params"];
22
+
23
+ type ConnectionOptions = {
24
+ onClose: () => void;
25
+ onError: (error: Error) => void;
26
+ };
20
27
 
21
28
  type AppConnections = {
22
- send(connectionId: string, message: Message): void;
29
+ onRequest: ConnectionRequestHandler;
23
30
  } & EventTarget;
24
31
 
25
- function connectionRequestEvent(connection: ProtocolConnection, request: Request): ConnectionRequestEvent {
26
- return new CustomEvent("request", { detail: { connection, request } });
27
- }
32
+ const MAX_MESSAGE_BYTES = 64 * 1024 * 1024;
33
+ const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
28
34
 
29
35
  function connectionDisconnectionEvent(connection: ProtocolConnection): ConnectionEvent {
30
36
  return new CustomEvent("disconnection", { detail: connection });
@@ -34,208 +40,137 @@ function connectionErrorEvent(error: Error): ConnectionErrorEvent {
34
40
  return new CustomEvent("error", { detail: error });
35
41
  }
36
42
 
37
- class ConnectionClosedError extends Error {
38
- constructor() {
39
- super("Connection stream is not writable");
40
- this.name = "ConnectionClosedError";
41
- }
42
- }
43
-
44
- class ProtocolConnection extends EventTarget {
45
- static fromSocket(
46
- socket: Socket,
47
- options: {
48
- onClose?: () => void;
49
- onError?: (error: Error) => void;
50
- } = {},
51
- ): ProtocolConnection {
52
- const connection = new ProtocolConnection(socket);
53
-
54
- socket.on("data", (data: Buffer) => {
55
- connection.feed(data);
56
- });
57
-
58
- socket.on("close", () => {
59
- connection.rejectPending(new Error("Connection closed"));
60
- options.onClose?.();
61
- });
62
-
63
- if (options.onError) {
64
- socket.on("error", options.onError);
65
- }
66
-
67
- return connection;
68
- }
69
-
70
- private buffer = "";
71
- private pending: Map<string, PendingRequest> = new Map();
72
- private writer: Duplex;
73
-
74
- id: string = crypto.randomUUID();
75
-
76
- constructor(writer: Duplex) {
77
- super();
78
- this.writer = writer;
79
- }
80
-
81
- private notify<K extends keyof ProtocolConnectionEvents>(type: K, detail: ProtocolConnectionEvents[K]): void {
82
- this.dispatchEvent(new CustomEvent(type, { detail }));
83
- }
43
+ class SocketTransport implements Transport {
44
+ private readonly readBuffer: ReadBuffer = new ReadBuffer({ maxBufferSize: MAX_MESSAGE_BYTES });
45
+ private readonly socket: Socket;
84
46
 
85
- private rejectWhenClosed(id: string, timeoutHandle: NodeJS.Timeout, reject: (error: Error) => void): void {
86
- if (this.writer.writable) {
87
- return;
88
- }
47
+ onclose?: () => void;
48
+ onerror?: (error: Error) => void;
49
+ onmessage?: (message: JSONRPCMessage) => void;
89
50
 
90
- clearTimeout(timeoutHandle);
91
- this.pending.delete(id);
92
- reject(new ConnectionClosedError());
51
+ constructor(socket: Socket) {
52
+ this.socket = socket;
93
53
  }
94
54
 
95
- private dispatchParsed(parsed: unknown): boolean {
96
- const message = parsed as Record<string, unknown>;
97
-
98
- if (typeof message.method === "string") {
99
- const requestResult = RequestSchema.safeParse(parsed);
55
+ private read(): void {
56
+ for (;;) {
57
+ let message: JSONRPCMessage | null;
100
58
 
101
- if (!requestResult.success) {
102
- return false;
59
+ try {
60
+ message = this.readBuffer.readMessage();
61
+ } catch (error) {
62
+ this.onerror?.(normalizeError(error));
63
+ continue;
103
64
  }
104
65
 
105
- this.notify("request", requestResult.data);
106
-
107
- return true;
108
- }
109
-
110
- const responseResult = ResponseSchema.safeParse(parsed);
66
+ if (message === null) {
67
+ return;
68
+ }
111
69
 
112
- if (!responseResult.success) {
113
- return false;
70
+ this.onmessage?.(message);
114
71
  }
72
+ }
115
73
 
116
- this.handleResponse(responseResult.data);
74
+ private drop(reason: string): Promise<void> {
75
+ const error = new McpError(ErrorCode.ConnectionClosed, reason);
76
+ this.onerror?.(error);
77
+ this.socket.destroy();
117
78
 
118
- return true;
79
+ return Promise.reject(error);
119
80
  }
120
81
 
121
- private processLine(line: string): void {
122
- let parsed: unknown;
123
-
82
+ private receive(chunk: Buffer): void {
124
83
  try {
125
- parsed = JSON.parse(line);
126
- } catch {
127
- this.notify("invalid", { id: "unknown", error: invalidRequestError("Invalid JSON") });
128
-
129
- return;
130
- }
84
+ this.readBuffer.append(chunk);
85
+ } catch (error) {
86
+ this.onerror?.(normalizeError(error));
87
+ this.socket.destroy();
131
88
 
132
- if (this.dispatchParsed(parsed)) {
133
89
  return;
134
90
  }
135
91
 
136
- const message = parsed as Record<string, unknown>;
137
- const id = typeof message.id === "string" ? message.id : "unknown";
138
- this.notify("invalid", { id, error: invalidRequestError("Invalid message format") });
92
+ this.read();
139
93
  }
140
94
 
141
- private handleResponse(response: Response): void {
142
- const entry = this.pending.get(response.id);
95
+ start(): Promise<void> {
96
+ this.socket.on("data", (chunk: Buffer) => {
97
+ this.receive(chunk);
98
+ });
143
99
 
144
- if (!entry) {
145
- return;
146
- }
100
+ this.socket.on("close", () => {
101
+ this.onclose?.();
102
+ });
147
103
 
148
- clearTimeout(entry.timeout);
149
- this.pending.delete(response.id);
104
+ this.socket.on("error", (error) => {
105
+ this.onerror?.(error);
106
+ });
150
107
 
151
- if (response.error) {
152
- const err = response.error;
108
+ return Promise.resolve();
109
+ }
153
110
 
154
- entry.reject(
155
- new ProtocolError(isErrorCode(err.code) ? err.code : ErrorCode.INTERNAL_ERROR, err.message, err.data),
156
- );
157
- } else {
158
- entry.resolve(response.result);
111
+ send(message: JSONRPCMessage): Promise<void> {
112
+ if (!this.socket.writable) {
113
+ return Promise.reject(new McpError(ErrorCode.ConnectionClosed, "Connection stream is not writable"));
159
114
  }
160
- }
161
115
 
162
- on<K extends keyof ProtocolConnectionEvents>(
163
- type: K,
164
- listener: (detail: ProtocolConnectionEvents[K]) => void,
165
- ): void {
166
- this.addEventListener(type, (event) => {
167
- listener((event as CustomEvent<ProtocolConnectionEvents[K]>).detail);
168
- });
169
- }
116
+ if (this.socket.writableLength > MAX_PENDING_WRITE_BYTES) {
117
+ return this.drop("Connection stream is not draining");
118
+ }
170
119
 
171
- feed(data: Buffer | string): void {
172
- this.buffer += typeof data === "string" ? data : data.toString();
173
- let newlineIndex = this.buffer.indexOf("\n");
120
+ this.socket.write(serializeMessage(message));
174
121
 
175
- while (newlineIndex !== -1) {
176
- const line = this.buffer.slice(0, newlineIndex);
177
- this.buffer = this.buffer.slice(newlineIndex + 1);
122
+ return Promise.resolve();
123
+ }
178
124
 
179
- if (line.trim()) {
180
- this.processLine(line);
181
- }
125
+ close(): Promise<void> {
126
+ this.socket.destroy();
182
127
 
183
- newlineIndex = this.buffer.indexOf("\n");
184
- }
128
+ return Promise.resolve();
185
129
  }
130
+ }
186
131
 
187
- write(message: Message): void {
188
- if (!this.writer.writable) {
189
- return;
190
- }
132
+ class ProtocolConnection extends Protocol<Request, Notification, Result> {
133
+ static fromSocket(socket: Socket, options: ConnectionOptions): ProtocolConnection {
134
+ const connection = Object.assign(new ProtocolConnection(), {
135
+ onclose: options.onClose,
136
+ onerror: options.onError,
137
+ });
138
+
139
+ void connection.connect(new SocketTransport(socket));
191
140
 
192
- this.writer.write(`${JSON.stringify(message)}\n`);
141
+ return connection;
193
142
  }
194
143
 
195
- send<T = unknown>(method: string, params: unknown, timeout: number): Promise<T> {
196
- return new Promise<T>((resolve, reject) => {
197
- if (!this.writer.writable) {
198
- reject(new ConnectionClosedError());
144
+ id: string = crypto.randomUUID();
199
145
 
200
- return;
201
- }
146
+ protected assertCapabilityForMethod(): void {}
202
147
 
203
- const id = crypto.randomUUID();
148
+ protected assertNotificationCapability(): void {}
204
149
 
205
- const timeoutHandle = setTimeout(() => {
206
- this.pending.delete(id);
207
- reject(requestTimeoutError(timeout));
208
- }, timeout);
150
+ protected assertRequestHandlerCapability(): void {}
209
151
 
210
- this.pending.set(id, {
211
- resolve: resolve as (result: unknown) => void,
212
- reject,
213
- timeout: timeoutHandle,
214
- });
152
+ protected assertTaskCapability(): void {}
215
153
 
216
- this.write({ id, method, params });
217
- this.rejectWhenClosed(id, timeoutHandle, reject);
218
- });
219
- }
154
+ protected assertTaskHandlerCapability(): void {}
220
155
 
221
- rejectPending(error: Error): void {
222
- for (const entry of this.pending.values()) {
223
- clearTimeout(entry.timeout);
224
- entry.reject(error);
225
- }
156
+ async send<T>(method: string, params?: RequestParams, timeout?: number): Promise<T> {
157
+ const result = await this.request(
158
+ { method, ...(params !== undefined && { params }) },
159
+ ResultSchema,
160
+ { ...(timeout !== undefined && { timeout }) },
161
+ );
226
162
 
227
- this.pending.clear();
163
+ return result as T;
228
164
  }
229
165
  }
230
166
 
231
167
  export {
232
- ConnectionClosedError,
233
168
  connectionDisconnectionEvent,
234
169
  connectionErrorEvent,
235
- connectionRequestEvent,
236
170
  ProtocolConnection,
237
171
  type AppConnections,
238
172
  type ConnectionEvent,
239
173
  type ConnectionErrorEvent,
240
- type ConnectionRequestEvent,
174
+ type ConnectionRequestHandler,
175
+ type RequestParams,
241
176
  };