@gtkx/mcp 1.3.0 → 1.5.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.
@@ -1,10 +1,10 @@
1
1
  import type { Socket } from "node:net";
2
- import type { Message } from "./protocol/schemas.js";
2
+ import { methodNotFoundError } from "./protocol/errors.js";
3
3
  import {
4
4
  type AppConnections,
5
5
  connectionDisconnectionEvent,
6
6
  connectionErrorEvent,
7
- connectionRequestEvent,
7
+ type ConnectionRequestHandler,
8
8
  ProtocolConnection,
9
9
  } from "./transport.js";
10
10
 
@@ -12,6 +12,8 @@ class ConnectionRegistry extends EventTarget implements AppConnections {
12
12
  private connections: Map<string, ProtocolConnection> = new Map();
13
13
  private sockets: Map<string, Socket> = new Map();
14
14
 
15
+ onRequest: ConnectionRequestHandler = (_connection, request) => Promise.reject(methodNotFoundError(request.method));
16
+
15
17
  register(socket: Socket): ProtocolConnection {
16
18
  const connection = ProtocolConnection.fromSocket(socket, {
17
19
  onClose: () => {
@@ -24,30 +26,12 @@ class ConnectionRegistry extends EventTarget implements AppConnections {
24
26
 
25
27
  this.connections.set(connection.id, connection);
26
28
  this.sockets.set(connection.id, socket);
27
- connection.on("request", (request) => this.dispatchEvent(connectionRequestEvent(connection, request)));
28
-
29
- connection.on("invalid", ({ id: badId, error }) => {
30
- connection.write({ id: badId, error: error.toErrorObject() });
31
- });
29
+ connection.fallbackRequestHandler = (request) => this.onRequest(connection, request);
32
30
 
33
31
  return connection;
34
32
  }
35
33
 
36
- send(connectionId: string, message: Message): void {
37
- const connection = this.connections.get(connectionId);
38
-
39
- if (!connection) {
40
- return;
41
- }
42
-
43
- connection.write(message);
44
- }
45
-
46
- dispose(reason: string): void {
47
- for (const connection of this.connections.values()) {
48
- connection.rejectPending(new Error(reason));
49
- }
50
-
34
+ dispose(): void {
51
35
  for (const socket of this.sockets.values()) {
52
36
  socket.destroy();
53
37
  }
package/src/internal.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  export {
2
- ErrorCode,
3
2
  invalidRequestError,
3
+ isConnectionClosedError,
4
4
  methodNotFoundError,
5
- ProtocolError,
6
5
  propertyNotFoundError,
7
6
  widgetNotFoundError,
8
7
  } from "./protocol/errors.js";
@@ -11,7 +10,6 @@ export {
11
10
  DEFAULT_SUBTREE_DEPTH,
12
11
  MAX_SUBTREE_WIDGETS,
13
12
  type ParamsSchema,
14
- type Request,
15
13
  type SerializedProperty,
16
14
  type SerializedWidget,
17
15
  type ServerInitiatedMethod,
@@ -19,3 +17,4 @@ export {
19
17
  ServerRequestParamsSchemas,
20
18
  } from "./protocol/schemas.js";
21
19
  export { ProtocolConnection } from "./transport.js";
20
+ export type { JSONRPCRequest, Result } from "@modelcontextprotocol/sdk/types.js";
@@ -1,3 +1,5 @@
1
+ import { McpError, ErrorCode as SdkErrorCode } from "@modelcontextprotocol/sdk/types.js";
2
+
1
3
  type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
2
4
 
3
5
  const ErrorCode = {
@@ -12,6 +14,9 @@ const ErrorCode = {
12
14
  PROPERTY_NOT_FOUND: 1008,
13
15
  } as const;
14
16
 
17
+ const CONNECTION_CLOSED_CODE: number = SdkErrorCode.ConnectionClosed;
18
+ const REQUEST_TIMEOUT_CODE: number = SdkErrorCode.RequestTimeout;
19
+
15
20
  function isErrorCode(code: number): code is ErrorCode {
16
21
  return (Object.values(ErrorCode) as number[]).includes(code);
17
22
  }
@@ -64,6 +69,17 @@ function methodNotFoundError(method: string): ProtocolError {
64
69
  return new ProtocolError(ErrorCode.METHOD_NOT_FOUND, `Method '${method}' not found`, { method });
65
70
  }
66
71
 
72
+ function isConnectionClosedError(value: unknown): boolean {
73
+ return value instanceof McpError && value.code === CONNECTION_CLOSED_CODE;
74
+ }
75
+
76
+ function protocolErrorFrom(error: McpError): ProtocolError {
77
+ const prefix = `MCP error ${String(error.code)}: `;
78
+ const message = error.message.startsWith(prefix) ? error.message.slice(prefix.length) : error.message;
79
+
80
+ return new ProtocolError(isErrorCode(error.code) ? error.code : ErrorCode.INTERNAL_ERROR, message, error.data);
81
+ }
82
+
67
83
  class ProtocolError extends Error {
68
84
  code: ErrorCode;
69
85
  data?: unknown;
@@ -74,19 +90,12 @@ class ProtocolError extends Error {
74
90
  this.data = data;
75
91
  this.name = "ProtocolError";
76
92
  }
77
-
78
- toErrorObject(): { code: number; message: string; data?: unknown } {
79
- return {
80
- code: this.code,
81
- message: this.message,
82
- ...(this.data !== undefined && { data: this.data }),
83
- };
84
- }
85
93
  }
86
94
 
87
95
  export {
88
- ErrorCode,
89
- isErrorCode,
96
+ CONNECTION_CLOSED_CODE,
97
+ isConnectionClosedError,
98
+ REQUEST_TIMEOUT_CODE,
90
99
  noAppConnectedError,
91
100
  appNotFoundError,
92
101
  connectionWriteFailedError,
@@ -96,4 +105,5 @@ export {
96
105
  invalidRequestError,
97
106
  methodNotFoundError,
98
107
  ProtocolError,
108
+ protocolErrorFrom,
99
109
  };
@@ -2,9 +2,6 @@ import { tmpdir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
 
5
- type Request = z.infer<typeof RequestSchema>;
6
- type Response = z.infer<typeof ResponseSchema>;
7
-
8
5
  type SerializedWidget = {
9
6
  id: string;
10
7
  type: string;
@@ -37,39 +34,6 @@ type ServerRequestParams<Method extends keyof typeof ServerRequestParamsSchemas>
37
34
 
38
35
  type ParamsSchema<Output> = z.ZodType<Output>;
39
36
  type ServerInitiatedMethod = keyof typeof ServerRequestParamsSchemas;
40
- type Message = Request | Response;
41
-
42
- const RequestSchema: z.ZodObject<
43
- {
44
- id: z.ZodString;
45
- method: z.ZodString;
46
- params: z.ZodOptional<z.ZodUnknown>;
47
- }
48
- > = z.object({
49
- id: z.string(),
50
- method: z.string(),
51
- params: z.unknown().optional(),
52
- });
53
-
54
- const ErrorSchema: z.ZodObject<
55
- { code: z.ZodNumber; message: z.ZodString; data: z.ZodOptional<z.ZodUnknown> }
56
- > = z.object({
57
- code: z.number(),
58
- message: z.string(),
59
- data: z.unknown().optional(),
60
- });
61
-
62
- const ResponseSchema: z.ZodObject<
63
- {
64
- id: z.ZodString;
65
- result: z.ZodOptional<z.ZodUnknown>;
66
- error: z.ZodOptional<typeof ErrorSchema>;
67
- }
68
- > = z.object({
69
- id: z.string(),
70
- result: z.unknown().optional(),
71
- error: ErrorSchema.optional(),
72
- });
73
37
 
74
38
  const RegisterParamsSchema: z.ZodObject<
75
39
  {
@@ -175,8 +139,6 @@ function getRuntimeDir(): string {
175
139
  export {
176
140
  DEFAULT_SUBTREE_DEPTH,
177
141
  MAX_SUBTREE_WIDGETS,
178
- RequestSchema,
179
- ResponseSchema,
180
142
  RegisterParamsSchema,
181
143
  widgetIdParams,
182
144
  widgetPropsParams,
@@ -187,13 +149,10 @@ export {
187
149
  screenshotParams,
188
150
  ServerRequestParamsSchemas,
189
151
  DEFAULT_SOCKET_PATH,
190
- type Request,
191
- type Response,
192
152
  type SerializedWidget,
193
153
  type SerializedProperty,
194
154
  type AppInfo,
195
155
  type ServerRequestParams,
196
156
  type ParamsSchema,
197
157
  type ServerInitiatedMethod,
198
- type Message,
199
158
  };
@@ -353,7 +353,7 @@ class SocketServer {
353
353
  }
354
354
 
355
355
  this.server = null;
356
- this.registry.dispose("Server stopping");
356
+ this.registry.dispose();
357
357
  await closeServer(server);
358
358
  await this.release();
359
359
  }
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
  };