@gtkx/mcp 0.21.0 → 1.0.0-rc.1
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/README.md +137 -34
- package/bin/gtkx-mcp.js +8 -1
- package/dist/app-router.d.ts +35 -0
- package/dist/app-router.d.ts.map +1 -0
- package/dist/app-router.js +130 -0
- package/dist/app-router.js.map +1 -0
- package/dist/connection-registry.d.ts +12 -0
- package/dist/connection-registry.d.ts.map +1 -0
- package/dist/connection-registry.js +38 -0
- package/dist/connection-registry.js.map +1 -0
- package/dist/internal.d.ts +4 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -0
- package/dist/protocol/errors.d.ts +24 -87
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +27 -91
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/schemas.d.ts +89 -0
- package/dist/protocol/schemas.d.ts.map +1 -0
- package/dist/protocol/schemas.js +56 -0
- package/dist/protocol/schemas.js.map +1 -0
- package/dist/reference.d.ts +13 -0
- package/dist/reference.d.ts.map +1 -0
- package/dist/reference.js +250 -0
- package/dist/reference.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +220 -0
- package/dist/server.js.map +1 -0
- package/dist/socket-server.d.ts +4 -38
- package/dist/socket-server.d.ts.map +1 -1
- package/dist/socket-server.js +26 -109
- package/dist/socket-server.js.map +1 -1
- package/dist/tool.d.ts +22 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +36 -0
- package/dist/tool.js.map +1 -0
- package/dist/transport.d.ts +41 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +121 -0
- package/dist/transport.js.map +1 -0
- package/package.json +17 -11
- package/src/app-router.ts +172 -0
- package/src/connection-registry.ts +42 -0
- package/src/internal.ts +17 -0
- package/src/protocol/errors.ts +43 -99
- package/src/protocol/schemas.ts +148 -0
- package/src/reference.ts +340 -0
- package/src/server.ts +281 -0
- package/src/socket-server.ts +30 -154
- package/src/tool.ts +66 -0
- package/src/transport.ts +165 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -399
- package/dist/cli.js.map +0 -1
- package/dist/connection-manager.d.ts +0 -48
- package/dist/connection-manager.d.ts.map +0 -1
- package/dist/connection-manager.js +0 -185
- package/dist/connection-manager.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -5
- package/dist/index.js.map +0 -1
- package/dist/protocol/types.d.ts +0 -132
- package/dist/protocol/types.d.ts.map +0 -1
- package/dist/protocol/types.js +0 -46
- package/dist/protocol/types.js.map +0 -1
- package/src/cli.ts +0 -449
- package/src/connection-manager.ts +0 -247
- package/src/index.ts +0 -27
- package/src/protocol/types.ts +0 -153
package/dist/tool.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ProtocolError } from "./protocol/errors.js";
|
|
2
|
+
export const textContent = (text) => ({ content: [{ type: "text", text }] });
|
|
3
|
+
export const textError = (text) => ({
|
|
4
|
+
content: [{ type: "text", text }],
|
|
5
|
+
isError: true,
|
|
6
|
+
});
|
|
7
|
+
export const imageContent = (data, mimeType) => ({
|
|
8
|
+
content: [{ type: "image", data, mimeType }],
|
|
9
|
+
});
|
|
10
|
+
const hasStringHint = (data) => typeof data === "object" && data !== null && "hint" in data && typeof data.hint === "string";
|
|
11
|
+
const runTool = async (handler, args) => {
|
|
12
|
+
try {
|
|
13
|
+
return await handler(args);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error instanceof ProtocolError) {
|
|
17
|
+
return textError(hasStringHint(error.data) ? `${error.message}\n${error.data.hint}` : error.message);
|
|
18
|
+
}
|
|
19
|
+
return textError(error instanceof Error ? error.message : String(error));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export const defineTool = (tool) => tool;
|
|
23
|
+
export const registerTool = (server, tool) => {
|
|
24
|
+
const callback = ((args, _extra) => runTool(tool.handler, args));
|
|
25
|
+
server.registerTool(tool.name, {
|
|
26
|
+
description: tool.description,
|
|
27
|
+
inputSchema: tool.inputSchema,
|
|
28
|
+
annotations: {
|
|
29
|
+
title: tool.title,
|
|
30
|
+
readOnlyHint: tool.kind === "readOnly",
|
|
31
|
+
destructiveHint: tool.kind === "action",
|
|
32
|
+
openWorldHint: true,
|
|
33
|
+
},
|
|
34
|
+
}, callback);
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=tool.js.map
|
package/dist/tool.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool.js","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAkB,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;AAErG,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAY,EAAkB,EAAE,CAAC,CAAC;IACxD,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACjC,OAAO,EAAE,IAAI;CAChB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,IAAY,EAAE,QAAgB,EAAkB,EAAE,CAAC,CAAC;IAC7E,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;CAC/C,CAAC,CAAC;AAeH,MAAM,aAAa,GAAG,CAAC,IAAa,EAA4B,EAAE,CAC9D,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC;AAEjG,MAAM,OAAO,GAAG,KAAK,EACjB,OAA+E,EAC/E,IAAyC,EAClB,EAAE;IACzB,IAAI,CAAC;QACD,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;YACjC,OAAO,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACzG,CAAC;QACD,OAAO,SAAS,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7E,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CAA0C,IAAiB,EAAQ,EAAE,CAAC,IAAY,CAAC;AAE7G,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAiB,EAAE,IAAU,EAAQ,EAAE;IAChE,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAyC,EAAE,MAAe,EAAE,EAAE,CAC7E,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAA4C,CAAC;IAC5E,MAAM,CAAC,YAAY,CACf,IAAI,CAAC,IAAI,EACT;QACI,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE;YACT,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,YAAY,EAAE,IAAI,CAAC,IAAI,KAAK,UAAU;YACtC,eAAe,EAAE,IAAI,CAAC,IAAI,KAAK,QAAQ;YACvC,aAAa,EAAE,IAAI;SACtB;KACJ,EACD,QAAQ,CACX,CAAC;AACN,CAAC,CAAC","sourcesContent":["import type { McpServer, ToolCallback } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport type { z } from \"zod\";\nimport { ProtocolError } from \"./protocol/errors.js\";\n\nexport const textContent = (text: string): CallToolResult => ({ content: [{ type: \"text\", text }] });\n\nexport const textError = (text: string): CallToolResult => ({\n content: [{ type: \"text\", text }],\n isError: true,\n});\n\nexport const imageContent = (data: string, mimeType: string): CallToolResult => ({\n content: [{ type: \"image\", data, mimeType }],\n});\n\nexport type ToolArgs<Shape extends Record<string, z.ZodType>> = { [K in keyof Shape]: z.output<Shape[K]> };\n\ntype ToolKind = \"readOnly\" | \"action\";\n\nexport type Tool<Shape extends Record<string, z.ZodType> = Record<string, z.ZodType>> = {\n name: string;\n title: string;\n kind: ToolKind;\n description: string;\n inputSchema: Shape;\n handler: (args: ToolArgs<Shape>) => Promise<CallToolResult>;\n};\n\nconst hasStringHint = (data: unknown): data is { hint: string } =>\n typeof data === \"object\" && data !== null && \"hint\" in data && typeof data.hint === \"string\";\n\nconst runTool = async (\n handler: (args: ToolArgs<Record<string, z.ZodType>>) => Promise<CallToolResult>,\n args: ToolArgs<Record<string, z.ZodType>>,\n): Promise<CallToolResult> => {\n try {\n return await handler(args);\n } catch (error) {\n if (error instanceof ProtocolError) {\n return textError(hasStringHint(error.data) ? `${error.message}\\n${error.data.hint}` : error.message);\n }\n return textError(error instanceof Error ? error.message : String(error));\n }\n};\n\nexport const defineTool = <Shape extends Record<string, z.ZodType>>(tool: Tool<Shape>): Tool => tool as Tool;\n\nexport const registerTool = (server: McpServer, tool: Tool): void => {\n const callback = ((args: ToolArgs<Record<string, z.ZodType>>, _extra: unknown) =>\n runTool(tool.handler, args)) as ToolCallback<Record<string, z.ZodType>>;\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: tool.inputSchema,\n annotations: {\n title: tool.title,\n readOnlyHint: tool.kind === \"readOnly\",\n destructiveHint: tool.kind === \"action\",\n openWorldHint: true,\n },\n },\n callback,\n );\n};\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import type { Socket } from "node:net";
|
|
3
|
+
import type { Duplex } from "node:stream";
|
|
4
|
+
import { ProtocolError } from "./protocol/errors.js";
|
|
5
|
+
import { type Message, type Request } from "./protocol/schemas.js";
|
|
6
|
+
export type ProtocolConnectionEvents = {
|
|
7
|
+
request: [Request];
|
|
8
|
+
invalid: [{
|
|
9
|
+
id: string;
|
|
10
|
+
error: ProtocolError;
|
|
11
|
+
}];
|
|
12
|
+
};
|
|
13
|
+
export declare class ConnectionClosedError extends Error {
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
export declare class ProtocolConnection extends EventEmitter<ProtocolConnectionEvents> {
|
|
17
|
+
id: string;
|
|
18
|
+
private buffer;
|
|
19
|
+
private pending;
|
|
20
|
+
private writer;
|
|
21
|
+
constructor(writer: Duplex);
|
|
22
|
+
feed(data: Buffer | string): void;
|
|
23
|
+
write(message: Message): void;
|
|
24
|
+
static fromSocket(socket: Socket, options?: {
|
|
25
|
+
onClose?: () => void;
|
|
26
|
+
onError?: (error: Error) => void;
|
|
27
|
+
}): ProtocolConnection;
|
|
28
|
+
send<T = unknown>(method: string, params: unknown, timeout: number): Promise<T>;
|
|
29
|
+
rejectPending(error: Error): void;
|
|
30
|
+
private processLine;
|
|
31
|
+
private handleResponse;
|
|
32
|
+
}
|
|
33
|
+
export type AppConnectionEvents = {
|
|
34
|
+
disconnection: [ProtocolConnection];
|
|
35
|
+
request: [ProtocolConnection, Request];
|
|
36
|
+
error: [Error];
|
|
37
|
+
};
|
|
38
|
+
export interface AppConnections extends EventEmitter<AppConnectionEvents> {
|
|
39
|
+
send(connectionId: string, message: Message): void;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAA+C,aAAa,EAAuB,MAAM,sBAAsB,CAAC;AACvH,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,OAAO,EAAgD,MAAM,uBAAuB,CAAC;AAEjH,MAAM,MAAM,wBAAwB,GAAG;IACnC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC;IACnB,OAAO,EAAE,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;CACnD,CAAC;AAQF,qBAAa,qBAAsB,SAAQ,KAAK;IAC5C,cAGC;CACJ;AAED,qBAAa,kBAAmB,SAAQ,YAAY,CAAC,wBAAwB,CAAC;IAC1E,EAAE,EAAE,MAAM,CAAuB;IACjC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,OAAO,CAA0C;IACzD,OAAO,CAAC,MAAM,CAAS;IAEvB,YAAY,MAAM,EAAE,MAAM,EAGzB;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAahC;IAED,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAG5B;IAED,MAAM,CAAC,UAAU,CACb,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;QACL,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;QACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;KAC/B,GACP,kBAAkB,CAWpB;IAED,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CA0B9E;IAED,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAMhC;IAED,OAAO,CAAC,WAAW;IA4BnB,OAAO,CAAC,cAAc;CAgBzB;AAED,MAAM,MAAM,mBAAmB,GAAG;IAC9B,aAAa,EAAE,CAAC,kBAAkB,CAAC,CAAC;IACpC,OAAO,EAAE,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IACvC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;CAClB,CAAC;AAEF,MAAM,WAAW,cAAe,SAAQ,YAAY,CAAC,mBAAmB,CAAC;IACrE,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACtD"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import { ErrorCode, invalidRequestError, isErrorCode, ProtocolError, requestTimeoutError } from "./protocol/errors.js";
|
|
3
|
+
import { RequestSchema, ResponseSchema } from "./protocol/schemas.js";
|
|
4
|
+
export class ConnectionClosedError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("Connection stream is not writable");
|
|
7
|
+
this.name = "ConnectionClosedError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class ProtocolConnection extends EventEmitter {
|
|
11
|
+
id = crypto.randomUUID();
|
|
12
|
+
buffer = "";
|
|
13
|
+
pending = new Map();
|
|
14
|
+
writer;
|
|
15
|
+
constructor(writer) {
|
|
16
|
+
super();
|
|
17
|
+
this.writer = writer;
|
|
18
|
+
}
|
|
19
|
+
feed(data) {
|
|
20
|
+
this.buffer += typeof data === "string" ? data : data.toString();
|
|
21
|
+
let newlineIndex = this.buffer.indexOf("\n");
|
|
22
|
+
while (newlineIndex !== -1) {
|
|
23
|
+
const line = this.buffer.slice(0, newlineIndex);
|
|
24
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
25
|
+
if (line.trim()) {
|
|
26
|
+
this.processLine(line);
|
|
27
|
+
}
|
|
28
|
+
newlineIndex = this.buffer.indexOf("\n");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
write(message) {
|
|
32
|
+
if (!this.writer.writable)
|
|
33
|
+
return;
|
|
34
|
+
this.writer.write(`${JSON.stringify(message)}\n`);
|
|
35
|
+
}
|
|
36
|
+
static fromSocket(socket, options = {}) {
|
|
37
|
+
const connection = new ProtocolConnection(socket);
|
|
38
|
+
socket.on("data", (data) => connection.feed(data));
|
|
39
|
+
socket.on("close", () => {
|
|
40
|
+
connection.rejectPending(new Error("Connection closed"));
|
|
41
|
+
options.onClose?.();
|
|
42
|
+
});
|
|
43
|
+
if (options.onError) {
|
|
44
|
+
socket.on("error", options.onError);
|
|
45
|
+
}
|
|
46
|
+
return connection;
|
|
47
|
+
}
|
|
48
|
+
send(method, params, timeout) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
if (!this.writer.writable) {
|
|
51
|
+
reject(new ConnectionClosedError());
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const id = crypto.randomUUID();
|
|
55
|
+
const timeoutHandle = setTimeout(() => {
|
|
56
|
+
this.pending.delete(id);
|
|
57
|
+
reject(requestTimeoutError(timeout));
|
|
58
|
+
}, timeout);
|
|
59
|
+
this.pending.set(id, {
|
|
60
|
+
resolve: resolve,
|
|
61
|
+
reject,
|
|
62
|
+
timeout: timeoutHandle,
|
|
63
|
+
});
|
|
64
|
+
this.write({ id, method, params });
|
|
65
|
+
if (!this.writer.writable) {
|
|
66
|
+
clearTimeout(timeoutHandle);
|
|
67
|
+
this.pending.delete(id);
|
|
68
|
+
reject(new ConnectionClosedError());
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
rejectPending(error) {
|
|
73
|
+
for (const entry of this.pending.values()) {
|
|
74
|
+
clearTimeout(entry.timeout);
|
|
75
|
+
entry.reject(error);
|
|
76
|
+
}
|
|
77
|
+
this.pending.clear();
|
|
78
|
+
}
|
|
79
|
+
processLine(line) {
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(line);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
this.emit("invalid", { id: "unknown", error: invalidRequestError("Invalid JSON") });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const message = parsed;
|
|
89
|
+
if (typeof message.method === "string") {
|
|
90
|
+
const requestResult = RequestSchema.safeParse(parsed);
|
|
91
|
+
if (requestResult.success) {
|
|
92
|
+
this.emit("request", requestResult.data);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const responseResult = ResponseSchema.safeParse(parsed);
|
|
98
|
+
if (responseResult.success) {
|
|
99
|
+
this.handleResponse(responseResult.data);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const id = typeof message.id === "string" ? message.id : "unknown";
|
|
104
|
+
this.emit("invalid", { id, error: invalidRequestError("Invalid message format") });
|
|
105
|
+
}
|
|
106
|
+
handleResponse(response) {
|
|
107
|
+
const entry = this.pending.get(response.id);
|
|
108
|
+
if (!entry)
|
|
109
|
+
return;
|
|
110
|
+
clearTimeout(entry.timeout);
|
|
111
|
+
this.pending.delete(response.id);
|
|
112
|
+
if (response.error) {
|
|
113
|
+
const err = response.error;
|
|
114
|
+
entry.reject(new ProtocolError(isErrorCode(err.code) ? err.code : ErrorCode.INTERNAL_ERROR, err.message, err.data));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
entry.resolve(response.result);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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","sourcesContent":["import EventEmitter from \"node:events\";\nimport type { Socket } from \"node:net\";\nimport type { Duplex } from \"node:stream\";\nimport { ErrorCode, invalidRequestError, isErrorCode, ProtocolError, requestTimeoutError } from \"./protocol/errors.js\";\nimport { type Message, type Request, RequestSchema, type Response, ResponseSchema } from \"./protocol/schemas.js\";\n\nexport type ProtocolConnectionEvents = {\n request: [Request];\n invalid: [{ id: string; error: ProtocolError }];\n};\n\ntype PendingRequest = {\n resolve: (result: unknown) => void;\n reject: (error: Error) => void;\n timeout: NodeJS.Timeout;\n};\n\nexport class ConnectionClosedError extends Error {\n constructor() {\n super(\"Connection stream is not writable\");\n this.name = \"ConnectionClosedError\";\n }\n}\n\nexport class ProtocolConnection extends EventEmitter<ProtocolConnectionEvents> {\n id: string = crypto.randomUUID();\n private buffer = \"\";\n private pending: Map<string, PendingRequest> = new Map();\n private writer: Duplex;\n\n constructor(writer: Duplex) {\n super();\n this.writer = writer;\n }\n\n feed(data: Buffer | string): void {\n this.buffer += typeof data === \"string\" ? data : data.toString();\n\n let newlineIndex = this.buffer.indexOf(\"\\n\");\n while (newlineIndex !== -1) {\n const line = this.buffer.slice(0, newlineIndex);\n this.buffer = this.buffer.slice(newlineIndex + 1);\n\n if (line.trim()) {\n this.processLine(line);\n }\n newlineIndex = this.buffer.indexOf(\"\\n\");\n }\n }\n\n write(message: Message): void {\n if (!this.writer.writable) return;\n this.writer.write(`${JSON.stringify(message)}\\n`);\n }\n\n static fromSocket(\n socket: Socket,\n options: {\n onClose?: () => void;\n onError?: (error: Error) => void;\n } = {},\n ): ProtocolConnection {\n const connection = new ProtocolConnection(socket);\n socket.on(\"data\", (data: Buffer) => connection.feed(data));\n socket.on(\"close\", () => {\n connection.rejectPending(new Error(\"Connection closed\"));\n options.onClose?.();\n });\n if (options.onError) {\n socket.on(\"error\", options.onError);\n }\n return connection;\n }\n\n send<T = unknown>(method: string, params: unknown, timeout: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n if (!this.writer.writable) {\n reject(new ConnectionClosedError());\n return;\n }\n\n const id = crypto.randomUUID();\n const timeoutHandle = setTimeout(() => {\n this.pending.delete(id);\n reject(requestTimeoutError(timeout));\n }, timeout);\n\n this.pending.set(id, {\n resolve: resolve as (result: unknown) => void,\n reject,\n timeout: timeoutHandle,\n });\n\n this.write({ id, method, params });\n if (!this.writer.writable) {\n clearTimeout(timeoutHandle);\n this.pending.delete(id);\n reject(new ConnectionClosedError());\n }\n });\n }\n\n rejectPending(error: Error): void {\n for (const entry of this.pending.values()) {\n clearTimeout(entry.timeout);\n entry.reject(error);\n }\n this.pending.clear();\n }\n\n private processLine(line: string): void {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n this.emit(\"invalid\", { id: \"unknown\", error: invalidRequestError(\"Invalid JSON\") });\n return;\n }\n\n const message = parsed as Record<string, unknown>;\n if (typeof message.method === \"string\") {\n const requestResult = RequestSchema.safeParse(parsed);\n if (requestResult.success) {\n this.emit(\"request\", requestResult.data);\n return;\n }\n } else {\n const responseResult = ResponseSchema.safeParse(parsed);\n if (responseResult.success) {\n this.handleResponse(responseResult.data);\n return;\n }\n }\n\n const id = typeof message.id === \"string\" ? message.id : \"unknown\";\n this.emit(\"invalid\", { id, error: invalidRequestError(\"Invalid message format\") });\n }\n\n private handleResponse(response: Response): void {\n const entry = this.pending.get(response.id);\n if (!entry) return;\n\n clearTimeout(entry.timeout);\n this.pending.delete(response.id);\n\n if (response.error) {\n const err = response.error;\n entry.reject(\n new ProtocolError(isErrorCode(err.code) ? err.code : ErrorCode.INTERNAL_ERROR, err.message, err.data),\n );\n } else {\n entry.resolve(response.result);\n }\n }\n}\n\nexport type AppConnectionEvents = {\n disconnection: [ProtocolConnection];\n request: [ProtocolConnection, Request];\n error: [Error];\n};\n\nexport interface AppConnections extends EventEmitter<AppConnectionEvents> {\n send(connectionId: string, message: Message): void;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
|
+
"description": "Model Context Protocol server that gives an agent the GTKX API reference and control of a running application",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
7
7
|
"gtk",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "https://github.com/gtkx-org/gtkx.git",
|
|
22
|
+
"url": "git+https://github.com/gtkx-org/gtkx.git",
|
|
23
23
|
"directory": "packages/mcp"
|
|
24
24
|
},
|
|
25
25
|
"license": "MPL-2.0",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"type": "module",
|
|
28
28
|
"exports": {
|
|
29
29
|
"./package.json": "./package.json",
|
|
30
|
-
"
|
|
31
|
-
"types": "./dist/
|
|
32
|
-
"default": "./dist/
|
|
30
|
+
"./internal": {
|
|
31
|
+
"types": "./dist/internal.d.ts",
|
|
32
|
+
"default": "./dist/internal.js"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"bin": {
|
|
@@ -41,13 +41,19 @@
|
|
|
41
41
|
"dist",
|
|
42
42
|
"src"
|
|
43
43
|
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=24"
|
|
46
|
+
},
|
|
44
47
|
"dependencies": {
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
46
|
-
"zod": "^4.3
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
+
"zod": "^4.4.3",
|
|
50
|
+
"@gtkx/codegen": "1.0.0-rc.1",
|
|
51
|
+
"@gtkx/config": "1.0.0-rc.1",
|
|
52
|
+
"@gtkx/utils": "1.0.0-rc.1"
|
|
47
53
|
},
|
|
48
54
|
"scripts": {
|
|
49
|
-
"build": "tsc -b
|
|
50
|
-
"
|
|
51
|
-
"
|
|
55
|
+
"build": "tsc -b tsconfig.lib.json",
|
|
56
|
+
"typecheck": "tsc -b --emitDeclarationOnly",
|
|
57
|
+
"release": "tsx ../../scripts/release-package.ts"
|
|
52
58
|
}
|
|
53
59
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import {
|
|
3
|
+
appNotFoundError,
|
|
4
|
+
connectionWriteFailedError,
|
|
5
|
+
invalidRequestError,
|
|
6
|
+
methodNotFoundError,
|
|
7
|
+
noAppConnectedError,
|
|
8
|
+
type ProtocolError,
|
|
9
|
+
} from "./protocol/errors.js";
|
|
10
|
+
import { type AppInfo, RegisterParamsSchema, type Request, type Response } from "./protocol/schemas.js";
|
|
11
|
+
import { type AppConnections, ConnectionClosedError, type ProtocolConnection } from "./transport.js";
|
|
12
|
+
|
|
13
|
+
type AppRouterEventMap = {
|
|
14
|
+
appRegistered: [AppInfo];
|
|
15
|
+
appUnregistered: [string];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type RegisteredApp = {
|
|
19
|
+
info: AppInfo;
|
|
20
|
+
connection: ProtocolConnection;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
24
|
+
|
|
25
|
+
export class AppRouter extends EventEmitter<AppRouterEventMap> {
|
|
26
|
+
private static DEFAULT_WAIT_TIMEOUT = 10000;
|
|
27
|
+
|
|
28
|
+
private apps: Map<string, RegisteredApp> = new Map();
|
|
29
|
+
private connectionToApp: Map<string, string> = new Map();
|
|
30
|
+
private requestTimeout: number;
|
|
31
|
+
private connections: AppConnections;
|
|
32
|
+
|
|
33
|
+
constructor(connections: AppConnections, options: { requestTimeout?: number } = {}) {
|
|
34
|
+
super();
|
|
35
|
+
this.connections = connections;
|
|
36
|
+
this.requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
37
|
+
|
|
38
|
+
this.connections.on("request", (connection, request) => {
|
|
39
|
+
this.handleRequest(connection, request);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.connections.on("disconnection", (connection) => {
|
|
43
|
+
this.removeApp(connection);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getApps(): AppInfo[] {
|
|
48
|
+
return Array.from(this.apps.values()).map((app) => app.info);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
hasConnectedApps(): boolean {
|
|
52
|
+
return this.apps.size > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getDefaultApp(): RegisteredApp | undefined {
|
|
56
|
+
const first = this.apps.values().next();
|
|
57
|
+
return first.done ? undefined : first.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getProjectRoot(): string | undefined {
|
|
61
|
+
return this.getDefaultApp()?.info.projectRoot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
waitForApp(timeout: number = AppRouter.DEFAULT_WAIT_TIMEOUT): Promise<AppInfo> {
|
|
65
|
+
const defaultApp = this.getDefaultApp();
|
|
66
|
+
if (defaultApp) {
|
|
67
|
+
return Promise.resolve(defaultApp.info);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const timeoutId = setTimeout(() => {
|
|
72
|
+
this.off("appRegistered", onRegister);
|
|
73
|
+
reject(
|
|
74
|
+
new Error(
|
|
75
|
+
`Timeout waiting for app registration after ${timeout}ms. ` +
|
|
76
|
+
"Make sure your GTKX app is running with 'gtkx dev'.",
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
}, timeout);
|
|
80
|
+
|
|
81
|
+
const onRegister = (appInfo: AppInfo) => {
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
this.off("appRegistered", onRegister);
|
|
84
|
+
resolve(appInfo);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
this.on("appRegistered", onRegister);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async sendToApp<T>(applicationId: string | undefined, method: string, params?: unknown): Promise<T> {
|
|
92
|
+
const app = applicationId ? this.apps.get(applicationId) : this.getDefaultApp();
|
|
93
|
+
|
|
94
|
+
if (!app) {
|
|
95
|
+
if (applicationId) {
|
|
96
|
+
throw appNotFoundError(applicationId);
|
|
97
|
+
}
|
|
98
|
+
throw noAppConnectedError();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
return await app.connection.send<T>(method, params, this.requestTimeout);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error instanceof ConnectionClosedError) {
|
|
105
|
+
this.removeApp(app.connection);
|
|
106
|
+
throw connectionWriteFailedError(app.info.applicationId);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private handleRequest(connection: ProtocolConnection, request: Request): void {
|
|
113
|
+
if (request.method === "app.register") {
|
|
114
|
+
this.handleRegister(connection, request);
|
|
115
|
+
} else if (request.method === "app.unregister") {
|
|
116
|
+
this.handleUnregister(connection, request);
|
|
117
|
+
} else {
|
|
118
|
+
this.sendError(connection, request, methodNotFoundError(request.method));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private handleRegister(connection: ProtocolConnection, request: Request): void {
|
|
123
|
+
const parseResult = RegisterParamsSchema.safeParse(request.params);
|
|
124
|
+
if (!parseResult.success) {
|
|
125
|
+
this.sendError(connection, request, invalidRequestError(parseResult.error.message));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const params = parseResult.data;
|
|
130
|
+
const appInfo: AppInfo = {
|
|
131
|
+
applicationId: params.applicationId,
|
|
132
|
+
pid: params.pid,
|
|
133
|
+
...(params.projectRoot === undefined ? {} : { projectRoot: params.projectRoot }),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
this.apps.set(params.applicationId, { info: appInfo, connection });
|
|
137
|
+
this.connectionToApp.set(connection.id, params.applicationId);
|
|
138
|
+
|
|
139
|
+
this.acknowledge(connection, request);
|
|
140
|
+
|
|
141
|
+
this.emit("appRegistered", appInfo);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private handleUnregister(connection: ProtocolConnection, request: Request): void {
|
|
145
|
+
this.removeApp(connection);
|
|
146
|
+
this.acknowledge(connection, request);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private acknowledge(connection: ProtocolConnection, request: Request): void {
|
|
150
|
+
const response: Response = {
|
|
151
|
+
id: request.id,
|
|
152
|
+
result: { success: true },
|
|
153
|
+
};
|
|
154
|
+
this.connections.send(connection.id, response);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private sendError(connection: ProtocolConnection, request: Request, error: ProtocolError): void {
|
|
158
|
+
this.connections.send(connection.id, {
|
|
159
|
+
id: request.id,
|
|
160
|
+
error: error.toErrorObject(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private removeApp(connection: ProtocolConnection): void {
|
|
165
|
+
const applicationId = this.connectionToApp.get(connection.id);
|
|
166
|
+
this.connectionToApp.delete(connection.id);
|
|
167
|
+
if (applicationId === undefined) return;
|
|
168
|
+
if (this.apps.get(applicationId)?.connection !== connection) return;
|
|
169
|
+
this.apps.delete(applicationId);
|
|
170
|
+
this.emit("appUnregistered", applicationId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import type { Socket } from "node:net";
|
|
3
|
+
import type { Message } from "./protocol/schemas.js";
|
|
4
|
+
import { type AppConnectionEvents, type AppConnections, ProtocolConnection } from "./transport.js";
|
|
5
|
+
|
|
6
|
+
export class ConnectionRegistry extends EventEmitter<AppConnectionEvents> implements AppConnections {
|
|
7
|
+
private connections: Map<string, ProtocolConnection> = new Map();
|
|
8
|
+
private sockets: Map<string, Socket> = new Map();
|
|
9
|
+
|
|
10
|
+
register(socket: Socket): ProtocolConnection {
|
|
11
|
+
const connection = ProtocolConnection.fromSocket(socket, {
|
|
12
|
+
onClose: () => {
|
|
13
|
+
this.connections.delete(connection.id);
|
|
14
|
+
this.sockets.delete(connection.id);
|
|
15
|
+
this.emit("disconnection", connection);
|
|
16
|
+
},
|
|
17
|
+
onError: (error) => this.emit("error", error),
|
|
18
|
+
});
|
|
19
|
+
this.connections.set(connection.id, connection);
|
|
20
|
+
this.sockets.set(connection.id, socket);
|
|
21
|
+
connection.on("request", (request) => this.emit("request", connection, request));
|
|
22
|
+
connection.on("invalid", ({ id: badId, error }) => {
|
|
23
|
+
connection.write({ id: badId, error: error.toErrorObject() });
|
|
24
|
+
});
|
|
25
|
+
return connection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
send(connectionId: string, message: Message): void {
|
|
29
|
+
const connection = this.connections.get(connectionId);
|
|
30
|
+
if (!connection) return;
|
|
31
|
+
connection.write(message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dispose(reason: string): void {
|
|
35
|
+
for (const connection of this.connections.values()) {
|
|
36
|
+
connection.rejectPending(new Error(reason));
|
|
37
|
+
}
|
|
38
|
+
for (const socket of this.sockets.values()) {
|
|
39
|
+
socket.destroy();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ErrorCode,
|
|
3
|
+
invalidRequestError,
|
|
4
|
+
methodNotFoundError,
|
|
5
|
+
ProtocolError,
|
|
6
|
+
widgetNotFoundError,
|
|
7
|
+
} from "./protocol/errors.js";
|
|
8
|
+
export {
|
|
9
|
+
DEFAULT_SOCKET_PATH,
|
|
10
|
+
type ParamsSchema,
|
|
11
|
+
type Request,
|
|
12
|
+
type SerializedWidget,
|
|
13
|
+
type ServerInitiatedMethod,
|
|
14
|
+
type ServerRequestParams,
|
|
15
|
+
ServerRequestParamsSchemas,
|
|
16
|
+
} from "./protocol/schemas.js";
|
|
17
|
+
export { ProtocolConnection } from "./transport.js";
|